diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index ea3d74630..be65394fc 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -15,6 +15,7 @@ Please attach game logs: `VCMI_client.txt`, `VCMI_server.txt` etc. **To Reproduce** Steps to reproduce the behavior: + 1. Go to '...' 2. Click on '....' 3. Scroll down to '....' @@ -24,7 +25,7 @@ Steps to reproduce the behavior: A clear and concise description of what you expected to happen. **Actual behavior** -A clear description what is currently happening +A clear description what is currently happening **Did it work earlier?** If this something which worked well some time ago, please let us know about version where it works or at date when it worked. @@ -33,8 +34,9 @@ If this something which worked well some time ago, please let us know about vers If applicable, add screenshots to help explain your problem. **Version** - - OS: [e.g. Windows, macOS Intel, macOS ARM, Android, Linux, iOS] - - Version: [VCMI version] + +- OS: [e.g. Windows, macOS Intel, macOS ARM, Android, Linux, iOS] +- Version: [VCMI version] **Additional context** Add any other context about the problem here. diff --git a/.github/workflows/github.yml b/.github/workflows/github.yml index 99dcbd8b7..b4beca40b 100644 --- a/.github/workflows/github.yml +++ b/.github/workflows/github.yml @@ -3,7 +3,6 @@ name: VCMI on: push: branches: - - features/* - beta - master - develop @@ -22,84 +21,112 @@ jobs: - platform: linux-qt6 os: ubuntu-24.04 test: 0 + before_install: linux_qt6.sh preset: linux-clang-test - platform: linux os: ubuntu-24.04 test: 1 + before_install: linux_qt5.sh preset: linux-gcc-test - platform: linux os: ubuntu-20.04 test: 0 + before_install: linux_qt5.sh preset: linux-gcc-debug - platform: mac-intel os: macos-13 test: 0 pack: 1 + upload: 1 pack_type: Release extension: dmg + before_install: macos.sh preset: macos-conan-ninja-release conan_profile: macos-intel + conan_prebuilts: dependencies-mac-intel conan_options: --options with_apple_system_libs=True artifact_platform: intel - platform: mac-arm os: macos-13 test: 0 pack: 1 + upload: 1 pack_type: Release extension: dmg + before_install: macos.sh preset: macos-arm-conan-ninja-release conan_profile: macos-arm + conan_prebuilts: dependencies-mac-arm conan_options: --options with_apple_system_libs=True artifact_platform: arm - platform: ios os: macos-13 test: 0 pack: 1 + upload: 1 pack_type: Release extension: ipa + before_install: macos.sh preset: ios-release-conan-ccache conan_profile: ios-arm64 + conan_prebuilts: dependencies-ios conan_options: --options with_apple_system_libs=True - - platform: msvc + - platform: msvc-x64 + os: windows-latest + test: 0 + pack: 1 + upload: 1 + pack_type: RelWithDebInfo + extension: exe + before_install: msvc.sh + preset: windows-msvc-release + - platform: msvc-x86 os: windows-latest test: 0 pack: 1 pack_type: RelWithDebInfo extension: exe - preset: windows-msvc-release - - platform: mingw - os: ubuntu-22.04 + before_install: msvc.sh + preset: windows-msvc-release-x86 + - platform: mingw_x86_64 + os: ubuntu-24.04 test: 0 pack: 1 pack_type: Release extension: exe - cpack_args: -D CPACK_NSIS_EXECUTABLE=`which makensis` cmake_args: -G Ninja + before_install: mingw.sh preset: windows-mingw-conan-linux conan_profile: mingw64-linux.jinja - - platform: mingw-32 - os: ubuntu-22.04 + conan_prebuilts: dependencies-mingw-x86-64 + - platform: mingw_x86 + os: ubuntu-24.04 test: 0 pack: 1 pack_type: Release extension: exe - cpack_args: -D CPACK_NSIS_EXECUTABLE=`which makensis` cmake_args: -G Ninja + before_install: mingw.sh preset: windows-mingw-conan-linux conan_profile: mingw32-linux.jinja + conan_prebuilts: dependencies-mingw-x86 - platform: android-32 - os: macos-14 + os: ubuntu-24.04 + upload: 1 extension: apk preset: android-conan-ninja-release - conan_profile: android-32 - conan_options: --conf tools.android:ndk_path=$ANDROID_NDK_ROOT + before_install: android.sh + conan_profile: android-32-ndk + conan_prebuilts: dependencies-android-armeabi-v7a artifact_platform: armeabi-v7a - platform: android-64 - os: macos-14 + os: ubuntu-24.04 + upload: 1 extension: apk preset: android-conan-ninja-release - conan_profile: android-64 - conan_options: --conf tools.android:ndk_path=$ANDROID_NDK_ROOT + before_install: android.sh + conan_profile: android-64-ndk + conan_prebuilts: dependencies-android-arm64-v8a artifact_platform: arm64-v8a runs-on: ${{ matrix.os }} defaults: @@ -107,15 +134,25 @@ jobs: shell: bash steps: - - uses: actions/checkout@v4 + - name: Checkout repository + uses: actions/checkout@v4 with: submodules: recursive - - name: Dependencies - run: source '${{github.workspace}}/CI/${{matrix.platform}}/before_install.sh' + - name: Prepare CI + if: "${{ matrix.before_install != '' }}" + run: source '${{github.workspace}}/CI/before_install/${{matrix.before_install}}' env: VCMI_BUILD_PLATFORM: x64 + - name: Install Conan Dependencies + if: "${{ matrix.conan_prebuilts != '' }}" + run: source '${{github.workspace}}/CI/install_conan_dependencies.sh' '${{matrix.conan_prebuilts}}' + + - name: Install vcpkg Dependencies + if: ${{ startsWith(matrix.platform, 'msvc') }} + run: source '${{github.workspace}}/CI/install_vcpkg_dependencies.sh' '${{matrix.platform}}' + # ensure the ccache for each PR is separate so they don't interfere with each other # fall back to ccache of the vcmi/vcmi repo if no PR-specific ccache is found - name: ccache for PRs @@ -157,15 +194,13 @@ jobs: mkdir -p ~/.local/share/vcmi/ mv h3_assets/* ~/.local/share/vcmi/ - - uses: actions/setup-python@v5 + - name: Install Conan if: "${{ matrix.conan_profile != '' }}" - with: - python-version: '3.10' + run: pipx install 'conan<2.0' - - name: Conan setup + - name: Install Conan profile if: "${{ matrix.conan_profile != '' }}" run: | - pip3 install 'conan<2.0' conan profile new default --detect conan install . \ --install-folder=conan-generated \ @@ -177,7 +212,13 @@ jobs: env: GENERATE_ONLY_BUILT_CONFIG: 1 - - uses: actions/setup-java@v4 + # Workaround for gradle not discovering SDK that was installed via conan + - name: Find Android NDK + if: ${{ startsWith(matrix.platform, 'android') }} + run: sudo ln -s -T /home/runner/.conan/data/android-ndk/r25c/_/_/package/4db1be536558d833e52e862fd84d64d75c2b3656/bin /usr/local/lib/android/sdk/ndk/25.2.9519653 + + - name: Install Java + uses: actions/setup-java@v4 if: ${{ startsWith(matrix.platform, 'android') }} with: distribution: 'temurin' @@ -208,11 +249,11 @@ jobs: elif [[ (${{matrix.preset}} == android-conan-ninja-release) && (${{github.ref}} != 'refs/heads/master') ]] then cmake -DENABLE_CCACHE:BOOL=ON -DANDROID_GRADLE_PROPERTIES="applicationIdSuffix=.daily;signingConfig=dailySigning;applicationLabel=VCMI daily" --preset ${{ matrix.preset }} - elif [[ ${{matrix.platform}} != msvc ]] + elif [[ ${{startsWith(matrix.platform, 'msvc') }} ]] then - cmake -DENABLE_CCACHE:BOOL=ON --preset ${{ matrix.preset }} - else cmake --preset ${{ matrix.preset }} + else + cmake -DENABLE_CCACHE:BOOL=ON --preset ${{ matrix.preset }} fi - name: Build @@ -242,10 +283,13 @@ jobs: if: ${{ matrix.pack == 1 }} run: | cd '${{github.workspace}}/out/build/${{matrix.preset}}' - CPACK_PATH=`which -a cpack | grep -m1 -v -i chocolatey` - counter=0; until "$CPACK_PATH" -C ${{matrix.pack_type}} ${{ matrix.cpack_args }} || ((counter > 20)); do sleep 3; ((counter++)); done - test -f '${{github.workspace}}/CI/${{matrix.platform}}/post_pack.sh' \ - && '${{github.workspace}}/CI/${{matrix.platform}}/post_pack.sh' '${{github.workspace}}' "$(ls '${{ env.VCMI_PACKAGE_FILE_NAME }}'.*)" + + # Workaround for CPack bug on macOS 13 + counter=0 + until cpack -C ${{matrix.pack_type}} || ((counter > 20)); do + sleep 3 + ((counter++)) + done rm -rf _CPack_Packages - name: Artifacts @@ -253,6 +297,7 @@ jobs: uses: actions/upload-artifact@v4 with: name: ${{ env.VCMI_PACKAGE_FILE_NAME }} - ${{ matrix.platform }} + compression-level: 0 path: | ${{github.workspace}}/out/build/${{matrix.preset}}/${{ env.VCMI_PACKAGE_FILE_NAME }}.${{ matrix.extension }} @@ -268,32 +313,35 @@ jobs: echo "ANDROID_APK_PATH=$ANDROID_APK_PATH" >> $GITHUB_ENV echo "ANDROID_AAB_PATH=$ANDROID_AAB_PATH" >> $GITHUB_ENV - - name: Android apk artifacts + - name: Upload android apk artifacts if: ${{ startsWith(matrix.platform, 'android') }} uses: actions/upload-artifact@v4 with: name: ${{ env.VCMI_PACKAGE_FILE_NAME }} - ${{ matrix.platform }} + compression-level: 0 path: | ${{ env.ANDROID_APK_PATH }} - - name: Android aab artifacts - if: ${{ startsWith(matrix.platform, 'android') }} + - name: Upload Android aab artifacts + if: ${{ startsWith(matrix.platform, 'android') && github.ref == 'refs/heads/master' }} uses: actions/upload-artifact@v4 with: name: ${{ env.VCMI_PACKAGE_FILE_NAME }} - ${{ matrix.platform }} - aab + compression-level: 0 path: | ${{ env.ANDROID_AAB_PATH }} - - name: Symbols - if: ${{ matrix.platform == 'msvc' }} + - name: Upload debug symbols + if: ${{ startsWith(matrix.platform, 'msvc') }} uses: actions/upload-artifact@v4 with: name: ${{ env.VCMI_PACKAGE_FILE_NAME }} - ${{ matrix.platform }} - symbols + compression-level: 9 path: | ${{github.workspace}}/**/*.pdb - name: Upload build - if: ${{ (matrix.pack == 1 || startsWith(matrix.platform, 'android')) && (github.ref == 'refs/heads/develop' || github.ref == 'refs/heads/beta' || github.ref == 'refs/heads/master' || startsWith(github.ref, 'refs/heads/features/')) && matrix.platform != 'msvc' && matrix.platform != 'mingw-32' }} + if: ${{ (matrix.upload == 1) && (github.ref == 'refs/heads/develop' || github.ref == 'refs/heads/beta' || github.ref == 'refs/heads/master') }} continue-on-error: true run: | if [ -z '${{ env.ANDROID_APK_PATH }}' ] ; then @@ -343,11 +391,6 @@ jobs: steps: - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 - if: "${{ matrix.conan_profile != '' }}" - with: - python-version: '3.10' - - name: Ensure LF line endings run: | find . -path ./.git -prune -o -path ./AI/FuzzyLite -prune -o -path ./test/googletest \ @@ -358,4 +401,10 @@ jobs: - name: Validate JSON run: | sudo apt install python3-jstyleson - python3 CI/linux-qt6/validate_json.py + python3 CI/validate_json.py + + - name: Validate Markdown + uses: DavidAnson/markdownlint-cli2-action@v18 + with: + config: 'CI/example.markdownlint-cli2.jsonc' + globs: '**/*.md' diff --git a/AI/BattleAI/AttackPossibility.cpp b/AI/BattleAI/AttackPossibility.cpp index c22b7ccad..c55de09b2 100644 --- a/AI/BattleAI/AttackPossibility.cpp +++ b/AI/BattleAI/AttackPossibility.cpp @@ -58,7 +58,7 @@ void DamageCache::buildObstacleDamageCache(std::shared_ptr hb, return u->alive() && !u->isTurret() && u->getPosition().isValid(); }); - std::shared_ptr inner = std::make_shared(hb->env, hb); + auto inner = std::make_shared(hb->env, hb); for(auto stack : stacks) { diff --git a/AI/BattleAI/BattleEvaluator.cpp b/AI/BattleAI/BattleEvaluator.cpp index 57014836e..d591b8d7d 100644 --- a/AI/BattleAI/BattleEvaluator.cpp +++ b/AI/BattleAI/BattleEvaluator.cpp @@ -394,7 +394,7 @@ BattleAction BattleEvaluator::goTowardsNearest(const CStack * stack, std::vector { std::set obstacleHexes; - auto insertAffected = [](const CObstacleInstance & spellObst, std::set obstacleHexes) { + auto insertAffected = [](const CObstacleInstance & spellObst, std::set & obstacleHexes) { auto affectedHexes = spellObst.getAffectedTiles(); obstacleHexes.insert(affectedHexes.cbegin(), affectedHexes.cend()); }; @@ -675,7 +675,7 @@ bool BattleEvaluator::attemptCastingSpell(const CStack * activeStack) spells::BattleCast cast(state.get(), hero, spells::Mode::HERO, ps.spell); cast.castEval(state->getServerCallback(), ps.dest); - auto allUnits = state->battleGetUnitsIf([](const battle::Unit * u) -> bool { return true; }); + auto allUnits = state->battleGetUnitsIf([](const battle::Unit * u) -> bool { return u->isValidTarget(); }); auto needFullEval = vstd::contains_if(allUnits, [&](const battle::Unit * u) -> bool { @@ -731,7 +731,6 @@ bool BattleEvaluator::attemptCastingSpell(const CStack * activeStack) ps.value = scoreEvaluator.evaluateExchange(updatedAttack, cachedAttack.turn, *targets, innerCache, state); } - for(const auto & unit : allUnits) { if(!unit->isValidTarget(true)) @@ -771,11 +770,31 @@ bool BattleEvaluator::attemptCastingSpell(const CStack * activeStack) ps.value -= 4 * dpsReduce * scoreEvaluator.getNegativeEffectMultiplier(); #if BATTLE_TRACE_LEVEL >= 1 - logAi->trace( - "Spell affects %s (%d), dps: %2f", - unit->creatureId().toCreature()->getNameSingularTranslated(), - unit->getCount(), - dpsReduce); + // Ensure ps.dest is not empty before accessing the first element + if (!ps.dest.empty()) + { + logAi->trace( + "Spell %s to %d affects %s (%d), dps: %2f oldHealth: %d newHealth: %d", + ps.spell->getNameTranslated(), + ps.dest.at(0).hexValue.hex, // Safe to access .at(0) now + unit->creatureId().toCreature()->getNameSingularTranslated(), + unit->getCount(), + dpsReduce, + oldHealth, + newHealth); + } + else + { + // Handle the case where ps.dest is empty + logAi->trace( + "Spell %s has no destination, affects %s (%d), dps: %2f oldHealth: %d newHealth: %d", + ps.spell->getNameTranslated(), + unit->creatureId().toCreature()->getNameSingularTranslated(), + unit->getCount(), + dpsReduce, + oldHealth, + newHealth); + } #endif } } diff --git a/AI/BattleAI/BattleExchangeVariant.cpp b/AI/BattleAI/BattleExchangeVariant.cpp index 0cffe63f5..07576eb1f 100644 --- a/AI/BattleAI/BattleExchangeVariant.cpp +++ b/AI/BattleAI/BattleExchangeVariant.cpp @@ -906,7 +906,7 @@ std::vector BattleExchangeEvaluator::getOneTurnReachableUn { std::vector result; - for(int i = 0; i < turnOrder.size(); i++) + for(int i = 0; i < turnOrder.size(); i++, turn++) { auto & turnQueue = turnOrder[i]; HypotheticBattle turnBattle(env.get(), cb); diff --git a/AI/BattleAI/StackWithBonuses.cpp b/AI/BattleAI/StackWithBonuses.cpp index fbecaf541..149eb33f5 100644 --- a/AI/BattleAI/StackWithBonuses.cpp +++ b/AI/BattleAI/StackWithBonuses.cpp @@ -531,44 +531,44 @@ vstd::RNG * HypotheticBattle::HypotheticServerCallback::getRNG() return &rngStub; } -void HypotheticBattle::HypotheticServerCallback::apply(CPackForClient * pack) +void HypotheticBattle::HypotheticServerCallback::apply(CPackForClient & pack) { logAi->error("Package of type %s is not allowed in battle evaluation", typeid(pack).name()); } -void HypotheticBattle::HypotheticServerCallback::apply(BattleLogMessage * pack) +void HypotheticBattle::HypotheticServerCallback::apply(BattleLogMessage & pack) { - pack->applyBattle(owner); + pack.applyBattle(owner); } -void HypotheticBattle::HypotheticServerCallback::apply(BattleStackMoved * pack) +void HypotheticBattle::HypotheticServerCallback::apply(BattleStackMoved & pack) { - pack->applyBattle(owner); + pack.applyBattle(owner); } -void HypotheticBattle::HypotheticServerCallback::apply(BattleUnitsChanged * pack) +void HypotheticBattle::HypotheticServerCallback::apply(BattleUnitsChanged & pack) { - pack->applyBattle(owner); + pack.applyBattle(owner); } -void HypotheticBattle::HypotheticServerCallback::apply(SetStackEffect * pack) +void HypotheticBattle::HypotheticServerCallback::apply(SetStackEffect & pack) { - pack->applyBattle(owner); + pack.applyBattle(owner); } -void HypotheticBattle::HypotheticServerCallback::apply(StacksInjured * pack) +void HypotheticBattle::HypotheticServerCallback::apply(StacksInjured & pack) { - pack->applyBattle(owner); + pack.applyBattle(owner); } -void HypotheticBattle::HypotheticServerCallback::apply(BattleObstaclesChanged * pack) +void HypotheticBattle::HypotheticServerCallback::apply(BattleObstaclesChanged & pack) { - pack->applyBattle(owner); + pack.applyBattle(owner); } -void HypotheticBattle::HypotheticServerCallback::apply(CatapultAttack * pack) +void HypotheticBattle::HypotheticServerCallback::apply(CatapultAttack & pack) { - pack->applyBattle(owner); + pack.applyBattle(owner); } HypotheticBattle::HypotheticEnvironment::HypotheticEnvironment(HypotheticBattle * owner_, const Environment * upperEnvironment) diff --git a/AI/BattleAI/StackWithBonuses.h b/AI/BattleAI/StackWithBonuses.h index f5b4fac23..3a34cc761 100644 --- a/AI/BattleAI/StackWithBonuses.h +++ b/AI/BattleAI/StackWithBonuses.h @@ -189,15 +189,15 @@ private: vstd::RNG * getRNG() override; - void apply(CPackForClient * pack) override; + void apply(CPackForClient & pack) override; - void apply(BattleLogMessage * pack) override; - void apply(BattleStackMoved * pack) override; - void apply(BattleUnitsChanged * pack) override; - void apply(SetStackEffect * pack) override; - void apply(StacksInjured * pack) override; - void apply(BattleObstaclesChanged * pack) override; - void apply(CatapultAttack * pack) override; + void apply(BattleLogMessage & pack) override; + void apply(BattleStackMoved & pack) override; + void apply(BattleUnitsChanged & pack) override; + void apply(SetStackEffect & pack) override; + void apply(StacksInjured & pack) override; + void apply(BattleObstaclesChanged & pack) override; + void apply(CatapultAttack & pack) override; private: HypotheticBattle * owner; RNGStub rngStub; diff --git a/AI/Nullkiller/AIGateway.cpp b/AI/Nullkiller/AIGateway.cpp index 3609182df..c803a9259 100644 --- a/AI/Nullkiller/AIGateway.cpp +++ b/AI/Nullkiller/AIGateway.cpp @@ -17,7 +17,6 @@ #include "../../lib/mapObjects/ObjectTemplate.h" #include "../../lib/mapObjects/CGHeroInstance.h" #include "../../lib/CConfigHandler.h" -#include "../../lib/CHeroHandler.h" #include "../../lib/IGameSettings.h" #include "../../lib/gameState/CGameState.h" #include "../../lib/serializer/CTypeList.h" @@ -35,11 +34,6 @@ namespace NKAI { -// our to enemy strength ratio constants -const float SAFE_ATTACK_CONSTANT = 1.1f; -const float RETREAT_THRESHOLD = 0.3f; -const double RETREAT_ABSOLUTE_THRESHOLD = 10000.; - //one thread may be turn of AI and another will be handling a side effect for AI2 thread_local CCallback * cb = nullptr; thread_local AIGateway * ai = nullptr; @@ -554,7 +548,7 @@ std::optional AIGateway::makeSurrenderRetreatDecision(const Battle double fightRatio = ourStrength / (double)battleState.getEnemyStrength(); // if we have no towns - things are already bad, so retreat is not an option. - if(cb->getTownsInfo().size() && ourStrength < RETREAT_ABSOLUTE_THRESHOLD && fightRatio < RETREAT_THRESHOLD && battleState.canFlee) + if(cb->getTownsInfo().size() && ourStrength < nullkiller->settings->getRetreatThresholdAbsolute() && fightRatio < nullkiller->settings->getRetreatThresholdRelative() && battleState.canFlee) { return BattleAction::makeRetreat(battleState.ourSide); } @@ -649,12 +643,12 @@ void AIGateway::showBlockingDialog(const std::string & text, const std::vectordangerEvaluator->evaluateDanger(target, hero.get()); auto ratio = static_cast(danger) / hero->getTotalStrength(); - answer = 1; + answer = true; if(topObj->id != goalObjectID && nullkiller->dangerEvaluator->evaluateDanger(topObj) > 0) { // no if we do not aim to visit this object - answer = 0; + answer = false; } logAi->trace("Query hook: %s(%s) by %s danger ratio %f", target.toString(), topObj->getObjectName(), hero.name(), ratio); @@ -671,7 +665,7 @@ void AIGateway::showBlockingDialog(const std::string & text, const std::vector (1 / SAFE_ATTACK_CONSTANT); + bool dangerTooHigh = ratio * nullkiller->settings->getSafeAttackRatio() > 1; answer = !dangerUnknown && !dangerTooHigh; } @@ -764,7 +758,7 @@ void AIGateway::showGarrisonDialog(const CArmedInstance * up, const CGHeroInstan //you can't request action from action-response thread requestActionASAP([=]() { - if(removableUnits && up->tempOwner == down->tempOwner && nullkiller->settings->isGarrisonTroopsUsageAllowed() && !cb->getStartInfo()->isSteadwickFallCampaignMission()) + if(removableUnits && up->tempOwner == down->tempOwner && nullkiller->settings->isGarrisonTroopsUsageAllowed() && !cb->getStartInfo()->isRestorationOfErathiaCampaign()) { pickBestCreatures(down, up); } @@ -864,7 +858,7 @@ void AIGateway::makeTurn() void AIGateway::performObjectInteraction(const CGObjectInstance * obj, HeroPtr h) { - LOG_TRACE_PARAMS(logAi, "Hero %s and object %s at %s", h->getNameTranslated() % obj->getObjectName() % obj->pos.toString()); + LOG_TRACE_PARAMS(logAi, "Hero %s and object %s at %s", h->getNameTranslated() % obj->getObjectName() % obj->anchorPos().toString()); switch(obj->ID) { case Obj::TOWN: @@ -1056,7 +1050,7 @@ void AIGateway::pickBestArtifacts(const CGHeroInstance * h, const CGHeroInstance //FIXME: why are the above possible to be null? bool emptySlotFound = false; - for(auto slot : artifact->artType->getPossibleSlots().at(target->bearerType())) + for(auto slot : artifact->getType()->getPossibleSlots().at(target->bearerType())) { if(target->isPositionFree(slot) && artifact->canBePutAt(target, slot, true)) //combined artifacts are not always allowed to move { @@ -1069,7 +1063,7 @@ void AIGateway::pickBestArtifacts(const CGHeroInstance * h, const CGHeroInstance } if(!emptySlotFound) //try to put that atifact in already occupied slot { - for(auto slot : artifact->artType->getPossibleSlots().at(target->bearerType())) + for(auto slot : artifact->getType()->getPossibleSlots().at(target->bearerType())) { auto otherSlot = target->getSlot(slot); if(otherSlot && otherSlot->artifact) //we need to exchange artifact for better one @@ -1080,8 +1074,8 @@ void AIGateway::pickBestArtifacts(const CGHeroInstance * h, const CGHeroInstance { logAi->trace( "Exchange artifacts %s <-> %s", - artifact->artType->getNameTranslated(), - otherSlot->artifact->artType->getNameTranslated()); + artifact->getType()->getNameTranslated(), + otherSlot->artifact->getType()->getNameTranslated()); if(!otherSlot->artifact->canBePutAt(artHolder, location.slot, true)) { @@ -1130,10 +1124,10 @@ void AIGateway::recruitCreatures(const CGDwelling * d, const CArmedInstance * re { for(auto stack : recruiter->Slots()) { - if(!stack.second->type) + if(!stack.second->getType()) continue; - auto duplicatingSlot = recruiter->getSlotFor(stack.second->type); + auto duplicatingSlot = recruiter->getSlotFor(stack.second->getCreature()); if(duplicatingSlot != stack.first) { @@ -1454,8 +1448,8 @@ bool AIGateway::moveHeroToTile(int3 dst, HeroPtr h) void AIGateway::buildStructure(const CGTownInstance * t, BuildingID building) { - auto name = t->town->buildings.at(building)->getNameTranslated(); - logAi->debug("Player %d will build %s in town of %s at %s", ai->playerID, name, t->getNameTranslated(), t->pos.toString()); + auto name = t->getTown()->buildings.at(building)->getNameTranslated(); + logAi->debug("Player %d will build %s in town of %s at %s", ai->playerID, name, t->getNameTranslated(), t->anchorPos().toString()); cb->buildBuilding(t, building); //just do this; } diff --git a/AI/Nullkiller/AIUtility.cpp b/AI/Nullkiller/AIUtility.cpp index 3e89e2ba2..4ee0e960e 100644 --- a/AI/Nullkiller/AIUtility.cpp +++ b/AI/Nullkiller/AIUtility.cpp @@ -14,7 +14,6 @@ #include "../../lib/UnlockGuard.h" #include "../../lib/CConfigHandler.h" -#include "../../lib/CHeroHandler.h" #include "../../lib/mapObjects/MapObjects.h" #include "../../lib/mapping/CMapDefines.h" #include "../../lib/gameState/QuestInfo.h" @@ -147,21 +146,21 @@ bool HeroPtr::operator==(const HeroPtr & rhs) const return h == rhs.get(true); } -bool isSafeToVisit(const CGHeroInstance * h, const CCreatureSet * heroArmy, uint64_t dangerStrength) +bool isSafeToVisit(const CGHeroInstance * h, const CCreatureSet * heroArmy, uint64_t dangerStrength, float safeAttackRatio) { - const ui64 heroStrength = h->getFightingStrength() * heroArmy->getArmyStrength(); + const ui64 heroStrength = h->getHeroStrength() * heroArmy->getArmyStrength(); if(dangerStrength) { - return heroStrength / SAFE_ATTACK_CONSTANT > dangerStrength; + return heroStrength > dangerStrength * safeAttackRatio; } return true; //there's no danger } -bool isSafeToVisit(const CGHeroInstance * h, uint64_t dangerStrength) +bool isSafeToVisit(const CGHeroInstance * h, uint64_t dangerStrength, float safeAttackRatio) { - return isSafeToVisit(h, h, dangerStrength); + return isSafeToVisit(h, h, dangerStrength, safeAttackRatio); } bool isObjectRemovable(const CGObjectInstance * obj) @@ -194,7 +193,7 @@ bool canBeEmbarkmentPoint(const TerrainTile * t, bool fromWater) { // TODO: Such information should be provided by pathfinder // Tile must be free or with unoccupied boat - if(!t->blocked) + if(!t->blocked()) { return true; } @@ -268,8 +267,8 @@ bool compareArmyStrength(const CArmedInstance * a1, const CArmedInstance * a2) bool compareArtifacts(const CArtifactInstance * a1, const CArtifactInstance * a2) { - auto art1 = a1->artType; - auto art2 = a2->artType; + auto art1 = a1->getType(); + auto art2 = a2->getType(); if(art1->getPrice() == art2->getPrice()) return art1->valOfBonuses(BonusType::PRIMARY_SKILL) > art2->valOfBonuses(BonusType::PRIMARY_SKILL); @@ -313,7 +312,7 @@ int getDuplicatingSlots(const CArmedInstance * army) for(auto stack : army->Slots()) { - if(stack.second->type && army->getSlotFor(stack.second->type) != stack.first) + if(stack.second->getCreature() && army->getSlotFor(stack.second->getCreature()) != stack.first) duplicatingSlots++; } @@ -388,7 +387,7 @@ bool shouldVisit(const Nullkiller * ai, const CGHeroInstance * h, const CGObject { for(auto slot : h->Slots()) { - if(slot.second->type->hasUpgrades()) + if(slot.second->getType()->hasUpgrades()) return true; //TODO: check price? } return false; diff --git a/AI/Nullkiller/AIUtility.h b/AI/Nullkiller/AIUtility.h index 728130ff6..4275bea9f 100644 --- a/AI/Nullkiller/AIUtility.h +++ b/AI/Nullkiller/AIUtility.h @@ -61,11 +61,6 @@ const int GOLD_MINE_PRODUCTION = 1000; const int WOOD_ORE_MINE_PRODUCTION = 2; const int RESOURCE_MINE_PRODUCTION = 1; const int ACTUAL_RESOURCE_COUNT = 7; -const int ALLOWED_ROAMING_HEROES = 8; - -//implementation-dependent -extern const float SAFE_ATTACK_CONSTANT; -extern const int GOLD_RESERVE; extern thread_local CCallback * cb; @@ -213,8 +208,8 @@ bool isBlockVisitObj(const int3 & pos); bool isWeeklyRevisitable(const Nullkiller * ai, const CGObjectInstance * obj); bool isObjectRemovable(const CGObjectInstance * obj); //FIXME FIXME: move logic to object property! -bool isSafeToVisit(const CGHeroInstance * h, uint64_t dangerStrength); -bool isSafeToVisit(const CGHeroInstance * h, const CCreatureSet *, uint64_t dangerStrength); +bool isSafeToVisit(const CGHeroInstance * h, uint64_t dangerStrength, float safeAttackRatio); +bool isSafeToVisit(const CGHeroInstance * h, const CCreatureSet *, uint64_t dangerStrength, float safeAttackRatio); bool compareHeroStrength(const CGHeroInstance * h1, const CGHeroInstance * h2); bool compareArmyStrength(const CArmedInstance * a1, const CArmedInstance * a2); diff --git a/AI/Nullkiller/Analyzers/ArmyManager.cpp b/AI/Nullkiller/Analyzers/ArmyManager.cpp index 5c2a7f4a8..b366fa176 100644 --- a/AI/Nullkiller/Analyzers/ArmyManager.cpp +++ b/AI/Nullkiller/Analyzers/ArmyManager.cpp @@ -13,6 +13,7 @@ #include "../Engine/Nullkiller.h" #include "../../../CCallback.h" #include "../../../lib/mapObjects/MapObjects.h" +#include "../../../lib/IGameSettings.h" #include "../../../lib/GameConstants.h" namespace NKAI @@ -90,7 +91,7 @@ std::vector ArmyManager::getSortedSlots(const CCreatureSet * target, c { for(auto & i : armyPtr->Slots()) { - auto cre = dynamic_cast(i.second->type); + auto cre = dynamic_cast(i.second->getType()); auto & slotInfp = creToPower[cre]; slotInfp.creature = cre; @@ -144,7 +145,7 @@ std::vector ArmyManager::getBestArmy(const IBonusBearer * armyCarrier, for(auto & slot : sortedSlots) { - alignmentMap[slot.creature->getFaction()] += slot.power; + alignmentMap[slot.creature->getFactionID()] += slot.power; } std::set allowedFactions; @@ -152,16 +153,6 @@ std::vector ArmyManager::getBestArmy(const IBonusBearer * armyCarrier, uint64_t armyValue = 0; TemporaryArmy newArmyInstance; - auto bonusModifiers = armyCarrier->getBonuses(Selector::type()(BonusType::MORALE)); - - for(auto bonus : *bonusModifiers) - { - // army bonuses will change and object bonuses are temporary - if(bonus->source != BonusSource::ARMY && bonus->source != BonusSource::OBJECT_INSTANCE && bonus->source != BonusSource::OBJECT_TYPE) - { - newArmyInstance.addNewBonus(std::make_shared(*bonus)); - } - } while(allowedFactions.size() < alignmentMap.size()) { @@ -178,7 +169,7 @@ std::vector ArmyManager::getBestArmy(const IBonusBearer * armyCarrier, for(auto & slot : sortedSlots) { - if(vstd::contains(allowedFactions, slot.creature->getFaction())) + if(vstd::contains(allowedFactions, slot.creature->getFactionID())) { auto slotID = newArmyInstance.getSlotFor(slot.creature->getId()); @@ -197,16 +188,18 @@ std::vector ArmyManager::getBestArmy(const IBonusBearer * armyCarrier, auto morale = slot.second->moraleVal(); auto multiplier = 1.0f; - const float BadMoraleChance = 0.083f; - const float HighMoraleChance = 0.04f; + const auto & badMoraleDice = cb->getSettings().getVector(EGameSettings::COMBAT_BAD_MORALE_DICE); + const auto & highMoraleDice = cb->getSettings().getVector(EGameSettings::COMBAT_GOOD_MORALE_DICE); - if(morale < 0) + if(morale < 0 && !badMoraleDice.empty()) { - multiplier += morale * BadMoraleChance; + size_t diceIndex = std::min(badMoraleDice.size(), -morale) - 1; + multiplier -= 1.0 / badMoraleDice.at(diceIndex); } - else if(morale > 0) + else if(morale > 0 && !highMoraleDice.empty()) { - multiplier += morale * HighMoraleChance; + size_t diceIndex = std::min(highMoraleDice.size(), morale) - 1; + multiplier += 1.0 / highMoraleDice.at(diceIndex); } newValue += multiplier * slot.second->getPower(); diff --git a/AI/Nullkiller/Analyzers/BuildAnalyzer.cpp b/AI/Nullkiller/Analyzers/BuildAnalyzer.cpp index 4496aec3f..01dfa0a82 100644 --- a/AI/Nullkiller/Analyzers/BuildAnalyzer.cpp +++ b/AI/Nullkiller/Analyzers/BuildAnalyzer.cpp @@ -17,7 +17,7 @@ namespace NKAI void BuildAnalyzer::updateTownDwellings(TownDevelopmentInfo & developmentInfo) { - auto townInfo = developmentInfo.town->town; + auto townInfo = developmentInfo.town->getTown(); auto creatures = townInfo->creatures; auto buildings = townInfo->getAllBuildings(); @@ -31,7 +31,7 @@ void BuildAnalyzer::updateTownDwellings(TownDevelopmentInfo & developmentInfo) } } - for(int level = 0; level < developmentInfo.town->town->creatures.size(); level++) + for(int level = 0; level < developmentInfo.town->getTown()->creatures.size(); level++) { logAi->trace("Checking dwelling level %d", level); BuildingInfo nextToBuild = BuildingInfo(); @@ -39,7 +39,6 @@ void BuildAnalyzer::updateTownDwellings(TownDevelopmentInfo & developmentInfo) for(int upgradeIndex : {1, 0}) { BuildingID building = BuildingID(BuildingID::getDwellingFromLevel(level, upgradeIndex)); - if(!vstd::contains(buildings, building)) continue; // no such building in town @@ -73,16 +72,23 @@ void BuildAnalyzer::updateOtherBuildings(TownDevelopmentInfo & developmentInfo) if(developmentInfo.existingDwellings.size() >= 2 && ai->cb->getDate(Date::DAY_OF_WEEK) > boost::date_time::Friday) { - otherBuildings.push_back({BuildingID::CITADEL, BuildingID::CASTLE}); otherBuildings.push_back({BuildingID::HORDE_1}); otherBuildings.push_back({BuildingID::HORDE_2}); } + otherBuildings.push_back({ BuildingID::CITADEL, BuildingID::CASTLE }); + otherBuildings.push_back({ BuildingID::RESOURCE_SILO }); + otherBuildings.push_back({ BuildingID::SPECIAL_1 }); + otherBuildings.push_back({ BuildingID::SPECIAL_2 }); + otherBuildings.push_back({ BuildingID::SPECIAL_3 }); + otherBuildings.push_back({ BuildingID::SPECIAL_4 }); + otherBuildings.push_back({ BuildingID::MARKETPLACE }); + for(auto & buildingSet : otherBuildings) { for(auto & buildingID : buildingSet) { - if(!developmentInfo.town->hasBuilt(buildingID) && developmentInfo.town->town->buildings.count(buildingID)) + if(!developmentInfo.town->hasBuilt(buildingID) && developmentInfo.town->getTown()->buildings.count(buildingID)) { developmentInfo.addBuildingToBuild(getBuildingOrPrerequisite(developmentInfo.town, buildingID)); @@ -141,6 +147,8 @@ void BuildAnalyzer::update() auto towns = ai->cb->getTownsInfo(); + float economyDevelopmentCost = 0; + for(const CGTownInstance* town : towns) { logAi->trace("Checking town %s", town->getNameTranslated()); @@ -153,6 +161,11 @@ void BuildAnalyzer::update() requiredResources += developmentInfo.requiredResources; totalDevelopmentCost += developmentInfo.townDevelopmentCost; + for(auto building : developmentInfo.toBuild) + { + if (building.dailyIncome[EGameResID::GOLD] > 0) + economyDevelopmentCost += building.buildCostWithPrerequisites[EGameResID::GOLD]; + } armyCost += developmentInfo.armyCost; for(auto bi : developmentInfo.toBuild) @@ -171,15 +184,7 @@ void BuildAnalyzer::update() updateDailyIncome(); - if(ai->cb->getDate(Date::DAY) == 1) - { - goldPressure = 1; - } - else - { - goldPressure = ai->getLockedResources()[EGameResID::GOLD] / 5000.0f - + (float)armyCost[EGameResID::GOLD] / (1 + 2 * ai->getFreeGold() + (float)dailyIncome[EGameResID::GOLD] * 7.0f); - } + goldPressure = (ai->getLockedResources()[EGameResID::GOLD] + (float)armyCost[EGameResID::GOLD] + economyDevelopmentCost) / (1 + 2 * ai->getFreeGold() + (float)dailyIncome[EGameResID::GOLD] * 7.0f); logAi->trace("Gold pressure: %f", goldPressure); } @@ -198,7 +203,7 @@ BuildingInfo BuildAnalyzer::getBuildingOrPrerequisite( bool excludeDwellingDependencies) const { BuildingID building = toBuild; - auto townInfo = town->town; + auto townInfo = town->getTown(); const CBuilding * buildPtr = townInfo->buildings.at(building); const CCreature * creature = nullptr; @@ -237,6 +242,12 @@ BuildingInfo BuildAnalyzer::getBuildingOrPrerequisite( logAi->trace("checking %s", info.name); logAi->trace("buildInfo %s", info.toString()); + int highestFort = 0; + for (auto twn : ai->cb->getTownsInfo()) + { + highestFort = std::max(highestFort, (int)twn->fortLevel()); + } + if(!town->hasBuilt(building)) { auto canBuild = ai->cb->canBuildStructure(town, building); @@ -281,7 +292,15 @@ BuildingInfo BuildAnalyzer::getBuildingOrPrerequisite( prerequisite.baseCreatureID = info.baseCreatureID; prerequisite.prerequisitesCount++; prerequisite.armyCost = info.armyCost; - prerequisite.dailyIncome = info.dailyIncome; + bool haveSameOrBetterFort = false; + if (prerequisite.id == BuildingID::FORT && highestFort >= CGTownInstance::EFortLevel::FORT) + haveSameOrBetterFort = true; + if (prerequisite.id == BuildingID::CITADEL && highestFort >= CGTownInstance::EFortLevel::CITADEL) + haveSameOrBetterFort = true; + if (prerequisite.id == BuildingID::CASTLE && highestFort >= CGTownInstance::EFortLevel::CASTLE) + haveSameOrBetterFort = true; + if(!haveSameOrBetterFort) + prerequisite.dailyIncome = info.dailyIncome; return prerequisite; } @@ -327,7 +346,7 @@ bool BuildAnalyzer::hasAnyBuilding(int32_t alignment, BuildingID bid) const { for(auto tdi : developmentInfos) { - if(tdi.town->getFaction() == alignment && tdi.town->hasBuilt(bid)) + if(tdi.town->getFactionID() == alignment && tdi.town->hasBuilt(bid)) return true; } diff --git a/AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.cpp b/AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.cpp index cd9c4806d..792225b7d 100644 --- a/AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.cpp +++ b/AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.cpp @@ -89,7 +89,6 @@ void DangerHitMapAnalyzer::updateHitMap() heroes[hero->tempOwner][hero] = HeroRole::MAIN; } - if(obj->ID == Obj::TOWN) { auto town = dynamic_cast(obj); @@ -140,6 +139,7 @@ void DangerHitMapAnalyzer::updateHitMap() newThreat.hero = path.targetHero; newThreat.turn = path.turn(); + newThreat.threat = path.getHeroStrength() * (1 - path.movementCost() / 2.0); newThreat.danger = path.getHeroStrength(); if(newThreat.value() > node.maximumDanger.value()) @@ -316,8 +316,8 @@ uint64_t DangerHitMapAnalyzer::enemyCanKillOurHeroesAlongThePath(const AIPath & const auto& info = getTileThreat(tile); - return (info.fastestDanger.turn <= turn && !isSafeToVisit(path.targetHero, path.heroArmy, info.fastestDanger.danger)) - || (info.maximumDanger.turn <= turn && !isSafeToVisit(path.targetHero, path.heroArmy, info.maximumDanger.danger)); + return (info.fastestDanger.turn <= turn && !isSafeToVisit(path.targetHero, path.heroArmy, info.fastestDanger.danger, ai->settings->getSafeAttackRatio())) + || (info.maximumDanger.turn <= turn && !isSafeToVisit(path.targetHero, path.heroArmy, info.maximumDanger.danger, ai->settings->getSafeAttackRatio())); } const HitMapNode & DangerHitMapAnalyzer::getObjectThreat(const CGObjectInstance * obj) const diff --git a/AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.h b/AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.h index fc2890846..2bd39a2d8 100644 --- a/AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.h +++ b/AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.h @@ -22,6 +22,7 @@ struct HitMapInfo uint64_t danger; uint8_t turn; + float threat; HeroPtr hero; HitMapInfo() @@ -33,6 +34,7 @@ struct HitMapInfo { danger = 0; turn = 255; + threat = 0; hero = HeroPtr(); } diff --git a/AI/Nullkiller/Analyzers/HeroManager.cpp b/AI/Nullkiller/Analyzers/HeroManager.cpp index f64fb4811..be3526fda 100644 --- a/AI/Nullkiller/Analyzers/HeroManager.cpp +++ b/AI/Nullkiller/Analyzers/HeroManager.cpp @@ -11,7 +11,6 @@ #include "../StdInc.h" #include "../Engine/Nullkiller.h" #include "../../../lib/mapObjects/MapObjects.h" -#include "../../../lib/CHeroHandler.h" #include "../../../lib/IGameSettings.h" namespace NKAI @@ -71,7 +70,7 @@ float HeroManager::evaluateSecSkill(SecondarySkill skill, const CGHeroInstance * float HeroManager::evaluateSpeciality(const CGHeroInstance * hero) const { - auto heroSpecial = Selector::source(BonusSource::HERO_SPECIAL, BonusSourceID(hero->type->getId())); + auto heroSpecial = Selector::source(BonusSource::HERO_SPECIAL, BonusSourceID(hero->getHeroTypeID())); auto secondarySkillBonus = Selector::targetSourceType()(BonusSource::SECONDARY_SKILL); auto specialSecondarySkillBonuses = hero->getBonuses(heroSpecial.And(secondarySkillBonus)); auto secondarySkillBonuses = hero->getBonuses(Selector::sourceTypeSel(BonusSource::SECONDARY_SKILL)); @@ -96,7 +95,7 @@ float HeroManager::evaluateSpeciality(const CGHeroInstance * hero) const float HeroManager::evaluateFightingStrength(const CGHeroInstance * hero) const { - return evaluateSpeciality(hero) + wariorSkillsScores.evaluateSecSkills(hero) + hero->level * 1.5f; + return evaluateSpeciality(hero) + wariorSkillsScores.evaluateSecSkills(hero) + hero->getBasePrimarySkillValue(PrimarySkill::ATTACK) + hero->getBasePrimarySkillValue(PrimarySkill::DEFENSE) + hero->getBasePrimarySkillValue(PrimarySkill::SPELL_POWER) + hero->getBasePrimarySkillValue(PrimarySkill::KNOWLEDGE); } void HeroManager::update() @@ -109,7 +108,7 @@ void HeroManager::update() for(auto & hero : myHeroes) { scores[hero] = evaluateFightingStrength(hero); - knownFightingStrength[hero->id] = hero->getFightingStrength(); + knownFightingStrength[hero->id] = hero->getHeroStrength(); } auto scoreSort = [&](const CGHeroInstance * h1, const CGHeroInstance * h2) -> bool @@ -148,7 +147,10 @@ void HeroManager::update() HeroRole HeroManager::getHeroRole(const HeroPtr & hero) const { - return heroRoles.at(hero); + if (heroRoles.find(hero) != heroRoles.end()) + return heroRoles.at(hero); + else + return HeroRole::SCOUT; } const std::map & HeroManager::getHeroRoles() const @@ -189,13 +191,11 @@ float HeroManager::evaluateHero(const CGHeroInstance * hero) const return evaluateFightingStrength(hero); } -bool HeroManager::heroCapReached() const +bool HeroManager::heroCapReached(bool includeGarrisoned) const { - const bool includeGarnisoned = true; - int heroCount = cb->getHeroCount(ai->playerID, includeGarnisoned); + int heroCount = cb->getHeroCount(ai->playerID, includeGarrisoned); - return heroCount >= ALLOWED_ROAMING_HEROES - || heroCount >= ai->settings->getMaxRoamingHeroes() + return heroCount >= ai->settings->getMaxRoamingHeroes() || heroCount >= cb->getSettings().getInteger(EGameSettings::HEROES_PER_PLAYER_ON_MAP_CAP) || heroCount >= cb->getSettings().getInteger(EGameSettings::HEROES_PER_PLAYER_TOTAL_CAP); } @@ -205,7 +205,7 @@ float HeroManager::getFightingStrengthCached(const CGHeroInstance * hero) const auto cached = knownFightingStrength.find(hero->id); //FIXME: fallback to hero->getFightingStrength() is VERY slow on higher difficulties (no object graph? map reveal?) - return cached != knownFightingStrength.end() ? cached->second : hero->getFightingStrength(); + return cached != knownFightingStrength.end() ? cached->second : hero->getHeroStrength(); } float HeroManager::getMagicStrength(const CGHeroInstance * hero) const @@ -282,7 +282,7 @@ const CGHeroInstance * HeroManager::findHeroWithGrail() const return nullptr; } -const CGHeroInstance * HeroManager::findWeakHeroToDismiss(uint64_t armyLimit) const +const CGHeroInstance * HeroManager::findWeakHeroToDismiss(uint64_t armyLimit, const CGTownInstance* townToSpare) const { const CGHeroInstance * weakestHero = nullptr; auto myHeroes = ai->cb->getHeroesInfo(); @@ -293,12 +293,13 @@ const CGHeroInstance * HeroManager::findWeakHeroToDismiss(uint64_t armyLimit) co || existingHero->getArmyStrength() >armyLimit || getHeroRole(existingHero) == HeroRole::MAIN || existingHero->movementPointsRemaining() + || (townToSpare != nullptr && existingHero->visitedTown == townToSpare) || existingHero->artifactsWorn.size() > (existingHero->hasSpellbook() ? 2 : 1)) { continue; } - if(!weakestHero || weakestHero->getFightingStrength() > existingHero->getFightingStrength()) + if(!weakestHero || weakestHero->getHeroStrength() > existingHero->getHeroStrength()) { weakestHero = existingHero; } diff --git a/AI/Nullkiller/Analyzers/HeroManager.h b/AI/Nullkiller/Analyzers/HeroManager.h index 675357626..383f3c13a 100644 --- a/AI/Nullkiller/Analyzers/HeroManager.h +++ b/AI/Nullkiller/Analyzers/HeroManager.h @@ -56,9 +56,9 @@ public: float evaluateSecSkill(SecondarySkill skill, const CGHeroInstance * hero) const; float evaluateHero(const CGHeroInstance * hero) const; bool canRecruitHero(const CGTownInstance * t = nullptr) const; - bool heroCapReached() const; + bool heroCapReached(bool includeGarrisoned = true) const; const CGHeroInstance * findHeroWithGrail() const; - const CGHeroInstance * findWeakHeroToDismiss(uint64_t armyLimit) const; + const CGHeroInstance * findWeakHeroToDismiss(uint64_t armyLimit, const CGTownInstance * townToSpare = nullptr) const; float getMagicStrength(const CGHeroInstance * hero) const; float getFightingStrengthCached(const CGHeroInstance * hero) const; diff --git a/AI/Nullkiller/Analyzers/ObjectClusterizer.cpp b/AI/Nullkiller/Analyzers/ObjectClusterizer.cpp index 5c628f288..5b9e0a12b 100644 --- a/AI/Nullkiller/Analyzers/ObjectClusterizer.cpp +++ b/AI/Nullkiller/Analyzers/ObjectClusterizer.cpp @@ -97,9 +97,10 @@ std::optional ObjectClusterizer::getBlocker(const AIPa { auto guardPos = ai->cb->getGuardingCreaturePosition(node.coord); - blockers = ai->cb->getVisitableObjs(node.coord); + if (ai->cb->isVisible(node.coord)) + blockers = ai->cb->getVisitableObjs(node.coord); - if(guardPos.valid()) + if(guardPos.valid() && ai->cb->isVisible(guardPos)) { auto guard = ai->cb->getTopObj(ai->cb->getGuardingCreaturePosition(node.coord)); @@ -474,9 +475,11 @@ void ObjectClusterizer::clusterizeObject( heroesProcessed.insert(path.targetHero); - float priority = priorityEvaluator->evaluate(Goals::sptr(Goals::ExecuteHeroChain(path, obj))); + float priority = priorityEvaluator->evaluate(Goals::sptr(Goals::ExecuteHeroChain(path, obj)), PriorityEvaluator::PriorityTier::HUNTER_GATHER); - if(priority < MIN_PRIORITY) + if(ai->settings->isUseFuzzy() && priority < MIN_PRIORITY) + continue; + else if (priority <= 0) continue; ClusterMap::accessor cluster; @@ -495,9 +498,11 @@ void ObjectClusterizer::clusterizeObject( heroesProcessed.insert(path.targetHero); - float priority = priorityEvaluator->evaluate(Goals::sptr(Goals::ExecuteHeroChain(path, obj))); + float priority = priorityEvaluator->evaluate(Goals::sptr(Goals::ExecuteHeroChain(path, obj)), PriorityEvaluator::PriorityTier::HUNTER_GATHER); - if(priority < MIN_PRIORITY) + if (ai->settings->isUseFuzzy() && priority < MIN_PRIORITY) + continue; + else if (priority <= 0) continue; bool interestingObject = path.turn() <= 2 || priority > 0.5f; diff --git a/AI/Nullkiller/Behaviors/BuildingBehavior.cpp b/AI/Nullkiller/Behaviors/BuildingBehavior.cpp index 2cdc2ead3..50f800913 100644 --- a/AI/Nullkiller/Behaviors/BuildingBehavior.cpp +++ b/AI/Nullkiller/Behaviors/BuildingBehavior.cpp @@ -49,26 +49,49 @@ Goals::TGoalVec BuildingBehavior::decompose(const Nullkiller * ai) const auto & developmentInfos = ai->buildAnalyzer->getDevelopmentInfo(); auto isGoldPressureLow = !ai->buildAnalyzer->isGoldPressureHigh(); + ai->dangerHitMap->updateHitMap(); + for(auto & developmentInfo : developmentInfos) { - for(auto & buildingInfo : developmentInfo.toBuild) + bool emergencyDefense = false; + uint8_t closestThreat = std::numeric_limits::max(); + for (auto threat : ai->dangerHitMap->getTownThreats(developmentInfo.town)) { - if(isGoldPressureLow || buildingInfo.dailyIncome[EGameResID::GOLD] > 0) + closestThreat = std::min(closestThreat, threat.turn); + } + for (auto& buildingInfo : developmentInfo.toBuild) + { + if (closestThreat <= 1 && developmentInfo.town->fortLevel() < CGTownInstance::EFortLevel::CASTLE && !buildingInfo.notEnoughRes) { - if(buildingInfo.notEnoughRes) + if (buildingInfo.id == BuildingID::CITADEL || buildingInfo.id == BuildingID::CASTLE) { - if(ai->getLockedResources().canAfford(buildingInfo.buildCost)) - continue; - - Composition composition; - - composition.addNext(BuildThis(buildingInfo, developmentInfo)); - composition.addNext(SaveResources(buildingInfo.buildCost)); - - tasks.push_back(sptr(composition)); - } - else tasks.push_back(sptr(BuildThis(buildingInfo, developmentInfo))); + emergencyDefense = true; + } + } + } + if (!emergencyDefense) + { + for (auto& buildingInfo : developmentInfo.toBuild) + { + if (isGoldPressureLow || buildingInfo.dailyIncome[EGameResID::GOLD] > 0) + { + if (buildingInfo.notEnoughRes) + { + if (ai->getLockedResources().canAfford(buildingInfo.buildCost)) + continue; + + Composition composition; + + composition.addNext(BuildThis(buildingInfo, developmentInfo)); + composition.addNext(SaveResources(buildingInfo.buildCost)); + tasks.push_back(sptr(composition)); + } + else + { + tasks.push_back(sptr(BuildThis(buildingInfo, developmentInfo))); + } + } } } } diff --git a/AI/Nullkiller/Behaviors/BuyArmyBehavior.cpp b/AI/Nullkiller/Behaviors/BuyArmyBehavior.cpp index d53adc023..738196e2d 100644 --- a/AI/Nullkiller/Behaviors/BuyArmyBehavior.cpp +++ b/AI/Nullkiller/Behaviors/BuyArmyBehavior.cpp @@ -28,9 +28,6 @@ Goals::TGoalVec BuyArmyBehavior::decompose(const Nullkiller * ai) const { Goals::TGoalVec tasks; - if(ai->cb->getDate(Date::DAY) == 1) - return tasks; - auto heroes = cb->getHeroesInfo(); if(heroes.empty()) @@ -38,19 +35,23 @@ Goals::TGoalVec BuyArmyBehavior::decompose(const Nullkiller * ai) const return tasks; } + ai->dangerHitMap->updateHitMap(); + for(auto town : cb->getTownsInfo()) { + uint8_t closestThreat = ai->dangerHitMap->getTileThreat(town->visitablePos()).fastestDanger.turn; + + if (closestThreat >=2 && ai->buildAnalyzer->isGoldPressureHigh() && !town->hasBuilt(BuildingID::CITY_HALL) && cb->canBuildStructure(town, BuildingID::CITY_HALL) != EBuildingState::FORBIDDEN) + { + return tasks; + } + auto townArmyAvailableToBuy = ai->armyManager->getArmyAvailableToBuyAsCCreatureSet( town, ai->getFreeResources()); for(const CGHeroInstance * targetHero : heroes) { - if(ai->buildAnalyzer->isGoldPressureHigh() && !town->hasBuilt(BuildingID::CITY_HALL)) - { - continue; - } - if(ai->heroManager->getHeroRole(targetHero) == HeroRole::MAIN) { auto reinforcement = ai->armyManager->howManyReinforcementsCanGet( diff --git a/AI/Nullkiller/Behaviors/CaptureObjectsBehavior.cpp b/AI/Nullkiller/Behaviors/CaptureObjectsBehavior.cpp index 576dedfeb..38e71b675 100644 --- a/AI/Nullkiller/Behaviors/CaptureObjectsBehavior.cpp +++ b/AI/Nullkiller/Behaviors/CaptureObjectsBehavior.cpp @@ -68,14 +68,6 @@ Goals::TGoalVec CaptureObjectsBehavior::getVisitGoals( logAi->trace("Path found %s", path.toString()); #endif - if(nullkiller->dangerHitMap->enemyCanKillOurHeroesAlongThePath(path)) - { -#if NKAI_TRACE_LEVEL >= 2 - logAi->trace("Ignore path. Target hero can be killed by enemy. Our power %lld", path.getHeroStrength()); -#endif - continue; - } - if(objToVisit && !force && !shouldVisit(nullkiller, path.targetHero, objToVisit)) { #if NKAI_TRACE_LEVEL >= 2 @@ -87,6 +79,9 @@ Goals::TGoalVec CaptureObjectsBehavior::getVisitGoals( auto hero = path.targetHero; auto danger = path.getTotalDanger(); + if (hero->getOwner() != nullkiller->playerID) + continue; + if(nullkiller->heroManager->getHeroRole(hero) == HeroRole::SCOUT && (path.getTotalDanger() == 0 || path.turn() > 0) && path.exchangeCount > 1) @@ -119,7 +114,7 @@ Goals::TGoalVec CaptureObjectsBehavior::getVisitGoals( continue; } - auto isSafe = isSafeToVisit(hero, path.heroArmy, danger); + auto isSafe = isSafeToVisit(hero, path.heroArmy, danger, nullkiller->settings->getSafeAttackRatio()); #if NKAI_TRACE_LEVEL >= 2 logAi->trace( diff --git a/AI/Nullkiller/Behaviors/DefenceBehavior.cpp b/AI/Nullkiller/Behaviors/DefenceBehavior.cpp index e23efcb1c..86bd7e77c 100644 --- a/AI/Nullkiller/Behaviors/DefenceBehavior.cpp +++ b/AI/Nullkiller/Behaviors/DefenceBehavior.cpp @@ -41,6 +41,9 @@ Goals::TGoalVec DefenceBehavior::decompose(const Nullkiller * ai) const for(auto town : ai->cb->getTownsInfo()) { evaluateDefence(tasks, town, ai); + //Let's do only one defence-task per pass since otherwise it can try to hire the same hero twice + if (!tasks.empty()) + break; } return tasks; @@ -130,7 +133,7 @@ bool handleGarrisonHeroFromPreviousTurn(const CGTownInstance * town, Goals::TGoa tasks.push_back(Goals::sptr(Goals::ExchangeSwapTownHeroes(town, nullptr).setpriority(5))); - return true; + return false; } else if(ai->heroManager->getHeroRole(town->garrisonHero.get()) == HeroRole::MAIN) { @@ -141,7 +144,7 @@ bool handleGarrisonHeroFromPreviousTurn(const CGTownInstance * town, Goals::TGoa { tasks.push_back(Goals::sptr(Goals::DismissHero(heroToDismiss).setpriority(5))); - return true; + return false; } } } @@ -158,11 +161,10 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta threats.push_back(threatNode.fastestDanger); // no guarantee that fastest danger will be there - if(town->garrisonHero && handleGarrisonHeroFromPreviousTurn(town, tasks, ai)) + if (town->garrisonHero && handleGarrisonHeroFromPreviousTurn(town, tasks, ai)) { return; } - if(!threatNode.fastestDanger.hero) { logAi->trace("No threat found for town %s", town->getNameTranslated()); @@ -250,6 +252,16 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta continue; } + if (!path.targetHero->canBeMergedWith(*town)) + { +#if NKAI_TRACE_LEVEL >= 1 + logAi->trace("Can't merge armies of hero %s and town %s", + path.targetHero->getObjectName(), + town->getObjectName()); +#endif + continue; + } + if(path.targetHero == town->visitingHero.get() && path.exchangeCount == 1) { #if NKAI_TRACE_LEVEL >= 1 @@ -261,6 +273,7 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta // dismiss creatures we are not able to pick to be able to hide in garrison if(town->garrisonHero || town->getUpperArmy()->stacksCount() == 0 + || path.targetHero->canBeMergedWith(*town) || (town->getUpperArmy()->getArmyStrength() < 500 && town->fortLevel() >= CGTownInstance::CITADEL)) { tasks.push_back( @@ -292,7 +305,7 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta continue; } - if(threat.turn == 0 || (path.turn() <= threat.turn && path.getHeroStrength() * SAFE_ATTACK_CONSTANT >= threat.danger)) + if(threat.turn == 0 || (path.turn() <= threat.turn && path.getHeroStrength() * ai->settings->getSafeAttackRatio() >= threat.danger)) { if(ai->arePathHeroesLocked(path)) { @@ -343,23 +356,14 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta } else if(town->visitingHero && path.targetHero != town->visitingHero && !path.containsHero(town->visitingHero)) { - if(town->garrisonHero) + if(town->garrisonHero && town->garrisonHero != path.targetHero) { - if(ai->heroManager->getHeroRole(town->visitingHero.get()) == HeroRole::SCOUT - && town->visitingHero->getArmyStrength() < path.heroArmy->getArmyStrength() / 20) - { - if(path.turn() == 0) - sequence.push_back(sptr(DismissHero(town->visitingHero.get()))); - } - else - { #if NKAI_TRACE_LEVEL >= 1 - logAi->trace("Cancel moving %s to defend town %s as the town has garrison hero", - path.targetHero->getObjectName(), - town->getObjectName()); + logAi->trace("Cancel moving %s to defend town %s as the town has garrison hero", + path.targetHero->getObjectName(), + town->getObjectName()); #endif - continue; - } + continue; } else if(path.turn() == 0) { @@ -405,6 +409,9 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta void DefenceBehavior::evaluateRecruitingHero(Goals::TGoalVec & tasks, const HitMapInfo & threat, const CGTownInstance * town, const Nullkiller * ai) const { + if (threat.turn > 0 || town->garrisonHero || town->visitingHero) + return; + if(town->hasBuilt(BuildingID::TAVERN) && ai->cb->getResourceAmount(EGameResID::GOLD) > GameConstants::HERO_GOLD_COST) { @@ -451,7 +458,7 @@ void DefenceBehavior::evaluateRecruitingHero(Goals::TGoalVec & tasks, const HitM } else if(ai->heroManager->heroCapReached()) { - heroToDismiss = ai->heroManager->findWeakHeroToDismiss(hero->getArmyStrength()); + heroToDismiss = ai->heroManager->findWeakHeroToDismiss(hero->getArmyStrength(), town); if(!heroToDismiss) continue; diff --git a/AI/Nullkiller/Behaviors/ExplorationBehavior.cpp b/AI/Nullkiller/Behaviors/ExplorationBehavior.cpp index 4a210d2c0..b9c6e568d 100644 --- a/AI/Nullkiller/Behaviors/ExplorationBehavior.cpp +++ b/AI/Nullkiller/Behaviors/ExplorationBehavior.cpp @@ -33,48 +33,32 @@ Goals::TGoalVec ExplorationBehavior::decompose(const Nullkiller * ai) const { Goals::TGoalVec tasks; - for(auto obj : ai->memory->visitableObjs) + for (auto obj : ai->memory->visitableObjs) { - if(!vstd::contains(ai->memory->alreadyVisited, obj)) + switch (obj->ID.num) { - switch(obj->ID.num) - { case Obj::REDWOOD_OBSERVATORY: case Obj::PILLAR_OF_FIRE: - tasks.push_back(sptr(Composition().addNext(ExplorationPoint(obj->visitablePos(), 200)).addNext(CaptureObject(obj)))); + { + auto rObj = dynamic_cast(obj); + if (!rObj->wasScouted(ai->playerID)) + tasks.push_back(sptr(Composition().addNext(ExplorationPoint(obj->visitablePos(), 200)).addNext(CaptureObject(obj)))); break; + } case Obj::MONOLITH_ONE_WAY_ENTRANCE: case Obj::MONOLITH_TWO_WAY: case Obj::SUBTERRANEAN_GATE: case Obj::WHIRLPOOL: - auto tObj = dynamic_cast(obj); - if(TeleportChannel::IMPASSABLE != ai->memory->knownTeleportChannels[tObj->channel]->passability) - { - tasks.push_back(sptr(Composition().addNext(ExplorationPoint(obj->visitablePos(), 50)).addNext(CaptureObject(obj)))); - } - break; - } - } - else - { - switch(obj->ID.num) { - case Obj::MONOLITH_TWO_WAY: - case Obj::SUBTERRANEAN_GATE: - case Obj::WHIRLPOOL: - auto tObj = dynamic_cast(obj); - if(TeleportChannel::IMPASSABLE == ai->memory->knownTeleportChannels[tObj->channel]->passability) - break; - for(auto exit : ai->memory->knownTeleportChannels[tObj->channel]->exits) + auto tObj = dynamic_cast(obj); + for (auto exit : cb->getTeleportChannelExits(tObj->channel)) { - if(!cb->getObj(exit)) - { - // Always attempt to visit two-way teleports if one of channel exits is not visible - tasks.push_back(sptr(Composition().addNext(ExplorationPoint(obj->visitablePos(), 50)).addNext(CaptureObject(obj)))); - break; + if (exit != tObj->id) + { + if (!cb->isVisible(cb->getObjInstance(exit))) + tasks.push_back(sptr(Composition().addNext(ExplorationPoint(obj->visitablePos(), 50)).addNext(CaptureObject(obj)))); } } - break; } } } diff --git a/AI/Nullkiller/Behaviors/GatherArmyBehavior.cpp b/AI/Nullkiller/Behaviors/GatherArmyBehavior.cpp index b9a675837..b6b0811cb 100644 --- a/AI/Nullkiller/Behaviors/GatherArmyBehavior.cpp +++ b/AI/Nullkiller/Behaviors/GatherArmyBehavior.cpp @@ -81,6 +81,9 @@ Goals::TGoalVec GatherArmyBehavior::deliverArmyToHero(const Nullkiller * ai, con logAi->trace("Path found %s, %s, %lld", path.toString(), path.targetHero->getObjectName(), path.heroArmy->getArmyStrength()); #endif + if (path.targetHero->getOwner() != ai->playerID) + continue; + if(path.containsHero(hero)) { #if NKAI_TRACE_LEVEL >= 2 @@ -89,14 +92,6 @@ Goals::TGoalVec GatherArmyBehavior::deliverArmyToHero(const Nullkiller * ai, con continue; } - if(path.turn() > 0 && ai->dangerHitMap->enemyCanKillOurHeroesAlongThePath(path)) - { -#if NKAI_TRACE_LEVEL >= 2 - logAi->trace("Ignore path. Target hero can be killed by enemy. Our power %lld", path.heroArmy->getArmyStrength()); -#endif - continue; - } - if(ai->arePathHeroesLocked(path)) { #if NKAI_TRACE_LEVEL >= 2 @@ -150,7 +145,7 @@ Goals::TGoalVec GatherArmyBehavior::deliverArmyToHero(const Nullkiller * ai, con } auto danger = path.getTotalDanger(); - auto isSafe = isSafeToVisit(hero, path.heroArmy, danger); + auto isSafe = isSafeToVisit(hero, path.heroArmy, danger, ai->settings->getSafeAttackRatio()); #if NKAI_TRACE_LEVEL >= 2 logAi->trace( @@ -292,17 +287,6 @@ Goals::TGoalVec GatherArmyBehavior::upgradeArmy(const Nullkiller * ai, const CGT continue; } - auto heroRole = ai->heroManager->getHeroRole(path.targetHero); - - if(heroRole == HeroRole::SCOUT - && ai->dangerHitMap->enemyCanKillOurHeroesAlongThePath(path)) - { -#if NKAI_TRACE_LEVEL >= 2 - logAi->trace("Ignore path. Target hero can be killed by enemy. Our power %lld", path.heroArmy->getArmyStrength()); -#endif - continue; - } - auto upgrade = ai->armyManager->calculateCreaturesUpgrade(path.heroArmy, upgrader, availableResources); if(!upgrader->garrisonHero @@ -320,14 +304,6 @@ Goals::TGoalVec GatherArmyBehavior::upgradeArmy(const Nullkiller * ai, const CGT armyToGetOrBuy.upgradeValue -= path.heroArmy->getArmyStrength(); - armyToGetOrBuy.addArmyToBuy( - ai->armyManager->toSlotInfo( - ai->armyManager->getArmyAvailableToBuy( - path.heroArmy, - upgrader, - ai->getFreeResources(), - path.turn()))); - upgrade.upgradeValue += armyToGetOrBuy.upgradeValue; upgrade.upgradeCost += armyToGetOrBuy.upgradeCost; vstd::concatenate(upgrade.resultingArmy, armyToGetOrBuy.resultingArmy); @@ -339,8 +315,7 @@ Goals::TGoalVec GatherArmyBehavior::upgradeArmy(const Nullkiller * ai, const CGT { for(auto hero : cb->getAvailableHeroes(upgrader)) { - auto scoutReinforcement = ai->armyManager->howManyReinforcementsCanBuy(hero, upgrader) - + ai->armyManager->howManyReinforcementsCanGet(hero, upgrader); + auto scoutReinforcement = ai->armyManager->howManyReinforcementsCanGet(hero, upgrader); if(scoutReinforcement >= armyToGetOrBuy.upgradeValue && ai->getFreeGold() >20000 @@ -366,7 +341,7 @@ Goals::TGoalVec GatherArmyBehavior::upgradeArmy(const Nullkiller * ai, const CGT auto danger = path.getTotalDanger(); - auto isSafe = isSafeToVisit(path.targetHero, path.heroArmy, danger); + auto isSafe = isSafeToVisit(path.targetHero, path.heroArmy, danger, ai->settings->getSafeAttackRatio()); #if NKAI_TRACE_LEVEL >= 2 logAi->trace( diff --git a/AI/Nullkiller/Behaviors/RecruitHeroBehavior.cpp b/AI/Nullkiller/Behaviors/RecruitHeroBehavior.cpp index f086b62cd..16c19ed62 100644 --- a/AI/Nullkiller/Behaviors/RecruitHeroBehavior.cpp +++ b/AI/Nullkiller/Behaviors/RecruitHeroBehavior.cpp @@ -31,9 +31,11 @@ Goals::TGoalVec RecruitHeroBehavior::decompose(const Nullkiller * ai) const auto ourHeroes = ai->heroManager->getHeroRoles(); auto minScoreToHireMain = std::numeric_limits::max(); + int currentArmyValue = 0; for(auto hero : ourHeroes) { + currentArmyValue += hero.first->getArmyCost(); if(hero.second != HeroRole::MAIN) continue; @@ -45,51 +47,88 @@ Goals::TGoalVec RecruitHeroBehavior::decompose(const Nullkiller * ai) const minScoreToHireMain = newScore; } } + // If we don't have any heros we might want to lower our expectations. + if (ourHeroes.empty()) + minScoreToHireMain = 0; + const CGHeroInstance* bestHeroToHire = nullptr; + const CGTownInstance* bestTownToHireFrom = nullptr; + float bestScore = 0; + bool haveCapitol = false; + + ai->dangerHitMap->updateHitMap(); + int treasureSourcesCount = 0; + for(auto town : towns) { + uint8_t closestThreat = UINT8_MAX; + for (auto threat : ai->dangerHitMap->getTownThreats(town)) + { + closestThreat = std::min(closestThreat, threat.turn); + } + //Don't hire a hero where there already is one present + if (town->visitingHero && town->garrisonHero) + continue; + float visitability = 0; + for (auto checkHero : ourHeroes) + { + if (ai->dangerHitMap->getClosestTown(checkHero.first.get()->visitablePos()) == town) + visitability++; + } if(ai->heroManager->canRecruitHero(town)) { auto availableHeroes = ai->cb->getAvailableHeroes(town); - - for(auto hero : availableHeroes) + + for (auto obj : ai->objectClusterizer->getNearbyObjects()) { - auto score = ai->heroManager->evaluateHero(hero); - - if(score > minScoreToHireMain) - { - tasks.push_back(Goals::sptr(Goals::RecruitHero(town, hero).setpriority(200))); - break; - } - } - - int treasureSourcesCount = 0; - - for(auto obj : ai->objectClusterizer->getNearbyObjects()) - { - if((obj->ID == Obj::RESOURCE) + if ((obj->ID == Obj::RESOURCE) || obj->ID == Obj::TREASURE_CHEST || obj->ID == Obj::CAMPFIRE || isWeeklyRevisitable(ai, obj) - || obj->ID ==Obj::ARTIFACT) + || obj->ID == Obj::ARTIFACT) { auto tile = obj->visitablePos(); auto closestTown = ai->dangerHitMap->getClosestTown(tile); - if(town == closestTown) + if (town == closestTown) treasureSourcesCount++; } } - if(treasureSourcesCount < 5 && (town->garrisonHero || town->getUpperArmy()->getArmyStrength() < 10000)) - continue; - - if(ai->cb->getHeroesInfo().size() < ai->cb->getTownsInfo().size() + 1 - || (ai->getFreeResources()[EGameResID::GOLD] > 10000 && !ai->buildAnalyzer->isGoldPressureHigh())) + for(auto hero : availableHeroes) { - tasks.push_back(Goals::sptr(Goals::RecruitHero(town).setpriority(3))); + auto score = ai->heroManager->evaluateHero(hero); + if(score > minScoreToHireMain) + { + score *= score / minScoreToHireMain; + } + score *= (hero->getArmyCost() + currentArmyValue); + if (hero->getFactionID() == town->getFactionID()) + score *= 1.5; + if (vstd::isAlmostZero(visitability)) + score *= 30 * town->getTownLevel(); + else + score *= town->getTownLevel() / visitability; + if (score > bestScore) + { + bestScore = score; + bestHeroToHire = hero; + bestTownToHireFrom = town; + } } } + if (town->hasCapitol()) + haveCapitol = true; + } + if (bestHeroToHire && bestTownToHireFrom) + { + if (ai->cb->getHeroesInfo().size() == 0 + || treasureSourcesCount > ai->cb->getHeroesInfo().size() * 5 + || (ai->getFreeResources()[EGameResID::GOLD] > 10000 && !ai->buildAnalyzer->isGoldPressureHigh() && haveCapitol) + || (ai->getFreeResources()[EGameResID::GOLD] > 30000 && !ai->buildAnalyzer->isGoldPressureHigh())) + { + tasks.push_back(Goals::sptr(Goals::RecruitHero(bestTownToHireFrom, bestHeroToHire).setpriority((float)3 / (ourHeroes.size() + 1)))); + } } return tasks; diff --git a/AI/Nullkiller/Behaviors/StayAtTownBehavior.cpp b/AI/Nullkiller/Behaviors/StayAtTownBehavior.cpp index 595830a66..1b2e0a04b 100644 --- a/AI/Nullkiller/Behaviors/StayAtTownBehavior.cpp +++ b/AI/Nullkiller/Behaviors/StayAtTownBehavior.cpp @@ -39,9 +39,6 @@ Goals::TGoalVec StayAtTownBehavior::decompose(const Nullkiller * ai) const for(auto town : towns) { - if(!town->hasBuilt(BuildingID::MAGES_GUILD_1)) - continue; - ai->pathfinder->calculatePathInfo(paths, town->visitablePos()); for(auto & path : paths) @@ -49,14 +46,8 @@ Goals::TGoalVec StayAtTownBehavior::decompose(const Nullkiller * ai) const if(town->visitingHero && town->visitingHero.get() != path.targetHero) continue; - if(!path.targetHero->hasSpellbook() || path.targetHero->mana >= 0.75f * path.targetHero->manaLimit()) - continue; - - if(path.turn() == 0 && !path.getFirstBlockedAction() && path.exchangeCount <= 1) + if(!path.getFirstBlockedAction() && path.exchangeCount <= 1) { - if(path.targetHero->mana == path.targetHero->manaLimit()) - continue; - Composition stayAtTown; stayAtTown.addNextSequence({ diff --git a/AI/Nullkiller/Engine/FuzzyEngines.cpp b/AI/Nullkiller/Engine/FuzzyEngines.cpp index 7659f1352..a05119d90 100644 --- a/AI/Nullkiller/Engine/FuzzyEngines.cpp +++ b/AI/Nullkiller/Engine/FuzzyEngines.cpp @@ -17,8 +17,7 @@ namespace NKAI { -#define MIN_AI_STRENGTH (0.5f) //lower when combat AI gets smarter -#define UNGUARDED_OBJECT (100.0f) //we consider unguarded objects 100 times weaker than us +constexpr float MIN_AI_STRENGTH = 0.5f; //lower when combat AI gets smarter engineBase::engineBase() { diff --git a/AI/Nullkiller/Engine/FuzzyHelper.cpp b/AI/Nullkiller/Engine/FuzzyHelper.cpp index 3907d5df3..a5fcd6a1a 100644 --- a/AI/Nullkiller/Engine/FuzzyHelper.cpp +++ b/AI/Nullkiller/Engine/FuzzyHelper.cpp @@ -52,6 +52,15 @@ ui64 FuzzyHelper::evaluateDanger(const int3 & tile, const CGHeroInstance * visit { objectDanger += evaluateDanger(hero->visitedTown.get()); } + objectDanger *= ai->heroManager->getFightingStrengthCached(hero); + } + if (objWithID(dangerousObject)) + { + auto town = dynamic_cast(dangerousObject); + auto hero = town->garrisonHero; + + if (hero) + objectDanger *= ai->heroManager->getFightingStrengthCached(hero); } if(objectDanger) @@ -117,10 +126,10 @@ ui64 FuzzyHelper::evaluateDanger(const CGObjectInstance * obj) { auto fortLevel = town->fortLevel(); - if(fortLevel == CGTownInstance::EFortLevel::CASTLE) - danger += 10000; + if (fortLevel == CGTownInstance::EFortLevel::CASTLE) + danger = std::max(danger * 2, danger + 10000); else if(fortLevel == CGTownInstance::EFortLevel::CITADEL) - danger += 4000; + danger = std::max(ui64(danger * 1.4), danger + 4000); } return danger; diff --git a/AI/Nullkiller/Engine/Nullkiller.cpp b/AI/Nullkiller/Engine/Nullkiller.cpp index fa15363f6..c21f1c57d 100644 --- a/AI/Nullkiller/Engine/Nullkiller.cpp +++ b/AI/Nullkiller/Engine/Nullkiller.cpp @@ -34,13 +34,12 @@ using namespace Goals; std::unique_ptr Nullkiller::baseGraph; Nullkiller::Nullkiller() - :activeHero(nullptr), scanDepth(ScanDepth::MAIN_FULL), useHeroChain(true) + : activeHero(nullptr) + , scanDepth(ScanDepth::MAIN_FULL) + , useHeroChain(true) + , memory(std::make_unique()) { - memory = std::make_unique(); - settings = std::make_unique(); - useObjectGraph = settings->isObjectGraphAllowed(); - openMap = settings->isOpenMap() || useObjectGraph; } bool canUseOpenMap(std::shared_ptr cb, PlayerColor playerID) @@ -62,17 +61,23 @@ bool canUseOpenMap(std::shared_ptr cb, PlayerColor playerID) return false; } - return cb->getStartInfo()->difficulty >= 3; + return true; } void Nullkiller::init(std::shared_ptr cb, AIGateway * gateway) { this->cb = cb; this->gateway = gateway; - - playerID = gateway->playerID; + this->playerID = gateway->playerID; - if(openMap && !canUseOpenMap(cb, playerID)) + settings = std::make_unique(cb->getStartInfo()->difficulty); + + if(canUseOpenMap(cb, playerID)) + { + useObjectGraph = settings->isObjectGraphAllowed(); + openMap = settings->isOpenMap() || useObjectGraph; + } + else { useObjectGraph = false; openMap = false; @@ -122,11 +127,14 @@ void TaskPlan::merge(TSubgoal task) { TGoalVec blockers; + if (task->asTask()->priority <= 0) + return; + for(auto & item : tasks) { for(auto objid : item.affectedObjects) { - if(task == item.task || task->asTask()->isObjectAffected(objid)) + if(task == item.task || task->asTask()->isObjectAffected(objid) || (task->asTask()->getHero() != nullptr && task->asTask()->getHero() == item.task->asTask()->getHero())) { if(item.task->asTask()->priority >= task->asTask()->priority) return; @@ -166,20 +174,19 @@ Goals::TTask Nullkiller::choseBestTask(Goals::TGoalVec & tasks) const return taskptr(*bestTask); } -Goals::TTaskVec Nullkiller::buildPlan(TGoalVec & tasks) const +Goals::TTaskVec Nullkiller::buildPlan(TGoalVec & tasks, int priorityTier) const { TaskPlan taskPlan; - tbb::parallel_for(tbb::blocked_range(0, tasks.size()), [this, &tasks](const tbb::blocked_range & r) + tbb::parallel_for(tbb::blocked_range(0, tasks.size()), [this, &tasks, priorityTier](const tbb::blocked_range & r) { auto evaluator = this->priorityEvaluators->acquire(); for(size_t i = r.begin(); i != r.end(); i++) { auto task = tasks[i]; - - if(task->asTask()->priority <= 0) - task->asTask()->priority = evaluator->evaluate(task); + if (task->asTask()->priority <= 0 || priorityTier != PriorityEvaluator::PriorityTier::BUILDINGS) + task->asTask()->priority = evaluator->evaluate(task, priorityTier); } }); @@ -326,7 +333,7 @@ bool Nullkiller::arePathHeroesLocked(const AIPath & path) const if(lockReason != HeroLockedReason::NOT_LOCKED) { #if NKAI_TRACE_LEVEL >= 1 - logAi->trace("Hero %s is locked by STARTUP. Discarding %s", path.targetHero->getObjectName(), path.toString()); + logAi->trace("Hero %s is locked by %d. Discarding %s", path.targetHero->getObjectName(), (int)lockReason, path.toString()); #endif return true; } @@ -347,12 +354,24 @@ void Nullkiller::makeTurn() boost::lock_guard sharedStorageLock(AISharedStorage::locker); const int MAX_DEPTH = 10; - const float FAST_TASK_MINIMAL_PRIORITY = 0.7f; resetAiState(); Goals::TGoalVec bestTasks; +#if NKAI_TRACE_LEVEL >= 1 + float totalHeroStrength = 0; + int totalTownLevel = 0; + for (auto heroInfo : cb->getHeroesInfo()) + { + totalHeroStrength += heroInfo->getTotalStrength(); + } + for (auto townInfo : cb->getTownsInfo()) + { + totalTownLevel += townInfo->getTownLevel(); + } + logAi->info("Beginning: Strength: %f Townlevel: %d Resources: %s", totalHeroStrength, totalTownLevel, cb->getResourceAmount().toString()); +#endif for(int i = 1; i <= settings->getMaxPass() && cb->getPlayerStatus(playerID) == EPlayerStatus::INGAME; i++) { auto start = std::chrono::high_resolution_clock::now(); @@ -360,17 +379,21 @@ void Nullkiller::makeTurn() Goals::TTask bestTask = taskptr(Goals::Invalid()); - for(;i <= settings->getMaxPass(); i++) + while(true) { bestTasks.clear(); + decompose(bestTasks, sptr(RecruitHeroBehavior()), 1); decompose(bestTasks, sptr(BuyArmyBehavior()), 1); decompose(bestTasks, sptr(BuildingBehavior()), 1); bestTask = choseBestTask(bestTasks); - if(bestTask->priority >= FAST_TASK_MINIMAL_PRIORITY) + if(bestTask->priority > 0) { +#if NKAI_TRACE_LEVEL >= 1 + logAi->info("Pass %d: Performing prio 0 task %s with prio: %d", i, bestTask->toString(), bestTask->priority); +#endif if(!executeTask(bestTask)) return; @@ -382,7 +405,6 @@ void Nullkiller::makeTurn() } } - decompose(bestTasks, sptr(RecruitHeroBehavior()), 1); decompose(bestTasks, sptr(CaptureObjectsBehavior()), 1); decompose(bestTasks, sptr(ClusterBehavior()), MAX_DEPTH); decompose(bestTasks, sptr(DefenceBehavior()), MAX_DEPTH); @@ -392,12 +414,24 @@ void Nullkiller::makeTurn() if(!isOpenMap()) decompose(bestTasks, sptr(ExplorationBehavior()), MAX_DEPTH); - if(cb->getDate(Date::DAY) == 1 || heroManager->getHeroRoles().empty()) + TTaskVec selectedTasks; +#if NKAI_TRACE_LEVEL >= 1 + int prioOfTask = 0; +#endif + for (int prio = PriorityEvaluator::PriorityTier::INSTAKILL; prio <= PriorityEvaluator::PriorityTier::DEFEND; ++prio) { - decompose(bestTasks, sptr(StartupBehavior()), 1); +#if NKAI_TRACE_LEVEL >= 1 + prioOfTask = prio; +#endif + selectedTasks = buildPlan(bestTasks, prio); + if (!selectedTasks.empty() || settings->isUseFuzzy()) + break; } - auto selectedTasks = buildPlan(bestTasks); + std::sort(selectedTasks.begin(), selectedTasks.end(), [](const TTask& a, const TTask& b) + { + return a->priority > b->priority; + }); logAi->debug("Decision madel in %ld", timeElapsed(start)); @@ -438,7 +472,7 @@ void Nullkiller::makeTurn() bestTask->priority); } - if(bestTask->priority < MIN_PRIORITY) + if((settings->isUseFuzzy() && bestTask->priority < MIN_PRIORITY) || (!settings->isUseFuzzy() && bestTask->priority <= 0)) { auto heroes = cb->getHeroesInfo(); auto hasMp = vstd::contains_if(heroes, [](const CGHeroInstance * h) -> bool @@ -463,7 +497,9 @@ void Nullkiller::makeTurn() continue; } - +#if NKAI_TRACE_LEVEL >= 1 + logAi->info("Pass %d: Performing prio %d task %s with prio: %d", i, prioOfTask, bestTask->toString(), bestTask->priority); +#endif if(!executeTask(bestTask)) { if(hasAnySuccess) @@ -471,13 +507,27 @@ void Nullkiller::makeTurn() else return; } - hasAnySuccess = true; } + hasAnySuccess |= handleTrading(); + if(!hasAnySuccess) { logAi->trace("Nothing was done this turn. Ending turn."); +#if NKAI_TRACE_LEVEL >= 1 + totalHeroStrength = 0; + totalTownLevel = 0; + for (auto heroInfo : cb->getHeroesInfo()) + { + totalHeroStrength += heroInfo->getTotalStrength(); + } + for (auto townInfo : cb->getTownsInfo()) + { + totalTownLevel += townInfo->getTownLevel(); + } + logAi->info("End: Strength: %f Townlevel: %d Resources: %s", totalHeroStrength, totalTownLevel, cb->getResourceAmount().toString()); +#endif return; } @@ -554,4 +604,102 @@ void Nullkiller::lockResources(const TResources & res) lockedResources += res; } +bool Nullkiller::handleTrading() +{ + bool haveTraded = false; + bool shouldTryToTrade = true; + int marketId = -1; + for (auto town : cb->getTownsInfo()) + { + if (town->hasBuiltSomeTradeBuilding()) + { + marketId = town->id; + } + } + if (marketId == -1) + return false; + if (const CGObjectInstance* obj = cb->getObj(ObjectInstanceID(marketId), false)) + { + if (const auto* m = dynamic_cast(obj)) + { + while (shouldTryToTrade) + { + shouldTryToTrade = false; + buildAnalyzer->update(); + TResources required = buildAnalyzer->getTotalResourcesRequired(); + TResources income = buildAnalyzer->getDailyIncome(); + TResources available = cb->getResourceAmount(); +#if NKAI_TRACE_LEVEL >= 2 + logAi->debug("Available %s", available.toString()); + logAi->debug("Required %s", required.toString()); +#endif + int mostWanted = -1; + int mostExpendable = -1; + float minRatio = std::numeric_limits::max(); + float maxRatio = std::numeric_limits::min(); + + for (int i = 0; i < required.size(); ++i) + { + if (required[i] <= 0) + continue; + float ratio = static_cast(available[i]) / required[i]; + + if (ratio < minRatio) { + minRatio = ratio; + mostWanted = i; + } + } + + for (int i = 0; i < required.size(); ++i) + { + float ratio = available[i]; + if (required[i] > 0) + ratio = static_cast(available[i]) / required[i]; + else + ratio = available[i]; + + bool okToSell = false; + + if (i == GameResID::GOLD) + { + if (income[i] > 0 && !buildAnalyzer->isGoldPressureHigh()) + okToSell = true; + } + else + { + if (required[i] <= 0 && income[i] > 0) + okToSell = true; + } + + if (ratio > maxRatio && okToSell) { + maxRatio = ratio; + mostExpendable = i; + } + } +#if NKAI_TRACE_LEVEL >= 2 + logAi->debug("mostExpendable: %d mostWanted: %d", mostExpendable, mostWanted); +#endif + if (mostExpendable == mostWanted || mostWanted == -1 || mostExpendable == -1) + return false; + + int toGive; + int toGet; + m->getOffer(mostExpendable, mostWanted, toGive, toGet, EMarketMode::RESOURCE_RESOURCE); + //logAi->info("Offer is: I get %d of %s for %d of %s at %s", toGet, mostWanted, toGive, mostExpendable, obj->getObjectName()); + //TODO trade only as much as needed + if (toGive && toGive <= available[mostExpendable]) //don't try to sell 0 resources + { + cb->trade(m->getObjInstanceID(), EMarketMode::RESOURCE_RESOURCE, GameResID(mostExpendable), GameResID(mostWanted), toGive); +#if NKAI_TRACE_LEVEL >= 1 + logAi->info("Traded %d of %s for %d of %s at %s", toGive, mostExpendable, toGet, mostWanted, obj->getObjectName()); +#endif + haveTraded = true; + shouldTryToTrade = true; + } + } + } + } + return haveTraded; +} + } diff --git a/AI/Nullkiller/Engine/Nullkiller.h b/AI/Nullkiller/Engine/Nullkiller.h index af05e354b..941e71f16 100644 --- a/AI/Nullkiller/Engine/Nullkiller.h +++ b/AI/Nullkiller/Engine/Nullkiller.h @@ -120,13 +120,14 @@ public: ScanDepth getScanDepth() const { return scanDepth; } bool isOpenMap() const { return openMap; } bool isObjectGraphAllowed() const { return useObjectGraph; } + bool handleTrading(); private: void resetAiState(); void updateAiState(int pass, bool fast = false); void decompose(Goals::TGoalVec & result, Goals::TSubgoal behavior, int decompositionMaxDepth) const; Goals::TTask choseBestTask(Goals::TGoalVec & tasks) const; - Goals::TTaskVec buildPlan(Goals::TGoalVec & tasks) const; + Goals::TTaskVec buildPlan(Goals::TGoalVec & tasks, int priorityTier) const; bool executeTask(Goals::TTask task); bool areAffectedObjectsPresent(Goals::TTask task) const; HeroRole getTaskRole(Goals::TTask task) const; diff --git a/AI/Nullkiller/Engine/PriorityEvaluator.cpp b/AI/Nullkiller/Engine/PriorityEvaluator.cpp index 2d5839040..92faf4bd8 100644 --- a/AI/Nullkiller/Engine/PriorityEvaluator.cpp +++ b/AI/Nullkiller/Engine/PriorityEvaluator.cpp @@ -15,6 +15,8 @@ #include "../../../lib/mapObjectConstructors/CObjectClassesHandler.h" #include "../../../lib/mapObjectConstructors/CBankInstanceConstructor.h" #include "../../../lib/mapObjects/MapObjects.h" +#include "../../../lib/mapping/CMapDefines.h" +#include "../../../lib/RoadHandler.h" #include "../../../lib/CCreatureHandler.h" #include "../../../lib/VCMI_Lib.h" #include "../../../lib/StartInfo.h" @@ -33,11 +35,9 @@ namespace NKAI { -#define MIN_AI_STRENGTH (0.5f) //lower when combat AI gets smarter -#define UNGUARDED_OBJECT (100.0f) //we consider unguarded objects 100 times weaker than us -const float MIN_CRITICAL_VALUE = 2.0f; +constexpr float MIN_CRITICAL_VALUE = 2.0f; -EvaluationContext::EvaluationContext(const Nullkiller * ai) +EvaluationContext::EvaluationContext(const Nullkiller* ai) : movementCost(0.0), manaCost(0), danger(0), @@ -51,9 +51,22 @@ EvaluationContext::EvaluationContext(const Nullkiller * ai) heroRole(HeroRole::SCOUT), turn(0), strategicalValue(0), + conquestValue(0), evaluator(ai), enemyHeroDangerRatio(0), - armyGrowth(0) + threat(0), + armyGrowth(0), + armyInvolvement(0), + defenseValue(0), + isDefend(false), + threatTurns(INT_MAX), + involvesSailing(false), + isTradeBuilding(false), + isExchange(false), + isArmyUpgrade(false), + isHero(false), + isEnemy(false), + explorePriority(0) { } @@ -155,10 +168,10 @@ uint64_t getCreatureBankArmyReward(const CGObjectInstance * target, const CGHero for (auto c : creatures) { //Only if hero has slot for this creature in the army - auto ccre = dynamic_cast(c.data.type); + auto ccre = dynamic_cast(c.data.getType()); if (hero->getSlotFor(ccre).validSlot() || duplicatingSlots > 0) { - result += (c.data.type->getAIValue() * c.data.count) * c.chance; + result += (c.data.getType()->getAIValue() * c.data.count) * c.chance; } /*else { @@ -225,7 +238,7 @@ int getDwellingArmyCost(const CGObjectInstance * target) auto creature = creLevel.second.back().toCreature(); auto creaturesAreFree = creature->getLevel() == 1; if(!creaturesAreFree) - cost += creature->getRecruitCost(EGameResID::GOLD) * creLevel.first; + cost += creature->getFullRecruitCost().marketValue() * creLevel.first; } } @@ -251,6 +264,8 @@ static uint64_t evaluateArtifactArmyValue(const CArtifact * art) switch(art->aClass) { + case CArtifact::EartClass::ART_TREASURE: + //FALL_THROUGH case CArtifact::EartClass::ART_MINOR: classValue = 1000; break; @@ -289,8 +304,10 @@ uint64_t RewardEvaluator::getArmyReward( case Obj::CREATURE_GENERATOR3: case Obj::CREATURE_GENERATOR4: return getDwellingArmyValue(ai->cb.get(), target, checkGold); + case Obj::SPELL_SCROLL: + //FALL_THROUGH case Obj::ARTIFACT: - return evaluateArtifactArmyValue(dynamic_cast(target)->storedArtifact->artType); + return evaluateArtifactArmyValue(dynamic_cast(target)->storedArtifact->getType()); case Obj::HERO: return relations == PlayerRelations::ENEMIES ? enemyArmyEliminationRewardRatio * dynamic_cast(target)->getArmyStrength() @@ -479,7 +496,7 @@ uint64_t RewardEvaluator::townArmyGrowth(const CGTownInstance * town) const return result; } -uint64_t RewardEvaluator::getManaRecoveryArmyReward(const CGHeroInstance * hero) const +float RewardEvaluator::getManaRecoveryArmyReward(const CGHeroInstance * hero) const { return ai->heroManager->getMagicStrength(hero) * 10000 * (1.0f - std::sqrt(static_cast(hero->mana) / hero->manaLimit())); } @@ -581,6 +598,54 @@ float RewardEvaluator::getStrategicalValue(const CGObjectInstance * target, cons return 0; } +float RewardEvaluator::getConquestValue(const CGObjectInstance* target) const +{ + if (!target) + return 0; + if (target->getOwner() == ai->playerID) + return 0; + switch (target->ID) + { + case Obj::TOWN: + { + if (ai->buildAnalyzer->getDevelopmentInfo().empty()) + return 10.0f; + + auto town = dynamic_cast(target); + + if (town->getOwner() == ai->playerID) + { + auto armyIncome = townArmyGrowth(town); + auto dailyIncome = town->dailyIncome()[EGameResID::GOLD]; + + return std::min(1.0f, std::sqrt(armyIncome / 40000.0f)) + std::min(0.3f, dailyIncome / 10000.0f); + } + + auto fortLevel = town->fortLevel(); + auto booster = 1.0f; + + if (town->hasCapitol()) + return booster * 1.5; + + if (fortLevel < CGTownInstance::CITADEL) + return booster * (town->hasFort() ? 1.0 : 0.8); + else + return booster * (fortLevel == CGTownInstance::CASTLE ? 1.4 : 1.2); + } + + case Obj::HERO: + return ai->cb->getPlayerRelations(target->tempOwner, ai->playerID) == PlayerRelations::ENEMIES + ? getEnemyHeroStrategicalValue(dynamic_cast(target)) + : 0; + + case Obj::KEYMASTER: + return 0.6f; + + default: + return 0; + } +} + float RewardEvaluator::evaluateWitchHutSkillScore(const CGObjectInstance * hut, const CGHeroInstance * hero, HeroRole role) const { auto rewardable = dynamic_cast(hut); @@ -705,7 +770,7 @@ int32_t getArmyCost(const CArmedInstance * army) for(auto stack : army->Slots()) { - value += stack.second->getCreatureID().toCreature()->getRecruitCost(EGameResID::GOLD) * stack.second->count; + value += stack.second->getCreatureID().toCreature()->getFullRecruitCost().marketValue() * stack.second->count; } return value; @@ -786,7 +851,9 @@ public: uint64_t armyStrength = heroExchange.getReinforcementArmyStrength(evaluationContext.evaluator.ai); evaluationContext.addNonCriticalStrategicalValue(2.0f * armyStrength / (float)heroExchange.hero->getArmyStrength()); + evaluationContext.conquestValue += 2.0f * armyStrength / (float)heroExchange.hero->getArmyStrength(); evaluationContext.heroRole = evaluationContext.evaluator.ai->heroManager->getHeroRole(heroExchange.hero); + evaluationContext.isExchange = true; } }; @@ -804,6 +871,7 @@ public: evaluationContext.armyReward += upgradeValue; evaluationContext.addNonCriticalStrategicalValue(upgradeValue / (float)armyUpgrade.hero->getArmyStrength()); + evaluationContext.isArmyUpgrade = true; } }; @@ -818,22 +886,46 @@ public: int tilesDiscovered = task->value; evaluationContext.addNonCriticalStrategicalValue(0.03f * tilesDiscovered); + for (auto obj : evaluationContext.evaluator.ai->cb->getVisitableObjs(task->tile)) + { + switch (obj->ID.num) + { + case Obj::MONOLITH_ONE_WAY_ENTRANCE: + case Obj::MONOLITH_TWO_WAY: + case Obj::SUBTERRANEAN_GATE: + evaluationContext.explorePriority = 1; + break; + case Obj::REDWOOD_OBSERVATORY: + case Obj::PILLAR_OF_FIRE: + evaluationContext.explorePriority = 2; + break; + } + } + if(evaluationContext.evaluator.ai->cb->getTile(task->tile)->roadType != RoadId::NO_ROAD) + evaluationContext.explorePriority = 1; + if (evaluationContext.explorePriority == 0) + evaluationContext.explorePriority = 3; } }; class StayAtTownManaRecoveryEvaluator : public IEvaluationContextBuilder { public: - void buildEvaluationContext(EvaluationContext & evaluationContext, Goals::TSubgoal task) const override + void buildEvaluationContext(EvaluationContext& evaluationContext, Goals::TSubgoal task) const override { - if(task->goalType != Goals::STAY_AT_TOWN) + if (task->goalType != Goals::STAY_AT_TOWN) return; - Goals::StayAtTown & stayAtTown = dynamic_cast(*task); + Goals::StayAtTown& stayAtTown = dynamic_cast(*task); evaluationContext.armyReward += evaluationContext.evaluator.getManaRecoveryArmyReward(stayAtTown.getHero()); - evaluationContext.movementCostByRole[evaluationContext.heroRole] += stayAtTown.getMovementWasted(); - evaluationContext.movementCost += stayAtTown.getMovementWasted(); + if (evaluationContext.armyReward == 0) + evaluationContext.isDefend = true; + else + { + evaluationContext.movementCost += stayAtTown.getMovementWasted(); + evaluationContext.movementCostByRole[evaluationContext.heroRole] += stayAtTown.getMovementWasted(); + } } }; @@ -844,15 +936,8 @@ void addTileDanger(EvaluationContext & evaluationContext, const int3 & tile, uin if(enemyDanger.danger) { auto dangerRatio = enemyDanger.danger / (double)ourStrength; - auto enemyHero = evaluationContext.evaluator.ai->cb->getObj(enemyDanger.hero.hid, false); - bool isAI = enemyHero && isAnotherAi(enemyHero, *evaluationContext.evaluator.ai->cb); - - if(isAI) - { - dangerRatio *= 1.5; // lets make AI bit more afraid of other AI. - } - vstd::amax(evaluationContext.enemyHeroDangerRatio, dangerRatio); + vstd::amax(evaluationContext.threat, enemyDanger.threat); } } @@ -896,6 +981,10 @@ public: else evaluationContext.addNonCriticalStrategicalValue(1.7f * multiplier * strategicalValue); + evaluationContext.defenseValue = town->fortLevel(); + evaluationContext.isDefend = true; + evaluationContext.threatTurns = treat.turn; + vstd::amax(evaluationContext.danger, defendTown.getTreat().danger); addTileDanger(evaluationContext, town->visitablePos(), defendTown.getTurn(), defendTown.getDefenceStrength()); } @@ -926,6 +1015,8 @@ public: for(auto & node : path.nodes) { vstd::amax(costsPerHero[node.targetHero], node.cost); + if (node.layer == EPathfindingLayer::SAIL) + evaluationContext.involvesSailing = true; } for(auto pair : costsPerHero) @@ -952,10 +1043,18 @@ public: evaluationContext.armyGrowth += evaluationContext.evaluator.getArmyGrowth(target, hero, army); evaluationContext.skillReward += evaluationContext.evaluator.getSkillReward(target, hero, heroRole); evaluationContext.addNonCriticalStrategicalValue(evaluationContext.evaluator.getStrategicalValue(target)); + evaluationContext.conquestValue += evaluationContext.evaluator.getConquestValue(target); + if (target->ID == Obj::HERO) + evaluationContext.isHero = true; + if (target->getOwner() != PlayerColor::NEUTRAL && ai->cb->getPlayerRelations(ai->playerID, target->getOwner()) == PlayerRelations::ENEMIES) + evaluationContext.isEnemy = true; evaluationContext.goldCost += evaluationContext.evaluator.getGoldCost(target, hero, army); + evaluationContext.armyInvolvement += army->getArmyCost(); + if(evaluationContext.danger > 0) + evaluationContext.skillReward += (float)evaluationContext.danger / (float)hero->getArmyStrength(); } - vstd::amax(evaluationContext.armyLossPersentage, path.getTotalArmyLoss() / (double)path.getHeroStrength()); + vstd::amax(evaluationContext.armyLossPersentage, (float)path.getTotalArmyLoss() / (float)army->getArmyStrength()); addTileDanger(evaluationContext, path.targetTile(), path.turn(), path.getHeroStrength()); vstd::amax(evaluationContext.turn, path.turn()); } @@ -996,6 +1095,7 @@ public: evaluationContext.armyReward += evaluationContext.evaluator.getArmyReward(target, hero, army, checkGold) / boost; evaluationContext.skillReward += evaluationContext.evaluator.getSkillReward(target, hero, role) / boost; evaluationContext.addNonCriticalStrategicalValue(evaluationContext.evaluator.getStrategicalValue(target) / boost); + evaluationContext.conquestValue += evaluationContext.evaluator.getConquestValue(target); evaluationContext.goldCost += evaluationContext.evaluator.getGoldCost(target, hero, army) / boost; evaluationContext.movementCostByRole[role] += objInfo.second.movementCost / boost; evaluationContext.movementCost += objInfo.second.movementCost / boost; @@ -1021,6 +1121,14 @@ public: Goals::ExchangeSwapTownHeroes & swapCommand = dynamic_cast(*task); const CGHeroInstance * garrisonHero = swapCommand.getGarrisonHero(); + logAi->trace("buildEvaluationContext ExchangeSwapTownHeroesContextBuilder %s affected objects: %d", swapCommand.toString(), swapCommand.getAffectedObjects().size()); + for (auto obj : swapCommand.getAffectedObjects()) + { + logAi->trace("affected object: %s", evaluationContext.evaluator.ai->cb->getObj(obj)->getObjectName()); + } + if (garrisonHero) + logAi->debug("with %s and %d", garrisonHero->getNameTranslated(), int(swapCommand.getLockingReason())); + if(garrisonHero && swapCommand.getLockingReason() == HeroLockedReason::DEFENCE) { auto defenderRole = evaluationContext.evaluator.ai->heroManager->getHeroRole(garrisonHero); @@ -1029,6 +1137,9 @@ public: evaluationContext.movementCost += mpLeft; evaluationContext.movementCostByRole[defenderRole] += mpLeft; evaluationContext.heroRole = defenderRole; + evaluationContext.isDefend = true; + evaluationContext.armyInvolvement = garrisonHero->getArmyStrength(); + logAi->debug("evaluationContext.isDefend: %d", evaluationContext.isDefend); } } }; @@ -1072,8 +1183,14 @@ public: evaluationContext.goldReward += 7 * bi.dailyIncome[EGameResID::GOLD] / 2; // 7 day income but half we already have evaluationContext.heroRole = HeroRole::MAIN; evaluationContext.movementCostByRole[evaluationContext.heroRole] += bi.prerequisitesCount; - evaluationContext.goldCost += bi.buildCostWithPrerequisites[EGameResID::GOLD]; + int32_t cost = bi.buildCost[EGameResID::GOLD]; + evaluationContext.goldCost += cost; evaluationContext.closestWayRatio = 1; + evaluationContext.buildingCost += bi.buildCostWithPrerequisites; + if (bi.id == BuildingID::MARKETPLACE || bi.dailyIncome[EGameResID::WOOD] > 0) + evaluationContext.isTradeBuilding = true; + + logAi->trace("Building costs for %s : %s MarketValue: %d",bi.toString(), evaluationContext.buildingCost.toString(), evaluationContext.buildingCost.marketValue()); if(bi.creatureID != CreatureID::NONE) { @@ -1100,7 +1217,18 @@ public: else if(bi.id >= BuildingID::MAGES_GUILD_1 && bi.id <= BuildingID::MAGES_GUILD_5) { evaluationContext.skillReward += 2 * (bi.id - BuildingID::MAGES_GUILD_1); + for (auto hero : evaluationContext.evaluator.ai->cb->getHeroesInfo()) + { + evaluationContext.armyInvolvement += hero->getArmyCost(); + } } + int sameTownBonus = 0; + for (auto town : evaluationContext.evaluator.ai->cb->getTownsInfo()) + { + if (buildThis.town->getFaction() == town->getFaction()) + sameTownBonus += town->getTownLevel(); + } + evaluationContext.armyReward *= sameTownBonus; if(evaluationContext.goldReward) { @@ -1120,7 +1248,7 @@ public: uint64_t RewardEvaluator::getUpgradeArmyReward(const CGTownInstance * town, const BuildingInfo & bi) const { - if(ai->buildAnalyzer->hasAnyBuilding(town->getFaction(), bi.id)) + if(ai->buildAnalyzer->hasAnyBuilding(town->getFactionID(), bi.id)) return 0; auto creaturesToUpgrade = ai->armyManager->getTotalCreaturesAvailable(bi.baseCreatureID); @@ -1162,6 +1290,7 @@ EvaluationContext PriorityEvaluator::buildEvaluationContext(Goals::TSubgoal goal for(auto subgoal : parts) { context.goldCost += subgoal->goldCost; + context.buildingCost += subgoal->buildingCost; for(auto builder : evaluationContextBuilders) { @@ -1172,7 +1301,7 @@ EvaluationContext PriorityEvaluator::buildEvaluationContext(Goals::TSubgoal goal return context; } -float PriorityEvaluator::evaluate(Goals::TSubgoal task) +float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier) { auto evaluationContext = buildEvaluationContext(task); @@ -1185,36 +1314,256 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task) double result = 0; - try + if (ai->settings->isUseFuzzy()) { - armyLossPersentageVariable->setValue(evaluationContext.armyLossPersentage); - heroRoleVariable->setValue(evaluationContext.heroRole); - mainTurnDistanceVariable->setValue(evaluationContext.movementCostByRole[HeroRole::MAIN]); - scoutTurnDistanceVariable->setValue(evaluationContext.movementCostByRole[HeroRole::SCOUT]); - goldRewardVariable->setValue(goldRewardPerTurn); - armyRewardVariable->setValue(evaluationContext.armyReward); - armyGrowthVariable->setValue(evaluationContext.armyGrowth); - skillRewardVariable->setValue(evaluationContext.skillReward); - dangerVariable->setValue(evaluationContext.danger); - rewardTypeVariable->setValue(rewardType); - closestHeroRatioVariable->setValue(evaluationContext.closestWayRatio); - strategicalValueVariable->setValue(evaluationContext.strategicalValue); - goldPressureVariable->setValue(ai->buildAnalyzer->getGoldPressure()); - goldCostVariable->setValue(evaluationContext.goldCost / ((float)ai->getFreeResources()[EGameResID::GOLD] + (float)ai->buildAnalyzer->getDailyIncome()[EGameResID::GOLD] + 1.0f)); - turnVariable->setValue(evaluationContext.turn); - fearVariable->setValue(evaluationContext.enemyHeroDangerRatio); + float fuzzyResult = 0; + try + { + armyLossPersentageVariable->setValue(evaluationContext.armyLossPersentage); + heroRoleVariable->setValue(evaluationContext.heroRole); + mainTurnDistanceVariable->setValue(evaluationContext.movementCostByRole[HeroRole::MAIN]); + scoutTurnDistanceVariable->setValue(evaluationContext.movementCostByRole[HeroRole::SCOUT]); + goldRewardVariable->setValue(goldRewardPerTurn); + armyRewardVariable->setValue(evaluationContext.armyReward); + armyGrowthVariable->setValue(evaluationContext.armyGrowth); + skillRewardVariable->setValue(evaluationContext.skillReward); + dangerVariable->setValue(evaluationContext.danger); + rewardTypeVariable->setValue(rewardType); + closestHeroRatioVariable->setValue(evaluationContext.closestWayRatio); + strategicalValueVariable->setValue(evaluationContext.strategicalValue); + goldPressureVariable->setValue(ai->buildAnalyzer->getGoldPressure()); + goldCostVariable->setValue(evaluationContext.goldCost / ((float)ai->getFreeResources()[EGameResID::GOLD] + (float)ai->buildAnalyzer->getDailyIncome()[EGameResID::GOLD] + 1.0f)); + turnVariable->setValue(evaluationContext.turn); + fearVariable->setValue(evaluationContext.enemyHeroDangerRatio); - engine->process(); + engine->process(); - result = value->getValue(); + fuzzyResult = value->getValue(); + } + catch (fl::Exception& fe) + { + logAi->error("evaluate VisitTile: %s", fe.getWhat()); + } + result = fuzzyResult; } - catch(fl::Exception & fe) + else { - logAi->error("evaluate VisitTile: %s", fe.getWhat()); + float score = 0; + float maxWillingToLose = ai->cb->getTownsInfo().empty() || (evaluationContext.isDefend && evaluationContext.threatTurns == 0) ? 1 : 0.25; + + bool arriveNextWeek = false; + if (ai->cb->getDate(Date::DAY_OF_WEEK) + evaluationContext.turn > 7) + arriveNextWeek = true; + +#if NKAI_TRACE_LEVEL >= 2 + logAi->trace("BEFORE: priorityTier %d, Evaluated %s, loss: %f, turn: %d, turns main: %f, scout: %f, gold: %f, cost: %d, army gain: %f, army growth: %f skill: %f danger: %d, threatTurns: %d, threat: %d, role: %s, strategical value: %f, conquest value: %f cwr: %f, fear: %f, explorePriority: %d isDefend: %d", + priorityTier, + task->toString(), + evaluationContext.armyLossPersentage, + (int)evaluationContext.turn, + evaluationContext.movementCostByRole[HeroRole::MAIN], + evaluationContext.movementCostByRole[HeroRole::SCOUT], + goldRewardPerTurn, + evaluationContext.goldCost, + evaluationContext.armyReward, + evaluationContext.armyGrowth, + evaluationContext.skillReward, + evaluationContext.danger, + evaluationContext.threatTurns, + evaluationContext.threat, + evaluationContext.heroRole == HeroRole::MAIN ? "main" : "scout", + evaluationContext.strategicalValue, + evaluationContext.conquestValue, + evaluationContext.closestWayRatio, + evaluationContext.enemyHeroDangerRatio, + evaluationContext.explorePriority, + evaluationContext.isDefend); +#endif + + switch (priorityTier) + { + case PriorityTier::INSTAKILL: //Take towns / kill heroes in immediate reach + { + if (evaluationContext.turn > 0) + return 0; + if(evaluationContext.conquestValue > 0) + score = 1000; + if (vstd::isAlmostZero(score) || (evaluationContext.enemyHeroDangerRatio > 1 && (evaluationContext.turn > 0 || evaluationContext.isExchange) && !ai->cb->getTownsInfo().empty())) + return 0; + if (maxWillingToLose - evaluationContext.armyLossPersentage < 0) + return 0; + score *= evaluationContext.closestWayRatio; + if (evaluationContext.movementCost > 0) + score /= evaluationContext.movementCost; + break; + } + case PriorityTier::INSTADEFEND: //Defend immediately threatened towns + { + if (evaluationContext.isDefend && evaluationContext.threatTurns == 0 && evaluationContext.turn == 0) + score = evaluationContext.armyInvolvement; + if (evaluationContext.isEnemy && maxWillingToLose - evaluationContext.armyLossPersentage < 0) + return 0; + score *= evaluationContext.closestWayRatio; + break; + } + case PriorityTier::KILL: //Take towns / kill heroes that are further away + { + if (evaluationContext.turn > 0 && evaluationContext.isHero) + return 0; + if (arriveNextWeek && evaluationContext.isEnemy) + return 0; + if (evaluationContext.conquestValue > 0) + score = 1000; + if (vstd::isAlmostZero(score) || (evaluationContext.enemyHeroDangerRatio > 1 && (evaluationContext.turn > 0 || evaluationContext.isExchange) && !ai->cb->getTownsInfo().empty())) + return 0; + if (maxWillingToLose - evaluationContext.armyLossPersentage < 0) + return 0; + score *= evaluationContext.closestWayRatio; + if (evaluationContext.movementCost > 0) + score /= evaluationContext.movementCost; + break; + } + case PriorityTier::UPGRADE: + { + if (!evaluationContext.isArmyUpgrade) + return 0; + if (evaluationContext.enemyHeroDangerRatio > 1) + return 0; + if (maxWillingToLose - evaluationContext.armyLossPersentage < 0) + return 0; + score = 1000; + score *= evaluationContext.closestWayRatio; + if (evaluationContext.movementCost > 0) + score /= evaluationContext.movementCost; + break; + } + case PriorityTier::HIGH_PRIO_EXPLORE: + { + if (evaluationContext.enemyHeroDangerRatio > 1) + return 0; + if (evaluationContext.explorePriority != 1) + return 0; + if (maxWillingToLose - evaluationContext.armyLossPersentage < 0) + return 0; + score = 1000; + score *= evaluationContext.closestWayRatio; + if (evaluationContext.movementCost > 0) + score /= evaluationContext.movementCost; + break; + } + case PriorityTier::HUNTER_GATHER: //Collect guarded stuff + { + if (evaluationContext.enemyHeroDangerRatio > 1 && !evaluationContext.isDefend) + return 0; + if (evaluationContext.buildingCost.marketValue() > 0) + return 0; + if (evaluationContext.isDefend && (evaluationContext.enemyHeroDangerRatio < 1 || evaluationContext.threatTurns > 0 || evaluationContext.turn > 0)) + return 0; + if (evaluationContext.explorePriority == 3) + return 0; + if (evaluationContext.isArmyUpgrade) + return 0; + if ((evaluationContext.enemyHeroDangerRatio > 0 && arriveNextWeek) || evaluationContext.enemyHeroDangerRatio > 1) + return 0; + if (maxWillingToLose - evaluationContext.armyLossPersentage < 0) + return 0; + score += evaluationContext.strategicalValue * 1000; + score += evaluationContext.goldReward; + score += evaluationContext.skillReward * evaluationContext.armyInvolvement * (1 - evaluationContext.armyLossPersentage) * 0.05; + score += evaluationContext.armyReward; + score += evaluationContext.armyGrowth; + score -= evaluationContext.goldCost; + score -= evaluationContext.armyInvolvement * evaluationContext.armyLossPersentage; + if (score > 0) + { + score = 1000; + score *= evaluationContext.closestWayRatio; + if (evaluationContext.movementCost > 0) + score /= evaluationContext.movementCost; + } + break; + } + case PriorityTier::LOW_PRIO_EXPLORE: + { + if (evaluationContext.enemyHeroDangerRatio > 1) + return 0; + if (evaluationContext.explorePriority != 3) + return 0; + if (maxWillingToLose - evaluationContext.armyLossPersentage < 0) + return 0; + score = 1000; + score *= evaluationContext.closestWayRatio; + if (evaluationContext.movementCost > 0) + score /= evaluationContext.movementCost; + break; + } + case PriorityTier::DEFEND: //Defend whatever if nothing else is to do + { + if (evaluationContext.enemyHeroDangerRatio > 1 && evaluationContext.isExchange) + return 0; + if (evaluationContext.isDefend || evaluationContext.isArmyUpgrade) + score = 1000; + score *= evaluationContext.closestWayRatio; + score /= (evaluationContext.turn + 1); + break; + } + case PriorityTier::BUILDINGS: //For buildings and buying army + { + if (maxWillingToLose - evaluationContext.armyLossPersentage < 0) + return 0; + //If we already have locked resources, we don't look at other buildings + if (ai->getLockedResources().marketValue() > 0) + return 0; + score += evaluationContext.conquestValue * 1000; + score += evaluationContext.strategicalValue * 1000; + score += evaluationContext.goldReward; + score += evaluationContext.skillReward * evaluationContext.armyInvolvement * (1 - evaluationContext.armyLossPersentage) * 0.05; + score += evaluationContext.armyReward; + score += evaluationContext.armyGrowth; + if (evaluationContext.buildingCost.marketValue() > 0) + { + if (!evaluationContext.isTradeBuilding && ai->getFreeResources()[EGameResID::WOOD] - evaluationContext.buildingCost[EGameResID::WOOD] < 5 && ai->buildAnalyzer->getDailyIncome()[EGameResID::WOOD] < 1) + { + logAi->trace("Should make sure to build market-place instead of %s", task->toString()); + for (auto town : ai->cb->getTownsInfo()) + { + if (!town->hasBuiltSomeTradeBuilding()) + return 0; + } + } + score += 1000; + auto resourcesAvailable = evaluationContext.evaluator.ai->getFreeResources(); + auto income = ai->buildAnalyzer->getDailyIncome(); + if(ai->buildAnalyzer->isGoldPressureHigh()) + score /= evaluationContext.buildingCost.marketValue(); + if (!resourcesAvailable.canAfford(evaluationContext.buildingCost)) + { + TResources needed = evaluationContext.buildingCost - resourcesAvailable; + needed.positive(); + int turnsTo = needed.maxPurchasableCount(income); + if (turnsTo == INT_MAX) + return 0; + else + score /= turnsTo; + } + } + else + { + if (evaluationContext.enemyHeroDangerRatio > 1 && !evaluationContext.isDefend && vstd::isAlmostZero(evaluationContext.conquestValue)) + return 0; + } + break; + } + } + result = score; + //TODO: Figure out the root cause for why evaluationContext.closestWayRatio has become -nan(ind). + if (std::isnan(result)) + return 0; } #if NKAI_TRACE_LEVEL >= 2 - logAi->trace("Evaluated %s, loss: %f, turn: %d, turns main: %f, scout: %f, gold: %f, cost: %d, army gain: %f, danger: %d, role: %s, strategical value: %f, cwr: %f, fear: %f, result %f", + logAi->trace("priorityTier %d, Evaluated %s, loss: %f, turn: %d, turns main: %f, scout: %f, gold: %f, cost: %d, army gain: %f, army growth: %f skill: %f danger: %d, threatTurns: %d, threat: %d, role: %s, strategical value: %f, conquest value: %f cwr: %f, fear: %f, result %f", + priorityTier, task->toString(), evaluationContext.armyLossPersentage, (int)evaluationContext.turn, @@ -1223,9 +1572,14 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task) goldRewardPerTurn, evaluationContext.goldCost, evaluationContext.armyReward, + evaluationContext.armyGrowth, + evaluationContext.skillReward, evaluationContext.danger, + evaluationContext.threatTurns, + evaluationContext.threat, evaluationContext.heroRole == HeroRole::MAIN ? "main" : "scout", evaluationContext.strategicalValue, + evaluationContext.conquestValue, evaluationContext.closestWayRatio, evaluationContext.enemyHeroDangerRatio, result); diff --git a/AI/Nullkiller/Engine/PriorityEvaluator.h b/AI/Nullkiller/Engine/PriorityEvaluator.h index a0e68d6c6..ee983e43b 100644 --- a/AI/Nullkiller/Engine/PriorityEvaluator.h +++ b/AI/Nullkiller/Engine/PriorityEvaluator.h @@ -41,6 +41,7 @@ public: float getResourceRequirementStrength(int resType) const; float getResourceRequirementStrength(const TResources & res) const; float getStrategicalValue(const CGObjectInstance * target, const CGHeroInstance * hero = nullptr) const; + float getConquestValue(const CGObjectInstance* target) const; float getTotalResourceRequirementStrength(int resType) const; float evaluateWitchHutSkillScore(const CGObjectInstance * hut, const CGHeroInstance * hero, HeroRole role) const; float getSkillReward(const CGObjectInstance * target, const CGHeroInstance * hero, HeroRole role) const; @@ -48,7 +49,7 @@ public: uint64_t getUpgradeArmyReward(const CGTownInstance * town, const BuildingInfo & bi) const; const HitMapInfo & getEnemyHeroDanger(const int3 & tile, uint8_t turn) const; uint64_t townArmyGrowth(const CGTownInstance * town) const; - uint64_t getManaRecoveryArmyReward(const CGHeroInstance * hero) const; + float getManaRecoveryArmyReward(const CGHeroInstance * hero) const; }; struct DLL_EXPORT EvaluationContext @@ -65,10 +66,24 @@ struct DLL_EXPORT EvaluationContext int32_t goldCost; float skillReward; float strategicalValue; + float conquestValue; HeroRole heroRole; uint8_t turn; RewardEvaluator evaluator; float enemyHeroDangerRatio; + float threat; + float armyInvolvement; + int defenseValue; + bool isDefend; + int threatTurns; + TResources buildingCost; + bool involvesSailing; + bool isTradeBuilding; + bool isExchange; + bool isArmyUpgrade; + bool isHero; + bool isEnemy; + int explorePriority; EvaluationContext(const Nullkiller * ai); @@ -91,7 +106,20 @@ public: ~PriorityEvaluator(); void initVisitTile(); - float evaluate(Goals::TSubgoal task); + float evaluate(Goals::TSubgoal task, int priorityTier = BUILDINGS); + + enum PriorityTier : int32_t + { + BUILDINGS = 0, + INSTAKILL, + INSTADEFEND, + KILL, + UPGRADE, + HIGH_PRIO_EXPLORE, + HUNTER_GATHER, + LOW_PRIO_EXPLORE, + DEFEND + }; private: const Nullkiller * ai; diff --git a/AI/Nullkiller/Engine/Settings.cpp b/AI/Nullkiller/Engine/Settings.cpp index db4e3f455..17f38ad64 100644 --- a/AI/Nullkiller/Engine/Settings.cpp +++ b/AI/Nullkiller/Engine/Settings.cpp @@ -11,6 +11,8 @@ #include #include "Settings.h" + +#include "../../../lib/constants/StringConstants.h" #include "../../../lib/mapObjectConstructors/AObjectTypeHandler.h" #include "../../../lib/mapObjectConstructors/CObjectClassesHandler.h" #include "../../../lib/mapObjectConstructors/CBankInstanceConstructor.h" @@ -22,56 +24,39 @@ namespace NKAI { - Settings::Settings() + Settings::Settings(int difficultyLevel) : maxRoamingHeroes(8), mainHeroTurnDistanceLimit(10), scoutHeroTurnDistanceLimit(5), - maxGoldPressure(0.3f), + maxGoldPressure(0.3f), + retreatThresholdRelative(0.3), + retreatThresholdAbsolute(10000), + safeAttackRatio(1.1), maxpass(10), + pathfinderBucketsCount(1), + pathfinderBucketSize(32), allowObjectGraph(true), useTroopsFromGarrisons(false), - openMap(true) + openMap(true), + useFuzzy(false) { - JsonNode node = JsonUtils::assembleFromFiles("config/ai/nkai/nkai-settings"); + const std::string & difficultyName = GameConstants::DIFFICULTY_NAMES[difficultyLevel]; + const JsonNode & rootNode = JsonUtils::assembleFromFiles("config/ai/nkai/nkai-settings"); + const JsonNode & node = rootNode[difficultyName]; - if(node.Struct()["maxRoamingHeroes"].isNumber()) - { - maxRoamingHeroes = node.Struct()["maxRoamingHeroes"].Integer(); - } - - if(node.Struct()["mainHeroTurnDistanceLimit"].isNumber()) - { - mainHeroTurnDistanceLimit = node.Struct()["mainHeroTurnDistanceLimit"].Integer(); - } - - if(node.Struct()["scoutHeroTurnDistanceLimit"].isNumber()) - { - scoutHeroTurnDistanceLimit = node.Struct()["scoutHeroTurnDistanceLimit"].Integer(); - } - - if(node.Struct()["maxpass"].isNumber()) - { - maxpass = node.Struct()["maxpass"].Integer(); - } - - if(node.Struct()["maxGoldPressure"].isNumber()) - { - maxGoldPressure = node.Struct()["maxGoldPressure"].Float(); - } - - if(!node.Struct()["allowObjectGraph"].isNull()) - { - allowObjectGraph = node.Struct()["allowObjectGraph"].Bool(); - } - - if(!node.Struct()["openMap"].isNull()) - { - openMap = node.Struct()["openMap"].Bool(); - } - - if(!node.Struct()["useTroopsFromGarrisons"].isNull()) - { - useTroopsFromGarrisons = node.Struct()["useTroopsFromGarrisons"].Bool(); - } + maxRoamingHeroes = node["maxRoamingHeroes"].Integer(); + mainHeroTurnDistanceLimit = node["mainHeroTurnDistanceLimit"].Integer(); + scoutHeroTurnDistanceLimit = node["scoutHeroTurnDistanceLimit"].Integer(); + maxpass = node["maxpass"].Integer(); + pathfinderBucketsCount = node["pathfinderBucketsCount"].Integer(); + pathfinderBucketSize = node["pathfinderBucketSize"].Integer(); + maxGoldPressure = node["maxGoldPressure"].Float(); + retreatThresholdRelative = node["retreatThresholdRelative"].Float(); + retreatThresholdAbsolute = node["retreatThresholdAbsolute"].Float(); + safeAttackRatio = node["safeAttackRatio"].Float(); + allowObjectGraph = node["allowObjectGraph"].Bool(); + openMap = node["openMap"].Bool(); + useFuzzy = node["useFuzzy"].Bool(); + useTroopsFromGarrisons = node["useTroopsFromGarrisons"].Bool(); } } diff --git a/AI/Nullkiller/Engine/Settings.h b/AI/Nullkiller/Engine/Settings.h index 775f7f399..769c5ae91 100644 --- a/AI/Nullkiller/Engine/Settings.h +++ b/AI/Nullkiller/Engine/Settings.h @@ -25,21 +25,33 @@ namespace NKAI int mainHeroTurnDistanceLimit; int scoutHeroTurnDistanceLimit; int maxpass; + int pathfinderBucketsCount; + int pathfinderBucketSize; float maxGoldPressure; + float retreatThresholdRelative; + float retreatThresholdAbsolute; + float safeAttackRatio; bool allowObjectGraph; bool useTroopsFromGarrisons; bool openMap; + bool useFuzzy; public: - Settings(); + explicit Settings(int difficultyLevel); int getMaxPass() const { return maxpass; } float getMaxGoldPressure() const { return maxGoldPressure; } + float getRetreatThresholdRelative() const { return retreatThresholdRelative; } + float getRetreatThresholdAbsolute() const { return retreatThresholdAbsolute; } + float getSafeAttackRatio() const { return safeAttackRatio; } int getMaxRoamingHeroes() const { return maxRoamingHeroes; } int getMainHeroTurnDistanceLimit() const { return mainHeroTurnDistanceLimit; } int getScoutHeroTurnDistanceLimit() const { return scoutHeroTurnDistanceLimit; } + int getPathfinderBucketsCount() const { return pathfinderBucketsCount; } + int getPathfinderBucketSize() const { return pathfinderBucketSize; } bool isObjectGraphAllowed() const { return allowObjectGraph; } bool isGarrisonTroopsUsageAllowed() const { return useTroopsFromGarrisons; } bool isOpenMap() const { return openMap; } + bool isUseFuzzy() const { return useFuzzy; } }; } diff --git a/AI/Nullkiller/Goals/AbstractGoal.h b/AI/Nullkiller/Goals/AbstractGoal.h index b191f96a5..27adf052e 100644 --- a/AI/Nullkiller/Goals/AbstractGoal.h +++ b/AI/Nullkiller/Goals/AbstractGoal.h @@ -104,6 +104,7 @@ namespace Goals bool isAbstract; SETTER(bool, isAbstract) int value; SETTER(int, value) ui64 goldCost; SETTER(ui64, goldCost) + TResources buildingCost; SETTER(TResources, buildingCost) int resID; SETTER(int, resID) int objid; SETTER(int, objid) int aid; SETTER(int, aid) diff --git a/AI/Nullkiller/Goals/AdventureSpellCast.cpp b/AI/Nullkiller/Goals/AdventureSpellCast.cpp index 1868d7c60..8e8df0241 100644 --- a/AI/Nullkiller/Goals/AdventureSpellCast.cpp +++ b/AI/Nullkiller/Goals/AdventureSpellCast.cpp @@ -53,6 +53,9 @@ void AdventureSpellCast::accept(AIGateway * ai) throw cannotFulfillGoalException("The town is already occupied by " + town->visitingHero->getNameTranslated()); } + if (hero->inTownGarrison) + ai->myCb->swapGarrisonHero(hero->visitedTown); + auto wait = cb->waitTillRealize; cb->waitTillRealize = true; diff --git a/AI/Nullkiller/Goals/BuildThis.cpp b/AI/Nullkiller/Goals/BuildThis.cpp index c9c89c0a9..414c8a03d 100644 --- a/AI/Nullkiller/Goals/BuildThis.cpp +++ b/AI/Nullkiller/Goals/BuildThis.cpp @@ -23,7 +23,7 @@ BuildThis::BuildThis(BuildingID Bid, const CGTownInstance * tid) : ElementarGoal(Goals::BUILD_STRUCTURE) { buildingInfo = BuildingInfo( - tid->town->buildings.at(Bid), + tid->getTown()->buildings.at(Bid), nullptr, CreatureID::NONE, tid, @@ -52,7 +52,7 @@ void BuildThis::accept(AIGateway * ai) if(cb->canBuildStructure(town, b) == EBuildingState::ALLOWED) { logAi->debug("Player %d will build %s in town of %s at %s", - ai->playerID, town->town->buildings.at(b)->getNameTranslated(), town->getNameTranslated(), town->pos.toString()); + ai->playerID, town->getTown()->buildings.at(b)->getNameTranslated(), town->getNameTranslated(), town->anchorPos().toString()); cb->buildBuilding(town, b); return; diff --git a/AI/Nullkiller/Goals/CaptureObject.h b/AI/Nullkiller/Goals/CaptureObject.h index e219e37ec..2073cd2fe 100644 --- a/AI/Nullkiller/Goals/CaptureObject.h +++ b/AI/Nullkiller/Goals/CaptureObject.h @@ -31,7 +31,7 @@ namespace Goals { objid = obj->id.getNum(); tile = obj->visitablePos(); - name = obj->typeName; + name = obj->getTypeName(); } bool operator==(const CaptureObject & other) const override; diff --git a/AI/Nullkiller/Goals/ExchangeSwapTownHeroes.cpp b/AI/Nullkiller/Goals/ExchangeSwapTownHeroes.cpp index e03901910..5e7f8df63 100644 --- a/AI/Nullkiller/Goals/ExchangeSwapTownHeroes.cpp +++ b/AI/Nullkiller/Goals/ExchangeSwapTownHeroes.cpp @@ -90,9 +90,12 @@ void ExchangeSwapTownHeroes::accept(AIGateway * ai) if(!town->garrisonHero) { - while(upperArmy->stacksCount() != 0) + if (!garrisonHero->canBeMergedWith(*town)) { - cb->dismissCreature(upperArmy, upperArmy->Slots().begin()->first); + while (upperArmy->stacksCount() != 0) + { + cb->dismissCreature(upperArmy, upperArmy->Slots().begin()->first); + } } } diff --git a/AI/Nullkiller/Goals/ExecuteHeroChain.cpp b/AI/Nullkiller/Goals/ExecuteHeroChain.cpp index 8fe4851b2..0391a4585 100644 --- a/AI/Nullkiller/Goals/ExecuteHeroChain.cpp +++ b/AI/Nullkiller/Goals/ExecuteHeroChain.cpp @@ -22,6 +22,7 @@ ExecuteHeroChain::ExecuteHeroChain(const AIPath & path, const CGObjectInstance * { hero = path.targetHero; tile = path.targetTile(); + closestWayRatio = 1; if(obj) { @@ -30,7 +31,7 @@ ExecuteHeroChain::ExecuteHeroChain(const AIPath & path, const CGObjectInstance * #if NKAI_TRACE_LEVEL >= 1 targetName = obj->getObjectName() + tile.toString(); #else - targetName = obj->typeName + tile.toString(); + targetName = obj->getTypeName() + tile.toString(); #endif } else @@ -85,6 +86,7 @@ void ExecuteHeroChain::accept(AIGateway * ai) ai->nullkiller->setActive(chainPath.targetHero, tile); ai->nullkiller->setTargetObject(objid); + ai->nullkiller->objectClusterizer->reset(); auto targetObject = ai->myCb->getObj(static_cast(objid), false); diff --git a/AI/Nullkiller/Goals/RecruitHero.cpp b/AI/Nullkiller/Goals/RecruitHero.cpp index 5b55c55ff..810a6162c 100644 --- a/AI/Nullkiller/Goals/RecruitHero.cpp +++ b/AI/Nullkiller/Goals/RecruitHero.cpp @@ -73,6 +73,7 @@ void RecruitHero::accept(AIGateway * ai) std::unique_lock lockGuard(ai->nullkiller->aiStateMutex); ai->nullkiller->heroManager->update(); + ai->nullkiller->objectClusterizer->reset(); } } diff --git a/AI/Nullkiller/Goals/RecruitHero.h b/AI/Nullkiller/Goals/RecruitHero.h index 101588f19..c49644948 100644 --- a/AI/Nullkiller/Goals/RecruitHero.h +++ b/AI/Nullkiller/Goals/RecruitHero.h @@ -44,6 +44,7 @@ namespace Goals } std::string toString() const override; + const CGHeroInstance* getHero() const override { return heroToBuy; } void accept(AIGateway * ai) override; }; } diff --git a/AI/Nullkiller/Goals/StayAtTown.cpp b/AI/Nullkiller/Goals/StayAtTown.cpp index 346b2c44d..817ddbe1c 100644 --- a/AI/Nullkiller/Goals/StayAtTown.cpp +++ b/AI/Nullkiller/Goals/StayAtTown.cpp @@ -36,16 +36,12 @@ std::string StayAtTown::toString() const { return "Stay at town " + town->getNameTranslated() + " hero " + hero->getNameTranslated() - + ", mana: " + std::to_string(hero->mana); + + ", mana: " + std::to_string(hero->mana) + + " / " + std::to_string(hero->manaLimit()); } void StayAtTown::accept(AIGateway * ai) { - if(hero->visitedTown != town) - { - logAi->error("Hero %s expected visiting town %s", hero->getNameTranslated(), town->getNameTranslated()); - } - ai->nullkiller->lockHero(hero, HeroLockedReason::DEFENCE); } diff --git a/AI/Nullkiller/Helpers/ExplorationHelper.cpp b/AI/Nullkiller/Helpers/ExplorationHelper.cpp index 75f40e9cb..0c17e0cc2 100644 --- a/AI/Nullkiller/Helpers/ExplorationHelper.cpp +++ b/AI/Nullkiller/Helpers/ExplorationHelper.cpp @@ -175,7 +175,7 @@ void ExplorationHelper::scanTile(const int3 & tile) continue; } - if(isSafeToVisit(hero, path.heroArmy, path.getTotalDanger())) + if(isSafeToVisit(hero, path.heroArmy, path.getTotalDanger(), ai->settings->getSafeAttackRatio())) { bestGoal = goal; bestValue = ourValue; diff --git a/AI/Nullkiller/Pathfinding/AINodeStorage.cpp b/AI/Nullkiller/Pathfinding/AINodeStorage.cpp index c7c318c95..a40fbd7d2 100644 --- a/AI/Nullkiller/Pathfinding/AINodeStorage.cpp +++ b/AI/Nullkiller/Pathfinding/AINodeStorage.cpp @@ -39,17 +39,17 @@ const uint64_t CHAIN_MAX_DEPTH = 4; const bool DO_NOT_SAVE_TO_COMMITTED_TILES = false; -AISharedStorage::AISharedStorage(int3 sizes) +AISharedStorage::AISharedStorage(int3 sizes, int numChains) { if(!shared){ shared.reset(new boost::multi_array( - boost::extents[sizes.z][sizes.x][sizes.y][AIPathfinding::NUM_CHAINS])); + boost::extents[sizes.z][sizes.x][sizes.y][numChains])); nodes = shared; foreach_tile_pos([&](const int3 & pos) { - for(auto i = 0; i < AIPathfinding::NUM_CHAINS; i++) + for(auto i = 0; i < numChains; i++) { auto & node = get(pos)[i]; @@ -92,8 +92,18 @@ void AIPathNode::addSpecialAction(std::shared_ptr action) } } +int AINodeStorage::getBucketCount() const +{ + return ai->settings->getPathfinderBucketsCount(); +} + +int AINodeStorage::getBucketSize() const +{ + return ai->settings->getPathfinderBucketSize(); +} + AINodeStorage::AINodeStorage(const Nullkiller * ai, const int3 & Sizes) - : sizes(Sizes), ai(ai), cb(ai->cb.get()), nodes(Sizes) + : sizes(Sizes), ai(ai), cb(ai->cb.get()), nodes(Sizes, ai->settings->getPathfinderBucketSize() * ai->settings->getPathfinderBucketsCount()) { accessibility = std::make_unique>( boost::extents[sizes.z][sizes.x][sizes.y][EPathfindingLayer::NUM_LAYERS]); @@ -130,10 +140,10 @@ void AINodeStorage::initialize(const PathfinderOptions & options, const CGameSta for(pos.y = 0; pos.y < sizes.y; ++pos.y) { const TerrainTile & tile = gs->map->getTile(pos); - if (!tile.terType->isPassable()) + if (!tile.getTerrain()->isPassable()) continue; - if (tile.terType->isWater()) + if (tile.isWater()) { resetTile(pos, ELayer::SAIL, PathfinderUtil::evaluateAccessibility(pos, tile, fow, player, gs)); if (useFlying) @@ -169,8 +179,8 @@ std::optional AINodeStorage::getOrCreateNode( const EPathfindingLayer layer, const ChainActor * actor) { - int bucketIndex = ((uintptr_t)actor + static_cast(layer)) % AIPathfinding::BUCKET_COUNT; - int bucketOffset = bucketIndex * AIPathfinding::BUCKET_SIZE; + int bucketIndex = ((uintptr_t)actor + static_cast(layer)) % ai->settings->getPathfinderBucketsCount(); + int bucketOffset = bucketIndex * ai->settings->getPathfinderBucketSize(); auto chains = nodes.get(pos); if(blocked(pos, layer)) @@ -178,7 +188,7 @@ std::optional AINodeStorage::getOrCreateNode( return std::nullopt; } - for(auto i = AIPathfinding::BUCKET_SIZE - 1; i >= 0; i--) + for(auto i = ai->settings->getPathfinderBucketSize() - 1; i >= 0; i--) { AIPathNode & node = chains[i + bucketOffset]; @@ -486,8 +496,8 @@ public: AINodeStorage & storage, const std::vector & tiles, uint64_t chainMask, int heroChainTurn) :existingChains(), newChains(), delayedWork(), storage(storage), chainMask(chainMask), heroChainTurn(heroChainTurn), heroChain(), tiles(tiles) { - existingChains.reserve(AIPathfinding::NUM_CHAINS); - newChains.reserve(AIPathfinding::NUM_CHAINS); + existingChains.reserve(storage.getBucketCount() * storage.getBucketSize()); + newChains.reserve(storage.getBucketCount() * storage.getBucketSize()); } void execute(const tbb::blocked_range& r) @@ -719,6 +729,7 @@ void HeroChainCalculationTask::calculateHeroChain( if(node->action == EPathNodeAction::BATTLE || node->action == EPathNodeAction::TELEPORT_BATTLE || node->action == EPathNodeAction::TELEPORT_NORMAL + || node->action == EPathNodeAction::DISEMBARK || node->action == EPathNodeAction::TELEPORT_BLOCKING_VISIT) { continue; @@ -961,7 +972,7 @@ void AINodeStorage::setHeroes(std::map heroes) // do not allow our own heroes in garrison to act on map if(hero.first->getOwner() == ai->playerID && hero.first->inTownGarrison - && (ai->isHeroLocked(hero.first) || ai->heroManager->heroCapReached())) + && (ai->isHeroLocked(hero.first) || ai->heroManager->heroCapReached(false))) { continue; } @@ -1196,6 +1207,11 @@ void AINodeStorage::calculateTownPortal( continue; } + if (targetTown->visitingHero + && (targetTown->visitingHero.get()->getFactionID() != actor->hero->getFactionID() + || targetTown->getUpperArmy()->stacksCount())) + continue; + auto nodeOptional = townPortalFinder.createTownPortalNode(targetTown); if(nodeOptional) @@ -1418,6 +1434,10 @@ void AINodeStorage::calculateChainInfo(std::vector & paths, const int3 & path.heroArmy = node.actor->creatureSet; path.armyLoss = node.armyLoss; path.targetObjectDanger = ai->dangerEvaluator->evaluateDanger(pos, path.targetHero, !node.actor->allowBattle); + for (auto pathNode : path.nodes) + { + path.targetObjectDanger = std::max(ai->dangerEvaluator->evaluateDanger(pathNode.coord, path.targetHero, !node.actor->allowBattle), path.targetObjectDanger); + } if(path.targetObjectDanger > 0) { @@ -1564,7 +1584,7 @@ uint8_t AIPath::turn() const uint64_t AIPath::getHeroStrength() const { - return targetHero->getFightingStrength() * getHeroArmyStrengthWithCommander(targetHero, heroArmy); + return targetHero->getHeroStrength() * getHeroArmyStrengthWithCommander(targetHero, heroArmy); } uint64_t AIPath::getTotalDanger() const diff --git a/AI/Nullkiller/Pathfinding/AINodeStorage.h b/AI/Nullkiller/Pathfinding/AINodeStorage.h index a7352acdb..2761cc692 100644 --- a/AI/Nullkiller/Pathfinding/AINodeStorage.h +++ b/AI/Nullkiller/Pathfinding/AINodeStorage.h @@ -29,9 +29,6 @@ namespace NKAI { namespace AIPathfinding { - const int BUCKET_COUNT = 3; - const int BUCKET_SIZE = 7; - const int NUM_CHAINS = BUCKET_COUNT * BUCKET_SIZE; const int CHAIN_MAX_DEPTH = 4; } @@ -157,7 +154,7 @@ public: static boost::mutex locker; static uint32_t version; - AISharedStorage(int3 mapSize); + AISharedStorage(int3 sizes, int numChains); ~AISharedStorage(); STRONG_INLINE @@ -197,6 +194,9 @@ public: bool selectFirstActor(); bool selectNextActor(); + int getBucketCount() const; + int getBucketSize() const; + std::vector getInitialNodes() override; virtual void calculateNeighbours( @@ -298,7 +298,7 @@ public: inline int getBucket(const ChainActor * actor) const { - return ((uintptr_t)actor * 395) % AIPathfinding::BUCKET_COUNT; + return ((uintptr_t)actor * 395) % getBucketCount(); } void calculateTownPortalTeleportations(std::vector & neighbours); diff --git a/AI/Nullkiller/Pathfinding/AIPathfinderConfig.cpp b/AI/Nullkiller/Pathfinding/AIPathfinderConfig.cpp index 98bcddcec..7bb43fabb 100644 --- a/AI/Nullkiller/Pathfinding/AIPathfinderConfig.cpp +++ b/AI/Nullkiller/Pathfinding/AIPathfinderConfig.cpp @@ -13,7 +13,7 @@ #include "Rules/AIMovementAfterDestinationRule.h" #include "Rules/AIMovementToDestinationRule.h" #include "Rules/AIPreviousNodeRule.h" -#include "../Engine//Nullkiller.h" +#include "../Engine/Nullkiller.h" #include "../../../lib/pathfinder/CPathfinder.h" diff --git a/AI/Nullkiller/Pathfinding/Actors.cpp b/AI/Nullkiller/Pathfinding/Actors.cpp index ae6b6446e..4d3a86a28 100644 --- a/AI/Nullkiller/Pathfinding/Actors.cpp +++ b/AI/Nullkiller/Pathfinding/Actors.cpp @@ -46,7 +46,7 @@ ChainActor::ChainActor(const CGHeroInstance * hero, HeroRole heroRole, uint64_t initialMovement = hero->movementPointsRemaining(); initialTurn = 0; armyValue = getHeroArmyStrengthWithCommander(hero, hero); - heroFightingStrength = hero->getFightingStrength(); + heroFightingStrength = hero->getHeroStrength(); tiCache.reset(new TurnInfo(hero)); } @@ -182,7 +182,7 @@ ExchangeResult HeroActor::tryExchangeNoLock(const ChainActor * specialActor, con return &actor == specialActor; }); - result.actor = &(dynamic_cast(result.actor)->specialActors[index]); + result.actor = &(dynamic_cast(result.actor)->specialActors.at(index)); return result; } @@ -440,7 +440,7 @@ int DwellingActor::getInitialTurn(bool waitForGrowth, int dayOfWeek) std::string DwellingActor::toString() const { - return dwelling->typeName + dwelling->visitablePos().toString(); + return dwelling->getTypeName() + dwelling->visitablePos().toString(); } CCreatureSet * DwellingActor::getDwellingCreatures(const CGDwelling * dwelling, bool waitForGrowth) diff --git a/AI/Nullkiller/Pathfinding/Actors.h b/AI/Nullkiller/Pathfinding/Actors.h index 4451bda24..1f653fbd3 100644 --- a/AI/Nullkiller/Pathfinding/Actors.h +++ b/AI/Nullkiller/Pathfinding/Actors.h @@ -113,7 +113,7 @@ public: static const int SPECIAL_ACTORS_COUNT = 7; private: - ChainActor specialActors[SPECIAL_ACTORS_COUNT]; + std::array specialActors; std::unique_ptr exchangeMap; void setupSpecialActors(); diff --git a/AI/VCAI/AIUtility.cpp b/AI/VCAI/AIUtility.cpp index a7c4c2f7a..ff391587f 100644 --- a/AI/VCAI/AIUtility.cpp +++ b/AI/VCAI/AIUtility.cpp @@ -15,7 +15,6 @@ #include "../../lib/UnlockGuard.h" #include "../../lib/CConfigHandler.h" -#include "../../lib/CHeroHandler.h" #include "../../lib/mapObjects/CGTownInstance.h" #include "../../lib/mapObjects/CQuest.h" #include "../../lib/mapping/CMapDefines.h" @@ -187,7 +186,7 @@ bool canBeEmbarkmentPoint(const TerrainTile * t, bool fromWater) { // TODO: Such information should be provided by pathfinder // Tile must be free or with unoccupied boat - if(!t->blocked) + if(!t->blocked()) { return true; } @@ -248,8 +247,8 @@ bool compareArmyStrength(const CArmedInstance * a1, const CArmedInstance * a2) bool compareArtifacts(const CArtifactInstance * a1, const CArtifactInstance * a2) { - auto art1 = a1->artType; - auto art2 = a2->artType; + auto art1 = a1->getType(); + auto art2 = a2->getType(); if(art1->getPrice() == art2->getPrice()) return art1->valOfBonuses(BonusType::PRIMARY_SKILL) > art2->valOfBonuses(BonusType::PRIMARY_SKILL); diff --git a/AI/VCAI/AIUtility.h b/AI/VCAI/AIUtility.h index 18b6133b4..c1a19d858 100644 --- a/AI/VCAI/AIUtility.h +++ b/AI/VCAI/AIUtility.h @@ -25,11 +25,9 @@ using crstring = const std::string &; using dwellingContent = std::pair>; const int ACTUAL_RESOURCE_COUNT = 7; -const int ALLOWED_ROAMING_HEROES = 8; //implementation-dependent extern const double SAFE_ATTACK_CONSTANT; -extern const int GOLD_RESERVE; extern thread_local CCallback * cb; extern thread_local VCAI * ai; diff --git a/AI/VCAI/ArmyManager.cpp b/AI/VCAI/ArmyManager.cpp index 72ab24d6b..efe6a17ea 100644 --- a/AI/VCAI/ArmyManager.cpp +++ b/AI/VCAI/ArmyManager.cpp @@ -36,7 +36,7 @@ std::vector ArmyManager::getSortedSlots(const CCreatureSet * target, c { for(auto & i : armyPtr->Slots()) { - auto cre = dynamic_cast(i.second->type); + auto cre = dynamic_cast(i.second->getType()); auto & slotInfp = creToPower[cre]; slotInfp.creature = cre; diff --git a/AI/VCAI/BuildingManager.cpp b/AI/VCAI/BuildingManager.cpp index 202661c33..60d971086 100644 --- a/AI/VCAI/BuildingManager.cpp +++ b/AI/VCAI/BuildingManager.cpp @@ -23,13 +23,13 @@ bool BuildingManager::tryBuildThisStructure(const CGTownInstance * t, BuildingID return false; } - if (!vstd::contains(t->town->buildings, building)) + if (!vstd::contains(t->getTown()->buildings, building)) return false; // no such building in town if (t->hasBuilt(building)) //Already built? Shouldn't happen in general return true; - const CBuilding * buildPtr = t->town->buildings.at(building); + const CBuilding * buildPtr = t->getTown()->buildings.at(building); auto toBuild = buildPtr->requirements.getFulfillmentCandidates([&](const BuildingID & buildID) { @@ -51,7 +51,7 @@ bool BuildingManager::tryBuildThisStructure(const CGTownInstance * t, BuildingID for (const auto & buildID : toBuild) { - const CBuilding * b = t->town->buildings.at(buildID); + const CBuilding * b = t->getTown()->buildings.at(buildID); EBuildingState canBuild = cb->canBuildStructure(t, buildID); if (canBuild == EBuildingState::ALLOWED) @@ -220,7 +220,7 @@ bool BuildingManager::getBuildingOptions(const CGTownInstance * t) //at the end, try to get and build any extra buildings with nonstandard slots (for example HotA 3rd level dwelling) std::vector extraBuildings; - for (auto buildingInfo : t->town->buildings) + for (auto buildingInfo : t->getTown()->buildings) { if (buildingInfo.first > BuildingID::DWELL_UP2_FIRST) extraBuildings.push_back(buildingInfo.first); diff --git a/AI/VCAI/Goals/BuildThis.cpp b/AI/VCAI/Goals/BuildThis.cpp index ee1e3d41a..62e3a1649 100644 --- a/AI/VCAI/Goals/BuildThis.cpp +++ b/AI/VCAI/Goals/BuildThis.cpp @@ -56,7 +56,7 @@ TSubgoal BuildThis::whatToDoToAchieve() case EBuildingState::ALLOWED: case EBuildingState::NO_RESOURCES: { - auto res = town->town->buildings.at(BuildingID(bid))->resources; + auto res = town->getTown()->buildings.at(BuildingID(bid))->resources; return ai->ah->whatToDo(res, iAmElementar()); //realize immediately or gather resources } break; diff --git a/AI/VCAI/Goals/CompleteQuest.cpp b/AI/VCAI/Goals/CompleteQuest.cpp index 39fa64245..fc085052d 100644 --- a/AI/VCAI/Goals/CompleteQuest.cpp +++ b/AI/VCAI/Goals/CompleteQuest.cpp @@ -162,7 +162,7 @@ TGoalVec CompleteQuest::missionArmy() const for(auto creature : q.quest->mission.creatures) { - solutions.push_back(sptr(GatherTroops(creature.type->getId(), creature.count))); + solutions.push_back(sptr(GatherTroops(creature.getId(), creature.count))); } return solutions; diff --git a/AI/VCAI/Goals/FindObj.cpp b/AI/VCAI/Goals/FindObj.cpp index 189a5c44b..288474eaa 100644 --- a/AI/VCAI/Goals/FindObj.cpp +++ b/AI/VCAI/Goals/FindObj.cpp @@ -46,7 +46,7 @@ TSubgoal FindObj::whatToDoToAchieve() } } } - if(o && ai->isAccessible(o->pos)) //we don't use isAccessibleForHero as we don't know which hero it is + if(o && ai->isAccessible(o->visitablePos())) //we don't use isAccessibleForHero as we don't know which hero it is return sptr(VisitObj(o->id.getNum())); else return sptr(Explore()); diff --git a/AI/VCAI/Goals/GatherTroops.cpp b/AI/VCAI/Goals/GatherTroops.cpp index 275bef9f5..50b8fdce5 100644 --- a/AI/VCAI/Goals/GatherTroops.cpp +++ b/AI/VCAI/Goals/GatherTroops.cpp @@ -88,13 +88,13 @@ TGoalVec GatherTroops::getAllPossibleSubgoals() } auto creature = VLC->creatures()->getByIndex(objid); - if(t->getFaction() == creature->getFaction()) //TODO: how to force AI to build unupgraded creatures? :O + if(t->getFactionID() == creature->getFactionID()) //TODO: how to force AI to build unupgraded creatures? :O { auto tryFindCreature = [&]() -> std::optional> { - if(vstd::isValidIndex(t->town->creatures, creature->getLevel() - 1)) + if(vstd::isValidIndex(t->getTown()->creatures, creature->getLevel() - 1)) { - auto itr = t->town->creatures.begin(); + auto itr = t->getTown()->creatures.begin(); std::advance(itr, creature->getLevel() - 1); return make_optional(*itr); } @@ -109,7 +109,7 @@ TGoalVec GatherTroops::getAllPossibleSubgoals() if(upgradeNumber < 0) continue; - BuildingID bid(BuildingID::DWELL_FIRST + creature->getLevel() - 1 + upgradeNumber * t->town->creatures.size()); + BuildingID bid(BuildingID::DWELL_FIRST + creature->getLevel() - 1 + upgradeNumber * t->getTown()->creatures.size()); if(t->hasBuilt(bid) && ai->ah->freeResources().canAfford(creature->getFullRecruitCost())) //this assumes only creatures with dwellings are assigned to faction { solutions.push_back(sptr(BuyArmy(t, creature->getAIValue() * this->value).setobjid(objid))); diff --git a/AI/VCAI/MapObjectsEvaluator.cpp b/AI/VCAI/MapObjectsEvaluator.cpp index 3d6382559..9833dc45d 100644 --- a/AI/VCAI/MapObjectsEvaluator.cpp +++ b/AI/VCAI/MapObjectsEvaluator.cpp @@ -12,7 +12,7 @@ #include "../../lib/GameConstants.h" #include "../../lib/VCMI_Lib.h" #include "../../lib/CCreatureHandler.h" -#include "../../lib/CHeroHandler.h" +#include "../../lib/mapObjects/CompoundMapObjectID.h" #include "../../lib/mapObjectConstructors/AObjectTypeHandler.h" #include "../../lib/mapObjects/CGHeroInstance.h" #include "../../lib/mapObjects/CGTownInstance.h" @@ -68,7 +68,7 @@ std::optional MapObjectsEvaluator::getObjectValue(const CGObjectInstance * { //special case handling: in-game heroes have hero ID as object subID, but when reading configs available hero object subID's are hero classes auto hero = dynamic_cast(obj); - return getObjectValue(obj->ID, hero->type->heroClass->getIndex()); + return getObjectValue(obj->ID, hero->getHeroClassID()); } else if(obj->ID == Obj::PRISON) { @@ -92,7 +92,7 @@ std::optional MapObjectsEvaluator::getObjectValue(const CGObjectInstance * else if(obj->ID == Obj::ARTIFACT) { auto artifactObject = dynamic_cast(obj); - switch(artifactObject->storedArtifact->artType->aClass) + switch(artifactObject->storedArtifact->getType()->aClass) { case CArtifact::EartClass::ART_TREASURE: return 2000; diff --git a/AI/VCAI/Pathfinding/AINodeStorage.cpp b/AI/VCAI/Pathfinding/AINodeStorage.cpp index 63bd2bd4e..1ff94d8c0 100644 --- a/AI/VCAI/Pathfinding/AINodeStorage.cpp +++ b/AI/VCAI/Pathfinding/AINodeStorage.cpp @@ -46,10 +46,10 @@ void AINodeStorage::initialize(const PathfinderOptions & options, const CGameSta for(pos.y=0; pos.y < sizes.y; ++pos.y) { const TerrainTile & tile = gs->map->getTile(pos); - if(!tile.terType->isPassable()) + if(!tile.getTerrain()->isPassable()) continue; - if(tile.terType->isWater()) + if(tile.getTerrain()->isWater()) { resetTile(pos, ELayer::SAIL, PathfinderUtil::evaluateAccessibility(pos, tile, fow, player, gs)); if(useFlying) diff --git a/AI/VCAI/ResourceManager.cpp b/AI/VCAI/ResourceManager.cpp index e8f9b75ce..44c0e53d8 100644 --- a/AI/VCAI/ResourceManager.cpp +++ b/AI/VCAI/ResourceManager.cpp @@ -14,8 +14,6 @@ #include "../../CCallback.h" #include "../../lib/mapObjects/MapObjects.h" -#define GOLD_RESERVE (10000); //at least we'll be able to reach capitol - ResourceObjective::ResourceObjective(const TResources & Res, Goals::TSubgoal Goal) : resources(Res), goal(Goal) { diff --git a/AI/VCAI/VCAI.cpp b/AI/VCAI/VCAI.cpp index 3757c56d3..8af203906 100644 --- a/AI/VCAI/VCAI.cpp +++ b/AI/VCAI/VCAI.cpp @@ -20,7 +20,6 @@ #include "../../lib/mapObjects/MapObjects.h" #include "../../lib/mapObjects/ObjectTemplate.h" #include "../../lib/CConfigHandler.h" -#include "../../lib/CHeroHandler.h" #include "../../lib/IGameSettings.h" #include "../../lib/gameState/CGameState.h" #include "../../lib/bonuses/Limiters.h" @@ -732,7 +731,7 @@ void VCAI::showGarrisonDialog(const CArmedInstance * up, const CGHeroInstance * //you can't request action from action-response thread requestActionASAP([=]() { - if(removableUnits && !cb->getStartInfo()->isSteadwickFallCampaignMission()) + if(removableUnits && !cb->getStartInfo()->isRestorationOfErathiaCampaign()) pickBestCreatures(down, up); answerQuery(queryID, 0); @@ -1032,7 +1031,7 @@ void VCAI::mainLoop() void VCAI::performObjectInteraction(const CGObjectInstance * obj, HeroPtr h) { - LOG_TRACE_PARAMS(logAi, "Hero %s and object %s at %s", h->getNameTranslated() % obj->getObjectName() % obj->pos.toString()); + LOG_TRACE_PARAMS(logAi, "Hero %s and object %s at %s", h->getNameTranslated() % obj->getObjectName() % obj->anchorPos().toString()); switch(obj->ID) { case Obj::TOWN: @@ -1181,7 +1180,7 @@ void VCAI::pickBestArtifacts(const CGHeroInstance * h, const CGHeroInstance * ot //FIXME: why are the above possible to be null? bool emptySlotFound = false; - for(auto slot : artifact->artType->getPossibleSlots().at(target->bearerType())) + for(auto slot : artifact->getType()->getPossibleSlots().at(target->bearerType())) { if(target->isPositionFree(slot) && artifact->canBePutAt(target, slot, true)) //combined artifacts are not always allowed to move { @@ -1194,7 +1193,7 @@ void VCAI::pickBestArtifacts(const CGHeroInstance * h, const CGHeroInstance * ot } if(!emptySlotFound) //try to put that atifact in already occupied slot { - for(auto slot : artifact->artType->getPossibleSlots().at(target->bearerType())) + for(auto slot : artifact->getType()->getPossibleSlots().at(target->bearerType())) { auto otherSlot = target->getSlot(slot); if(otherSlot && otherSlot->artifact) //we need to exchange artifact for better one @@ -1315,8 +1314,6 @@ bool VCAI::canRecruitAnyHero(const CGTownInstance * t) const return false; if(cb->getResourceAmount(EGameResID::GOLD) < GameConstants::HERO_GOLD_COST) //TODO: use ResourceManager return false; - if(cb->getHeroesInfo().size() >= ALLOWED_ROAMING_HEROES) - return false; if(cb->getHeroesInfo().size() >= cb->getSettings().getInteger(EGameSettings::HEROES_PER_PLAYER_ON_MAP_CAP)) return false; if(!cb->getAvailableHeroes(t).size()) @@ -1417,11 +1414,11 @@ void VCAI::wander(HeroPtr h) //TODO pick the truly best const CGTownInstance * t = *boost::max_element(townsNotReachable, compareReinforcements); logAi->debug("%s can't reach any town, we'll try to make our way to %s at %s", h->getNameTranslated(), t->getNameTranslated(), t->visitablePos().toString()); - int3 pos1 = h->pos; + int3 posBefore = h->visitablePos(); striveToGoal(sptr(Goals::ClearWayTo(t->visitablePos()).sethero(h))); //TODO: drop "strive", add to mainLoop //if out hero is stuck, we may need to request another hero to clear the way we see - if(pos1 == h->pos && h == primaryHero()) //hero can't move + if(posBefore == h->visitablePos() && h == primaryHero()) //hero can't move { if(canRecruitAnyHero(t)) recruitHero(t); @@ -1471,7 +1468,7 @@ void VCAI::wander(HeroPtr h) { auto chosenObject = cb->getObjInstance(ObjectInstanceID(bestObjectGoal->objid)); if(chosenObject != nullptr) - logAi->debug("Of all %d destinations, object %s at pos=%s seems nice", dests.size(), chosenObject->getObjectName(), chosenObject->pos.toString()); + logAi->debug("Of all %d destinations, object %s at pos=%s seems nice", dests.size(), chosenObject->getObjectName(), chosenObject->anchorPos().toString()); } else logAi->debug("Trying to realize goal of type %s as part of wandering.", bestObjectGoal->name()); @@ -1994,8 +1991,8 @@ bool VCAI::moveHeroToTile(int3 dst, HeroPtr h) void VCAI::buildStructure(const CGTownInstance * t, BuildingID building) { - auto name = t->town->buildings.at(building)->getNameTranslated(); - logAi->debug("Player %d will build %s in town of %s at %s", ai->playerID, name, t->getNameTranslated(), t->pos.toString()); + auto name = t->getTown()->buildings.at(building)->getNameTranslated(); + logAi->debug("Player %d will build %s in town of %s at %s", ai->playerID, name, t->getNameTranslated(), t->anchorPos().toString()); cb->buildBuilding(t, building); //just do this; } @@ -2081,7 +2078,7 @@ void VCAI::tryRealize(Goals::BuildThis & g) if (cb->canBuildStructure(t, b) == EBuildingState::ALLOWED) { logAi->debug("Player %d will build %s in town of %s at %s", - playerID, t->town->buildings.at(b)->getNameTranslated(), t->getNameTranslated(), t->pos.toString()); + playerID, t->getTown()->buildings.at(b)->getNameTranslated(), t->getNameTranslated(), t->anchorPos().toString()); cb->buildBuilding(t, b); throw goalFulfilledException(sptr(g)); } @@ -2819,7 +2816,7 @@ bool shouldVisit(HeroPtr h, const CGObjectInstance * obj) { for(auto slot : h->Slots()) { - if(slot.second->type->hasUpgrades()) + if(slot.second->getType()->hasUpgrades()) return true; //TODO: check price? } return false; diff --git a/AUTHORS.h b/AUTHORS.h index a9ff41e66..fb6552959 100644 --- a/AUTHORS.h +++ b/AUTHORS.h @@ -46,6 +46,7 @@ const std::vector> contributors = { { "Developing", "", "vmarkovtsev", "" }, { "Developing", "Tom Zielinski", "Warmonger", "Warmonger@vp.pl" }, { "Developing", "Xiaomin Ding", "", "dingding303@gmail.com" }, + { "Developing", "Fenghuang Rumeng", "kdmcser", "zqtndfj@gmail.com" }, { "Testing", "Ben Yan", "by003", "benyan9110@gmail.com," }, { "Testing", "", "Misiokles", "" }, diff --git a/CCallback.cpp b/CCallback.cpp index 8d2709f3d..61c741cc8 100644 --- a/CCallback.cpp +++ b/CCallback.cpp @@ -18,31 +18,31 @@ #include "lib/mapObjects/CGHeroInstance.h" #include "lib/mapObjects/CGTownInstance.h" #include "lib/texts/CGeneralTextHandler.h" -#include "lib/CHeroHandler.h" #include "lib/CArtHandler.h" #include "lib/GameConstants.h" #include "lib/CPlayerState.h" #include "lib/UnlockGuard.h" #include "lib/battle/BattleInfo.h" #include "lib/networkPacks/PacksForServer.h" +#include "lib/networkPacks/SaveLocalState.h" bool CCallback::teleportHero(const CGHeroInstance *who, const CGTownInstance *where) { CastleTeleportHero pack(who->id, where->id, 1); - sendRequest(&pack); + sendRequest(pack); return true; } void CCallback::moveHero(const CGHeroInstance *h, const int3 & destination, bool transit) { MoveHero pack({destination}, h->id, transit); - sendRequest(&pack); + sendRequest(pack); } void CCallback::moveHero(const CGHeroInstance *h, const std::vector & path, bool transit) { MoveHero pack(path, h->id, transit); - sendRequest(&pack); + sendRequest(pack); } int CCallback::selectionMade(int selection, QueryID queryID) @@ -61,7 +61,7 @@ int CCallback::sendQueryReply(std::optional reply, QueryID queryID) QueryReply pack(queryID, reply); pack.player = *player; - return sendRequest(&pack); + return sendRequest(pack); } void CCallback::recruitCreatures(const CGDwelling * obj, const CArmedInstance * dst, CreatureID ID, ui32 amount, si32 level) @@ -71,7 +71,7 @@ void CCallback::recruitCreatures(const CGDwelling * obj, const CArmedInstance * return; RecruitCreatures pack(obj->id, dst->id, ID, amount, level); - sendRequest(&pack); + sendRequest(pack); } bool CCallback::dismissCreature(const CArmedInstance *obj, SlotID stackPos) @@ -80,14 +80,14 @@ bool CCallback::dismissCreature(const CArmedInstance *obj, SlotID stackPos) return false; DisbandCreature pack(stackPos,obj->id); - sendRequest(&pack); + sendRequest(pack); return true; } bool CCallback::upgradeCreature(const CArmedInstance *obj, SlotID stackPos, CreatureID newID) { UpgradeCreature pack(stackPos,obj->id,newID); - sendRequest(&pack); + sendRequest(pack); return false; } @@ -95,54 +95,54 @@ void CCallback::endTurn() { logGlobal->trace("Player %d ended his turn.", player->getNum()); EndTurn pack; - sendRequest(&pack); + sendRequest(pack); } int CCallback::swapCreatures(const CArmedInstance *s1, const CArmedInstance *s2, SlotID p1, SlotID p2) { ArrangeStacks pack(1,p1,p2,s1->id,s2->id,0); - sendRequest(&pack); + sendRequest(pack); return 0; } int CCallback::mergeStacks(const CArmedInstance *s1, const CArmedInstance *s2, SlotID p1, SlotID p2) { ArrangeStacks pack(2,p1,p2,s1->id,s2->id,0); - sendRequest(&pack); + sendRequest(pack); return 0; } int CCallback::splitStack(const CArmedInstance *s1, const CArmedInstance *s2, SlotID p1, SlotID p2, int val) { ArrangeStacks pack(3,p1,p2,s1->id,s2->id,val); - sendRequest(&pack); + sendRequest(pack); return 0; } int CCallback::bulkMoveArmy(ObjectInstanceID srcArmy, ObjectInstanceID destArmy, SlotID srcSlot) { BulkMoveArmy pack(srcArmy, destArmy, srcSlot); - sendRequest(&pack); + sendRequest(pack); return 0; } int CCallback::bulkSplitStack(ObjectInstanceID armyId, SlotID srcSlot, int howMany) { BulkSplitStack pack(armyId, srcSlot, howMany); - sendRequest(&pack); + sendRequest(pack); return 0; } int CCallback::bulkSmartSplitStack(ObjectInstanceID armyId, SlotID srcSlot) { BulkSmartSplitStack pack(armyId, srcSlot); - sendRequest(&pack); + sendRequest(pack); return 0; } int CCallback::bulkMergeStacks(ObjectInstanceID armyId, SlotID srcSlot) { BulkMergeStacks pack(armyId, srcSlot); - sendRequest(&pack); + sendRequest(pack); return 0; } @@ -151,7 +151,7 @@ bool CCallback::dismissHero(const CGHeroInstance *hero) if(player!=hero->tempOwner) return false; DismissHero pack(hero->id); - sendRequest(&pack); + sendRequest(pack); return true; } @@ -160,7 +160,7 @@ bool CCallback::swapArtifacts(const ArtifactLocation &l1, const ArtifactLocation ExchangeArtifacts ea; ea.src = l1; ea.dst = l2; - sendRequest(&ea); + sendRequest(ea); return true; } @@ -175,13 +175,13 @@ bool CCallback::swapArtifacts(const ArtifactLocation &l1, const ArtifactLocation void CCallback::assembleArtifacts(const ObjectInstanceID & heroID, ArtifactPosition artifactSlot, bool assemble, ArtifactID assembleTo) { AssembleArtifacts aa(heroID, artifactSlot, assemble, assembleTo); - sendRequest(&aa); + sendRequest(aa); } void CCallback::bulkMoveArtifacts(ObjectInstanceID srcHero, ObjectInstanceID dstHero, bool swap, bool equipped, bool backpack) { BulkExchangeArtifacts bma(srcHero, dstHero, swap, equipped, backpack); - sendRequest(&bma); + sendRequest(bma); } void CCallback::scrollBackpackArtifacts(ObjectInstanceID hero, bool left) @@ -189,19 +189,37 @@ void CCallback::scrollBackpackArtifacts(ObjectInstanceID hero, bool left) ManageBackpackArtifacts mba(hero, ManageBackpackArtifacts::ManageCmd::SCROLL_RIGHT); if(left) mba.cmd = ManageBackpackArtifacts::ManageCmd::SCROLL_LEFT; - sendRequest(&mba); + sendRequest(mba); +} + +void CCallback::sortBackpackArtifactsBySlot(const ObjectInstanceID hero) +{ + ManageBackpackArtifacts mba(hero, ManageBackpackArtifacts::ManageCmd::SORT_BY_SLOT); + sendRequest(mba); +} + +void CCallback::sortBackpackArtifactsByCost(const ObjectInstanceID hero) +{ + ManageBackpackArtifacts mba(hero, ManageBackpackArtifacts::ManageCmd::SORT_BY_COST); + sendRequest(mba); +} + +void CCallback::sortBackpackArtifactsByClass(const ObjectInstanceID hero) +{ + ManageBackpackArtifacts mba(hero, ManageBackpackArtifacts::ManageCmd::SORT_BY_CLASS); + sendRequest(mba); } void CCallback::manageHeroCostume(ObjectInstanceID hero, size_t costumeIndex, bool saveCostume) { ManageEquippedArtifacts mea(hero, costumeIndex, saveCostume); - sendRequest(&mea); + sendRequest(mea); } void CCallback::eraseArtifactByClient(const ArtifactLocation & al) { EraseArtifactByClient ea(al); - sendRequest(&ea); + sendRequest(ea); } bool CCallback::buildBuilding(const CGTownInstance *town, BuildingID buildingID) @@ -213,7 +231,7 @@ bool CCallback::buildBuilding(const CGTownInstance *town, BuildingID buildingID) return false; BuildStructure pack(town->id,buildingID); - sendRequest(&pack); + sendRequest(pack); return true; } @@ -223,7 +241,7 @@ bool CCallback::visitTownBuilding(const CGTownInstance *town, BuildingID buildin return false; VisitTownBuilding pack(town->id, buildingID); - sendRequest(&pack); + sendRequest(pack); return true; } @@ -232,10 +250,10 @@ void CBattleCallback::battleMakeSpellAction(const BattleID & battleID, const Bat assert(action.actionType == EActionType::HERO_SPELL); MakeAction mca(action); mca.battleID = battleID; - sendRequest(&mca); + sendRequest(mca); } -int CBattleCallback::sendRequest(const CPackForServer * request) +int CBattleCallback::sendRequest(const CPackForServer & request) { int requestID = cl->sendRequest(request, *getPlayerID()); if(waitTillRealize) @@ -249,12 +267,18 @@ int CBattleCallback::sendRequest(const CPackForServer * request) return requestID; } +void CCallback::spellResearch( const CGTownInstance *town, SpellID spellAtSlot, bool accepted ) +{ + SpellResearch pack(town->id, spellAtSlot, accepted); + sendRequest(pack); +} + void CCallback::swapGarrisonHero( const CGTownInstance *town ) { if(town->tempOwner == *player || (town->garrisonHero && town->garrisonHero->tempOwner == *player )) { GarrisonHeroSwap pack(town->id); - sendRequest(&pack); + sendRequest(pack); } } @@ -263,7 +287,7 @@ void CCallback::buyArtifact(const CGHeroInstance *hero, ArtifactID aid) if(hero->tempOwner != *player) return; BuyArtifact pack(hero->id,aid); - sendRequest(&pack); + sendRequest(pack); } void CCallback::trade(const ObjectInstanceID marketId, EMarketMode mode, TradeItemSell id1, TradeItemBuy id2, ui32 val1, const CGHeroInstance * hero) @@ -280,13 +304,13 @@ void CCallback::trade(const ObjectInstanceID marketId, EMarketMode mode, const s pack.r1 = id1; pack.r2 = id2; pack.val = val1; - sendRequest(&pack); + sendRequest(pack); } void CCallback::setFormation(const CGHeroInstance * hero, EArmyFormation mode) { SetFormation pack(hero->id, mode); - sendRequest(&pack); + sendRequest(pack); } void CCallback::recruitHero(const CGObjectInstance *townOrTavern, const CGHeroInstance *hero, const HeroTypeID & nextHero) @@ -294,9 +318,18 @@ void CCallback::recruitHero(const CGObjectInstance *townOrTavern, const CGHeroIn assert(townOrTavern); assert(hero); - HireHero pack(hero->getHeroType(), townOrTavern->id, nextHero); + HireHero pack(hero->getHeroTypeID(), townOrTavern->id, nextHero); pack.player = *player; - sendRequest(&pack); + sendRequest(pack); +} + +void CCallback::saveLocalState(const JsonNode & data) +{ + SaveLocalState state; + state.data = data; + state.player = *player; + + sendRequest(state); } void CCallback::save( const std::string &fname ) @@ -310,7 +343,7 @@ void CCallback::gamePause(bool pause) { GamePause pack; pack.player = *player; - sendRequest(&pack); + sendRequest(pack); } else { @@ -324,14 +357,14 @@ void CCallback::sendMessage(const std::string &mess, const CGObjectInstance * cu PlayerMessage pm(mess, currentObject? currentObject->id : ObjectInstanceID(-1)); if(player) pm.player = *player; - sendRequest(&pm); + sendRequest(pm); } void CCallback::buildBoat( const IShipyard *obj ) { BuildBoat bb; bb.objid = dynamic_cast(obj)->id; - sendRequest(&bb); + sendRequest(bb); } CCallback::CCallback(CGameState * GS, std::optional Player, CClient * C) @@ -373,7 +406,7 @@ void CCallback::dig( const CGObjectInstance *hero ) { DigWithHero dwh; dwh.id = hero->id; - sendRequest(&dwh); + sendRequest(dwh); } void CCallback::castSpell(const CGHeroInstance *hero, SpellID spellID, const int3 &pos) @@ -382,7 +415,7 @@ void CCallback::castSpell(const CGHeroInstance *hero, SpellID spellID, const int cas.hid = hero->id; cas.sid = spellID; cas.pos = pos; - sendRequest(&cas); + sendRequest(cas); } int CCallback::mergeOrSwapStacks(const CArmedInstance *s1, const CArmedInstance *s2, SlotID p1, SlotID p2) @@ -415,7 +448,7 @@ void CBattleCallback::battleMakeUnitAction(const BattleID & battleID, const Batt MakeAction ma; ma.ba = action; ma.battleID = battleID; - sendRequest(&ma); + sendRequest(ma); } void CBattleCallback::battleMakeTacticAction(const BattleID & battleID, const BattleAction & action ) @@ -424,7 +457,7 @@ void CBattleCallback::battleMakeTacticAction(const BattleID & battleID, const Ba MakeAction ma; ma.ba = action; ma.battleID = battleID; - sendRequest(&ma); + sendRequest(ma); } std::optional CBattleCallback::makeSurrenderRetreatDecision(const BattleID & battleID, const BattleStateInfoForRetreat & battleState) diff --git a/CCallback.h b/CCallback.h index a934113ce..6e30299c6 100644 --- a/CCallback.h +++ b/CCallback.h @@ -78,6 +78,7 @@ public: virtual bool visitTownBuilding(const CGTownInstance *town, BuildingID buildingID)=0; virtual void recruitCreatures(const CGDwelling *obj, const CArmedInstance * dst, CreatureID ID, ui32 amount, si32 level=-1)=0; virtual bool upgradeCreature(const CArmedInstance *obj, SlotID stackPos, CreatureID newID=CreatureID::NONE)=0; //if newID==-1 then best possible upgrade will be made + virtual void spellResearch(const CGTownInstance *town, SpellID spellAtSlot, bool accepted)=0; virtual void swapGarrisonHero(const CGTownInstance *town)=0; virtual void trade(const ObjectInstanceID marketId, EMarketMode mode, TradeItemSell id1, TradeItemBuy id2, ui32 val1, const CGHeroInstance * hero)=0; //mode==0: sell val1 units of id1 resource for id2 resiurce @@ -92,10 +93,14 @@ public: //virtual bool swapArtifacts(const CGHeroInstance * hero1, ui16 pos1, const CGHeroInstance * hero2, ui16 pos2)=0; //swaps artifacts between two given heroes virtual bool swapArtifacts(const ArtifactLocation &l1, const ArtifactLocation &l2)=0; virtual void scrollBackpackArtifacts(ObjectInstanceID hero, bool left) = 0; + virtual void sortBackpackArtifactsBySlot(const ObjectInstanceID hero) = 0; + virtual void sortBackpackArtifactsByCost(const ObjectInstanceID hero) = 0; + virtual void sortBackpackArtifactsByClass(const ObjectInstanceID hero) = 0; virtual void manageHeroCostume(ObjectInstanceID hero, size_t costumeIndex, bool saveCostume) = 0; virtual void assembleArtifacts(const ObjectInstanceID & heroID, ArtifactPosition artifactSlot, bool assemble, ArtifactID assembleTo)=0; virtual void eraseArtifactByClient(const ArtifactLocation & al)=0; virtual bool dismissCreature(const CArmedInstance *obj, SlotID stackPos)=0; + virtual void saveLocalState(const JsonNode & data)=0; virtual void endTurn()=0; virtual void buyArtifact(const CGHeroInstance *hero, ArtifactID aid)=0; //used to buy artifacts in towns (including spell book in the guild and war machines in blacksmith) virtual void setFormation(const CGHeroInstance * hero, EArmyFormation mode)=0; @@ -123,7 +128,7 @@ class CBattleCallback : public IBattleCallback std::optional player; protected: - int sendRequest(const CPackForServer * request); //returns requestID (that'll be matched to requestID in PackageApplied) + int sendRequest(const CPackForServer & request); //returns requestID (that'll be matched to requestID in PackageApplied) CClient *cl; public: @@ -179,6 +184,9 @@ public: void assembleArtifacts(const ObjectInstanceID & heroID, ArtifactPosition artifactSlot, bool assemble, ArtifactID assembleTo) override; void bulkMoveArtifacts(ObjectInstanceID srcHero, ObjectInstanceID dstHero, bool swap, bool equipped = true, bool backpack = true) override; void scrollBackpackArtifacts(ObjectInstanceID hero, bool left) override; + void sortBackpackArtifactsBySlot(const ObjectInstanceID hero) override; + void sortBackpackArtifactsByCost(const ObjectInstanceID hero) override; + void sortBackpackArtifactsByClass(const ObjectInstanceID hero) override; void manageHeroCostume(ObjectInstanceID hero, size_t costumeIdx, bool saveCostume) override; void eraseArtifactByClient(const ArtifactLocation & al) override; bool buildBuilding(const CGTownInstance *town, BuildingID buildingID) override; @@ -186,7 +194,9 @@ public: void recruitCreatures(const CGDwelling * obj, const CArmedInstance * dst, CreatureID ID, ui32 amount, si32 level=-1) override; bool dismissCreature(const CArmedInstance *obj, SlotID stackPos) override; bool upgradeCreature(const CArmedInstance *obj, SlotID stackPos, CreatureID newID=CreatureID::NONE) override; + void saveLocalState(const JsonNode & data) override; void endTurn() override; + void spellResearch(const CGTownInstance *town, SpellID spellAtSlot, bool accepted) override; void swapGarrisonHero(const CGTownInstance *town) override; void buyArtifact(const CGHeroInstance *hero, ArtifactID aid) override; void trade(const ObjectInstanceID marketId, EMarketMode mode, TradeItemSell id1, TradeItemBuy id2, ui32 val1, const CGHeroInstance * hero = nullptr) override; diff --git a/CI/android-32/before_install.sh b/CI/android-32/before_install.sh deleted file mode 100755 index 67cacaddf..000000000 --- a/CI/android-32/before_install.sh +++ /dev/null @@ -1,4 +0,0 @@ -#!/usr/bin/env bash - -DEPS_FILENAME=dependencies-android-32 -. CI/android/before_install.sh diff --git a/CI/android-64/before_install.sh b/CI/android-64/before_install.sh deleted file mode 100755 index af0a36874..000000000 --- a/CI/android-64/before_install.sh +++ /dev/null @@ -1,4 +0,0 @@ -#!/usr/bin/env bash - -DEPS_FILENAME=dependencies-android-64 -. CI/android/before_install.sh diff --git a/CI/android/before_install.sh b/CI/android/before_install.sh deleted file mode 100755 index 146d52110..000000000 --- a/CI/android/before_install.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/usr/bin/env bash - -echo "ANDROID_NDK_ROOT=$ANDROID_HOME/ndk/25.2.9519653" >> $GITHUB_ENV - -brew install ninja - -. CI/install_conan_dependencies.sh "$DEPS_FILENAME" \ No newline at end of file diff --git a/CI/before_install/android.sh b/CI/before_install/android.sh new file mode 100644 index 000000000..9fba7de9f --- /dev/null +++ b/CI/before_install/android.sh @@ -0,0 +1,4 @@ +#!/bin/sh + +sudo apt-get update +sudo apt-get install ninja-build diff --git a/CI/linux/before_install.sh b/CI/before_install/linux_qt5.sh similarity index 80% rename from CI/linux/before_install.sh rename to CI/before_install/linux_qt5.sh index 5df49a521..ebf9faeb1 100644 --- a/CI/linux/before_install.sh +++ b/CI/before_install/linux_qt5.sh @@ -1,6 +1,5 @@ #!/bin/sh -sudo apt remove needrestart sudo apt-get update # Dependencies @@ -9,6 +8,6 @@ sudo apt-get update # - debian build settings at debian/control sudo apt-get install libboost-dev libboost-filesystem-dev libboost-system-dev libboost-thread-dev libboost-program-options-dev libboost-locale-dev libboost-iostreams-dev \ libsdl2-dev libsdl2-image-dev libsdl2-mixer-dev libsdl2-ttf-dev \ -qtbase5-dev \ +qtbase5-dev qttools5-dev \ ninja-build zlib1g-dev libavformat-dev libswscale-dev libtbb-dev libluajit-5.1-dev \ -libminizip-dev libfuzzylite-dev qttools5-dev libsqlite3-dev # Optional dependencies +libminizip-dev libfuzzylite-dev libsqlite3-dev # Optional dependencies diff --git a/CI/linux-qt6/before_install.sh b/CI/before_install/linux_qt6.sh similarity index 76% rename from CI/linux-qt6/before_install.sh rename to CI/before_install/linux_qt6.sh index b88d42704..422b50e98 100644 --- a/CI/linux-qt6/before_install.sh +++ b/CI/before_install/linux_qt6.sh @@ -1,9 +1,11 @@ #!/bin/sh -sudo apt remove needrestart sudo apt-get update # Dependencies +# In case of change in dependencies list please also update: +# - developer docs at docs/developer/Building_Linux.md +# - debian build settings at debian/control sudo apt-get install libboost-dev libboost-filesystem-dev libboost-system-dev libboost-thread-dev libboost-program-options-dev libboost-locale-dev libboost-iostreams-dev \ libsdl2-dev libsdl2-image-dev libsdl2-mixer-dev libsdl2-ttf-dev \ qt6-base-dev qt6-base-dev-tools qt6-tools-dev qt6-tools-dev-tools qt6-l10n-tools \ diff --git a/CI/mac/before_install.sh b/CI/before_install/macos.sh old mode 100755 new mode 100644 similarity index 66% rename from CI/mac/before_install.sh rename to CI/before_install/macos.sh index ed11e87df..0664cc910 --- a/CI/mac/before_install.sh +++ b/CI/before_install/macos.sh @@ -3,5 +3,3 @@ echo DEVELOPER_DIR=/Applications/Xcode_14.2.app >> $GITHUB_ENV brew install ninja - -. CI/install_conan_dependencies.sh "$DEPS_FILENAME" diff --git a/CI/before_install/mingw.sh b/CI/before_install/mingw.sh new file mode 100644 index 000000000..30a865d49 --- /dev/null +++ b/CI/before_install/mingw.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash + +sudo apt-get update +sudo apt-get install ninja-build mingw-w64 nsis + +sudo update-alternatives --set i686-w64-mingw32-g++ /usr/bin/i686-w64-mingw32-g++-posix +sudo update-alternatives --set x86_64-w64-mingw32-g++ /usr/bin/x86_64-w64-mingw32-g++-posix diff --git a/CI/before_install/msvc.sh b/CI/before_install/msvc.sh new file mode 100644 index 000000000..a0f7687f6 --- /dev/null +++ b/CI/before_install/msvc.sh @@ -0,0 +1,17 @@ +#!/usr/bin/env bash + +MSVC_INSTALL_PATH=$(vswhere -latest -property installationPath) +echo "MSVC_INSTALL_PATH = $MSVC_INSTALL_PATH" +echo "Installed toolset versions:" +ls -vr "$MSVC_INSTALL_PATH/VC/Tools/MSVC" + +TOOLS_DIR=$(ls -vr "$MSVC_INSTALL_PATH/VC/Tools/MSVC/" | head -1) +DUMPBIN_PATH="$MSVC_INSTALL_PATH/VC/Tools/MSVC/$TOOLS_DIR/bin/Hostx64/x64/dumpbin.exe" + +# This command should work as well, but for some reason it is *extremely* slow on the Github CI (~7 minutes) +#DUMPBIN_PATH=$(vswhere -latest -find **/dumpbin.exe | head -n 1) + +echo "TOOLS_DIR = $TOOLS_DIR" +echo "DUMPBIN_PATH = $DUMPBIN_PATH" + +dirname "$DUMPBIN_PATH" > "$GITHUB_PATH" diff --git a/CI/conan/base/cross-macro.j2 b/CI/conan/base/cross-macro.j2 index 7f4edf0ee..ba3c53212 100644 --- a/CI/conan/base/cross-macro.j2 +++ b/CI/conan/base/cross-macro.j2 @@ -10,7 +10,7 @@ STRIP={{ target_host }}-strip {%- endmacro -%} {% macro generate_env_win32(target_host) -%} -CONAN_SYSTEM_LIBRARY_LOCATION=/usr/lib/gcc/{{ target_host }}/10-posix/ +CONAN_SYSTEM_LIBRARY_LOCATION=/usr/lib/gcc/{{ target_host }}/13-posix/ RC={{ target_host }}-windres {%- endmacro -%} diff --git a/CI/example.markdownlint-cli2.jsonc b/CI/example.markdownlint-cli2.jsonc new file mode 100644 index 000000000..13e8155eb --- /dev/null +++ b/CI/example.markdownlint-cli2.jsonc @@ -0,0 +1,280 @@ +{ + "config" : { + "default" : true, + + // MD001/heading-increment : Heading levels should only increment by one level at a time : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md001.md + "MD001": false, + + // MD003/heading-style : Heading style : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md003.md + "MD003": { + "style": "atx" + }, + + // MD004/ul-style : Unordered list style : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md004.md + "MD004": false, + // FIXME: enable and consider fixing + //{ + // "style": "consistent" + //}, + + // MD005/list-indent : Inconsistent indentation for list items at the same level : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md005.md + "MD005": true, + + // MD007/ul-indent : Unordered list indentation : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md007.md + "MD007": { + // Spaces for indent + "indent": 2, + // Whether to indent the first level of the list + "start_indented": false, + // Spaces for first level indent (when start_indented is set) + "start_indent": 0 + }, + + // MD009/no-trailing-spaces : Trailing spaces : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md009.md + "MD009": { + // Spaces for line break + "br_spaces": 2, + // Allow spaces for empty lines in list items + "list_item_empty_lines": false, + // Include unnecessary breaks + "strict": false + }, + + // MD010/no-hard-tabs : Hard tabs : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md010.md + "MD010": { + // Include code blocks + "code_blocks": false, + // Fenced code languages to ignore + "ignore_code_languages": [], + // Number of spaces for each hard tab + "spaces_per_tab": 4 + }, + + // MD011/no-reversed-links : Reversed link syntax : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md011.md + "MD011": true, + + // MD012/no-multiple-blanks : Multiple consecutive blank lines : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md012.md + "MD012": { + // Consecutive blank lines + "maximum": 1 + }, + + // MD013/line-length : Line length : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md013.md + "MD013": false, + + // MD014/commands-show-output : Dollar signs used before commands without showing output : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md014.md + "MD014": true, + + // MD018/no-missing-space-atx : No space after hash on atx style heading : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md018.md + "MD018": true, + + // MD019/no-multiple-space-atx : Multiple spaces after hash on atx style heading : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md019.md + "MD019": true, + + // MD020/no-missing-space-closed-atx : No space inside hashes on closed atx style heading : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md020.md + "MD020": true, + + // MD021/no-multiple-space-closed-atx : Multiple spaces inside hashes on closed atx style heading : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md021.md + "MD021": true, + + // MD022/blanks-around-headings : Headings should be surrounded by blank lines : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md022.md + "MD022": { + // Blank lines above heading + "lines_above": 1, + // Blank lines below heading + "lines_below": 1 + }, + + // MD023/heading-start-left : Headings must start at the beginning of the line : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md023.md + "MD023": true, + + // MD024/no-duplicate-heading : Multiple headings with the same content : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md024.md + "MD024": false, + // FIXME: false positives? + //{ + // // Only check sibling headings + // "allow_different_nesting": true, + // // Only check sibling headings + // "siblings_only": true + //}, + + // MD025/single-title/single-h1 : Multiple top-level headings in the same document : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md025.md + "MD025": { + // Heading level + "level": 1, + // RegExp for matching title in front matter + "front_matter_title": "^\\s*title\\s*[:=]" + }, + + // MD026/no-trailing-punctuation : Trailing punctuation in heading : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md026.md + "MD026": { + // Punctuation characters + "punctuation": ".,;:!。,;:!" + }, + + // MD027/no-multiple-space-blockquote : Multiple spaces after blockquote symbol : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md027.md + "MD027": true, + + // MD028/no-blanks-blockquote : Blank line inside blockquote : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md028.md + "MD028": true, + + // MD029/ol-prefix : Ordered list item prefix : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md029.md + "MD029": false, + // FIXME: false positives or broken formatting + //{ + // // List style + // "style": "ordered" + //}, + + // MD030/list-marker-space : Spaces after list markers : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md030.md + "MD030": { + // Spaces for single-line unordered list items + "ul_single": 1, + // Spaces for single-line ordered list items + "ol_single": 1, + // Spaces for multi-line unordered list items + "ul_multi": 1, + // Spaces for multi-line ordered list items + "ol_multi": 1 + }, + + // MD031/blanks-around-fences : Fenced code blocks should be surrounded by blank lines : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md031.md + "MD031": { + // Include list items + "list_items": false + }, + + // MD032/blanks-around-lists : Lists should be surrounded by blank lines : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md032.md + "MD032": true, + + // MD033/no-inline-html : Inline HTML : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md033.md + "MD033": false, + // FIXME: enable and consider fixing + //{ + // // Allowed elements + // "allowed_elements": [] + //}, + + // MD034/no-bare-urls : Bare URL used : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md034.md + "MD034": true, + + // MD035/hr-style : Horizontal rule style : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md035.md + "MD035": { + // Horizontal rule style + "style": "consistent" + }, + + // MD036/no-emphasis-as-heading : Emphasis used instead of a heading : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md036.md + "MD036": false, + // FIXME: enable and consider fixing + // { + // // Punctuation characters + // "punctuation": ".,;:!?。,;:!?" + // }, + + // MD037/no-space-in-emphasis : Spaces inside emphasis markers : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md037.md + "MD037": true, + + // MD038/no-space-in-code : Spaces inside code span elements : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md038.md + "MD038": true, + + // MD039/no-space-in-links : Spaces inside link text : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md039.md + "MD039": true, + + // MD040/fenced-code-language : Fenced code blocks should have a language specified : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md040.md + "MD040": false, + // FIXME: enable and consider fixing + //{ + //// List of languages + // "allowed_languages": [ "cpp", "json5", "sh" ], + //// Require language only + // "language_only": true + //}, + + // MD041/first-line-heading/first-line-h1 : First line in a file should be a top-level heading : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md041.md + "MD041": { + // Heading level + "level": 1, + // RegExp for matching title in front matter + "front_matter_title": "^\\s*title\\s*[:=]" + }, + + // MD042/no-empty-links : No empty links : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md042.md + "MD042": true, + + // MD043/required-headings : Required heading structure : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md043.md + "MD043": false, + + // MD044/proper-names : Proper names should have the correct capitalization : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md044.md + "MD044": false, + + // MD045/no-alt-text : Images should have alternate text (alt text) : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md045.md + "MD045": false, + + // MD046/code-block-style : Code block style : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md046.md + "MD046": { + // Block style + "style": "fenced" + }, + + // MD047/single-trailing-newline : Files should end with a single newline character : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md047.md + "MD047": true, + + // MD048/code-fence-style : Code fence style : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md048.md + "MD048": { + // Code fence style + "style": "backtick" + }, + + // MD049/emphasis-style : Emphasis style : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md049.md + "MD049": { + // Emphasis style + "style": "asterisk" + }, + + // MD050/strong-style : Strong style : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md050.md + "MD050": { + // Strong style + "style": "asterisk" + }, + + + + // MD051/link-fragments : Link fragments should be valid : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md051.md + "MD051": true, + + // MD052/reference-links-images : Reference links and images should use a label that is defined : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md052.md + "MD052": { + // Include shortcut syntax + "shortcut_syntax": false + }, + + // MD053/link-image-reference-definitions : Link and image reference definitions should be needed : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md053.md + "MD053": { + // Ignored definitions + "ignored_definitions": [ + "//" + ] + }, + + // MD054/link-image-style : Link and image style : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md054.md + "MD054": { + // Allow autolinks + "autolink": true, + // Allow inline links and images + "inline": true, + // Allow full reference links and images + "full": true, + // Allow collapsed reference links and images + "collapsed": true, + // Allow shortcut reference links and images + "shortcut": true, + // Allow URLs as inline links + "url_inline": true + }, + + // MD058 - Tables should be surrounded by blank lines + "MD058" : true + + } +} \ No newline at end of file diff --git a/CI/install_conan_dependencies.sh b/CI/install_conan_dependencies.sh index 2b14811d0..593f96e30 100644 --- a/CI/install_conan_dependencies.sh +++ b/CI/install_conan_dependencies.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash -RELEASE_TAG="1.2" +RELEASE_TAG="1.3" FILENAME="$1" DOWNLOAD_URL="https://github.com/vcmi/vcmi-dependencies/releases/download/$RELEASE_TAG/$FILENAME.txz" diff --git a/CI/install_vcpkg_dependencies.sh b/CI/install_vcpkg_dependencies.sh new file mode 100644 index 000000000..637491245 --- /dev/null +++ b/CI/install_vcpkg_dependencies.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash + +RELEASE_TAG="v1.8" +FILENAME="dependencies-$1" +DOWNLOAD_URL="https://github.com/vcmi/vcmi-deps-windows/releases/download/$RELEASE_TAG/$FILENAME.txz" + +curl -L "$DOWNLOAD_URL" | tar -xf - --xz diff --git a/CI/ios/before_install.sh b/CI/ios/before_install.sh deleted file mode 100755 index a8326e1fd..000000000 --- a/CI/ios/before_install.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/usr/bin/env bash - -echo DEVELOPER_DIR=/Applications/Xcode_14.2.app >> $GITHUB_ENV - -. CI/install_conan_dependencies.sh "dependencies-ios" diff --git a/CI/linux-qt6/upload_package.sh b/CI/linux-qt6/upload_package.sh deleted file mode 100644 index 1a2485251..000000000 --- a/CI/linux-qt6/upload_package.sh +++ /dev/null @@ -1 +0,0 @@ -#!/bin/sh diff --git a/CI/linux/upload_package.sh b/CI/linux/upload_package.sh deleted file mode 100644 index 1a2485251..000000000 --- a/CI/linux/upload_package.sh +++ /dev/null @@ -1 +0,0 @@ -#!/bin/sh diff --git a/CI/mac-arm/before_install.sh b/CI/mac-arm/before_install.sh deleted file mode 100755 index 41701758b..000000000 --- a/CI/mac-arm/before_install.sh +++ /dev/null @@ -1,4 +0,0 @@ -#!/usr/bin/env bash - -DEPS_FILENAME=dependencies-mac-arm -. CI/mac/before_install.sh diff --git a/CI/mac-intel/before_install.sh b/CI/mac-intel/before_install.sh deleted file mode 100755 index a96955b20..000000000 --- a/CI/mac-intel/before_install.sh +++ /dev/null @@ -1,4 +0,0 @@ -#!/usr/bin/env bash - -DEPS_FILENAME=dependencies-mac-intel -. CI/mac/before_install.sh diff --git a/CI/mingw-32/before_install.sh b/CI/mingw-32/before_install.sh deleted file mode 100644 index 857f4a716..000000000 --- a/CI/mingw-32/before_install.sh +++ /dev/null @@ -1,14 +0,0 @@ -#!/usr/bin/env bash - -sudo apt-get update -sudo apt-get install ninja-build mingw-w64 nsis -sudo update-alternatives --set i686-w64-mingw32-g++ /usr/bin/i686-w64-mingw32-g++-posix - -# Workaround for getting new MinGW headers on Ubuntu 22.04. -# Remove it once MinGW headers version in repository will be 10.0 at least -curl -O -L http://mirrors.kernel.org/ubuntu/pool/universe/m/mingw-w64/mingw-w64-common_10.0.0-3_all.deb \ - && sudo dpkg -i mingw-w64-common_10.0.0-3_all.deb; -curl -O -L http://mirrors.kernel.org/ubuntu/pool/universe/m/mingw-w64/mingw-w64-i686-dev_10.0.0-3_all.deb \ - && sudo dpkg -i mingw-w64-i686-dev_10.0.0-3_all.deb; - -. CI/install_conan_dependencies.sh "dependencies-mingw-32" diff --git a/CI/mingw/before_install.sh b/CI/mingw/before_install.sh deleted file mode 100755 index 70fbaf0d5..000000000 --- a/CI/mingw/before_install.sh +++ /dev/null @@ -1,14 +0,0 @@ -#!/usr/bin/env bash - -sudo apt-get update -sudo apt-get install ninja-build mingw-w64 nsis -sudo update-alternatives --set x86_64-w64-mingw32-g++ /usr/bin/x86_64-w64-mingw32-g++-posix - -# Workaround for getting new MinGW headers on Ubuntu 22.04. -# Remove it once MinGW headers version in repository will be 10.0 at least -curl -O -L http://mirrors.kernel.org/ubuntu/pool/universe/m/mingw-w64/mingw-w64-common_10.0.0-3_all.deb \ - && sudo dpkg -i mingw-w64-common_10.0.0-3_all.deb; -curl -O -L http://mirrors.kernel.org/ubuntu/pool/universe/m/mingw-w64/mingw-w64-x86-64-dev_10.0.0-3_all.deb \ - && sudo dpkg -i mingw-w64-x86-64-dev_10.0.0-3_all.deb; - -. CI/install_conan_dependencies.sh "dependencies-mingw" diff --git a/CI/msvc/before_install.sh b/CI/msvc/before_install.sh deleted file mode 100644 index 5388e84f8..000000000 --- a/CI/msvc/before_install.sh +++ /dev/null @@ -1,10 +0,0 @@ -curl -LfsS -o "vcpkg-export-${VCMI_BUILD_PLATFORM}-windows-v143.7z" \ - "https://github.com/vcmi/vcmi-deps-windows/releases/download/v1.7/vcpkg-export-${VCMI_BUILD_PLATFORM}-windows-v143.7z" -7z x "vcpkg-export-${VCMI_BUILD_PLATFORM}-windows-v143.7z" - -#rm -r -f vcpkg/installed/${VCMI_BUILD_PLATFORM}-windows/debug -#mkdir -p vcpkg/installed/${VCMI_BUILD_PLATFORM}-windows/debug/bin -#cp vcpkg/installed/${VCMI_BUILD_PLATFORM}-windows/bin/* vcpkg/installed/${VCMI_BUILD_PLATFORM}-windows/debug/bin - -DUMPBIN_DIR=$(vswhere -latest -find **/dumpbin.exe | head -n 1) -dirname "$DUMPBIN_DIR" > $GITHUB_PATH diff --git a/CI/msvc/build_script.bat b/CI/msvc/build_script.bat deleted file mode 100644 index 5bd1d8485..000000000 --- a/CI/msvc/build_script.bat +++ /dev/null @@ -1,6 +0,0 @@ -cd %APPVEYOR_BUILD_FOLDER% -cd build_%VCMI_BUILD_PLATFORM% - -cmake --build . --config %VCMI_BUILD_CONFIGURATION% -- /maxcpucount:2 - -cpack diff --git a/CI/msvc/coverity_build_script.bat b/CI/msvc/coverity_build_script.bat deleted file mode 100644 index 9fe2cbf2a..000000000 --- a/CI/msvc/coverity_build_script.bat +++ /dev/null @@ -1,5 +0,0 @@ -cd %APPVEYOR_BUILD_FOLDER% -cd build_%VCMI_BUILD_PLATFORM% - -echo Building with coverity... -cov-build.exe --dir cov-int cmake --build . --config %VCMI_BUILD_CONFIGURATION% -- /maxcpucount:2 diff --git a/CI/msvc/coverity_upload_script.ps b/CI/msvc/coverity_upload_script.ps deleted file mode 100644 index e830ae970..000000000 --- a/CI/msvc/coverity_upload_script.ps +++ /dev/null @@ -1,17 +0,0 @@ -7z a "$Env:APPVEYOR_BUILD_FOLDER\$Env:APPVEYOR_PROJECT_NAME.zip" "$Env:APPVEYOR_BUILD_FOLDER\build_$Env:VCMI_BUILD_PLATFORM\cov-int\" - -# cf. http://stackoverflow.com/a/25045154/335418 -Remove-item alias:curl - -Write-Host "Uploading Coverity analysis result..." -ForegroundColor "Green" - -curl --silent --show-error ` - --output curl-out.txt ` - --form token="$Env:coverity_token" ` - --form email="$Env:coverity_email" ` - --form "file=@$Env:APPVEYOR_BUILD_FOLDER\$Env:APPVEYOR_PROJECT_NAME.zip" ` - --form version="$Env:APPVEYOR_REPO_COMMIT" ` - --form description="CI server scheduled build." ` - https://scan.coverity.com/builds?project=vcmi%2Fvcmi - -cat .\curl-out.txt diff --git a/CI/linux-qt6/validate_json.py b/CI/validate_json.py similarity index 100% rename from CI/linux-qt6/validate_json.py rename to CI/validate_json.py diff --git a/CMakeLists.txt b/CMakeLists.txt index a9918c0a3..0629cf1ad 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -180,11 +180,6 @@ else() add_definitions(-DVCMI_NO_EXTRA_VERSION) endif(ENABLE_GITVERSION) -# Precompiled header configuration -if (CMAKE_CXX_COMPILER_ID STREQUAL "GNU" AND CMAKE_CXX_COMPILER_VERSION VERSION_LESS 6.0 ) - set(ENABLE_PCH OFF) # broken -endif() - if(ENABLE_PCH) macro(enable_pch name) target_precompile_headers(${name} PRIVATE $<$:>) @@ -328,7 +323,6 @@ if(MINGW OR MSVC) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /wd4244") # 4244: conversion from 'xxx' to 'yyy', possible loss of data set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /wd4267") # 4267: conversion from 'xxx' to 'yyy', possible loss of data set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /wd4275") # 4275: non dll-interface class 'xxx' used as base for dll-interface class - #set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /wd4800") # 4800: implicit conversion from 'xxx' to bool. Possible information loss if(ENABLE_STRICT_COMPILATION) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /WX") # Treats all compiler warnings as errors @@ -361,13 +355,6 @@ if(MINGW OR MSVC) if(ICONV_FOUND) set(SYSTEM_LIBS ${SYSTEM_LIBS} iconv) endif() - - # Prevent compiler issues when building Debug - # Assembler might fail with "too many sections" - # With big-obj or 64-bit build will take hours - if(CMAKE_BUILD_TYPE STREQUAL "Debug") - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Og") - endif() endif(MINGW) endif(MINGW OR MSVC) @@ -486,25 +473,30 @@ if(NOT FORCE_BUNDLED_MINIZIP) endif() if (ENABLE_CLIENT) - set(FFMPEG_COMPONENTS avutil swscale avformat avcodec) - if(APPLE_IOS AND NOT USING_CONAN) - list(APPEND FFMPEG_COMPONENTS swresample) - endif() - find_package(ffmpeg COMPONENTS ${FFMPEG_COMPONENTS}) + find_package(ffmpeg COMPONENTS avutil swscale avformat avcodec swresample) find_package(SDL2 REQUIRED) find_package(SDL2_image REQUIRED) if(TARGET SDL2_image::SDL2_image) add_library(SDL2::Image ALIAS SDL2_image::SDL2_image) endif() + if(TARGET SDL2_image::SDL2_image-static) + add_library(SDL2::Image ALIAS SDL2_image::SDL2_image-static) + endif() find_package(SDL2_mixer REQUIRED) if(TARGET SDL2_mixer::SDL2_mixer) add_library(SDL2::Mixer ALIAS SDL2_mixer::SDL2_mixer) endif() + if(TARGET SDL2_mixer::SDL2_mixer-static) + add_library(SDL2::Mixer ALIAS SDL2_mixer::SDL2_mixer-static) + endif() find_package(SDL2_ttf REQUIRED) if(TARGET SDL2_ttf::SDL2_ttf) add_library(SDL2::TTF ALIAS SDL2_ttf::SDL2_ttf) endif() + if(TARGET SDL2_ttf::SDL2_ttf-static) + add_library(SDL2::TTF ALIAS SDL2_ttf::SDL2_ttf-static) + endif() endif() if(ENABLE_LOBBY) @@ -666,6 +658,10 @@ if(NOT TARGET minizip::minizip) add_library(minizip::minizip ALIAS minizip) endif() +if(ENABLE_LAUNCHER OR ENABLE_EDITOR) + add_subdirectory(vcmiqt) +endif() + if(ENABLE_LAUNCHER) add_subdirectory(launcher) endif() @@ -727,7 +723,7 @@ endif() if(WIN32) if(TBB_FOUND AND MSVC) - install_vcpkg_imported_tgt(TBB::tbb) + install_vcpkg_imported_tgt(TBB::tbb) endif() if(USING_CONAN) @@ -737,7 +733,9 @@ if(WIN32) ${dep_files} "${CMAKE_SYSROOT}/bin/*.dll" "${CMAKE_SYSROOT}/lib/*.dll" - "${CONAN_SYSTEM_LIBRARY_LOCATION}/*.dll") + "${CONAN_SYSTEM_LIBRARY_LOCATION}/libgcc_s_dw2-1.dll" # for 32-bit only? + "${CONAN_SYSTEM_LIBRARY_LOCATION}/libgcc_s_seh-1.dll" # for 64-bit only? + "${CONAN_SYSTEM_LIBRARY_LOCATION}/libstdc++-6.dll") else() file(GLOB dep_files ${dep_files} diff --git a/CMakePresets.json b/CMakePresets.json index 3746ce676..1dbf2df4c 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -134,7 +134,9 @@ "description": "VCMI Windows Ninja using MinGW", "inherits": "default-release", "cacheVariables": { - "CMAKE_BUILD_TYPE": "Release" + "CMAKE_BUILD_TYPE": "Release", + "CMAKE_C_COMPILER": "gcc", + "CMAKE_CXX_COMPILER": "g++" } }, { @@ -154,6 +156,19 @@ } }, + { + "name": "windows-msvc-release-x86", + "displayName": "Windows x86 RelWithDebInfo", + "description": "VCMI RelWithDebInfo build", + "inherits": "default-release", + "generator": "Visual Studio 17 2022", + "cacheVariables": { + "CMAKE_TOOLCHAIN_FILE": "${sourceDir}/vcpkg/scripts/buildsystems/vcpkg.cmake", + "CMAKE_POLICY_DEFAULT_CMP0091": "NEW", + "FORCE_BUNDLED_MINIZIP": "ON", + "CMAKE_GENERATOR_PLATFORM": "WIN32" + } + }, { "name": "windows-msvc-release-ccache", "displayName": "Windows x64 RelWithDebInfo with ccache", @@ -382,6 +397,11 @@ "configurePreset": "windows-msvc-release", "inherits": "default-release" }, + { + "name": "windows-msvc-release-x86", + "configurePreset": "windows-msvc-release-x86", + "inherits": "default-release" + }, { "name": "windows-msvc-release-ccache", "configurePreset": "windows-msvc-release-ccache", diff --git a/ChangeLog.md b/ChangeLog.md index 0063094e3..3dd77dbb7 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -1,38 +1,84 @@ -# 1.5.7 -> 1.6.0 (in development) +# VCMI Project Changelog + +## 1.5.7 -> 1.6.0 (in development) ### Major changes + * Implemented handicap system, with options to reduce income and growth in addition to starting resources restriction * Game will now show statistics after scenario completion, such as resources or army strength over time * Implemented spell quick selection panel in combat * Implemented adventure map overlay accessible via Alt key that highlights all interactive objects on screen * Implemented xBRZ upscaling filter * It is now possible to import data from Heroes Chronicles (gog.com installer only) as custom campaigns +* Added simple support for spell research feature from HotA that can be enabled via mod or game configuration editing +* Implemented automatic selection of interface scaling. Selecting interface scaling manually will restore old behavior +* VCMI will now launch in fullscreen on desktop systems. Use F4 hotkey or toggle option in settings to restore old behavior ### General + * Saved game size reduced by approximately 3 times, especially for large maps or games with a large number of mods. +* Mods that modify game texts, such as descriptions of secondary skills, will now correctly override translation mods +* Game will now correctly restore information such as hero path, order of heroes and towns, and list of sleeping heroes on loading a save game +* Added translation for missing texts, such as random map descriptions, quick exchange buttons, wog commander abilities, moat names + +### Multiplayer + * Added option to start vcmi server on randomly selected TCP port * Fixed potential desynchronization between server and clients on randomization of map objects if client and server run on different operating systems +* Fixed possible freeze on receiving turn in multiplayer when player has town window opened +* Fixed possible freeze if player is attacked by another player on first day of the week +* If player disconnects from a multiplayer game, all other players will now receive notification in form of popup message instead of chat message +* Fixed potentially missing disconnection notification in multiplayer if player disconnects due to connection loss +* Game will now correctly show turn timers and simultaneous turns state on loading game ### Stability + * Fixed possible crash on connecting bluetooth mouse during gameplay on Android * VCMI will now write more detailed information to log file on crash due to uncaught exception +* Fixed crash on transfer of multiple artifacts in a backpack to another hero on starting next campaign scenario without hero that held these artifacts before ### Mechanics + * Arrow tower will now prefer to attack more units that are viewed most dangerous instead of simply attacking top-most unit * Score in campaigns will now be correctly tracked for games loaded from a save * Fixed incorrect direction of Dragon Breath attack in some cases if wide creature attacks another wide creature * Map events and town events are now triggered on start of turn of player affected by event, in line with H3 instead of triggering on start of new day for all players * Neutral towns should now have initial garrison and weekly growth of garrison identical to H3 * It is now possible to buy a new war machine in town if hero has different war machine in the slot +* Fixed possible integer overflow if hero has unit with total AI value of over 2^31 +* Unicorn Glade in Rampart now correctly requires Dendroid Arches and not Homestead +* Game will no longer place obstacles on ship-to-ship battles, in line with H3 +* Game will now place obstacles in battles in villages (towns without forts) +* Battles in villages (towns without forts) now always occur on battlefield of native terrain +* Fixed pathfinding through subterranean gates located on right edge of the map or next to terra incognita +* Chain Lightning will now skip over creatures that are immune to this spell instead of targeting them but dealing no damage +* Commanders spell resistance now uses golem-like logic which reduces damage instead of using dwarf-style change to block spell +* It is now possible to target empty hex for shooters with area attack, such as Magog or Lich + +### Video / Audio -### Interface -* Added option to drag map with right-click -* Added hotkeys to reorder list of owned towns or heroes -* The number of units resurrected using the Life Drain ability is now written to the combat log. * Fixed playback of audio stream with different formats from video files in some Heroes 3 versions * Video playback will not be replaced by a black square when another dialogue box is on top of the video. * When resuming video playback, the video will now be continued instead of being restarted. * Reduced video decompression artefacts for video formats that store RGB rather than YUV data. +* Intro videos are now played inside a frame on resolutions higher than 800x600 instead of filling entire screen +* Re-enabled idle animations for Conflux creatures in battles +* .webm video with vp8 / vp9 codec are now supported on every platform +* It is now possible to provide external audio stream for a video file (e.g. for translations) +* It is now possible to provide external subtitles for a video file +* Game will now correctly resume playback of terrain music on closing scenario information window in campaigns instead of playing main theme +* Background music theme will now play at lower volume while intro speech in campaign intro / outro is playing +* Added workaround for playback of corrupted `BladeFWCampaign.mp3` music file +* Fixed computation of audio length for formats other than .wav. This fixes incorrect text scrolling speed in campaign intro/outro +* Game will now use Noto family true type font to display characters not preset in Heroes III fonts +* Added option to scale all in-game fonts when scalable true type fonts are in use + +### Interface + +* It is now possible to search for a map object using Ctrl+F hotkey +* Added option to drag map with right-click +* Added hotkeys to reorder list of owned towns or heroes +* The number of units resurrected using the Life Drain ability is now written to the combat log. * Fixed order of popup dialogs after battle. * Right-click on wandering monster on adventure map will now also show creature level and faction it belongs to * Added additional information to map right-click popup dialog: map author, map creation date, map version @@ -41,30 +87,43 @@ * Semi-transparent shadows now correctly update their transparency during fading effects, such as resource pickups * Game will now save all names for human player in hotseat mode * Added unassigned by default shortcuts for toggling visibility of visitable and blocked tiles -* Spellbook button in battle is now blocked if hero has no spellbook +* Spellbook button in battle is now blocked if hero has no spellbook * Adventure map will no longer scroll if window is not in focus * Removed second info window when player loses his last town * Fixed hero path not updating correctly after hiring or dismissing creatures * Fixed missing description of a stack artifact when accessed through unit window * Fixed text overflow on campaign scenario window if campaign name is too long -* Intro videos are now played inside a frame on resolutions higher than 800x600 instead of filling entire screen +* Recruiting hero in town will now play "new construction" sound +* Game will now correctly update displayed hero path when hiring or dismissing creatures that give movement penalty +* Game will now show level, faction and attack range of wandering monsters in right-click popup window +* Hovering over owned hero will now show movement points information in status bar +* Quick backpack window is now also accessible via Shift+mouse click, similar to HD Mod +* It is now possible to sort artifacts in backpack by cost, slot, or rarity class +* Fixed incorrect display of names of VCMI maps in scenario selection if multiple VCMI map are present in list ### Random Maps Generator + * Implemented connection option 'forcePortal' * It is now possible to connect zone to itself using pair of portals * It is now possible for a random map template to change game settings * Road settings will now be correctly loaded when opening random map setup tab +* Added support for banning objects per zones +* Added support for customizing objects frequency, value, and count per zone +* Fixed values of Pandora Boxes with creatures to be in line with H3:SoD ### Campaigns + * It is now possible to use .zip archive for VCMI campaigns instead of raw gzip stream * Fixed handling of hero placeholders in VCMI map format (.vmap) * Fixed not functioning hero carryover in VCMI campaigns * Added support for campaign outro videos, such as outro in "Song for the Father" campaign * Added support for rim image for intro video, such as opening videos in Heroes Chronicles * Added support for custom loading screen in campaigns -* Added support for custom region definitions (such as background images) for VCMI campaigns +* Added support for custom region definitions (such as background images) for VCMI campaigns ### AI + +* VCMI will now use BattleAI for battles with neutral enemies by default * Fixed bug where BattleAI attempts to move double-wide unit to an unreachable hex * Fixed several cases where Nullkiller AI can count same dangerous object twice, doubling expected army loss. * Nullkiller is now capable of visiting configurable objects from mods @@ -74,20 +133,35 @@ * Fixed case where BattleAI will go around the map to attack ranged units if direct path is blocked by another unit * Fixed evaluation of effects of waiting if unit is under haste effect by Battle AI * Battle AI can now use location spells +* Battle AI will now correctly avoid moving flying units into dangerous obstacles such as moat +* Fixed possible crash on AI attempting to visit town that is already being visited by this hero ### Launcher + * Added Swedish translation ### Map Editor + * Implemented tracking of building requirements for Building Dialog * Added build/demolish/enable/disable all buildings options to Building Dialog in town properties +* Implemented configuration of patrol radius for heroes * It is now possible to set spells allowed or required to be present in Mages Guild * It is now possible to add timed events to a town * Fixed editor not marking mod as dependency if spells from mod are used in town Mages Guild or in hero starting spells * It is now possible to choose road types for random map generation in editor * Validator will now warn in case if map has players with no heroes or towns +* Fixed broken transparency handling on some adventure map objects from mods +* Fixed duplicated list of spells in Mage Guild in copy-pasted towns +* Removed separate versioning of map editor. Map editor now has same version as VCMI +* Timed events interfaces now counts days from 1, instead of from 0 ### Modding + +* Added support for configurable flaggable objects that can provide bonuses or daily income to owning player +* Added support for soft dependencies for mods, that only affect mod loading order (and as result - override order), without requiring dependent mod or allowing access to its identifiers +* It is now possible to provide translations for mods that modify strings from original game, such as secondary skill descriptions +* It is now possible to embed json data directly into mod.json instead of using list of files +* Implemented detection of potential conflicts between mods. To enable, open Launcher and set "Mod Validation" option to "Full" * Fixed multiple issues with configurable town buildings * Added documentation for configurable town buildings. See docs/Moddders/Entities_Format/Town_Buildings_Format.md * Replaced some of hardcoded town buildings with configurable buildings. These building types are now deprecated and will be removed in future. @@ -95,7 +169,9 @@ * It is now possible to add guards to a configurable objects. All H3 creature banks are now implemented as configurable object. * It is now possible to define starting position of units in a guarded configurable object * Added `canCastWithoutSkip` parameter to a spell. If such spell is cast by a creature, its turn will not end after a spellcast +* Added `castOnlyOnSelf` parameter to a spell. Creature that can cast this spell can only cast it on themselves * Mod can now provide pregenerated assets in place of autogenerated, such as large spellbook. +* Added support for 'fused' artifacts, as alternative to combined artifacts * Added support for custom music and opening sound for a battlefield * Added support for multiple music tracks for towns * Added support for multiple music tracks for terrains on adventure map @@ -108,13 +184,14 @@ * Town building can now define provided fortifications - health of walls, towers, presence of moat, identifier of creature shooter on tower * Added DISINTEGRATE bonus * Added INVINCIBLE bonus +* Added PRISM_HEX_ATTACK_BREATH bonus * Added THIEVES_GUILD_ACCESS bonus that changes amount of information available in thieves guild * TimesStackLevelUpdater now supports commanders * Black market restock period setting now correctly restocks on specified date instead of restocking on all dates other than specified one -* Game now supports vp8 and vp9 encoding for video files on all platforms * Json Validator will now attempt to detect typos when encountering unknown property in Json +* Added `translate missing` command that will export only untranslated strings into `translationsMissing` directory, separated per mod -# 1.5.6 -> 1.5.7 +## 1.5.6 -> 1.5.7 * Fixed game freeze if player is attacked in online multiplayer game by another player when he has unread dialogs, such as new week notification * Fixed possible game crash after being attacked by enemy with artifact that blocks spellcasting @@ -126,14 +203,16 @@ * Fixed excessive removal of open dialogs such as new week or map events on new turn * Fixed objects like Mystical Gardens not resetting their state on new week correctly -# 1.5.5 -> 1.5.6 +## 1.5.5 -> 1.5.6 ### Stability + * Fixed possible crash on transferring hero to next campaign scenario if hero has combined artifact some components of which can be transferred * Fixed possible crash on transferring hero to next campaign scenario that has creature with faction limiter in his army * Fixed possible crash on application shutdown due to incorrect destruction order of UI entities ### Multiplayer + * Mod compatibility issues when joining a lobby room now use color coding to make them less easy to miss. * Incompatible mods are now placed before compatible mods when joining lobby room. * Fixed text overflow in online lobby interface @@ -141,21 +220,24 @@ * Fixed non-functioning slider in invite to game room dialog ### Interface + * Fixed some shortcuts that were not active during the enemy's turn, such as Thieves' Guild. * Game now correctly uses melee damage calculation when forcing a melee attack with a shooter. * Game will now close all open dialogs on start of our turn, to avoid bugs like locked right-click popups ### Map Objects + * Spells the hero can't learn are no longer hidden when received from a rewardable object, such as the Pandora Box * Spells that cannot be learned are now displayed with gray text in the name of the spell. * Configurable objects with scouted state such as Witch Hut in HotA now correctly show their reward on right click after vising them but refusing to accept reward * Right-click tooltip on map dwelling now always shows produced creatures. Player that owns the dwelling can also see number of creatures available for recruit ### Modding + * Fixed possible crash on invalid SPELL_LIKE_ATTACK bonus * Added compatibility check when loading maps with old names for boats -# 1.5.4 -> 1.5.5 +## 1.5.4 -> 1.5.5 * Fixed crash when advancing to the next scenario in campaigns when the hero not transferring has a combination artefact that can be transferred to the next scenario. * Fixed game not updating information such as hero path and current music on new day @@ -165,20 +247,23 @@ * Shift+left click now directly opens the hero window when two heroes are in town * Fixed handling of alternative actions for creatures that have more than two potential actions, such as move, shoot, and cast spells. -# 1.5.3 -> 1.5.4 +## 1.5.3 -> 1.5.4 ### Stability + * Fixed a possible crash when clicking on an adventure map when another player is taking a turn in multiplayer mode. * Failure to extract a mod will now display an error message instead of a silent crash. * Fixed crash on opening town hall screen of a town from a mod with invalid building identifier * Fixed crash when faerie dragons die after casting Ice Ring on themselves. ### Mechanics + * The scholar will now correctly upgrade a skill if the visiting hero has offered a skill at either the basic or advanced level. * Hero now reveals Fog of War when receiving new or upgraded secondary skills (such as scouting). * AI will now always act after all human players during simturns instead of acting after host player ### Interface + * Pressing the up and down keys on the town screen will now move to the next or previous town instead of scrolling through the list of towns. * Long text in scenario name and highscore screen now shortened to fit the interface * Game now moves cursor to tap event position when using software cursor with touch screen input @@ -186,16 +271,19 @@ * Damage estimation tooltip will no longer show damage greater than the targeted unit's health. ### Random Maps Generator + * Generator will try to place roads even further away from zone borders * Fixed rare crash when placing two quest artefacts in the same location at the same time ### AI + * Improved performance of Nullkiller AI * Stupid AI no longer overestimates damage when killing entire unit * Fixed a bug leading to Battle AI not using spells when sieging town with Citadel or Castle built * Fixed an unsigned integer overflow that caused the Nullkiller AI to overestimate the total army strength after merging two armies. ### Launcher + * Added button to reset touchscreen tutorial on mobile systems * Launcher will now warn if player selects Gog Galaxy installer instead of offline installer * Launcher will now ask for the .bin file first as it is usually listed first in the file system view @@ -205,16 +293,19 @@ * Fixed manual file installation on Android ### Map Editor + * Icons and translations now embedded in executable file ### Modding + * Improved bonus format validation * Validator now reports valid values for enumeration fields * Fixed missing addInfo field for bonuses that use the BONUS_OWNER_UPDATER propagation updater. -# 1.5.2 -> 1.5.3 +## 1.5.2 -> 1.5.3 ### Stability + * Fixed possible crash when hero class has no valid commander. * Fixed crash when pressing spacebar or enter during combat when hero has no tactics skill. * Fixed crash when receiving a commander level-up after winning a battle in a garrison owned by an enemy player. @@ -227,8 +318,9 @@ * Game will now display an error message instead of silent crash if game data directory is not accessible ### Mechanics + * Transport Artefact victory condition will no longer trigger if another player has completed it. -* Fixed wandering monster combat not triggering when landing in its zone of control when flying from above the monster using the Fly spell. +* Fixed wandering monster combat not triggering when landing in its zone of control when flying from above the monster using the Fly spell. * Fixed potentially infinite movement loop when the hero has Admiral's Hat whirlpool immunity and the hero tries to enter and exit the same whirlpool. * If game picks gold for a random resource pile that has predetermined by map amount, its amount will be correctly multiplied by 100 * Fixed hero not being able to learn spells from a mod in some cases, even if they are available from the town's mage guild. @@ -237,6 +329,7 @@ * If turn timer runs out during pve battle game will end player turn after a battle instead of forcing retreat ### Interface + * Fixed reversed button functions in Exchange Window * Fixed allied towns being missing from the list when using the advanced or expert Town Portal spell. * Fixed corrupted UI that could appear for a frame under certain conditions @@ -249,6 +342,7 @@ * It is now possible to scroll through artifacts backpack using mouse wheel or swipe ### Launcher + * Android now uses the same Qt-based launcher as other systems * Fixed attempt to install a submod when installing new mod that depends on a submod of another mod * Fixed wrong order of activating mods in chain when installing multiple mods at once @@ -262,15 +356,16 @@ * Replaced checkboxes with toggle buttons for easier of access on touchscreens. * Icons and translations now embedded in executable file * Added interface for configuring several previously existing but inaccessible options in Launcher: - * Selection of input tolerance precision for all input types - * Relative cursor mode for mobile systems (was only available on Android) - * Haptic feedback toggle for mobile systems (was only available on Android) - * Sound and music volume (was only available in game) - * Selection of long touch interval (was only available in game) - * Selection of upscaling filter used by SDL - * Controller input sensitivity and acceleration. + * Selection of input tolerance precision for all input types + * Relative cursor mode for mobile systems (was only available on Android) + * Haptic feedback toggle for mobile systems (was only available on Android) + * Sound and music volume (was only available in game) + * Selection of long touch interval (was only available in game) + * Selection of upscaling filter used by SDL + * Controller input sensitivity and acceleration. ### AI + * Fixed crash when Nullkiller AI tries to explore after losing the hero in combat. * Fixed rare crash when Nullkiller AI tries to use portals * Fixed potential crash when Nullkiller AI has access to Town Portal spell @@ -279,19 +374,23 @@ * Fixed bug leading to Battle AI doing nothing if targeted unit is unreachable ### Random Maps Generator + * Fixed crash when player selects a random number of players and selects a different colour to play, resulting in a non-continuous list of players. * Fixed rare crash when generating maps with water ### Map Editor + * Fixed crash on closing map editor ### Modding + * Added new building type 'thievesGuild' which implements HotA building in Cove. * Creature terrain limiter now actually accepts terrain as parameter -# 1.5.1 -> 1.5.2 +## 1.5.1 -> 1.5.2 ### Stability + * Fixed crash on closing game while combat or map animations are playing * Fixed crash on closing game while network thread is waiting for dialog to be closed * Fixed random crash on starting random map with 'random' number of players @@ -305,6 +404,7 @@ * Game will now abort loading if a corrupt mod is detected instead of crashing without explanation later ### Multiplayer + * Contact between allied players will no longer break simturns * Having hero in range of object owned by another player will now be registered as contact * Multiplayer saves are now visible when starting a single player game @@ -313,11 +413,13 @@ * All multiplayer chat commands now use a leading exclamation mark ### Campaigns + * If the hero attacks an enemy player and is defeated, he will be correctly registered as defeated by the defending player. * Allow standard victory condition on 'To kill a hero' campaign mission in line with H3 * Fixes Adrienne starting without Inferno spell in campaign ### Interface + * For artefacts that are part of a combined artefact, the game will now show which component of that artefact your hero has. * Fixed broken in 1.5.1 shortcut for artifact sets saving * Fixed full screen toggle (F4) not applying changes immediately @@ -329,37 +431,38 @@ * Added keyboard shortcuts to markets and altars. 'Space' to confirm deal and 'M' to trade maximum possible amount * Pressing 'Escape' in main menu will now trigger 'Back' and 'Quit' buttons * Added keyboard shortcuts to hero exchange window: - * 'F10' will now swap armies - * 'F11' will now swap artifacts. Additionally, 'Ctrl+F11' will swap equipped artifacts, and 'Shift+F11' will swap backpacks - * Added unassigned shortcuts to move armies or artifacts to left or right side + * 'F10' will now swap armies + * 'F11' will now swap artifacts. Additionally, 'Ctrl+F11' will swap equipped artifacts, and 'Shift+F11' will swap backpacks + * Added unassigned shortcuts to move armies or artifacts to left or right side * Added keyboard shortcuts to access buildings from town interface: - * 'F' will now open Fort window - * 'B' will now open Town Hall window - * 'G' will now open Mage Guild window - * 'M' will now open Marketplace - * 'R' will now open recruitment interface - * 'T' will now open Tavern window - * 'G' will now open Thieves Guild - * 'E' will now open hero exchange screen, if both heroes are present in town - * 'H' will now open hero screen. Additionally, 'Shift+H' will open garrisoned hero screen, and 'Ctrl+H' will open visiting hero screen - * 'Space' will now swap visiting and garrisoned heroes + * 'F' will now open Fort window + * 'B' will now open Town Hall window + * 'G' will now open Mage Guild window + * 'M' will now open Marketplace + * 'R' will now open recruitment interface + * 'T' will now open Tavern window + * 'G' will now open Thieves Guild + * 'E' will now open hero exchange screen, if both heroes are present in town + * 'H' will now open hero screen. Additionally, 'Shift+H' will open garrisoned hero screen, and 'Ctrl+H' will open visiting hero screen + * 'Space' will now swap visiting and garrisoned heroes * Added keyboard shortcuts to switch between tabs in Scenario Selection window: - * 'E' will open Extra Options tab - * 'T' will open Turn Options tab - * 'I' will open Invite Players window (only for lobby games) - * 'R' will now replay video in campaigns + * 'E' will open Extra Options tab + * 'T' will open Turn Options tab + * 'I' will open Invite Players window (only for lobby games) + * 'R' will now replay video in campaigns * Added keyboard shortcuts to Adventure map: - * 'Ctrl+L' will now prompt to open Load Game screen - * 'Ctrl+M' will now prompt to go to main menu - * 'Ctrl+N' will now prompt to go to New Game screen - * 'Ctrl+Q' will now prompt to quit game - * Page Up, Page Down, Home and End keys will now move hero on adventure map similar to numpad equivalents - * Fixed non-functioning shortcuts '+' and '-' on numpad to zoom adventure map + * 'Ctrl+L' will now prompt to open Load Game screen + * 'Ctrl+M' will now prompt to go to main menu + * 'Ctrl+N' will now prompt to go to New Game screen + * 'Ctrl+Q' will now prompt to quit game + * Page Up, Page Down, Home and End keys will now move hero on adventure map similar to numpad equivalents + * Fixed non-functioning shortcuts '+' and '-' on numpad to zoom adventure map * Added keyboard shortcuts to Battle interface: - * 'V' now allows to view information of hovered unit - * 'I' now allows to view information of active unit + * 'V' now allows to view information of hovered unit + * 'I' now allows to view information of active unit ### Mechanics + * Game will no longer pick creatures exclusive to AB campaigns for random creatures or for Refugee Camp, in line with H3 * If original movement rules are on, it is not possible to attack guards from visitable object directly, only from free tile * Fixed bug leading that allowed picking up objects while flying on top of water @@ -367,24 +470,29 @@ * Interface will now use same arrow for U-turns in path as H3 ### AI + * Nullkiller AI can now explore the map * Nullkiller AI will no longer use the map reveal cheat when allied with a human or when playing on low difficulty * Nullkiller AI is now used by default for allied players ### Launcher + * When extracting data from gog.com offline installer game will extract files directly into used data directory instead of temporary directory ### Map Editor + * Fixed victory / loss conditions widget initialization ### Modding + * Hero specialties with multiple bonuses that have TIMES_HERO_LEVEL updater now work as expected * Spells that apply multiple bonuses with same type and subtype but different value type now work as expected * Added option to toggle layout of guards in creature banks -# 1.5.0 -> 1.5.1 +## 1.5.0 -> 1.5.1 ### Stability + * Fixed possible crash on accessing faction description * Fixed possible thread race on exit to main menu * Game will now show error message instead of silent crash on corrupted H3 data @@ -397,6 +505,7 @@ * If json file specified in mod.json is missing, vcmi will now only log an error instead of crashing ### Interface + * Added retaliation damage and kills preview when hovering over units that can be attacked in melee during combat * Clicking on combat log would now open a window with full combat log history * Removed message length limit in text input fields, such as global lobby chat @@ -410,10 +519,12 @@ * Small windows no longer dim the entire screen by default ### Mechanics + * Recruiting a hero will now immediately reveal the fog of war around him * When both a visiting hero and a garrisoned hero are in town, the garrisoned hero will visit town buildings first. ### Multiplayer + * Fixed in-game chat text not being visible after switching from achannel with a long history * Fixed lag when switching to channel with long history * Game now automatically scrolls in-game chat on new messages @@ -428,23 +539,27 @@ * Fixed overflow in invite window when there are more than 8 players in the lobby ### Random Maps Generator + * Generator will now prefer to place roads away from zone borders ### AI + * Fixed possible crash when Nullkiller AI tries to upgrade army * Nullkiller AI will now recruit new heroes if he left with 0 heroes * AI in combat now knows when an enemy unit has used all of its retaliations. ### Map Editor + * Fixed setting up hero types of heroes in Prisons placed in map editor * Fixed crash on setting up Seer Hut in map editor * Added text auto-completion hints for army widget * Editor will now automatically add .vmap extensions when saving map * Fixed text size in map validation window -# 1.4.5 -> 1.5.0 +## 1.4.5 -> 1.5.0 ### General + * Added Portuguese (Brazilian) translation * Added basic support for game controllers * Added option to disable cheats in game @@ -453,6 +568,7 @@ * Implemented switchable artifact sets from HD Mod ### Stability + * Fixed possible crash in Altar of Sacrifice * Fixed possible crash on activation of 'Enchanted' bonus * Fixed possible race condition on random maps generation on placement treasures near border with water zone @@ -469,6 +585,7 @@ * Fixed possible hanging app on attempt to close game during loading ### Multiplayer + * Game map will no longer be locked during turn of other human players, allowing to change hero paths or inspect towns or heroes * Game will now correctly block most of player actions outside of their turn * Implemented new lobby, available in game with persistent accounts and chat @@ -481,6 +598,7 @@ * Implemented rolling and banning of towns before game start ### Interface + * Implemented configurable keyboard shortcuts, editable in file config/shortcutsConfig.json * Fixed broken keyboard shortcuts in main menu * If UI Enhancements are enabled, the game will skip confirmation dialogs when entering owned dwellings or refugee camp. @@ -518,6 +636,7 @@ * Recruitment costs that consist from 3 different resources should now fit recruitment window UI better ### Campaigns + * Game will now correctly track who defeated the hero or wandering monsters for related quests and victory conditions * Birth of a Barbarian: Yog will now start the third scenario with Angelic Alliance in his inventory * Birth of a Barbarian: Heroes with Angelic Alliance components are now considered to be mission-critical and can't be dismissed or lost in combat @@ -535,6 +654,7 @@ * Fixed invalid string on right-clicking secondary skill starting bonus ### Battles + * Added option to enable unlimited combat replays during game setup * Added option to instantly end battle using quick combat (shortcut: 'e') * Added option to replace auto-combat button action with instant end using quick combat @@ -543,6 +663,7 @@ * Fixed positioning of unit stack size label ### Mechanics + * It is no longer possible to learn spells from Pandora or events if hero can not learn them * Fixed behavior of 'Dimension Door' spell to be in line with H3:SoD * Fixed behavior of 'Fly' spell to be in line with H3:SoD @@ -565,6 +686,7 @@ * Fixed regression leading to large elemental dwellings being used as replacements for random dwellings ### Random Maps Generator + * Game will now save last used RMG settings in game and in editor * Reduced number of obstacles placed in water zones * Treasure values in water zone should now be similar to values from HotA, due to bugs in H3:SoD values @@ -581,6 +703,7 @@ * Windmill will now appear on top of all other objects ### Launcher + * Launcher now supports installation of Heroes 3 data using gog.com offline installer thanks to innoextract tool * Fixed loading of mod screenshots if player opens screenshots tab without any preloaded screenshots * Fixed installation of mods if it has non-installed submod as dependency @@ -590,6 +713,7 @@ * Added Portuguese translation to launcher ### Map Editor + * Added Chinese translation to map editor * Added Portuguese translation to map editor * Mod list in settings will now correctly show submods of submods @@ -599,6 +723,7 @@ * It is now possible to customize hero spells ### AI + * Fixed possible crash on updating NKAI pathfinding data * Fixed possible crash if hero has only commander left without army * Fixed possible crash on attempt to build tavern in a town @@ -609,15 +734,16 @@ * It is now possible to configure AI settings via config file * Improved parallelization when AI has multiple heroes * AI-controlled creatures will now correctly move across wide moat in Fortress -* Fixed system error messages caused by visitation of Trading Posts by VCAI +* Fixed system error messages caused by visitation of Trading Posts by VCAI * Patrolling heroes will never retreat from the battle * AI will now consider strength of town garrison and not just strength of visiting hero when deciding to attack town ### Modding + * Added new game setting that allows inviting heroes to taverns * It is now possible to add creature or faction description accessible via right-click of the icon * Fixed reversed Overlord and Warlock classes mapping -* Added 'selectAll' mode for configurable objects which grants all potential rewards +* Added 'selectAll' mode for configurable objects which grants all potential rewards * It is now possible to use most of json5 format in vcmi json files * Main mod.json file (including any submods) now requires strict json, without comments or extra commas * Replaced bonus MANA_PER_KNOWLEDGE with MANA_PER_KNOWLEDGE_PERCENTAGE to avoid rounding error with mysticism @@ -626,9 +752,10 @@ * Game will now report cases where minimal damage of a creature is greater than maximal damage * Added bonuses RESOURCES_CONSTANT_BOOST and RESOURCES_TOWN_MULTIPLYING_BOOST -# 1.4.4 -> 1.4.5 +## 1.4.4 -> 1.4.5 ### Stability + * Fixed crash on creature spellcasting * Fixed crash on unit entering magical obstacles such as quicksands * Fixed freeze on map loading on some systems @@ -636,24 +763,29 @@ * Fixed crash on opening creature information window with invalid SPELL_IMMUNITY bonus ### Random Maps Generator + * Fixed placement of guards sometimes resulting into open connection into third zone * Fixed rare crash on multithreaded access during placement of artifacts or wandering monsters ### Map Editor + * Fixed inspector using wrong editor for some values ### AI -* Fixed bug leading to AI not attacking wandering monsters in some cases -* Fixed crash on using StupidAI for autocombat or for enemy players -# 1.4.3 -> 1.4.4 +* Fixed bug leading to AI not attacking wandering monsters in some cases +* Fixed crash on using StupidAI for autocombat or for enemy players + +## 1.4.3 -> 1.4.4 ### General + * Fixed crash on generation of random maps -# 1.4.2 -> 1.4.3 +## 1.4.2 -> 1.4.3 ### General + * Fixed the synchronisation of the audio and video of the opening movies. * Fixed a bug that caused spells from mods to not show up in the Mage's Guild. * Changed the default SDL driver on Windows from opengl to autodetection @@ -661,6 +793,7 @@ * Movement and mana points are now replenished for new heroes in taverns. ### Multiplayer + * Simturn contact detection will now correctly check for hero moving range * Simturn contact detection will now ignore wandering monsters * Right-clicking the Simturns AI option now displays a tooltip @@ -670,12 +803,14 @@ * Ending a turn during simturns will now block the interface correctly. ### Campaigns + * Player will no longer start the United Front of Song for the Father campaign with two Nimbuses. * Fixed missing campaign description after loading saved game * Campaign completion checkmarks will now be displayed after the entire campaign has been completed, rather than just after the first scenario. * Fixed positioning of prologue and epilogue text during campaign scenario intros ### Interface + * Added an option to hide adventure map window when town or battle window are open * Fixed switching between pages on small version of spellbook * Saves with long filenames are now truncated in the UI to prevent overflow. @@ -686,10 +821,11 @@ * Fixed incorrect cursor display when hovering over water objects accessible from shore ### Stability + * Fixed a crash when using the 'vcmiobelisk' cheat more than once. * Fixed crash when reaching level 201. The maximum level is now limited to 197. * Fixed crash when accessing a spell with an invalid SPELLCASTER bonus -* Fixed crash when trying to play music for an inaccessible tile +* Fixed crash when trying to play music for an inaccessible tile * Fixed memory corruption on loading of old mods with illegal 'index' field * Fixed possible crash on server shutdown on Android * Fixed possible crash when the affinity of the hero class is set to an invalid value @@ -697,11 +833,13 @@ * Failure to initialise video subsystem now displays error message instead of silent crash ### Random Maps Generator + * Fixed possible creation of a duplicate hero in a random map when the player has chosen the starting hero. * Fixed banning of quest artifacts on random maps * Fixed banning of heroes in prison on random maps ### Battles + * Battle turn queue now displays current turn * Added option to show unit statistics sidebar in battle * Right-clicking on a unit in the battle turn queue now displays the unit details popup. @@ -712,6 +850,7 @@ * Coronius specialty will now correctly select affected units ### Launcher + * Welcome screen will automatically detect existing Heroes 3 installation on Windows * It is now possible to install mods by dragging and dropping onto the launcher. * It is now possible to install maps and campaigns by dragging and dropping onto the launcher. @@ -719,15 +858,18 @@ * Added option to select preferred SDL driver in launcher ### Map Editor + * Fixed saving of allowed abilities, spells, artifacts or heroes ### AI + * AI will no longer attempt to move immobilized units, such as those under the effect of Dendroid Bind. * Fixed shooters not shooting when they have a range penalty * Fixed Fire Elemental spell casting * Fixed rare bug where unit would sometimes do nothing in battle ### Modding + * Added better reporting of "invalid identifiers" errors with suggestions on how to fix them * Added FEROCITY bonus (HotA Aysiud) * Added ENEMY_ATTACK_REDUCTION bonus (HotA Nix) @@ -739,9 +881,10 @@ * BLOCKS_RETALIATION now also blocks FIRST_STRIKE bonus * Added 'canCastOnSelf' field for spells to allow creatures to cast spells on themselves. -# 1.4.1 -> 1.4.2 +## 1.4.1 -> 1.4.2 ### General + * Restored support for Windows 7 * Restored support for 32-bit builds * Implemented quick backpack window for slot-specific artifact selection, activated via mouse wheel / swipe gesture @@ -755,10 +898,12 @@ * added nwctheone / vcmigod cheat: reveals the whole map, gives 5 archangels in each empty slot, unlimited movement points and permanent flight to currently selected hero ### Launcher + * Launcher will now properly show mod installation progress * Launcher will now correctly select preferred language on first start ### Multiplayer + * Timers for all players will now be visible at once * Turn options menu will correctly open for guests when host switches to it * Guests will correctly see which roads are allowed for random maps by host @@ -769,6 +914,7 @@ * Game will now send notifications to players when simultaneous turns end ### Stability + * Fixed crash on clicking town or hero list on MacOS and iOS * Fixed crash on closing vcmi on Android * Fixed crash on disconnection from multiplayer game @@ -785,6 +931,7 @@ * Added check for presence of Armageddon Blade campaign files to avoid crash on some Heroes 3 versions ### Random Maps Generator + * Improved performance of random maps generation * Rebalance of treasure values and density * Improve junction zones generation by spacing Monoliths @@ -797,11 +944,12 @@ * Fixed spawning of Armageddon's Blade and Vial of Dragon Blood on random maps ### Interface + * Right-clicking hero icon during levelup dialog will now show hero status window * Added indicator of current turn to unit turn order panel in battles * Reduces upscaling artifacts on large spellbook * Game will now display correct date of saved games on Android -* Fixed black screen appearing during spellbook page flip animation +* Fixed black screen appearing during spellbook page flip animation * Fixed description of "Start map with hero" bonus in campaigns * Fixed invisible chat text input in game lobby * Fixed positioning of chat history in game lobby @@ -809,6 +957,7 @@ * "Large Spellbook" option is now enabled by default ### Mechanics + * Anti-magic garrison now actually blocks spell casting * Berserk spell will no longer cancel if affected unit performs counterattack * Frenzy spell can no longer be casted on units that should be immune to it @@ -816,12 +965,14 @@ * Vitality and damage skills of a commander will now correctly grow with level ### Modding + * Added UNTIL_OWN_ATTACK duration type for bonuses * Configurable objects with visit mode "first" and "random" now respect "canRefuse" flag -# 1.4.0 -> 1.4.1 +## 1.4.0 -> 1.4.1 ### General + * Fixed position for interaction with starting heroes * Fixed smooth map scrolling when running at high framerate * Fixed calculation of Fire Shield damage when caster has artifacts that increase its damage @@ -838,15 +989,17 @@ * Reverted ban on U-turns in pathfinder ### Stability + * Fixed crash on using mods made for VCMI 1.3 * Fixed crash on generating random map with large number of monoliths * Fixed crash on losing mission-critical hero in battle * Fixed crash on generating growth detalization in some localizations * Fixed crash on loading of some user-made maps -# 1.3.2 -> 1.4.0 +## 1.3.2 -> 1.4.0 ### General + * Implemented High Score screen * Implemented tracking of completed campaigns * "Secret" Heroes 3 campaigns now require completion of prerequisite campaigns first @@ -870,6 +1023,7 @@ * Spectator mode in single player is now disabled ### Multiplayer + * Implemented simultaneous turns * Implemented turn timers, including chess timers version * Game will now hide entire adventure map on hotseat turn transfer @@ -879,10 +1033,12 @@ * Multiple fixes to validation of player requests by server ### Android + * Heroes 3 data import now accepts files in any case * Fixed detection of Heroes 3 data presence when 'data' directory uses lower case ### Touchscreen + * Added tutorial video clips that explain supported touch gestures * Double tap will now be correctly interpreted as double click, e.g. to start scenario via double-click * Implemented snapping to 100% scale for adventure map zooming @@ -891,6 +1047,7 @@ * Implemented radial wheel for hero exchange in towns ### Launcher + * When a mod is being downloaded, the launcher will now correctly show progress as well as its total size * Double-clicking mod name will now perform expected action, e.g. install/update/enable or disable * Launcher will now show mod extraction progress instead of freezing @@ -902,6 +1059,7 @@ * Added option to reconnect to game lobby ### Editor + * It is now possible to configure rewards for Seer Hut, Pandora Boxes and Events * It is now possible to configure quest (limiter) in Seer Hut and Quest Guards * It is now possible to configure events and rumors in map editor @@ -909,14 +1067,15 @@ * Added option to customize hero skills * It is now possible to select object on map for win/loss conditions or for main town * Random dwellings can now be linked to a random town -* Added map editor zoom -* Added objects lock functionality +* Added map editor zoom +* Added objects lock functionality * It is now possible to configure hero placeholders in map editor -* Fixed duplicate artifact image on mouse drag +* Fixed duplicate artifact image on mouse drag * Lasso tool will no longer skip tiles * Fixed layout of roads and rivers ### Stability + * Fix possible crash on generating random map * Fixed multiple memory leaks in game client * Fixed crash on casting Hypnotize multiple times @@ -925,6 +1084,7 @@ * Fixed crash on clicking on empty Altar of Sacrifice slots ### AI + * BattleAI should now see strong stacks even if blocked by weak stacks. * BattleAI will now prefers targets slower than own stack even if they are not reachable this turn. * Improved BattleAI performance when selecting spell to cast @@ -933,11 +1093,13 @@ * Nullkiller AI can now use Fly and Water Walk spells ### Campaigns + * Implemented voice-over audio support for Heroes 3 campaigns -* Fixes victory condition on 1st scenario of "Long Live the King" campaign +* Fixes victory condition on 1st scenario of "Long Live the King" campaign * Fixed loading of defeat/victory icon and message for some campaign scenarios ### Interface + * Implemented adventure map dimming on opening windows * Clicking town hall icon on town screen will now open town hall * Clicking buildings in town hall will now show which resources are missing (if any) @@ -952,6 +1114,7 @@ * Attempting to recruit creature in town with no free slots in garrisons will now correctly show error message ### Main Menu + * Implemented window for quick selection of starting hero, town and bonus * Implemented map preview in scenario selection and game load screen accessible via right click on map * Show exact map size in map selection @@ -967,6 +1130,7 @@ * Main menu animation will no longer appear on top of new game / load game text ### Adventure Map Interface + * Picking up an artifact on adventure map will now show artifact assembly dialog if such option exists * Minimap will now preserve correct aspect ratio on rectangular maps * Fixed slot highlighting when an artifact is being assembled @@ -984,6 +1148,7 @@ * Right-clicking objects that give bonus to hero will show object description ### Mechanics + * Heroes in tavern will correctly lose effects from spells or visited objects on new day * Fixed multiple bugs in offering of Wisdom and Spell Schools on levelup. Mechanic should now work identically to Heroes 3 * Retreated heroes will no longer restore their entire mana pool on new day @@ -991,7 +1156,7 @@ * Added support for repeatable quests in Seer Huts * Using "Sacrifice All" on Altar will now correctly place all creatures but one on altar * Fixed probabilities of luck and morale -* Blinded stack no longer can get morale +* Blinded stack no longer can get morale * Creature that attacks while standing in moat will now correctly receive moat damage * Player resources are now limited to 1 000 000 000 to prevent overflow * It is no longer possible to escape from town without fort @@ -1003,6 +1168,7 @@ * Gundula is now Offense specialist and not Sorcery, as in H3 ### Random Maps Generator + * Increased tolerance for placement of Subterranean Gates * Game will now select random object template out of available options instead of picking first one * It is no longer possible to create map with a single team @@ -1013,6 +1179,7 @@ * Fixed bug leading to AI players defeated on day one. ### Modding + * All bonuses now require string as a subtype. See documentation for exact list of possible strings for each bonus. * Changes to existing objects parameters in mods will now be applied to ongoing saves * Fixed handling of engine version compatibility check @@ -1033,9 +1200,10 @@ * Object limiter now allows checking whether hero can learn skill * Object reward may now reveal terrain around visiting hero (e.g. Redwood Observatory) -# 1.3.1 -> 1.3.2 +## 1.3.1 -> 1.3.2 ### GENERAL + * VCMI now uses new application icon * Added initial version of Czech translation * Game will now use tile hero is moving from for movement cost calculations, in line with H3 @@ -1047,6 +1215,7 @@ * Added "vcmiartifacts angelWings" form to "give artifacts" cheat ### STABILITY + * Fixed freeze in Launcher on repository checkout and on mod install * Fixed crash on loading VCMI map with placed Abandoned Mine * Fixed crash on loading VCMI map with neutral towns @@ -1055,9 +1224,11 @@ * Fixed crash on switching fullscreen mode during AI turn ### CAMPAIGNS + * Fixed reorderging of hero primary skills after moving to next scenario in campaigns ### BATTLES + * Conquering a town will now correctly award additional 500 experience points * Quick combat is now enabled by default * Fixed invisible creatures from SUMMON_GUARDIANS and TRANSMUTATION bonuses @@ -1070,6 +1241,7 @@ * Long tap during spell casting will now properly abort the spell ### INTERFACE + * Added "Fill all empty slots with 1 creature" option to radial wheel in garrison windows * Context popup for adventure map monsters will now show creature icon * Game will now show correct victory message for gather troops victory condition @@ -1087,17 +1259,20 @@ * Removed invalid error message on attempting to move non-existing unit in exchange window ### RANDOM MAP GENERATOR + * Fixed bug leading to unreachable resources around mines ### MAP EDITOR + * Fixed crash on maps containing abandoned mines * Fixed crash on maps containing neutral objects * Fixed problem with random map initialized in map editor * Fixed problem with initialization of random dwellings -# 1.3.0 -> 1.3.1 +## 1.3.0 -> 1.3.1 + +### GENERAL -### GENERAL: * Fixed framerate drops on hero movement with active hota mod * Fade-out animations will now be skipped when instant hero movement speed is used * Restarting loaded campaign scenario will now correctly reapply starting bonus @@ -1109,7 +1284,8 @@ * Added option to configure reserved screen area in Launcher on iOS * Fixed border scrolling when game window is maximized -### AI PLAYER: +### AI PLAYER + * BattleAI: Improved performance of AI spell selection * NKAI: Fixed freeze on attempt to exchange army between garrisoned and visiting hero * NKAI: Fixed town threat calculation @@ -1117,13 +1293,15 @@ * VCAI: Added workaround to avoid freeze on attempting to reach unreachable location * VCAI: Fixed spellcasting by Archangels -### RANDOM MAP GENERATOR: +### RANDOM MAP GENERATOR + * Fixed placement of roads inside rock in underground * Fixed placement of shifted creature animations from HotA * Fixed placement of treasures at the boundary of wide connections * Added more potential locations for quest artifacts in zone -### STABILITY: +### STABILITY + * When starting client without H3 data game will now show message instead of silently crashing * When starting invalid map in campaign, game will now show message instead of silently crashing * Blocked loading of saves made with different set of mods to prevent crashes @@ -1143,9 +1321,10 @@ * Fixed possible crash on displaying animated main menu * Fixed crash on recruiting hero in town located on the border of map -# 1.2.1 -> 1.3.0 +## 1.2.1 -> 1.3.0 + +### GENERAL -### GENERAL: * Implemented automatic interface scaling to any resolution supported by monitor * Implemented UI scaling option to scale game interface * Game resolution and UI scaling can now be changed without game restart @@ -1163,7 +1342,8 @@ * Fixed artifact lock icon in localized versions of the game * Fixed possible crash on changing hardware cursor -### TOUCHSCREEN SUPPORT: +### TOUCHSCREEN SUPPORT + * VCMI will now properly recognizes touch screen input * Implemented long tap gesture that shows popup window. Tap once more to close popup * Long tap gesture duration can now be configured in settings @@ -1174,7 +1354,8 @@ * Implemented pinch gesture for zooming adventure map * Implemented haptic feedback (vibration) for long press gesture -### LAUNCHER: +### LAUNCHER + * Launcher will now attempt to automatically detect language of OS on first launch * Added "About" tab with information about project and environment * Added separate options for Allied AI and Enemy AI for adventure map @@ -1182,14 +1363,16 @@ * Fixed potential crash on opening mod information for mods with a changelog * Added option to configure number of autosaves -### MAP EDITOR: +### MAP EDITOR + * Fixed crash on cutting random town * Added option to export entire map as an image * Added validation for placing multiple heroes into starting town * It is now possible to have single player on a map * It is now possible to configure teams in editor -### AI PLAYER: +### AI PLAYER + * Fixed potential crash on accessing market (VCAI) * Fixed potentially infinite turns (VCAI) * Reworked object prioritizing @@ -1198,6 +1381,7 @@ * Various behavior fixes ### GAME MECHANICS + * Hero retreating after end of 7th turn will now correctly appear in tavern * Implemented hero backpack limit (disabled by default) * Fixed Admiral's Hat movement points calculation @@ -1213,7 +1397,8 @@ * Rescued hero from prison will now correctly reveal map around him * Lighthouses will no longer give movement bonus on land -### CAMPAIGNS: +### CAMPAIGNS + * Fixed transfer of artifacts into next scenario * Fixed crash on advancing to next scenario with heroes from mods * Fixed handling of "Start with building" campaign bonus @@ -1223,7 +1408,8 @@ * Fixed frequent crash on moving to next scenario during campaign * Fixed inability to dismiss heroes on maps with "capture town" victory condition -### RANDOM MAP GENERATOR: +### RANDOM MAP GENERATOR + * Improved zone placement, shape and connections * Improved zone passability for better gameplay * Improved treasure distribution and treasure values to match SoD closely @@ -1238,7 +1424,8 @@ * RMG will now run faster, utilizing many CPU cores * Removed random seed number from random map description -### INTERFACE: +### INTERFACE + * Adventure map is now scalable and can be used with any resolution without mods * Adventure map interface is now correctly blocked during enemy turn * Visiting creature banks will now show amount of guards in bank @@ -1259,7 +1446,8 @@ * Right-clicking in town fort window will now show creature information popup * Implemented pasting from clipboard (Ctrl+V) for text input -### BATTLES: +### BATTLES + * Implemented Tower moat (Land Mines) * Implemented defence reduction for units in moat * Added option to always show hero status window @@ -1271,7 +1459,8 @@ * Added distinct overlay image for showing movement range of highlighted unit * Added overlay for displaying shooting range penalties of units -### MODDING: +### MODDING + * Implemented initial version of VCMI campaign format * Implemented spell cast as possible reward for configurable object * Implemented support for configurable buildings in towns @@ -1294,9 +1483,10 @@ * Configurable objects can now be translated * Fixed loading of custom battlefield identifiers for map objects -# 1.2.0 -> 1.2.1 +## 1.2.0 -> 1.2.1 + +### GENERAL -### GENERAL: * Implemented spell range overlay for Dimension Door and Scuttle Boat * Fixed movement cost penalty from terrain * Fixed empty Black Market on game start @@ -1317,9 +1507,10 @@ * Map editor will now correctly save message property for events and pandoras * Fixed incorrect saving of heroes portraits in editor -# 1.1.1 -> 1.2.0 +## 1.1.1 -> 1.2.0 + +### GENERAL -### GENERAL: * Adventure map rendering was entirely rewritten with better, more functional code * Client battle code was heavily reworked, leading to better visual look & feel and fixing multiple minor battle bugs / glitches * Client mechanics are now framerate-independent, rather than speeding up with higher framerate @@ -1330,7 +1521,7 @@ * Fixed bonus values of heroes who specialize in secondary skills * Fixed bonus values of heroes who specialize in creatures * Fixed damage increase from Adela's Bless specialty -* Fixed missing obstacles in battles on subterranean terrain +* Fixed missing obstacles in battles on subterranean terrain * Video files now play at correct speed * Fixed crash on switching to second mission in campaigns * New cheat code: vcmiazure - give 5000 azure dragons in every empty slot @@ -1346,14 +1537,16 @@ * Default game difficulty is now set to "normal" instead of "easy" * Fixed crash on missing music files -### MAP EDITOR: +### MAP EDITOR + * Added translations to German, Polish, Russian, Spanish, Ukrainian * Implemented cut/copy/paste operations * Implemented lasso brush for terrain editing * Toolbar actions now have names * Added basic victory and lose conditions -### LAUNCHER: +### LAUNCHER + * Added initial Welcome/Setup screen for new players * Added option to install translation mod if such mod exists and player's H3 version has different language * Icons now have higher resolution, to prevent upscaling artifacts @@ -1366,7 +1559,8 @@ * Launcher now uses separate mod repository from vcmi-1.1 version to prevent mod updates to unsupported versions * Size of mod list and mod details sub-windows can now be adjusted by player -### AI PLAYER: +### AI PLAYER + * Nullkiller AI is now used by default * AI should now be more active in destroying heroes causing treat on AI towns * AI now has higher priority for resource-producing mines @@ -1382,6 +1576,7 @@ * AI will consider retreat during siege if it can not do anything (catapult is destroyed, no destroyed walls exist) ### RANDOM MAP GENERATOR + * Random map generator can now be used without vcmi-extras mod * RMG will no longer place shipyards or boats at very small lakes * Fixed placement of shipyards in invalid locations @@ -1399,7 +1594,8 @@ * Fixed amount of creatures found in Pandora Boxes to match H3 * Visitable objects will no longer be placed on top of the map, obscured by map border -### ADVENTURE MAP: +### ADVENTURE MAP + * Added option to replace popup messages on object visiting with messages in status window * Implemented different hero movement sounds for offroad movement * Cartographers now reveal terrain in the same way as in H3 @@ -1424,6 +1620,7 @@ * Seer Hut tooltips will now show messages for correct quest type ### INTERFACE + * Implemented new settings window * Added framerate display option * Fixed white status bar on server connection screen @@ -1436,12 +1633,14 @@ * Implemented extended options for random map tab: generate G+U size, select RMG template, manage teams and roads ### HERO SCREEN + * Fixed cases of incorrect artifact slot highlighting * Improved performance of artifact exchange operation * Picking up composite artifact will immediately unlock slots * It is now possible to swap two composite artifacts ### TOWN SCREEN + * Fixed gradual fade-in of a newly built building * Fixed duration of building fade-in to match H3 * Fixed rendering of Shipyard in Castle @@ -1451,7 +1650,8 @@ * Fixed missing left-click message popup for some town buildings * Moving hero from garrison by pressing space will now correctly show message "Cannot have more than 8 adventuring heroes" -### BATTLES: +### BATTLES + * Added settings for even faster animation speed than in H3 * Added display of potential kills numbers into attack tooltip in status bar * Added option to skip battle opening music entirely @@ -1470,7 +1670,7 @@ * Arrow Tower base damage should now match H3 * Destruction of wall segments will now remove ranged attack penalty * Force Field cast in front of drawbridge will now block it as in H3 -* Fixed computations for Behemoth defense reduction ability +* Fixed computations for Behemoth defense reduction ability * Bad luck (if enabled) will now multiple all damage by 50%, in line with other damage reducing mechanics * Fixed highlighting of movement range for creatures standing on a corpse * All battle animations now have same duration/speed as in H3 @@ -1478,7 +1678,7 @@ * Fixed visibility of blue border around targeted creature when spellcaster is making turn * Fixed selection highlight when in targeted creature spellcasting mode * Hovering over hero now correctly shows hero cursor -* Creature currently making turn is now highlighted in the Battle Queue +* Creature currently making turn is now highlighted in the Battle Queue * Hovering over creature icon in Battle Queue will highlight this creature in the battlefield * New battle UI extension allows control over creatures' special abilities * Fixed crash on activating auto-combat in battle @@ -1487,7 +1687,8 @@ * Unicorn Magic Damper Aura ability now works multiplicatively with Resistance * Orb of Vulnerability will now negate Resistance skill -### SPELLS: +### SPELLS + * Hero casting animation will play before spell effect * Fire Shield: added sound effect * Fire Shield: effect now correctly plays on defending creature @@ -1508,7 +1709,8 @@ * All spells that can affecte multiple targets will now highlight affected stacks * Bless and Curse now provide +1 or -1 to base damage on Advanced & Expert levels -### ABILITIES: +### ABILITIES + * Rebirth (Phoenix): Sound will now play in the same time as animation effect * Master Genie spellcasting: Sound will now play in the same time as animation effect * Power Lich, Magogs: Sound will now play in the same time as attack animation effect @@ -1518,13 +1720,14 @@ * Blind: Stacks will no longer retaliate on attack that blinds them * Demon Summon: Added animation effect for summoning * Fire shield will no longer trigger on non-adjacent attacks, e.g. from Dragon Breath -* Weakness now has correct visual effect +* Weakness now has correct visual effect * Added damage bonus for opposite elements for Elementals * Added damage reduction for Magic Elemental attacks against creatures immune to magic * Added incoming damage reduction to Petrify * Added counter-attack damage reduction for Paralyze -### MODDING: +### MODDING + * All configurable objects from H3 now have their configuration in json * Improvements to functionality of configurable objects * Replaced `SECONDARY_SKILL_PREMY` bonus with separate bonuses for each skill. @@ -1545,10 +1748,11 @@ * It is now possible for spellcaster units to have multiple spells (but only for targeting different units) * Fixed incorrect resolving of identifiers in commander abilities and stack experience definitions -# 1.1.0 -> 1.1.1 +## 1.1.0 -> 1.1.1 -### GENERAL: -* Fixed missing sound in Polish version from gog.com +### GENERAL + +* Fixed missing sound in Polish version from gog.com * Fixed positioning of main menu buttons in localized versions of H3 * Fixed crash on transferring artifact to commander * Fixed game freeze on receiving multiple artifact assembly dialogs after combat @@ -1559,27 +1763,32 @@ * Improved map loading speed * Ubuntu PPA: game will no longer crash on assertion failure -### ADVENTURE MAP: +### ADVENTURE MAP + * Fixed hero movement lag in single-player games * Fixed number of drowned troops on visiting Sirens to match H3 * iOS: pinch gesture visits current object (Spacebar behavior) instead of activating in-game console -### TOWNS: +### TOWNS + * Fixed displaying growth bonus from Statue of Legion * Growth bonus tooltip ordering now matches H3 * Buy All Units dialog will now buy units starting from the highest level -### LAUNCHER: +### LAUNCHER + * Local mods can be disabled or uninstalled * Fixed styling of Launcher interface -### MAP EDITOR: +### MAP EDITOR + * Fixed saving of roads and rivers * Fixed placement of heroes on map -# 1.0.0 -> 1.1.0 +## 1.0.0 -> 1.1.0 + +### GENERAL -### GENERAL: * iOS is supported * Mods and their versions and serialized into save files. Game checks mod compatibility before loading * Logs are stored in system default logs directory @@ -1587,7 +1796,8 @@ * FFMpeg dependency is optional now * Conan package manager is supported for MacOS and iOS -### MULTIPLAYER: +### MULTIPLAYER + * Map is passed over network, so different platforms are compatible with each other * Server self-killing is more robust * Unlock in-game console while opponent's turn @@ -1596,7 +1806,8 @@ * Reconnection mode for crashed client processes * Playing online is available using proxy server -### ADVENTURE MAP: +### ADVENTURE MAP + * Fix for digging while opponent's turn * Supported right click for quick recruit window * Fixed problem with quests are requiring identical artefacts @@ -1605,27 +1816,31 @@ * Feature to assemble/disassemble artefacts in backpack * Clickable status bar to send messages * Heroes no longer have chance to receive forbidden skill on leveling up -* Fixed visibility of newly recruited heroes near town +* Fixed visibility of newly recruited heroes near town * Fixed missing artifact slot in Artifact Merchant window -### BATTLES: +### BATTLES + * Fix healing/regeneration behaviour and effect * Fix crashes related to auto battle * Implemented ray projectiles for shooters * Introduced default tower shooter icons * Towers destroyed during battle will no longer be listed as casualties -### AI: +### AI + * BattleAI: Target prioritizing is now based on damage difference instead of health difference * Nullkiller AI can retreat and surrender * Nullkiller AI doesn't visit allied dwellings anymore * Fixed a few freezes in Nullkiller AI -### RANDOM MAP GENERATOR: +### RANDOM MAP GENERATOR + * Speedup generation of random maps * Necromancy cannot be learned in Witch Hut on random maps -### MODS: +### MODS + * Supported rewardable objects customization * Battleground obstacles are extendable now with VLC mechanism * Introduced "compatibility" section into mods settings @@ -1633,19 +1848,22 @@ * Supported customisable town entrance placement * Fixed validation of mods with new adventure map objects -### LAUNCHER: +### LAUNCHER + * Fixed problem with duplicated mods in the list * Launcher shows compatible mods only * Uninstall button was moved to the left of layout * Unsupported resolutions are not shown * Lobby for online gameplay is implemented -### MAP EDITOR: +### MAP EDITOR + * Basic version of Qt-based map editor -# 0.99 -> 1.0.0 +## 0.99 -> 1.0.0 + +### GENERAL -### GENERAL: * Spectator mode was implemented through command-line options * Some main menu settings get saved after returning to main menu - last selected map, save etc. * Restart scenario button should work correctly now @@ -1653,30 +1871,33 @@ * Lodestar Grail implemented * Fixed Gargoyles immunity * New bonuses: - * SOUL_STEAL - "WoG ghost" ability, should work somewhat same as in H3 - * TRANSMUTATION - "WoG werewolf"-like ability - * SUMMON_GUARDIANS - "WoG santa gremlin"-like ability + two-hex unit extension - * CATAPULT_EXTRA_SHOTS - defines number of extra wall attacks for units that can do so - * RANGED_RETALIATION - allows ranged counterattack - * BLOCKS_RANGED_RETALIATION - disallow enemy ranged counterattack - * SECONDARY_SKILL_VAL2 - set additional parameter for certain secondary skills - * MANUAL_CONTROL - grant manual control over war machine - * WIDE_BREATH - melee creature attacks affect many nearby hexes - * FIRST_STRIKE - creature counterattacks before attack if possible - * SYNERGY_TARGET - placeholder bonus for Mod Design Team (subject to removal in future) - * SHOOTS_ALL_ADJACENT - makes creature shots affect all neighbouring hexes - * BLOCK_MAGIC_BELOW - allows blocking spells below particular spell level. HotA cape artifact can be implemented with this - * DESTRUCTION - creature ability for killing extra units after hit, configurable + * SOUL_STEAL - "WoG ghost" ability, should work somewhat same as in H3 + * TRANSMUTATION - "WoG werewolf"-like ability + * SUMMON_GUARDIANS - "WoG santa gremlin"-like ability + two-hex unit extension + * CATAPULT_EXTRA_SHOTS - defines number of extra wall attacks for units that can do so + * RANGED_RETALIATION - allows ranged counterattack + * BLOCKS_RANGED_RETALIATION - disallow enemy ranged counterattack + * SECONDARY_SKILL_VAL2 - set additional parameter for certain secondary skills + * MANUAL_CONTROL - grant manual control over war machine + * WIDE_BREATH - melee creature attacks affect many nearby hexes + * FIRST_STRIKE - creature counterattacks before attack if possible + * SYNERGY_TARGET - placeholder bonus for Mod Design Team (subject to removal in future) + * SHOOTS_ALL_ADJACENT - makes creature shots affect all neighbouring hexes + * BLOCK_MAGIC_BELOW - allows blocking spells below particular spell level. HotA cape artifact can be implemented with this + * DESTRUCTION - creature ability for killing extra units after hit, configurable + +### MULTIPLAYER -### MULTIPLAYER: * Loading support. Save from single client could be used to load all clients. * Restart support. All clients will restart together on same server. * Hotseat mixed with network game. Multiple colors can be controlled by each client. -### SPELLS: +### SPELLS + * Implemented cumulative effects for spells -### MODS: +### MODS + * Improve support for WoG commander artifacts and skill descriptions * Added support for modding of original secondary skills and creation of new ones. * Map object sounds can now be configured via json @@ -1685,19 +1906,21 @@ * Added bonus limiters: alignment, faction and terrain * Supported new terrains, new battlefields, custom water and rock terrains * Following special buildings becomes available in the fan towns: - * attackVisitingBonus - * defenceVisitingBonus - * spellPowerVisitingBonus - * knowledgeVisitingBonus - * experienceVisitingBonus - * lighthouse - * treasury + * attackVisitingBonus + * defenceVisitingBonus + * spellPowerVisitingBonus + * knowledgeVisitingBonus + * experienceVisitingBonus + * lighthouse + * treasury + +### SOUND -### SOUND: * Fixed many missing or wrong pickup and visit sounds for map objects * All map objects now have ambient sounds identical to OH3 -### RANDOM MAP GENERATOR: +### RANDOM MAP GENERATOR + * Random map generator supports water modes (normal, islands) * Added config randomMap.json with settings for map generator * Added parameter for template allowedWaterContent @@ -1708,7 +1931,8 @@ * RMG works more stable, various crashes have been fixed * Treasures requiring guards are guaranteed to be protected -### VCAI: +### VCAI + * Reworked goal decomposition engine, fixing many loopholes. AI will now pick correct goals faster. * AI will now use universal pathfinding globally * AI can use Summon Boat and Town Portal @@ -1717,50 +1941,57 @@ * AI can distinguish the value of all map objects * General speed optimizations -### BATTLES: +### BATTLES + * Towers should block ranged retaliation * AI can bypass broken wall with moat instead of standing and waiting until gate is destroyed * Towers do not attack war machines automatically * Draw is possible now as battle outcome in case the battle ends with only summoned creatures (both sides loose) -### ADVENTURE MAP: +### ADVENTURE MAP + * Added buttons and keyboard shortcuts to quickly exchange army and artifacts between heroes * Fix: Captured town should not be duplicated on the UI -### LAUNCHER: +### LAUNCHER + * Implemented notifications about updates * Supported redirection links for downloading mods -# 0.98 -> 0.99 +## 0.98 -> 0.99 + +### GENERAL -### GENERAL: * New Bonus NO_TERRAIN_PENALTY * Nomads will remove Sand movement penalty from army * Flying and water walking is now supported in pathfinder * New artifacts supported - * Angel Wings - * Boots of Levitation + * Angel Wings + * Boots of Levitation * Implemented rumors in tavern window * New cheat codes: - * vcmiglaurung - gives 5000 crystal dragons into each slot - * vcmiungoliant - conceal fog of war for current player + * vcmiglaurung - gives 5000 crystal dragons into each slot + * vcmiungoliant - conceal fog of war for current player * New console commands: - * gosolo - AI take control over human players and vice versa - * controlai - give control of one or all AIs to player - * set hideSystemMessages on/off - suppress server messages in chat + * gosolo - AI take control over human players and vice versa + * controlai - give control of one or all AIs to player + * set hideSystemMessages on/off - suppress server messages in chat + +### BATTLES -### BATTLES: * Drawbridge mechanics implemented (animation still missing) * Merging of town and visiting hero armies on siege implemented * Hero info tooltip for skills and mana implemented -### ADVENTURE AI: +### ADVENTURE AI + * Fixed AI trying to go through underground rock * Fixed several cases causing AI wandering aimlessly * AI can again pick best artifacts and exchange artifacts between heroes * AI heroes with patrol enabled won't leave patrol area anymore -### RANDOM MAP GENERATOR: +### RANDOM MAP GENERATOR + * Changed fractalization algorithm so it can create cycles * Zones will not have straight paths anymore, they are totally random * Generated zones will have different size depending on template setting @@ -1768,94 +1999,112 @@ * Added Seer Huts with quests that match OH3 * RMG will guarantee at least 100 pairs of Monoliths are available even if there are not enough different defs -# 0.97 -> 0.98 +## 0.97 -> 0.98 + +### GENERAL -### GENERAL: * Pathfinder can now find way using Monoliths and Whirlpools (only used if hero has protection) -### ADVENTURE AI: +### ADVENTURE AI + * AI will try to use Monolith entrances for exploration * AI will now always revisit each exit of two way monolith if exit no longer visible * AI will eagerly pick guarded and blocked treasures -### ADVENTURE MAP: +### ADVENTURE MAP + * Implemented world view * Added graphical fading effects -### SPELLS: +### SPELLS + * New spells handled: - * Earthquake - * View Air - * View Earth - * Visions - * Disguise + * Earthquake + * View Air + * View Earth + * Visions + * Disguise * Implemented CURE spell negative dispel effect * Added LOCATION target for spells castable on any hex with new target modifiers -### BATTLES: +### BATTLES + * Implemented OH3 stack split / upgrade formulas according to AlexSpl -### RANDOM MAP GENERATOR: +### RANDOM MAP GENERATOR + * Underground tunnels are working now * Implemented "junction" zone type * Improved zone placing algorithm * More balanced distribution of treasure piles * More obstacles within zones -# 0.96 -> 0.97 (Nov 01 2014) +## 0.96 -> 0.97 (Nov 01 2014) + +### GENERAL -### GENERAL: * (windows) Moved VCMI data directory from '%userprofile%\vcmi' to '%userprofile%\Documents\My Games\vcmi' * (windows) (OSX) Moved VCMI save directory from 'VCMI_DATA\Games' to 'VCMI_DATA\Saves' * (linux) * Changes in used librries: - * VCMI can now be compiled with SDL2 - * Movies will use ffmpeg library - * change boost::bind to std::bind - * removed boost::assign - * Updated FuzzyLite to 5.0 + * VCMI can now be compiled with SDL2 + * Movies will use ffmpeg library + * change boost::bind to std::bind + * removed boost::assign + * Updated FuzzyLite to 5.0 * Multiplayer load support was implemented through command-line options -### ADVENTURE AI: +### ADVENTURE AI + * Significantly optimized execution time, AI should be much faster now. -### ADVENTURE MAP: +### ADVENTURE MAP + * Non-latin characters can now be entered in chat window or used for save names. * Implemented separate speed for owned heroes and heroes owned by other players -### GRAPHICS: +### GRAPHICS + * Better upscaling when running in fullscreen mode. * New creature/commader window * New resolutions and bonus icons are now part of a separate mod * Added graphics for GENERAL_DAMAGE_REDUCTION bonus (Kuririn) -### RANDOM MAP GENERATOR: +### RANDOM MAP GENERATOR + * Random map generator now creates complete and playable maps, should match original RMG * All important features from original map templates are implemented * Fixed major crash on removing objects * Undeground zones will look just like surface zones -### LAUNCHER: +### LAUNCHER + * Implemented switch to disable intro movies in game -# 0.95 -> 0.96 (Jul 01 2014) +## 0.95 -> 0.96 (Jul 01 2014) -### GENERAL: -* (linux) now VCMI follows XDG specifications. See http://forum.vcmi.eu/viewtopic.php?t=858 +### GENERAL + +* (linux) now VCMI follows XDG specifications. See + +### ADVENTURE AI -### ADVENTURE AI: * Optimized speed and removed various bottlenecks. -### ADVENTURE MAP: +### ADVENTURE MAP + * Heroes auto-level primary and secondary skill levels according to experience -### BATTLES: +### BATTLES + * Wall hit/miss sound will be played when using catapult during siege -### SPELLS: +### SPELLS + * New configuration format -### RANDOM MAP GENERATOR: +### RANDOM MAP GENERATOR + * Towns from mods can be used * Reading connections, terrains, towns and mines from template * Zone placement @@ -1863,31 +2112,35 @@ * Guard generation * Treasure piles generation (so far only few removable objects) -### MODS: +### MODS + * Support for submods - mod may have their own "submods" located in /Mods directory * Mods may provide their own changelogs and screenshots that will be visible in Launcher * Mods can now add new (offensive, buffs, debuffs) spells and change existing * Mods can use custom mage guild background pictures and videos for taverns, setting of resources daily income for buildings -### GENERAL: +### GENERAL + * Added configuring of heroes quantity per player allowed in game -# 0.94 -> 0.95 (Mar 01 2014) +## 0.94 -> 0.95 (Mar 01 2014) + +### GENERAL -### GENERAL: * Components of combined artifacts will now display info about entire set. * Implements level limit * Added WoG creature abilities by Kuririn -* Implemented a confirmation dialog when pressing Alt + F4 to quit the game +* Implemented a confirmation dialog when pressing Alt + F4 to quit the game * Added precompiled header compilation for CMake (can be enabled per flag) * VCMI will detect changes in text files using crc-32 checksum * Basic support for unicode. Internally vcmi always uses utf-8 * (linux) Launcher will be available as "VCMI" menu entry from system menu/launcher * (linux) Added a SIGSEV violation handler to vcmiserver executable for logging stacktrace (for convenience) -### ADVENTURE AI: +### ADVENTURE AI + * AI will use fuzzy logic to compare and choose multiple possible subgoals. -* AI will now use SectorMap to find a way to guarded / covered objects. +* AI will now use SectorMap to find a way to guarded / covered objects. * Significantly improved exploration algorithm. * Locked heroes now try to decompose their goals exhaustively. * Fixed (common) issue when AI found neutral stacks infinitely strong. @@ -1896,26 +2149,30 @@ * AI should now conquer map more aggressively and much faster * Fuzzy rules will be printed out at map launch (if AI log is enabled) -### CAMPAIGNS: +### CAMPAIGNS + * Implemented move heroes to next scenario * Support for non-standard victory conditions for H3 campaigns * Campaigns use window with bonus & scenario selection than scenario information window from normal maps * Implemented hero recreate handling (e.g. Xeron will be recreated on AB campaign) * Moved place bonus hero before normal random hero and starting hero placement -> same behaviour as in OH3 -* Moved placing campaign heroes before random object generation -> same behaviour as in OH3 +* Moved placing campaign heroes before random object generation -> same behaviour as in OH3 + +### TOWNS -### TOWNS: * Extended building dependencies support -### MODS: +### MODS + * Custom victory/loss conditions for maps or campaigns * 7 days without towns loss condition is no longer hardcoded * Only changed mods will be validated -# 0.93 -> 0.94 (Oct 01 2013) +## 0.93 -> 0.94 (Oct 01 2013) -### GENERAL: -* New Launcher application, see +### GENERAL + +* New Launcher application, see * Filesystem now supports zip archives. They can be loaded similarly to other archives in filesystem.json. Mods can use Content.zip instead of Content/ directory. * fixed "get txt" console command * command "extract" to extract file by name @@ -1926,12 +2183,14 @@ * Upgrade cost will never be negative. * support for Chinese fonts (GBK 2-byte encoding) -### ADVENTURE MAP: +### ADVENTURE MAP + * if Quick Combat option is turned on, battles will be resolved by AI * first hero is awakened on new turn * fixed 3000 gems reward in shipwreck -### BATTLES: +### BATTLES + * autofight implemented * most of the animations is time-based * simplified postioning of units in battle, should fix remaining issues with unit positioning @@ -1950,13 +2209,15 @@ * damage done by turrets is properly increased by built buldings * Wyverns will cast Poison instead of Stone Gaze. -### TOWN: +### TOWN + * Fixed issue that allowed to build multiple boats in town. * fix for lookout tower -# 0.92 -> 0.93 (Jun 01 2013) +## 0.92 -> 0.93 (Jun 01 2013) + +### GENERAL -### GENERAL: * Support for SoD-only installations, WoG becomes optional addition * New logging framework * Negative luck support, disabled by default @@ -1964,7 +2225,8 @@ * Fixed stack artifact (and related buttons) not displaying in creature window. * Fixed crash at month of double population. -### MODS: +### MODS + * Improved json validation. Now it support most of features from latest json schema draft. * Icons use path to icon instead of image indexes. * It is possible to edit data of another mod or H3 data via mods. @@ -1972,7 +2234,8 @@ * Removed no longer needed field "projectile spins" * Heroes: split heroes.json in manner similar to creatures\factions; string ID's for H3 heroes; h3 hero classes and artifacts can be modified via json. -### BATTLES: +### BATTLES + * Fixed Death Stare of Commanders * Projectile blitting should be closer to original H3. But still not perfect. * Fixed missing Mirth effects @@ -1981,34 +2244,39 @@ * Fixed abilities of Efreet. * Fixed broken again palette in some battle backgrounds -### TOWN: +### TOWN + * VCMI will not crash if building selection area is smaller than def * Detection of transparency on selection area is closer to H3 * Improved handling buildings with mode "auto": - * they will be properly processed (new creatures will be added if dwelling, spells learned if mage guild, and so on) - * transitive dependencies are handled (A makes B build, and B makes C and D) + * they will be properly processed (new creatures will be added if dwelling, spells learned if mage guild, and so on) + * transitive dependencies are handled (A makes B build, and B makes C and D) + +### SOUND -### SOUND: * Added missing WoG creature sounds (from Kuririn). * The Windows package comes with DLLs needed to play .ogg files * (linux) convertMP3 option for vcmibuilder for systems where SDL_Mixer can't play mp3's * some missing sounds for battle effects -### ARTIFACTS: +### ARTIFACTS + * Several fixes to combined artifacts added via mods. * Fixed Spellbinder's Hat giving level 1 spells instead of 5. * Fixed incorrect components of Cornucopia. * Cheat code with grant all artifacts, including the ones added by mods -# 0.91 -> 0.92 (Mar 01 2013) +## 0.91 -> 0.92 (Mar 01 2013) + +### GENERAL -### GENERAL: * hero crossover between missions in campaigns * introduction before missions in campaigns -### MODS: +### MODS + * Added CREATURE_SPELL_POWER for commanders -* Added spell modifiers to various spells: Hypnotize (Astral), Firewall (Luna), Landmine +* Added spell modifiers to various spells: Hypnotize (Astral), Firewall (Luna), Landmine * Fixed ENEMY_DEFENCE_REDUCTION, GENERAL_ATTACK_REDUCTION * Extended usefulness of ONLY_DISTANCE_FIGHT, ONLY_MELEE_FIGHT ranges * Double growth creatures are configurable now @@ -2016,55 +2284,61 @@ * Stack can use more than 2 attacks. Additional attacks can now be separated as "ONLY_MELEE_FIGHT and "ONLY_DISTANCE_FIGHT". * Moat damage configurable * More config options for spells: - * mind immunity handled by config - * direct damage immunity handled by config - * immunity icon configurable - * removed mind_spell flag -* creature config use string ids now. + * mind immunity handled by config + * direct damage immunity handled by config + * immunity icon configurable + * removed mind_spell flag +* creature config use string ids now. * support for string subtype id in short bonus format * primary skill identifiers for bonuses -# 0.9 -> 0.91 (Feb 01 2013) +## 0.9 -> 0.91 (Feb 01 2013) + +### GENERAL -### GENERAL: * VCMI build on OS X is now supported * Completely removed autotools * Added RMG interace and ability to generate simplest working maps * Added loading screen -### MODS: +### MODS + * Simplified mod structure. Mods from 0.9 will not be compatible. * Mods can be turned on and off in config/modSettings.json file * Support for new factions, including: - * New towns - * New hero classes - * New heroes - * New town-related external dwellings + * New towns + * New hero classes + * New heroes + * New town-related external dwellings * Support for new artifact, including combined, commander and stack artifacts * Extended configuration options - * All game objects are referenced by string identifiers - * Subtype resolution for bonuses + * All game objects are referenced by string identifiers + * Subtype resolution for bonuses + +### BATTLES -### BATTLES: * Support for "enchanted" WoG ability -### ADVENTURE AI: +### ADVENTURE AI + * AI will try to use Subterranean Gate, Redwood Observatory and Cartographer for exploration * Improved exploration algorithm * AI will prioritize dwellings and mines when there are no opponents visible -# 0.89 -> 0.9 (Oct 01 2012) +## 0.89 -> 0.9 (Oct 01 2012) + +### GENERAL -### GENERAL: * Provisional support creature-adding mods * New filesystem allowing easier resource adding/replacing * Reorganized package for better compatibility with HotA and not affecting the original game * Moved many hard-coded settings into text config files * Commander level-up dialog * New Quest Log window -* Fixed a number of bugs in campaigns, support for starting hero selection bonus. +* Fixed a number of bugs in campaigns, support for starting hero selection bonus. + +### BATTLES -### BATTLES: * New graphics for Stack Queue * Death Stare works identically to H3 * No explosion when catapult fails to damage the wall @@ -2072,9 +2346,10 @@ * Fixed crash when attacking stack dies in the Moat just before the attack * Fixed Orb of Inhibition and Recanter's Cloak (they were incorrectly implemented) * Fleeing hero won't lose artifacts. -* Spellbook won't be captured. +* Spellbook won't be captured. + +### ADVENTURE AI -### ADVENTURE AI: * support for quests (Seer Huts, Quest Guardians, and so) * AI will now wander with all the heroes that have spare movement points. It should prevent stalling. * AI will now understand threat of Abandoned Mine. @@ -2084,13 +2359,15 @@ * Fixed crash when hero assigned to goal was lost when attempting realizing it * Fixed a possible freeze when exchanging resources at marketplace -### BATTLE AI: +### BATTLE AI + * It is possible to select a battle AI module used by VCMI by typing into the console "setBattleAI ". The names of available modules are "StupidAI" and "BattleAI". BattleAI may be a little smarter but less stable. By the default, StupidAI will be used, as in previous releases. * New battle AI module: "BattleAI" that is smarter and capable of casting some offensive and enchantment spells -# 0.88 -> 0.89 (Jun 01 2012) +## 0.88 -> 0.89 (Jun 01 2012) + +### GENERAL -### GENERAL: * Mostly implemented Commanders feature (missing level-up dialog) * Support for stack artifacts * New creature window graphics contributed by fishkebab @@ -2102,11 +2379,13 @@ * Simple mechanism for detecting game desynchronization after init * 1280x800 resolution graphics, contributed by Topas -### ADVENTURE MAP: +### ADVENTURE MAP + * Fixed monsters regenerating casualties from battle at the start of new week. * T in adventure map will switch to next town -### BATTLES: +### BATTLES + * It's possible to switch active creature during tacts phase by clicking on stack * After battle artifacts of the defeated hero (and his army) will be taken by winner * Rewritten handling of battle obstacles. They will be now placed following H3 algorithm. @@ -2123,22 +2402,25 @@ * Fixed and simplified Teleport casting * Fixed Remove Obstacle spell * New spells supported: - * Chain Lightning - * Fire Wall - * Force Field - * Land Mine - * Quicksands - * Sacrifice + * Chain Lightning + * Fire Wall + * Force Field + * Land Mine + * Quicksands + * Sacrifice + +### TOWNS -### TOWNS: * T in castle window will open a tavern window (if available) -### PREGAME: +### PREGAME + * Pregame will use same resolution as main game * Support for scaling background image * Customization of graphics with config file. -### ADVENTURE AI: +### ADVENTURE AI + * basic rule system for threat evaluation * new town development logic * AI can now use external dwellings @@ -2147,7 +2429,7 @@ * AI will recruit multiple heroes for exploration * AI won't try attacking its own heroes -# 0.87 -> 0.88 (Mar 01 2012) +## 0.87 -> 0.88 (Mar 01 2012) * added an initial version of new adventure AI: VCAI * system settings window allows to change default resolution @@ -2156,202 +2438,226 @@ * Creature Window can handle descriptions of spellcasting abilities * Support for the clone spell -# 0.86 -> 0.87 (Dec 01 2011) +## 0.86 -> 0.87 (Dec 01 2011) + +### GENERAL -### GENERAL: * Pathfinder can find way using ships and subterranean gates * Hero reminder & sleep button -### PREGAME: +### PREGAME + * Credits are implemented -### BATTLES: +### BATTLES + * All attacked hexes will be highlighted * New combat abilities supported: - * Spell Resistance aura - * Random spellcaster (Genies) - * Mana channeling - * Daemon summoning - * Spellcaster (Archangel Ogre Mage, Elementals, Faerie Dragon) - * Fear - * Fearless - * No wall penalty - * Enchanter - * Bind - * Dispel helpful spells + * Spell Resistance aura + * Random spellcaster (Genies) + * Mana channeling + * Daemon summoning + * Spellcaster (Archangel Ogre Mage, Elementals, Faerie Dragon) + * Fear + * Fearless + * No wall penalty + * Enchanter + * Bind + * Dispel helpful spells -# 0.85 -> 0.86 (Sep 01 2011) +## 0.85 -> 0.86 (Sep 01 2011) + +### GENERAL -### GENERAL: * Reinstated music support * Bonus system optimizations (caching) * converted many config files to JSON * .tga file support * New artifacts supported - * Admiral's Hat - * Statue of Legion - * Titan's Thunder + * Admiral's Hat + * Statue of Legion + * Titan's Thunder + +### BATTLES -### BATTLES: * Correct handling of siege obstacles * Catapult animation * New combat abilities supported - * Dragon Breath - * Three-headed Attack - * Attack all around - * Death Cloud / Fireball area attack - * Death Blow - * Lightning Strike - * Rebirth + * Dragon Breath + * Three-headed Attack + * Attack all around + * Death Cloud / Fireball area attack + * Death Blow + * Lightning Strike + * Rebirth * New WoG abilities supported - * Defense Bonus - * Cast before attack - * Immunity to direct damage spells + * Defense Bonus + * Cast before attack + * Immunity to direct damage spells * New spells supported - * Magic Mirror - * Titan's Lightning Bolt + * Magic Mirror + * Titan's Lightning Bolt -# 0.84 -> 0.85 (Jun 01 2011) +## 0.84 -> 0.85 (Jun 01 2011) + +### GENERAL -### GENERAL: * Support for stack experience * Implemented original campaign selection screens * New artifacts supported: - * Statesman's Medal - * Diplomat's Ring - * Ambassador's Sash + * Statesman's Medal + * Diplomat's Ring + * Ambassador's Sash + +### TOWNS -### TOWNS: * Implemented animation for new town buildings * It's possible to sell artifacts at Artifact Merchants -### BATTLES: +### BATTLES + * Neutral monsters will be split into multiple stacks * Hero can surrender battle to keep army * Support for Death Stare, Support for Poison, Age, Disease, Acid Breath, Fire / Water / Earth / Air immunities and Receptiveness * Partial support for Stone Gaze, Paralyze, Mana drain -# 0.83 -> 0.84 (Mar 01 2011) +## 0.83 -> 0.84 (Mar 01 2011) + +### GENERAL -### GENERAL: * Bonus system has been rewritten * Partial support for running VCMI in duel mode (no adventure map, only one battle, ATM only AI-AI battles) * New artifacts supported: - * Angellic Alliance - * Bird of Perception - * Emblem of Cognizance - * Spell Scroll - * Stoic Watchman + * Angellic Alliance + * Bird of Perception + * Emblem of Cognizance + * Spell Scroll + * Stoic Watchman + +### BATTLES -### BATTLES: * Better animations handling * Defensive stance is supported -### HERO: -* New secondary skills supported: - * Artillery - * Eagle Eye - * Tactics +### HERO + +* New secondary skills supported: + * Artillery + * Eagle Eye + * Tactics + +### AI PLAYER -### AI PLAYER: * new AI leading neutral creatures in combat, slightly better then previous -# 0.82 -> 0.83 (Nov 01 2010) +## 0.82 -> 0.83 (Nov 01 2010) + +### GENERAL -### GENERAL: * Alliances support * Week of / Month of events * Mostly done pregame for MP games (temporarily only for local clients) * Support for 16bpp displays * Campaigns: - * support for building bonus - * moving to next map after victory + * support for building bonus + * moving to next map after victory * Town Portal supported * Vial of Dragon Blood and Statue of Legion supported -### HERO: +### HERO + * remaining specialities have been implemented -### TOWNS: -* town events supported -* Support for new town structures: Deiety of Fire and Escape Tunnel +### TOWNS + +* town events supported +* Support for new town structures: Deiety of Fire and Escape Tunnel + +### BATTLES -### BATTLES: * blocked retreating from castle -# 0.81 -> 0.82 (Aug 01 2010) +## 0.81 -> 0.82 (Aug 01 2010) + +### GENERAL -### GENERAL: * Some of the starting bonuses in campaigns are supported * It's possible to select difficulty level of mission in campaign * new cheat codes: - * vcmisilmaril - player wins - * vcmimelkor - player loses + * vcmisilmaril - player wins + * vcmimelkor - player loses + +### ADVENTURE MAP -### ADVENTURE MAP: * Neutral armies growth implemented (10% weekly) * Power rating of neutral stacks * Favourable Winds reduce sailing cost -### HERO: +### HERO + * Learning secondary skill supported. * Most of hero specialities are supported, including: - * Creature specialities (progressive, fixed, Sir Mullich) - * Spell damage specialities (Deemer), fixed bonus (Ciele) - * Secondary skill bonuses - * Creature Upgrades (Gelu) - * Resource generation - * Starting Skill (Adrienne) + * Creature specialities (progressive, fixed, Sir Mullich) + * Spell damage specialities (Deemer), fixed bonus (Ciele) + * Secondary skill bonuses + * Creature Upgrades (Gelu) + * Resource generation + * Starting Skill (Adrienne) + +### TOWNS -### TOWNS: * Support for new town structures: - * Artifact Merchant - * Aurora Borealis - * Castle Gates - * Magic University - * Portal of Summoning - * Skeleton transformer - * Veil of Darkness + * Artifact Merchant + * Aurora Borealis + * Castle Gates + * Magic University + * Portal of Summoning + * Skeleton transformer + * Veil of Darkness + +### OBJECTS -### OBJECTS: * Stables will now upgrade Cavaliers to Champions. * New object supported: - * Abandoned Mine - * Altar of Sacrifice - * Black Market - * Cover of Darkness - * Hill Fort - * Refugee Camp - * Sanctuary - * Tavern - * University - * Whirlpool + * Abandoned Mine + * Altar of Sacrifice + * Black Market + * Cover of Darkness + * Hill Fort + * Refugee Camp + * Sanctuary + * Tavern + * University + * Whirlpool -# 0.8 -> 0.81 (Jun 01 2010) +## 0.8 -> 0.81 (Jun 01 2010) + +### GENERAL -### GENERAL: * It's possible to start campaign * Support for build grail victory condition * New artifacts supported: - * Angel's Wings - * Boots of levitation - * Orb of Vulnerability - * Ammo cart - * Golden Bow - * Hourglass of Evil Hour - * Bow of Sharpshooter - * Armor of the Damned + * Angel's Wings + * Boots of levitation + * Orb of Vulnerability + * Ammo cart + * Golden Bow + * Hourglass of Evil Hour + * Bow of Sharpshooter + * Armor of the Damned + +### ADVENTURE MAP -### ADVENTURE MAP: * Creatures now guard surrounding tiles * New adventura map spells supported: - * Summon Boat - * Scuttle Boat - * Dimension Door - * Fly - * Water walk + * Summon Boat + * Scuttle Boat + * Dimension Door + * Fly + * Water walk + +### BATTLES -### BATTLES: * A number of new creature abilities supported * First Aid Tent is functional * Support for distance/wall/melee penalties & no * penalty abilities @@ -2359,107 +2665,123 @@ * Luck support * Teleportation spell -### HERO: +### HERO + * First Aid secondary skill * Improved formula for necromancy to match better OH3 -### TOWNS: +### TOWNS + * Sending resources to other players by marketplace * Support for new town structures: - * Lighthouse - * Colossus - * Freelancer's Guild - * Guardian Spirit - * Necromancy Amplifier - * Soul Prison + * Lighthouse + * Colossus + * Freelancer's Guild + * Guardian Spirit + * Necromancy Amplifier + * Soul Prison + +### OBJECTS -### OBJECTS: * New object supported: - * Freelancer's Guild - * Trading Post - * War Machine Factory + * Freelancer's Guild + * Trading Post + * War Machine Factory -# 0.75 -> 0.8 (Mar 01 2010) +## 0.75 -> 0.8 (Mar 01 2010) + +### GENERAL -### GENERAL: * Victory and loss conditions are supported. It's now possible to win or lose the game. * Implemented assembling and disassembling of combination artifacts. * Kingdom Overview screen is now available. * Implemented Grail (puzzle map, digging, constructing ultimate building) * Replaced TTF fonts with original ones. -### ADVENTURE MAP: +### ADVENTURE MAP + * Implemented rivers animations (thx to GrayFace). -### BATTLES: +### BATTLES + * Fire Shield spell (and creature ability) supported * affecting morale/luck and casting spell after attack creature abilities supported -### HERO: +### HERO + * Implementation of Scholar secondary skill -### TOWN: +### TOWN + * New left-bottom info panel functionalities. -### TOWNS: -* new town structures supported: - * Ballista Yard - * Blood Obelisk - * Brimstone Clouds - * Dwarven Treasury - * Fountain of Fortune - * Glyphs of Fear - * Mystic Pond - * Thieves Guild - * Special Grail functionalities for Dungeon, Stronghold and Fortress +### TOWNS + +* new town structures supported: + * Ballista Yard + * Blood Obelisk + * Brimstone Clouds + * Dwarven Treasury + * Fountain of Fortune + * Glyphs of Fear + * Mystic Pond + * Thieves Guild + * Special Grail functionalities for Dungeon, Stronghold and Fortress + +### OBJECTS -### OBJECTS: * New objects supported: - * Border gate - * Den of Thieves - * Lighthouse - * Obelisk - * Quest Guard - * Seer hut + * Border gate + * Den of Thieves + * Lighthouse + * Obelisk + * Quest Guard + * Seer hut A lot of of various bugfixes and improvements: -http://bugs.vcmi.eu/changelog_page.php?version_id=14 + -# 0.74 -> 0.75 (Dec 01 2009) +## 0.74 -> 0.75 (Dec 01 2009) + +### GENERAL -### GENERAL: * Implemented "main menu" in-game option. * Hide the mouse cursor while displaying a popup window. * Better handling of huge and empty message boxes (still needs more changes) * Fixed several crashes when exiting. -### ADVENTURE INTERFACE: +### ADVENTURE INTERFACE + * Movement cursor shown for unguarded enemy towns. * Battle cursor shown for guarded enemy garrisons. * Clicking on the border no longer opens an empty info windows -### HERO WINDOW: -* Improved artifact moving. Available slots are highlighted. Moved artifact is bound to mouse cursor. +### HERO WINDOW + +* Improved artifact moving. Available slots are highlighted. Moved artifact is bound to mouse cursor. + +### TOWNS -### TOWNS: * new special town structures supported: - * Academy of Battle Scholars - * Cage of Warlords - * Mana Vortex - * Stables - * Skyship (revealing entire map only) + * Academy of Battle Scholars + * Cage of Warlords + * Mana Vortex + * Stables + * Skyship (revealing entire map only) + +### OBJECTS -### OBJECTS: * External dwellings increase town growth * Right-click info window for castles and garrisons you do not own shows a rough amount of creatures instead of none * Scholar won't give unavailable spells anymore. A lot of of various bugfixes and improvements: -http://bugs.vcmi.eu/changelog_page.php?version_id=2 + -# 0.73 -> 0.74 (Oct 01 2009) +## 0.73 -> 0.74 (Oct 01 2009) + +### GENERAL -### GENERAL: * Scenario Information window * Save Game window * VCMI window should start centered @@ -2471,13 +2793,15 @@ http://bugs.vcmi.eu/changelog_page.php?version_id=2 * fixed issue when splitting stack to the hero with only one creatures * a few fixes for shipyard window -### ADVENTURE INTERFACE: +### ADVENTURE INTERFACE + * Cursor shows if tile is accessible and how many turns away * moving hero with arrow keys / numpad * fixed Next Hero button behaviour * fixed Surface/Underground switch button in higher resolutions -### BATTLES: +### BATTLES + * partial siege support * new stack queue for higher resolutions (graphics made by Dru, thx!) * 'Q' pressing toggles the stack queue displaying (so it can be enabled/disabled it with single key press) @@ -2487,72 +2811,77 @@ http://bugs.vcmi.eu/changelog_page.php?version_id=2 * fixed crash when clicking on enemy stack without moving mouse just after receiving action * even large stack numbers will fit the boxes * when active stack is killed by spell, game behaves properly -* shooters attacking twice (like Grand Elves) won't attack twice in melee -* ballista can shoot even if there's an enemy creature next to it +* shooters attacking twice (like Grand Elves) won't attack twice in melee +* ballista can shoot even if there's an enemy creature next to it * improved obstacles placement, so they'll better fit hexes (thx to Ivan!) * selecting attack directions works as in H3 * estimating damage that will be dealt while choosing stack to be attacked * modified the positioning of battle effects, they should look about right now. -* after selecting a spell during combat, l-click is locked for any action other than casting. +* after selecting a spell during combat, l-click is locked for any action other than casting. * flying creatures will be blitted over all other creatures, obstacles and wall * obstacles and units should be printed in better order (not tested) * fixed armageddon animation * new spells supported: - * Anti-Magic - * Cure - * Resurrection - * Animate Dead - * Counterstrike - * Berserk - * Hypnotize - * Blind - * Fire Elemental - * Earth Elemental - * Water Elemental - * Air Elemental - * Remove obstacle + * Anti-Magic + * Cure + * Resurrection + * Animate Dead + * Counterstrike + * Berserk + * Hypnotize + * Blind + * Fire Elemental + * Earth Elemental + * Water Elemental + * Air Elemental + * Remove obstacle + +### TOWNS -### TOWNS: * enemy castle can be taken over * only one capitol per player allowed (additional ones will be lost) * garrisoned hero can buy a spellbook * heroes available in tavern should be always different * ship bought in town will be correctly placed * new special town structures supported: - * Lookout Tower - * Temple of Valhalla - * Wall of Knowledge - * Order of Fire + * Lookout Tower + * Temple of Valhalla + * Wall of Knowledge + * Order of Fire + +### HERO WINDOW -### HERO WINDOW: * war machines cannot be unequiped -### PREGAME: +### PREGAME + * sorting: a second click on the column header sorts in descending order. * advanced options tab: r-click popups for selected town, hero and bonus * starting scenario / game by double click -* arrows in options tab are hidden when not available +* arrows in options tab are hidden when not available * subtitles for chosen hero/town/bonus in pregame -### OBJECTS: +### OBJECTS + * fixed pairing Subterranean Gates * New objects supported: - * Borderguard & Keymaster Tent - * Cartographer - * Creature banks - * Eye of the Magi & Hut of the Magi - * Garrison - * Stables - * Pandora Box - * Pyramid + * Borderguard & Keymaster Tent + * Cartographer + * Creature banks + * Eye of the Magi & Hut of the Magi + * Garrison + * Stables + * Pandora Box + * Pyramid -# 0.72 -> 0.73 (Aug 01 2009) +## 0.72 -> 0.73 (Aug 01 2009) + +### GENERAL -### GENERAL: * infowindow popup will be completely on screen * fixed possible crash with in game console * fixed crash when gaining artifact after r-click on hero in tavern -* Estates / hero bonuses won't give resources on first day. +* Estates / hero bonuses won't give resources on first day. * video handling (intro, main menu animation, tavern animation, spellbook animation, battle result window) * hero meeting window allowing exchanging armies and artifacts between heroes on adventure map * 'T' hotkey opens marketplace window @@ -2571,13 +2900,15 @@ http://bugs.vcmi.eu/changelog_page.php?version_id=2 * r-click popups on enemy heroes and towns * hero leveling formula matches the H3 -### ADVENTURE INTERFACE: +### ADVENTURE INTERFACE + * Garrisoning, then removing hero from garrison move him at the end of the heroes list * The size of the frame around the map depends on the screen size. * spellbook shows adventure spells when opened on adventure map * erasing path after picking objects with last movement point -### BATTLES: +### BATTLES + * spell resistance supported (secondary skill, artifacts, creature skill) * corrected damage inflicted by spells and ballista * added some missing projectile infos @@ -2586,49 +2917,54 @@ http://bugs.vcmi.eu/changelog_page.php?version_id=2 * non-living and undead creatures have now always 0 morale * displaying luck effect animation * support for battleground overlays: - * cursed ground - * magic plains - * fiery fields - * rock lands - * magic clouds - * lucid pools - * holy ground - * clover field - * evil fog + * cursed ground + * magic plains + * fiery fields + * rock lands + * magic clouds + * lucid pools + * holy ground + * clover field + * evil fog + +### TOWNS -### TOWNS: * fixes for horde buildings * garrisoned hero can buy a spellbook if he is selected or if there is no visiting hero * capitol bar in town hall is grey (not red) if already one exists * fixed crash on entering hall when town was near map edge -### HERO WINDOW: +### HERO WINDOW + * garrisoned heroes won't be shown on the list * artifacts will be present on morale/luck bonuses list -### PREGAME: +### PREGAME + * saves are sorted primary by map format, secondary by name * fixed displaying date of saved game (uses local time, removed square character) -### OBJECTS: +### OBJECTS + * Fixed primary/secondary skill levels given by a scholar. * fixed problems with 3-tiles monoliths * fixed crash with flaggable building next to map edge * fixed some descriptions for events * New objects supported: - * Buoy - * Creature Generators - * Flotsam - * Mermaid - * Ocean bottle - * Sea Chest - * Shipwreck Survivor - * Shipyard - * Sirens + * Buoy + * Creature Generators + * Flotsam + * Mermaid + * Ocean bottle + * Sea Chest + * Shipwreck Survivor + * Shipyard + * Sirens -# 0.71 -> 0.72 (Jun 1 2009) +## 0.71 -> 0.72 (Jun 1 2009) + +### GENERAL -### GENERAL: * many sound effects and music * autosave (to 5 subsequent files) * artifacts support (most of them) @@ -2644,7 +2980,8 @@ http://bugs.vcmi.eu/changelog_page.php?version_id=2 * redundant quotation marks from artifact descriptions are removed * no income at the first day -### ADVENTURE INTERFACE: +### ADVENTURE INTERFACE + * fixed crasbug occurring on revisiting objects (by pressing space) * always restoring default cursor when movng mouse out of the terrain * fixed map scrolling with ctrl+arrows when some windows are opened @@ -2652,7 +2989,8 @@ http://bugs.vcmi.eu/changelog_page.php?version_id=2 * pathfinder will now look for a path going via printed positions of roads when it's possible * enter can be used to open window with selected hero/town -### BATTLES: +### BATTLES + * many creatures special skills implemented * battle will end when one side has only war machines * fixed some problems with handling obstacles info @@ -2662,41 +3000,45 @@ http://bugs.vcmi.eu/changelog_page.php?version_id=2 * canceling of casting a spell by pressing Escape or R-click (R-click on a creatures does not cancel a spell) * spellbook cannot be opened by L-click on hero in battle when it shouldn't be possible * new spells: - * frost ring - * fireball - * inferno - * meteor shower - * death ripple - * destroy undead - * dispel - * armageddon - * disrupting ray - * protection from air - * protection from fire - * protection from water - * protection from earth - * precision - * slayer + * frost ring + * fireball + * inferno + * meteor shower + * death ripple + * destroy undead + * dispel + * armageddon + * disrupting ray + * protection from air + * protection from fire + * protection from water + * protection from earth + * precision + * slayer + +### TOWNS -### TOWNS: * resting in town with mage guild will replenih all the mana points * fixed Blacksmith * the number of creatures at the beginning of game is their base growth * it's possible to enter Tavern via Brotherhood of Sword -### HERO WINDOW: +### HERO WINDOW + * fixed mana limit info in the hero window * war machines can't be removed * fixed problems with removing artifacts when all visible slots in backpack are full -### PREGAME: +### PREGAME + * clicking on "advanced options" a second time now closes the tab instead of refreshing it. -* Fix position of maps names. +* Fix position of maps names. * Made the slider cursor much more responsive. Speedup the map select screen. * Try to behave when no maps/saves are present. * Page Up / Page Down / Home / End hotkeys for scrolling through scenarios / games list -### OBJECTS: +### OBJECTS + * Neutral creatures can join or escape depending on hero strength (escape formula needs to be improved) * leaving guardians in flagged mines. * support for Scholar object @@ -2710,46 +3052,49 @@ http://bugs.vcmi.eu/changelog_page.php?version_id=2 * support for Event * Corpse (Skeleton) will be accessible from all directions -# 0.7 -> 0.71 (Apr 01 2009) +## 0.7 -> 0.71 (Apr 01 2009) -### GENERAL: -* fixed scrolling behind window problem (now it's possible to scroll with CTRL + arrows) -* morale/luck system and corresponding sec. skills supported -* fixed crash when hero get level and has less than two sec. skills to choose between +### GENERAL + +* fixed scrolling behind window problem (now it's possible to scroll with CTRL + arrows) +* morale/luck system and corresponding sec. skills supported +* fixed crash when hero get level and has less than two sec. skills to choose between * added keybindings for components in selection window (eg. for treasure chest dialog): 1, 2, and so on. Selection dialog can be closed with Enter key * proper handling of custom portraits of heroes * fixed problems with non-hero/town defs not present in def list but present on map (occurring probably only in case of def substitution in map editor) -* fixed crash when there was no hero available to hire for some player +* fixed crash when there was no hero available to hire for some player * fixed problems with 1024x600 screen resolution -* updating blockmap/visitmap of randomized objects +* updating blockmap/visitmap of randomized objects * fixed crashes on loading maps with flag all mines/dwelling victory condition * further fixes for leveling-up (stability and identical offered skills bug) * splitting window allows to rebalance two stack with the same creatures * support for numpad keyboard * support for timed events -### ADVENTURE INTERFACE: +### ADVENTURE INTERFACE + * added "Next hero" button functionality * added missing path arrows -* corrected centering on hero's position +* corrected centering on hero's position * recalculating hero path after reselecting hero * further changes in pathfinder making it more like original one -* orientation of hero can't be change if movement points are exhausted +* orientation of hero can't be change if movement points are exhausted * campfire, borderguard, bordergate, questguard will be accessible from the top * new movement cost calculation algorithm * fixed sight radious calculation * it's possible to stop hero movement -* faster minimap refreshing +* faster minimap refreshing * provisional support for "Save" button in System Options Window * it's possible to revisit object under hero by pressing Space -### BATTLES: +### BATTLES + * partial support for battle obstacles * only one spell can be casted per turn * blocked opening sepllbook if hero doesn't have a one -* spells not known by hero can't be casted +* spells not known by hero can't be casted * spell books won't be placed in War Machine slots after battle -* attack is now possible when hex under cursor is not displayed +* attack is now possible when hex under cursor is not displayed * glowing effect of yellow border around creatures * blue glowing border around hovered creature * made animation on battlefield more smooth @@ -2763,82 +3108,90 @@ http://bugs.vcmi.eu/changelog_page.php?version_id=2 * correct handling of flying creatures in battles * a few tweaks in battle path/available hexes calculation (more of them is needed) * amounts of units taking actions / being an object of actions won't be shown until action ends -* fixed positions of stack queue and battle result window when resolution is != 800x600 -* corrected duration of frenzy spell which was incorrect in certain cases +* fixed positions of stack queue and battle result window when resolution is != 800x600 +* corrected duration of frenzy spell which was incorrect in certain cases * corrected hero spell casting animation -* better support for battle backgrounds -* blocked "save" command during battle +* better support for battle backgrounds +* blocked "save" command during battle * spellbook displays only spells known by Hero * New spells supported: - * Mirth - * Sorrow - * Fortune - * Misfortune + * Mirth + * Sorrow + * Fortune + * Misfortune + +### TOWN INTERFACE -### TOWN INTERFACE: * cannot build more than one capitol * cannot build shipyard if town is not near water -* Rampart's Treasury requires Miner's Guild +* Rampart's Treasury requires Miner's Guild * minor improvements in Recruitment Window * fixed crash occurring when clicking on hero portrait in Tavern Window, minor improvements for Tavern Window * proper updating resdatabar after building structure in town or buying creatures (non 800x600 res) -* fixed blinking resdatabar in town screen when buying (800x600) +* fixed blinking resdatabar in town screen when buying (800x600) * fixed horde buildings displaying in town hall * forbidden buildings will be shown as forbidden, even if there are no res / other conditions are not fulfilled -### PREGAME: +### PREGAME + * added scrolling scenario list with mouse wheel * fixed mouse slow downs -* cannot select heroes for computer player (pregame) +* cannot select heroes for computer player (pregame) * no crash if uses gives wrong resolution ID number * minor fixes -### OBJECTS: -* windmill gives 500 gold only during first week ever (not every month) -* After the first visit to the Witch Hut, right-click/hover tip mentions the skill available. -* New objects supported: - * Prison - * Magic Well - * Faerie Ring - * Swan Pond - * Idol of Fortune - * Fountain of Fortune - * Rally Flag - * Oasis - * Temple - * Watering Hole - * Fountain of Youth - * support for Redwood Observatory - * support for Shrine of Magic Incantation / Gesture / Thought - * support for Sign / Ocean Bottle +### OBJECTS + +* windmill gives 500 gold only during first week ever (not every month) +* After the first visit to the Witch Hut, right-click/hover tip mentions the skill available. +* New objects supported: + * Prison + * Magic Well + * Faerie Ring + * Swan Pond + * Idol of Fortune + * Fountain of Fortune + * Rally Flag + * Oasis + * Temple + * Watering Hole + * Fountain of Youth + * support for Redwood Observatory + * support for Shrine of Magic Incantation / Gesture / Thought + * support for Sign / Ocean Bottle + +### AI PLAYER -### AI PLAYER: * Minor improvements and fixes. -# 0.64 -> 0.7 (Feb 01 2009) +## 0.64 -> 0.7 (Feb 01 2009) + +### GENERAL -### GENERAL: * move some settings to the config/settings.txt file * partial support for new screen resolutions -* it's possible to set game resolution in pregame (type 'resolution' in the console) +* it's possible to set game resolution in pregame (type 'resolution' in the console) * /Data and /Sprites subfolders can be used for adding files not present in .lod archives * fixed crashbug occurring when hero levelled above 15 level * support for non-standard screen resolutions * F4 toggles between full-screen and windowed mode * minor improvements in creature card window -* splitting stacks with the shift+click -* creature card window contains info about modified speed +* splitting stacks with the shift+click +* creature card window contains info about modified speed + +### ADVENTURE INTERFACE -### ADVENTURE INTERFACE: * added water animation * speed of scrolling map and hero movement can be adjusted in the System Options Window * partial handling r-clicks on adventure map -### TOWN INTERFACE: +### TOWN INTERFACE + * the scroll tab won't remain hanged to our mouse position if we move the mouse is away from the scroll bar * fixed cloning creatures bug in garrisons (and related issues) -### BATTLES: +### BATTLES + * support for the Wait command * magic arrow *really* works * war machines support partially added @@ -2847,39 +3200,42 @@ http://bugs.vcmi.eu/changelog_page.php?version_id=2 * positive/negative spells cannot be cast on hostile/our stacks * showing spell effects affecting stack in creature info window * more appropriate coloring of stack amount box when stack is affected by a spell -* battle console displays notifications about wait/defend commands +* battle console displays notifications about wait/defend commands * several reported bugs fixed * new spells supported: - * Haste - * lightning bolt - * ice bolt - * slow - * implosion - * forgetfulness - * shield - * air shield - * bless - * curse - * bloodlust - * weakness - * stone skin - * prayer - * frenzy + * Haste + * lightning bolt + * ice bolt + * slow + * implosion + * forgetfulness + * shield + * air shield + * bless + * curse + * bloodlust + * weakness + * stone skin + * prayer + * frenzy + +### AI PLAYER -### AI PLAYER: * Genius AI (first VCMI AI) will control computer creatures during the combat. -### OBJECTS: +### OBJECTS + * Guardians property for resources is handled * support for Witch Hut * support for Arena -* support for Library of Enlightenment +* support for Library of Enlightenment And a lot of minor fixes -# 0.63 -> 0.64 (Nov 01 2008) +## 0.63 -> 0.64 (Nov 01 2008) + +### GENERAL -### GENERAL: * sprites from /Sprites folder are handled correctly * several fixes for pathfinder and path arrows * better handling disposed/predefined heroes @@ -2891,39 +3247,43 @@ And a lot of minor fixes * many minor improvements * Added some kind of simple chatting functionality through console. Implemented several WoG cheats equivalents: - * woggaladriel -> vcmiainur - * wogoliphaunt -> vcminoldor - * wogshadowfax -> vcminahar - * wogeyeofsauron -> vcmieagles - * wogisengard -> vcmiformenos - * wogsaruman -> vcmiistari - * wogpathofthedead -> vcmiangband - * woggandalfwhite -> vcmiglorfindel + * woggaladriel -> vcmiainur + * wogoliphaunt -> vcminoldor + * wogshadowfax -> vcminahar + * wogeyeofsauron -> vcmieagles + * wogisengard -> vcmiformenos + * wogsaruman -> vcmiistari + * wogpathofthedead -> vcmiangband + * woggandalfwhite -> vcmiglorfindel -### ADVENTURE INTERFACE: -* clicking on a tile in advmap view when a path is shown will not only hide it but also calculate a new one -* slowed map scrolling +### ADVENTURE INTERFACE + +* clicking on a tile in advmap view when a path is shown will not only hide it but also calculate a new one +* slowed map scrolling * blocked scrolling adventure map with mouse when left ctrl is pressed * blocked map scrolling when dialog window is opened * scholar will be accessible from the top -### TOWN INTERFACE: +### TOWN INTERFACE + * partially done tavern window (only hero hiring functionality) -### BATTLES: +### BATTLES + * water elemental will really be treated as 2 hex creature * potential infinite loop in reverseCreature removed -* better handling of battle cursor +* better handling of battle cursor * fixed blocked shooter behavior * it's possible in battles to check remeaining HP of neutral stacks * partial support for Magic Arrow spell * fixed bug with dying unit * stack queue hotkey is now 'Q' -* added shots limit +* added shots limit -# 0.62 -> 0.63 (Oct 01 2008) +## 0.62 -> 0.63 (Oct 01 2008) + +### GENERAL -### GENERAL: * coloured console output, logging all info to txt files * it's possible to use other port than 3030 by passing it as an additional argument * removed some redundant warnings @@ -2932,49 +3292,54 @@ And a lot of minor fixes * some crashbugs was fixed * added handling of navigation, logistics, pathfinding, scouting end estates secondary skill * magical hero are given spellbook at the beginning -* added initial secondary skills for heroes +* added initial secondary skills for heroes -### BATTLES: -* very significant optimization of battles +### BATTLES + +* very significant optimization of battles * battle summary window -* fixed crashbug occurring sometimes on exiting battle -* confirm window is shown before retreat +* fixed crashbug occurring sometimes on exiting battle +* confirm window is shown before retreat * graphic stack queue in battle (shows when 'c' key is pressed) * it's possible to attack enemy hero * neutral monster army disappears when defeated * casualties among hero army and neutral creatures are saved * better animation handling in battles -* directional attack in battles +* directional attack in battles * mostly done battle options (although they're not saved) -* added receiving exp (and leveling-up) after a won battle -* added support for archery, offence and armourer secondary abilities +* added receiving exp (and leveling-up) after a won battle +* added support for archery, offence and armourer secondary abilities * hero's primary skills accounted for damage dealt by creatures in battle -### TOWNS: -* mostly done marketplace +### TOWNS + +* mostly done marketplace * fixed crashbug with battles on swamps and rough terrain -* counterattacks +* counterattacks * heroes can learn new spells in towns * working resource silo * fixed bug with the mage guild when no spells available * it's possible to build lighthouse -### HERO WINDOW: +### HERO WINDOW + * setting army formation * tooltips for artifacts in backpack -### ADVENTURE INTERFACE: +### ADVENTURE INTERFACE + * fixed bug with disappearing head of a hero in adventure map -* some objects are no longer accessible from the top +* some objects are no longer accessible from the top * no tooltips for objects under FoW * events won't be shown * working Subterranean Gates, Monoliths -* minimap shows all flaggable objects (towns, mines, etc.) +* minimap shows all flaggable objects (towns, mines, etc.) * artifacts we pick up go to the appropriate slot (if free) -# 0.61 -> 0.62 (Sep 01 2008) +## 0.61 -> 0.62 (Sep 01 2008) + +### GENERAL -### GENERAL: * restructured to the server-client model * support for heroes placed in towns * upgrading creatures @@ -2983,7 +3348,8 @@ And a lot of minor fixes * showing creature amount in the creature info window * giving starting bonus -### CASTLES: +### CASTLES + * icon in infobox showing that there is hero in town garrison * fort/citadel/castle screen * taking last stack from the heroes army should be impossible (or at least harder) @@ -2991,20 +3357,23 @@ And a lot of minor fixes * randomizing spells in towns * viewing hero window in the town screen * possibility of moving hero into the garrison -* mage guild screen +* mage guild screen * support for blacksmith * if hero doesn't have a spell book, he can buy one in a mage guild * it's possible to build glyph of fear in fortress * creatures placeholders work properly -### ADVENTURE INTERFACE: +### ADVENTURE INTERFACE + * hopefully fixed problems with wrong town defs (village/fort/capitol) -### HERO WINDOW: +### HERO WINDOW + * bugfix: splitting stacks works in hero window * removed bug causing significant increase of CPU consumption -### BATTLES: +### BATTLES + * shooting * removed some displaying problems * showing last group of frames in creature animation won't crash @@ -3017,20 +3386,23 @@ And a lot of minor fixes * improved pathfinding in battles, removed problems with displaying movement, adventure map interface won't be called during battles. * minor optimizations -### PREGAME: +### PREGAME + * updates settings when selecting new map after changing sorting criteria * if sorting not by name, name will be used as a secondary criteria * when filter is applied a first available map is selected automatically * slider position updated after sorting in pregame -### OBJECTS: +### OBJECTS + * support for the Tree of knowledge * support for Campfires * added event message when picking artifact -# 0.6 -> 0.61 (Jun 15 2008) +## 0.6 -> 0.61 (Jun 15 2008) + +### IMPROVEMENTS -### IMPROVEMENTS: * improved attacking in the battles * it's possible to kill hostile stack * animations won't go in the same phase @@ -3046,7 +3418,8 @@ And a lot of minor fixes * battle log is scrolled down when new event occurs * console is closed when application exits -### BUGFIXES: +### BUGFIXES + * stack at the limit of unit's range can now be attacked * good background for the town hall screen in Stronghold * fixed typo in hall.txt @@ -3056,7 +3429,7 @@ And a lot of minor fixes * properly displaying two-hex creatures in recruit/split/info window * corrupted map file won't cause crash on initializing main menu -# 0.59 -> 0.6 (Jun 1 2008) +## 0.59 -> 0.6 (Jun 1 2008) * partially done attacking in battles * screen isn't now refreshed while blitting creature info window @@ -3069,7 +3442,7 @@ And a lot of minor fixes * new pathfinder * several minor improvements -# 0.58 -> 0.59 (May 24 2008 - closed, test release) +## 0.58 -> 0.59 (May 24 2008 - closed, test release) * fixed memory leak in battles * blitting creature animations to rects in the recruitment window @@ -3089,9 +3462,10 @@ And a lot of minor fixes * callback for buttons/lists based on boost::function * a lot of minor improvements -# 0.55 -> 0.58 (Apr 20 2008 - closed, test release) +## 0.55 -> 0.58 (Apr 20 2008 - closed, test release) + +### TOWNS -### TOWNS: * recruiting creatures * working creature growths (including castle and horde building influences) * towns give income @@ -3100,21 +3474,24 @@ And a lot of minor fixes * hints for structures * updating town infobox -### GARRISONS: +### GARRISONS + * merging stacks * splitting stacks -### BATTLES: +### BATTLES + * starting battles * displaying terrain, animations of heroes, units, grid, range of units, battle menu with console, amounts of units in stacks * leaving battle by pressing flee button * moving units in battles and displaying their ranges * defend command for units -### GENERAL: +### GENERAL + * a number of minor fixes and improvements -# 0.54 -> 0.55 (Feb 29 2008) +## 0.54 -> 0.55 (Feb 29 2008) * Sprites/ folder works for h3sprite.lod same as Data/ for h3bitmap.lod (but it's still experimental) * randomization quantity of creatures on the map @@ -3127,7 +3504,8 @@ And a lot of minor fixes * hints for most of creature generators * some minor stuff -# 0.53b -> 0.54 (Feb 23 2008 - first public release) +## 0.53b -> 0.54 (Feb 23 2008 - first public release) + * given hero is placed in the town entrance * some objects such as river delta won't be blitted "on" hero * tiles under FoW are inaccessible @@ -3140,12 +3518,12 @@ And a lot of minor fixes * added hints in town lists * eliminated square from city hints -# 0.53 - 0.53b (Feb 20 2008) +## 0.53 - 0.53b (Feb 20 2008) * added giving default buildings in towns * town infobox won't crash on empty town -# 0.52 - 0.53 (Feb 18 2008): +## 0.52 - 0.53 (Feb 18 2008) * hopefully the last bugfix of Pandora's Box * fixed blockmaps of generated heroes @@ -3162,7 +3540,7 @@ And a lot of minor fixes * mostly done town infobox * town daily income is properly calculated -# 0.51 - 0.52 (Feb 7 2008): +## 0.51 - 0.52 (Feb 7 2008) * [feature] giving starting hero * [feature] VCMI will try to use files from /Data folder instead of those from h3bitmap.lod @@ -3173,7 +3551,7 @@ And a lot of minor fixes * [bugfix] improved randomization * [bugfix] pathfinder can't be cheated (what caused errors) -# 0.5 - 0.51 (Feb 3 2008): +## 0.5 - 0.51 (Feb 3 2008) * close button properly closes (same does 'q' key) * two players can't have selected same hero @@ -3186,7 +3564,7 @@ And a lot of minor fixes * better console messages * map reading speed up (though it's still slow, especially on bigger maps) -# 0.0 -> 0.5 (Feb 2 2008 - first closed release): +## 0.0 -> 0.5 (Feb 2 2008 - first closed release) * Main menu and New game screens * Scenario selection, part of advanced options support diff --git a/Global.h b/Global.h index 6fa656a17..5e9b2c681 100644 --- a/Global.h +++ b/Global.h @@ -154,7 +154,10 @@ static_assert(sizeof(bool) == 1, "Bool needs to be 1 byte in size."); #endif #define BOOST_THREAD_DONT_PROVIDE_THREAD_DESTRUCTOR_CALLS_TERMINATE_IF_JOINABLE 1 //need to link boost thread dynamically to avoid https://stackoverflow.com/questions/35978572/boost-thread-interupt-does-not-work-when-crossing-a-dll-boundary -#define BOOST_THREAD_USE_DLL //for example VCAI::finish() may freeze on thread join after interrupt when linking this statically +//for example VCAI::finish() may freeze on thread join after interrupt when linking this statically +#ifndef BOOST_THREAD_USE_DLL +# define BOOST_THREAD_USE_DLL +#endif #define BOOST_BIND_NO_PLACEHOLDERS #if BOOST_VERSION >= 106600 diff --git a/Mods/vcmi/Data/NotoSans-Medium.ttf b/Mods/vcmi/Content/Data/NotoSans-Medium.ttf similarity index 100% rename from Mods/vcmi/Data/NotoSans-Medium.ttf rename to Mods/vcmi/Content/Data/NotoSans-Medium.ttf diff --git a/Mods/vcmi/Data/NotoSerif-Black.ttf b/Mods/vcmi/Content/Data/NotoSerif-Black.ttf similarity index 100% rename from Mods/vcmi/Data/NotoSerif-Black.ttf rename to Mods/vcmi/Content/Data/NotoSerif-Black.ttf diff --git a/Mods/vcmi/Data/NotoSerif-Bold.ttf b/Mods/vcmi/Content/Data/NotoSerif-Bold.ttf similarity index 100% rename from Mods/vcmi/Data/NotoSerif-Bold.ttf rename to Mods/vcmi/Content/Data/NotoSerif-Bold.ttf diff --git a/Mods/vcmi/Data/NotoSerif-Medium.ttf b/Mods/vcmi/Content/Data/NotoSerif-Medium.ttf similarity index 100% rename from Mods/vcmi/Data/NotoSerif-Medium.ttf rename to Mods/vcmi/Content/Data/NotoSerif-Medium.ttf diff --git a/Mods/vcmi/Data/s/std.verm b/Mods/vcmi/Content/Data/s/std.verm similarity index 100% rename from Mods/vcmi/Data/s/std.verm rename to Mods/vcmi/Content/Data/s/std.verm diff --git a/Mods/vcmi/Data/s/testy.erm b/Mods/vcmi/Content/Data/s/testy.erm similarity index 100% rename from Mods/vcmi/Data/s/testy.erm rename to Mods/vcmi/Content/Data/s/testy.erm diff --git a/Mods/vcmi/Sounds/we5.wav b/Mods/vcmi/Content/Sounds/we5.wav similarity index 100% rename from Mods/vcmi/Sounds/we5.wav rename to Mods/vcmi/Content/Sounds/we5.wav diff --git a/Mods/vcmi/Sprites/PortraitsLarge.json b/Mods/vcmi/Content/Sprites/PortraitsLarge.json similarity index 100% rename from Mods/vcmi/Sprites/PortraitsLarge.json rename to Mods/vcmi/Content/Sprites/PortraitsLarge.json diff --git a/Mods/vcmi/Sprites/PortraitsSmall.json b/Mods/vcmi/Content/Sprites/PortraitsSmall.json similarity index 100% rename from Mods/vcmi/Sprites/PortraitsSmall.json rename to Mods/vcmi/Content/Sprites/PortraitsSmall.json diff --git a/Mods/vcmi/Data/QuickRecruitmentWindow/CreaturePurchaseCard.png b/Mods/vcmi/Content/Sprites/QuickRecruitmentWindow/CreaturePurchaseCard.png similarity index 100% rename from Mods/vcmi/Data/QuickRecruitmentWindow/CreaturePurchaseCard.png rename to Mods/vcmi/Content/Sprites/QuickRecruitmentWindow/CreaturePurchaseCard.png diff --git a/Mods/vcmi/Sprites/QuickRecruitmentWindow/QuickRecruitmentAllButton.def b/Mods/vcmi/Content/Sprites/QuickRecruitmentWindow/QuickRecruitmentAllButton.def similarity index 100% rename from Mods/vcmi/Sprites/QuickRecruitmentWindow/QuickRecruitmentAllButton.def rename to Mods/vcmi/Content/Sprites/QuickRecruitmentWindow/QuickRecruitmentAllButton.def diff --git a/Mods/vcmi/Sprites/QuickRecruitmentWindow/QuickRecruitmentNoneButton.def b/Mods/vcmi/Content/Sprites/QuickRecruitmentWindow/QuickRecruitmentNoneButton.def similarity index 100% rename from Mods/vcmi/Sprites/QuickRecruitmentWindow/QuickRecruitmentNoneButton.def rename to Mods/vcmi/Content/Sprites/QuickRecruitmentWindow/QuickRecruitmentNoneButton.def diff --git a/Mods/vcmi/Sprites/QuickRecruitmentWindow/costBackground.png b/Mods/vcmi/Content/Sprites/QuickRecruitmentWindow/costBackground.png similarity index 100% rename from Mods/vcmi/Sprites/QuickRecruitmentWindow/costBackground.png rename to Mods/vcmi/Content/Sprites/QuickRecruitmentWindow/costBackground.png diff --git a/Mods/vcmi/Sprites/ScSelC.json b/Mods/vcmi/Content/Sprites/ScSelC.json similarity index 100% rename from Mods/vcmi/Sprites/ScSelC.json rename to Mods/vcmi/Content/Sprites/ScSelC.json diff --git a/Mods/vcmi/Data/StackQueueLarge.png b/Mods/vcmi/Content/Sprites/StackQueueLarge.png similarity index 100% rename from Mods/vcmi/Data/StackQueueLarge.png rename to Mods/vcmi/Content/Sprites/StackQueueLarge.png diff --git a/Mods/vcmi/Data/StackQueueSmall.png b/Mods/vcmi/Content/Sprites/StackQueueSmall.png similarity index 100% rename from Mods/vcmi/Data/StackQueueSmall.png rename to Mods/vcmi/Content/Sprites/StackQueueSmall.png diff --git a/Mods/vcmi/Data/UnitMaxMovementHighlight.png b/Mods/vcmi/Content/Sprites/UnitMaxMovementHighlight.png similarity index 100% rename from Mods/vcmi/Data/UnitMaxMovementHighlight.png rename to Mods/vcmi/Content/Sprites/UnitMaxMovementHighlight.png diff --git a/Mods/vcmi/Data/UnitMovementHighlight.png b/Mods/vcmi/Content/Sprites/UnitMovementHighlight.png similarity index 100% rename from Mods/vcmi/Data/UnitMovementHighlight.png rename to Mods/vcmi/Content/Sprites/UnitMovementHighlight.png diff --git a/Mods/vcmi/Content/Sprites/battle/queueDefend.png b/Mods/vcmi/Content/Sprites/battle/queueDefend.png new file mode 100644 index 000000000..87e61b3ab Binary files /dev/null and b/Mods/vcmi/Content/Sprites/battle/queueDefend.png differ diff --git a/Mods/vcmi/Content/Sprites/battle/queueWait.png b/Mods/vcmi/Content/Sprites/battle/queueWait.png new file mode 100644 index 000000000..82572bd8e Binary files /dev/null and b/Mods/vcmi/Content/Sprites/battle/queueWait.png differ diff --git a/Mods/vcmi/Sprites/battle/rangeHighlights/green/empty.png b/Mods/vcmi/Content/Sprites/battle/rangeHighlights/green/empty.png similarity index 100% rename from Mods/vcmi/Sprites/battle/rangeHighlights/green/empty.png rename to Mods/vcmi/Content/Sprites/battle/rangeHighlights/green/empty.png diff --git a/Mods/vcmi/Sprites/battle/rangeHighlights/green/fullHex.png b/Mods/vcmi/Content/Sprites/battle/rangeHighlights/green/fullHex.png similarity index 100% rename from Mods/vcmi/Sprites/battle/rangeHighlights/green/fullHex.png rename to Mods/vcmi/Content/Sprites/battle/rangeHighlights/green/fullHex.png diff --git a/Mods/vcmi/Sprites/battle/rangeHighlights/green/left.png b/Mods/vcmi/Content/Sprites/battle/rangeHighlights/green/left.png similarity index 100% rename from Mods/vcmi/Sprites/battle/rangeHighlights/green/left.png rename to Mods/vcmi/Content/Sprites/battle/rangeHighlights/green/left.png diff --git a/Mods/vcmi/Sprites/battle/rangeHighlights/green/leftHalf.png b/Mods/vcmi/Content/Sprites/battle/rangeHighlights/green/leftHalf.png similarity index 100% rename from Mods/vcmi/Sprites/battle/rangeHighlights/green/leftHalf.png rename to Mods/vcmi/Content/Sprites/battle/rangeHighlights/green/leftHalf.png diff --git a/Mods/vcmi/Sprites/battle/rangeHighlights/green/top.png b/Mods/vcmi/Content/Sprites/battle/rangeHighlights/green/top.png similarity index 100% rename from Mods/vcmi/Sprites/battle/rangeHighlights/green/top.png rename to Mods/vcmi/Content/Sprites/battle/rangeHighlights/green/top.png diff --git a/Mods/vcmi/Sprites/battle/rangeHighlights/green/topLeft.png b/Mods/vcmi/Content/Sprites/battle/rangeHighlights/green/topLeft.png similarity index 100% rename from Mods/vcmi/Sprites/battle/rangeHighlights/green/topLeft.png rename to Mods/vcmi/Content/Sprites/battle/rangeHighlights/green/topLeft.png diff --git a/Mods/vcmi/Sprites/battle/rangeHighlights/green/topLeftCorner.png b/Mods/vcmi/Content/Sprites/battle/rangeHighlights/green/topLeftCorner.png similarity index 100% rename from Mods/vcmi/Sprites/battle/rangeHighlights/green/topLeftCorner.png rename to Mods/vcmi/Content/Sprites/battle/rangeHighlights/green/topLeftCorner.png diff --git a/Mods/vcmi/Sprites/battle/rangeHighlights/green/topLeftHalfCorner.png b/Mods/vcmi/Content/Sprites/battle/rangeHighlights/green/topLeftHalfCorner.png similarity index 100% rename from Mods/vcmi/Sprites/battle/rangeHighlights/green/topLeftHalfCorner.png rename to Mods/vcmi/Content/Sprites/battle/rangeHighlights/green/topLeftHalfCorner.png diff --git a/Mods/vcmi/Sprites/battle/rangeHighlights/rangeHighlightsGreen.json b/Mods/vcmi/Content/Sprites/battle/rangeHighlights/rangeHighlightsGreen.json similarity index 100% rename from Mods/vcmi/Sprites/battle/rangeHighlights/rangeHighlightsGreen.json rename to Mods/vcmi/Content/Sprites/battle/rangeHighlights/rangeHighlightsGreen.json diff --git a/Mods/vcmi/Sprites/battle/rangeHighlights/rangeHighlightsRed.json b/Mods/vcmi/Content/Sprites/battle/rangeHighlights/rangeHighlightsRed.json similarity index 100% rename from Mods/vcmi/Sprites/battle/rangeHighlights/rangeHighlightsRed.json rename to Mods/vcmi/Content/Sprites/battle/rangeHighlights/rangeHighlightsRed.json diff --git a/Mods/vcmi/Sprites/battle/rangeHighlights/red/empty.png b/Mods/vcmi/Content/Sprites/battle/rangeHighlights/red/empty.png similarity index 100% rename from Mods/vcmi/Sprites/battle/rangeHighlights/red/empty.png rename to Mods/vcmi/Content/Sprites/battle/rangeHighlights/red/empty.png diff --git a/Mods/vcmi/Sprites/battle/rangeHighlights/red/fullHex.png b/Mods/vcmi/Content/Sprites/battle/rangeHighlights/red/fullHex.png similarity index 100% rename from Mods/vcmi/Sprites/battle/rangeHighlights/red/fullHex.png rename to Mods/vcmi/Content/Sprites/battle/rangeHighlights/red/fullHex.png diff --git a/Mods/vcmi/Sprites/battle/rangeHighlights/red/left.png b/Mods/vcmi/Content/Sprites/battle/rangeHighlights/red/left.png similarity index 100% rename from Mods/vcmi/Sprites/battle/rangeHighlights/red/left.png rename to Mods/vcmi/Content/Sprites/battle/rangeHighlights/red/left.png diff --git a/Mods/vcmi/Sprites/battle/rangeHighlights/red/leftHalf.png b/Mods/vcmi/Content/Sprites/battle/rangeHighlights/red/leftHalf.png similarity index 100% rename from Mods/vcmi/Sprites/battle/rangeHighlights/red/leftHalf.png rename to Mods/vcmi/Content/Sprites/battle/rangeHighlights/red/leftHalf.png diff --git a/Mods/vcmi/Sprites/battle/rangeHighlights/red/top.png b/Mods/vcmi/Content/Sprites/battle/rangeHighlights/red/top.png similarity index 100% rename from Mods/vcmi/Sprites/battle/rangeHighlights/red/top.png rename to Mods/vcmi/Content/Sprites/battle/rangeHighlights/red/top.png diff --git a/Mods/vcmi/Sprites/battle/rangeHighlights/red/topLeft.png b/Mods/vcmi/Content/Sprites/battle/rangeHighlights/red/topLeft.png similarity index 100% rename from Mods/vcmi/Sprites/battle/rangeHighlights/red/topLeft.png rename to Mods/vcmi/Content/Sprites/battle/rangeHighlights/red/topLeft.png diff --git a/Mods/vcmi/Sprites/battle/rangeHighlights/red/topLeftCorner.png b/Mods/vcmi/Content/Sprites/battle/rangeHighlights/red/topLeftCorner.png similarity index 100% rename from Mods/vcmi/Sprites/battle/rangeHighlights/red/topLeftCorner.png rename to Mods/vcmi/Content/Sprites/battle/rangeHighlights/red/topLeftCorner.png diff --git a/Mods/vcmi/Sprites/battle/rangeHighlights/red/topLeftHalfCorner.png b/Mods/vcmi/Content/Sprites/battle/rangeHighlights/red/topLeftHalfCorner.png similarity index 100% rename from Mods/vcmi/Sprites/battle/rangeHighlights/red/topLeftHalfCorner.png rename to Mods/vcmi/Content/Sprites/battle/rangeHighlights/red/topLeftHalfCorner.png diff --git a/Mods/vcmi/Content/Sprites/cprsmall.json b/Mods/vcmi/Content/Sprites/cprsmall.json new file mode 100644 index 000000000..3eec15da7 --- /dev/null +++ b/Mods/vcmi/Content/Sprites/cprsmall.json @@ -0,0 +1,8 @@ +{ + "images" : + [ + // Fix for swapped in H3 icons of Wight and Wraith + { "frame" : 62, "defFile" : "cprsmall.def", "defFrame" : 63}, + { "frame" : 63, "defFile" : "cprsmall.def", "defFrame" : 62} + ] +} diff --git a/Mods/vcmi/Data/debug/blocked.png b/Mods/vcmi/Content/Sprites/debug/blocked.png similarity index 100% rename from Mods/vcmi/Data/debug/blocked.png rename to Mods/vcmi/Content/Sprites/debug/blocked.png diff --git a/Mods/vcmi/Data/debug/grid.png b/Mods/vcmi/Content/Sprites/debug/grid.png similarity index 100% rename from Mods/vcmi/Data/debug/grid.png rename to Mods/vcmi/Content/Sprites/debug/grid.png diff --git a/Mods/vcmi/Data/debug/spellRange.png b/Mods/vcmi/Content/Sprites/debug/spellRange.png similarity index 100% rename from Mods/vcmi/Data/debug/spellRange.png rename to Mods/vcmi/Content/Sprites/debug/spellRange.png diff --git a/Mods/vcmi/Data/debug/visitable.png b/Mods/vcmi/Content/Sprites/debug/visitable.png similarity index 100% rename from Mods/vcmi/Data/debug/visitable.png rename to Mods/vcmi/Content/Sprites/debug/visitable.png diff --git a/Mods/vcmi/Data/heroWindow/artifactSlotEmpty.png b/Mods/vcmi/Content/Sprites/heroWindow/artifactSlotEmpty.png similarity index 100% rename from Mods/vcmi/Data/heroWindow/artifactSlotEmpty.png rename to Mods/vcmi/Content/Sprites/heroWindow/artifactSlotEmpty.png diff --git a/Mods/vcmi/Data/heroWindow/backpackButtonIcon.png b/Mods/vcmi/Content/Sprites/heroWindow/backpackButtonIcon.png similarity index 100% rename from Mods/vcmi/Data/heroWindow/backpackButtonIcon.png rename to Mods/vcmi/Content/Sprites/heroWindow/backpackButtonIcon.png diff --git a/Mods/vcmi/Data/heroWindow/commanderButtonIcon.png b/Mods/vcmi/Content/Sprites/heroWindow/commanderButtonIcon.png similarity index 100% rename from Mods/vcmi/Data/heroWindow/commanderButtonIcon.png rename to Mods/vcmi/Content/Sprites/heroWindow/commanderButtonIcon.png diff --git a/Mods/vcmi/Sprites/itpa.json b/Mods/vcmi/Content/Sprites/itpa.json similarity index 100% rename from Mods/vcmi/Sprites/itpa.json rename to Mods/vcmi/Content/Sprites/itpa.json diff --git a/Mods/vcmi/Sprites/lobby/checkbox.json b/Mods/vcmi/Content/Sprites/lobby/checkbox.json similarity index 100% rename from Mods/vcmi/Sprites/lobby/checkbox.json rename to Mods/vcmi/Content/Sprites/lobby/checkbox.json diff --git a/Mods/vcmi/Sprites/lobby/checkboxBlueOff.png b/Mods/vcmi/Content/Sprites/lobby/checkboxBlueOff.png similarity index 100% rename from Mods/vcmi/Sprites/lobby/checkboxBlueOff.png rename to Mods/vcmi/Content/Sprites/lobby/checkboxBlueOff.png diff --git a/Mods/vcmi/Sprites/lobby/checkboxBlueOn.png b/Mods/vcmi/Content/Sprites/lobby/checkboxBlueOn.png similarity index 100% rename from Mods/vcmi/Sprites/lobby/checkboxBlueOn.png rename to Mods/vcmi/Content/Sprites/lobby/checkboxBlueOn.png diff --git a/Mods/vcmi/Sprites/lobby/checkboxOff.png b/Mods/vcmi/Content/Sprites/lobby/checkboxOff.png similarity index 100% rename from Mods/vcmi/Sprites/lobby/checkboxOff.png rename to Mods/vcmi/Content/Sprites/lobby/checkboxOff.png diff --git a/Mods/vcmi/Sprites/lobby/checkboxOn.png b/Mods/vcmi/Content/Sprites/lobby/checkboxOn.png similarity index 100% rename from Mods/vcmi/Sprites/lobby/checkboxOn.png rename to Mods/vcmi/Content/Sprites/lobby/checkboxOn.png diff --git a/Mods/vcmi/Content/Sprites/lobby/delete-normal.png b/Mods/vcmi/Content/Sprites/lobby/delete-normal.png new file mode 100644 index 000000000..75913ed54 Binary files /dev/null and b/Mods/vcmi/Content/Sprites/lobby/delete-normal.png differ diff --git a/Mods/vcmi/Content/Sprites/lobby/delete-pressed.png b/Mods/vcmi/Content/Sprites/lobby/delete-pressed.png new file mode 100644 index 000000000..10df2b640 Binary files /dev/null and b/Mods/vcmi/Content/Sprites/lobby/delete-pressed.png differ diff --git a/Mods/vcmi/Content/Sprites/lobby/deleteButton.json b/Mods/vcmi/Content/Sprites/lobby/deleteButton.json new file mode 100644 index 000000000..100145767 --- /dev/null +++ b/Mods/vcmi/Content/Sprites/lobby/deleteButton.json @@ -0,0 +1,8 @@ +{ + "basepath" : "lobby/", + "images" : + [ + { "frame" : 0, "file" : "delete-normal.png"}, + { "frame" : 1, "file" : "delete-pressed.png"} + ] +} diff --git a/Mods/vcmi/Sprites/lobby/dropdown.json b/Mods/vcmi/Content/Sprites/lobby/dropdown.json similarity index 100% rename from Mods/vcmi/Sprites/lobby/dropdown.json rename to Mods/vcmi/Content/Sprites/lobby/dropdown.json diff --git a/Mods/vcmi/Sprites/lobby/dropdownNormal.png b/Mods/vcmi/Content/Sprites/lobby/dropdownNormal.png similarity index 100% rename from Mods/vcmi/Sprites/lobby/dropdownNormal.png rename to Mods/vcmi/Content/Sprites/lobby/dropdownNormal.png diff --git a/Mods/vcmi/Sprites/lobby/dropdownPressed.png b/Mods/vcmi/Content/Sprites/lobby/dropdownPressed.png similarity index 100% rename from Mods/vcmi/Sprites/lobby/dropdownPressed.png rename to Mods/vcmi/Content/Sprites/lobby/dropdownPressed.png diff --git a/Mods/vcmi/Data/lobby/iconFolder.png b/Mods/vcmi/Content/Sprites/lobby/iconFolder.png similarity index 100% rename from Mods/vcmi/Data/lobby/iconFolder.png rename to Mods/vcmi/Content/Sprites/lobby/iconFolder.png diff --git a/Mods/vcmi/Data/lobby/iconPlayer.png b/Mods/vcmi/Content/Sprites/lobby/iconPlayer.png similarity index 100% rename from Mods/vcmi/Data/lobby/iconPlayer.png rename to Mods/vcmi/Content/Sprites/lobby/iconPlayer.png diff --git a/Mods/vcmi/Data/lobby/iconSend.png b/Mods/vcmi/Content/Sprites/lobby/iconSend.png similarity index 100% rename from Mods/vcmi/Data/lobby/iconSend.png rename to Mods/vcmi/Content/Sprites/lobby/iconSend.png diff --git a/Mods/vcmi/Data/lobby/selectionTabSortDate.png b/Mods/vcmi/Content/Sprites/lobby/selectionTabSortDate.png similarity index 100% rename from Mods/vcmi/Data/lobby/selectionTabSortDate.png rename to Mods/vcmi/Content/Sprites/lobby/selectionTabSortDate.png diff --git a/Mods/vcmi/Data/lobby/townBorderBig.png b/Mods/vcmi/Content/Sprites/lobby/townBorderBig.png similarity index 100% rename from Mods/vcmi/Data/lobby/townBorderBig.png rename to Mods/vcmi/Content/Sprites/lobby/townBorderBig.png diff --git a/Mods/vcmi/Data/lobby/townBorderBigActivated.png b/Mods/vcmi/Content/Sprites/lobby/townBorderBigActivated.png similarity index 100% rename from Mods/vcmi/Data/lobby/townBorderBigActivated.png rename to Mods/vcmi/Content/Sprites/lobby/townBorderBigActivated.png diff --git a/Mods/vcmi/Data/lobby/townBorderBigGrayedOut.png b/Mods/vcmi/Content/Sprites/lobby/townBorderBigGrayedOut.png similarity index 100% rename from Mods/vcmi/Data/lobby/townBorderBigGrayedOut.png rename to Mods/vcmi/Content/Sprites/lobby/townBorderBigGrayedOut.png diff --git a/Mods/vcmi/Data/lobby/townBorderSmallActivated.png b/Mods/vcmi/Content/Sprites/lobby/townBorderSmallActivated.png similarity index 100% rename from Mods/vcmi/Data/lobby/townBorderSmallActivated.png rename to Mods/vcmi/Content/Sprites/lobby/townBorderSmallActivated.png diff --git a/Mods/vcmi/Sprites/mapFormatIcons/vcmi1.png b/Mods/vcmi/Content/Sprites/mapFormatIcons/vcmi1.png similarity index 100% rename from Mods/vcmi/Sprites/mapFormatIcons/vcmi1.png rename to Mods/vcmi/Content/Sprites/mapFormatIcons/vcmi1.png diff --git a/Mods/vcmi/Data/questDialog.png b/Mods/vcmi/Content/Sprites/questDialog.png similarity index 100% rename from Mods/vcmi/Data/questDialog.png rename to Mods/vcmi/Content/Sprites/questDialog.png diff --git a/Mods/vcmi/Data/radialMenu/altDown.png b/Mods/vcmi/Content/Sprites/radialMenu/altDown.png similarity index 100% rename from Mods/vcmi/Data/radialMenu/altDown.png rename to Mods/vcmi/Content/Sprites/radialMenu/altDown.png diff --git a/Mods/vcmi/Data/radialMenu/altDownBottom.png b/Mods/vcmi/Content/Sprites/radialMenu/altDownBottom.png similarity index 100% rename from Mods/vcmi/Data/radialMenu/altDownBottom.png rename to Mods/vcmi/Content/Sprites/radialMenu/altDownBottom.png diff --git a/Mods/vcmi/Data/radialMenu/altUp.png b/Mods/vcmi/Content/Sprites/radialMenu/altUp.png similarity index 100% rename from Mods/vcmi/Data/radialMenu/altUp.png rename to Mods/vcmi/Content/Sprites/radialMenu/altUp.png diff --git a/Mods/vcmi/Data/radialMenu/altUpTop.png b/Mods/vcmi/Content/Sprites/radialMenu/altUpTop.png similarity index 100% rename from Mods/vcmi/Data/radialMenu/altUpTop.png rename to Mods/vcmi/Content/Sprites/radialMenu/altUpTop.png diff --git a/Mods/vcmi/Data/radialMenu/dismissHero.png b/Mods/vcmi/Content/Sprites/radialMenu/dismissHero.png similarity index 100% rename from Mods/vcmi/Data/radialMenu/dismissHero.png rename to Mods/vcmi/Content/Sprites/radialMenu/dismissHero.png diff --git a/Mods/vcmi/Data/radialMenu/heroMove.png b/Mods/vcmi/Content/Sprites/radialMenu/heroMove.png similarity index 100% rename from Mods/vcmi/Data/radialMenu/heroMove.png rename to Mods/vcmi/Content/Sprites/radialMenu/heroMove.png diff --git a/Mods/vcmi/Data/radialMenu/heroSwap.png b/Mods/vcmi/Content/Sprites/radialMenu/heroSwap.png similarity index 100% rename from Mods/vcmi/Data/radialMenu/heroSwap.png rename to Mods/vcmi/Content/Sprites/radialMenu/heroSwap.png diff --git a/Mods/vcmi/Data/radialMenu/itemEmpty.png b/Mods/vcmi/Content/Sprites/radialMenu/itemEmpty.png similarity index 100% rename from Mods/vcmi/Data/radialMenu/itemEmpty.png rename to Mods/vcmi/Content/Sprites/radialMenu/itemEmpty.png diff --git a/Mods/vcmi/Data/radialMenu/itemEmptyAlt.png b/Mods/vcmi/Content/Sprites/radialMenu/itemEmptyAlt.png similarity index 100% rename from Mods/vcmi/Data/radialMenu/itemEmptyAlt.png rename to Mods/vcmi/Content/Sprites/radialMenu/itemEmptyAlt.png diff --git a/Mods/vcmi/Data/radialMenu/itemInactive.png b/Mods/vcmi/Content/Sprites/radialMenu/itemInactive.png similarity index 100% rename from Mods/vcmi/Data/radialMenu/itemInactive.png rename to Mods/vcmi/Content/Sprites/radialMenu/itemInactive.png diff --git a/Mods/vcmi/Data/radialMenu/itemInactiveAlt.png b/Mods/vcmi/Content/Sprites/radialMenu/itemInactiveAlt.png similarity index 100% rename from Mods/vcmi/Data/radialMenu/itemInactiveAlt.png rename to Mods/vcmi/Content/Sprites/radialMenu/itemInactiveAlt.png diff --git a/Mods/vcmi/Data/radialMenu/moveArtifacts.png b/Mods/vcmi/Content/Sprites/radialMenu/moveArtifacts.png similarity index 100% rename from Mods/vcmi/Data/radialMenu/moveArtifacts.png rename to Mods/vcmi/Content/Sprites/radialMenu/moveArtifacts.png diff --git a/Mods/vcmi/Data/radialMenu/moveTroops.png b/Mods/vcmi/Content/Sprites/radialMenu/moveTroops.png similarity index 100% rename from Mods/vcmi/Data/radialMenu/moveTroops.png rename to Mods/vcmi/Content/Sprites/radialMenu/moveTroops.png diff --git a/Mods/vcmi/Data/radialMenu/stackFillOne.png b/Mods/vcmi/Content/Sprites/radialMenu/stackFillOne.png similarity index 100% rename from Mods/vcmi/Data/radialMenu/stackFillOne.png rename to Mods/vcmi/Content/Sprites/radialMenu/stackFillOne.png diff --git a/Mods/vcmi/Data/radialMenu/stackMerge.png b/Mods/vcmi/Content/Sprites/radialMenu/stackMerge.png similarity index 100% rename from Mods/vcmi/Data/radialMenu/stackMerge.png rename to Mods/vcmi/Content/Sprites/radialMenu/stackMerge.png diff --git a/Mods/vcmi/Data/radialMenu/stackSplitDialog.png b/Mods/vcmi/Content/Sprites/radialMenu/stackSplitDialog.png similarity index 100% rename from Mods/vcmi/Data/radialMenu/stackSplitDialog.png rename to Mods/vcmi/Content/Sprites/radialMenu/stackSplitDialog.png diff --git a/Mods/vcmi/Data/radialMenu/stackSplitEqual.png b/Mods/vcmi/Content/Sprites/radialMenu/stackSplitEqual.png similarity index 100% rename from Mods/vcmi/Data/radialMenu/stackSplitEqual.png rename to Mods/vcmi/Content/Sprites/radialMenu/stackSplitEqual.png diff --git a/Mods/vcmi/Data/radialMenu/stackSplitOne.png b/Mods/vcmi/Content/Sprites/radialMenu/stackSplitOne.png similarity index 100% rename from Mods/vcmi/Data/radialMenu/stackSplitOne.png rename to Mods/vcmi/Content/Sprites/radialMenu/stackSplitOne.png diff --git a/Mods/vcmi/Data/radialMenu/statusBar.png b/Mods/vcmi/Content/Sprites/radialMenu/statusBar.png similarity index 100% rename from Mods/vcmi/Data/radialMenu/statusBar.png rename to Mods/vcmi/Content/Sprites/radialMenu/statusBar.png diff --git a/Mods/vcmi/Data/radialMenu/swapArtifacts.png b/Mods/vcmi/Content/Sprites/radialMenu/swapArtifacts.png similarity index 100% rename from Mods/vcmi/Data/radialMenu/swapArtifacts.png rename to Mods/vcmi/Content/Sprites/radialMenu/swapArtifacts.png diff --git a/Mods/vcmi/Data/radialMenu/tradeHeroes.png b/Mods/vcmi/Content/Sprites/radialMenu/tradeHeroes.png similarity index 100% rename from Mods/vcmi/Data/radialMenu/tradeHeroes.png rename to Mods/vcmi/Content/Sprites/radialMenu/tradeHeroes.png diff --git a/Mods/vcmi/Data/settingsWindow/frameAudio.png b/Mods/vcmi/Content/Sprites/settingsWindow/frameAudio.png similarity index 100% rename from Mods/vcmi/Data/settingsWindow/frameAudio.png rename to Mods/vcmi/Content/Sprites/settingsWindow/frameAudio.png diff --git a/Mods/vcmi/Data/settingsWindow/frameMovement.png b/Mods/vcmi/Content/Sprites/settingsWindow/frameMovement.png similarity index 100% rename from Mods/vcmi/Data/settingsWindow/frameMovement.png rename to Mods/vcmi/Content/Sprites/settingsWindow/frameMovement.png diff --git a/Mods/vcmi/Data/settingsWindow/frameStackQueue.png b/Mods/vcmi/Content/Sprites/settingsWindow/frameStackQueue.png similarity index 100% rename from Mods/vcmi/Data/settingsWindow/frameStackQueue.png rename to Mods/vcmi/Content/Sprites/settingsWindow/frameStackQueue.png diff --git a/Mods/vcmi/Content/Sprites/settingsWindow/gear.png b/Mods/vcmi/Content/Sprites/settingsWindow/gear.png new file mode 100644 index 000000000..d59b548c7 Binary files /dev/null and b/Mods/vcmi/Content/Sprites/settingsWindow/gear.png differ diff --git a/Mods/vcmi/Data/settingsWindow/scrollSpeed1.png b/Mods/vcmi/Content/Sprites/settingsWindow/scrollSpeed1.png similarity index 100% rename from Mods/vcmi/Data/settingsWindow/scrollSpeed1.png rename to Mods/vcmi/Content/Sprites/settingsWindow/scrollSpeed1.png diff --git a/Mods/vcmi/Data/settingsWindow/scrollSpeed2.png b/Mods/vcmi/Content/Sprites/settingsWindow/scrollSpeed2.png similarity index 100% rename from Mods/vcmi/Data/settingsWindow/scrollSpeed2.png rename to Mods/vcmi/Content/Sprites/settingsWindow/scrollSpeed2.png diff --git a/Mods/vcmi/Data/settingsWindow/scrollSpeed3.png b/Mods/vcmi/Content/Sprites/settingsWindow/scrollSpeed3.png similarity index 100% rename from Mods/vcmi/Data/settingsWindow/scrollSpeed3.png rename to Mods/vcmi/Content/Sprites/settingsWindow/scrollSpeed3.png diff --git a/Mods/vcmi/Data/settingsWindow/scrollSpeed4.png b/Mods/vcmi/Content/Sprites/settingsWindow/scrollSpeed4.png similarity index 100% rename from Mods/vcmi/Data/settingsWindow/scrollSpeed4.png rename to Mods/vcmi/Content/Sprites/settingsWindow/scrollSpeed4.png diff --git a/Mods/vcmi/Data/settingsWindow/scrollSpeed5.png b/Mods/vcmi/Content/Sprites/settingsWindow/scrollSpeed5.png similarity index 100% rename from Mods/vcmi/Data/settingsWindow/scrollSpeed5.png rename to Mods/vcmi/Content/Sprites/settingsWindow/scrollSpeed5.png diff --git a/Mods/vcmi/Data/settingsWindow/scrollSpeed6.png b/Mods/vcmi/Content/Sprites/settingsWindow/scrollSpeed6.png similarity index 100% rename from Mods/vcmi/Data/settingsWindow/scrollSpeed6.png rename to Mods/vcmi/Content/Sprites/settingsWindow/scrollSpeed6.png diff --git a/Mods/vcmi/Content/Sprites/spellResearch/accept.png b/Mods/vcmi/Content/Sprites/spellResearch/accept.png new file mode 100644 index 000000000..d3958902c Binary files /dev/null and b/Mods/vcmi/Content/Sprites/spellResearch/accept.png differ diff --git a/Mods/vcmi/Content/Sprites/spellResearch/close.png b/Mods/vcmi/Content/Sprites/spellResearch/close.png new file mode 100644 index 000000000..99e92bc9c Binary files /dev/null and b/Mods/vcmi/Content/Sprites/spellResearch/close.png differ diff --git a/Mods/vcmi/Content/Sprites/spellResearch/reroll.png b/Mods/vcmi/Content/Sprites/spellResearch/reroll.png new file mode 100644 index 000000000..f5603dde0 Binary files /dev/null and b/Mods/vcmi/Content/Sprites/spellResearch/reroll.png differ diff --git a/Mods/vcmi/Content/Sprites/spells.json b/Mods/vcmi/Content/Sprites/spells.json new file mode 100644 index 000000000..4c13e5c79 --- /dev/null +++ b/Mods/vcmi/Content/Sprites/spells.json @@ -0,0 +1,8 @@ +{ + "images" : + [ + // Fix for swapped in H3 icons of View Earth and View Air + { "frame" : 3, "defFile" : "spells.def", "defFrame" : 5}, + { "frame" : 5, "defFile" : "spells.def", "defFrame" : 3} + ] +} diff --git a/Mods/vcmi/Content/Sprites/stackWindow/bonus-effects.png b/Mods/vcmi/Content/Sprites/stackWindow/bonus-effects.png new file mode 100644 index 000000000..67fe2ce82 Binary files /dev/null and b/Mods/vcmi/Content/Sprites/stackWindow/bonus-effects.png differ diff --git a/Mods/vcmi/Data/stackWindow/button-panel.png b/Mods/vcmi/Content/Sprites/stackWindow/button-panel.png similarity index 100% rename from Mods/vcmi/Data/stackWindow/button-panel.png rename to Mods/vcmi/Content/Sprites/stackWindow/button-panel.png diff --git a/Mods/vcmi/Sprites/stackWindow/cancel-normal.png b/Mods/vcmi/Content/Sprites/stackWindow/cancel-normal.png similarity index 100% rename from Mods/vcmi/Sprites/stackWindow/cancel-normal.png rename to Mods/vcmi/Content/Sprites/stackWindow/cancel-normal.png diff --git a/Mods/vcmi/Sprites/stackWindow/cancel-pressed.png b/Mods/vcmi/Content/Sprites/stackWindow/cancel-pressed.png similarity index 100% rename from Mods/vcmi/Sprites/stackWindow/cancel-pressed.png rename to Mods/vcmi/Content/Sprites/stackWindow/cancel-pressed.png diff --git a/Mods/vcmi/Sprites/stackWindow/cancelButton.json b/Mods/vcmi/Content/Sprites/stackWindow/cancelButton.json similarity index 100% rename from Mods/vcmi/Sprites/stackWindow/cancelButton.json rename to Mods/vcmi/Content/Sprites/stackWindow/cancelButton.json diff --git a/Mods/vcmi/Data/stackWindow/commander-abilities.png b/Mods/vcmi/Content/Sprites/stackWindow/commander-abilities.png similarity index 100% rename from Mods/vcmi/Data/stackWindow/commander-abilities.png rename to Mods/vcmi/Content/Sprites/stackWindow/commander-abilities.png diff --git a/Mods/vcmi/Data/stackWindow/commander-bg.png b/Mods/vcmi/Content/Sprites/stackWindow/commander-bg.png similarity index 100% rename from Mods/vcmi/Data/stackWindow/commander-bg.png rename to Mods/vcmi/Content/Sprites/stackWindow/commander-bg.png diff --git a/Mods/vcmi/Data/stackWindow/icons.png b/Mods/vcmi/Content/Sprites/stackWindow/icons.png similarity index 100% rename from Mods/vcmi/Data/stackWindow/icons.png rename to Mods/vcmi/Content/Sprites/stackWindow/icons.png diff --git a/Mods/vcmi/Data/stackWindow/info-panel-0.png b/Mods/vcmi/Content/Sprites/stackWindow/info-panel-0.png similarity index 100% rename from Mods/vcmi/Data/stackWindow/info-panel-0.png rename to Mods/vcmi/Content/Sprites/stackWindow/info-panel-0.png diff --git a/Mods/vcmi/Data/stackWindow/info-panel-1.png b/Mods/vcmi/Content/Sprites/stackWindow/info-panel-1.png similarity index 100% rename from Mods/vcmi/Data/stackWindow/info-panel-1.png rename to Mods/vcmi/Content/Sprites/stackWindow/info-panel-1.png diff --git a/Mods/vcmi/Data/stackWindow/info-panel-2.png b/Mods/vcmi/Content/Sprites/stackWindow/info-panel-2.png similarity index 100% rename from Mods/vcmi/Data/stackWindow/info-panel-2.png rename to Mods/vcmi/Content/Sprites/stackWindow/info-panel-2.png diff --git a/Mods/vcmi/Sprites/stackWindow/level-0.png b/Mods/vcmi/Content/Sprites/stackWindow/level-0.png similarity index 100% rename from Mods/vcmi/Sprites/stackWindow/level-0.png rename to Mods/vcmi/Content/Sprites/stackWindow/level-0.png diff --git a/Mods/vcmi/Sprites/stackWindow/level-1.png b/Mods/vcmi/Content/Sprites/stackWindow/level-1.png similarity index 100% rename from Mods/vcmi/Sprites/stackWindow/level-1.png rename to Mods/vcmi/Content/Sprites/stackWindow/level-1.png diff --git a/Mods/vcmi/Sprites/stackWindow/level-10.png b/Mods/vcmi/Content/Sprites/stackWindow/level-10.png similarity index 100% rename from Mods/vcmi/Sprites/stackWindow/level-10.png rename to Mods/vcmi/Content/Sprites/stackWindow/level-10.png diff --git a/Mods/vcmi/Sprites/stackWindow/level-2.png b/Mods/vcmi/Content/Sprites/stackWindow/level-2.png similarity index 100% rename from Mods/vcmi/Sprites/stackWindow/level-2.png rename to Mods/vcmi/Content/Sprites/stackWindow/level-2.png diff --git a/Mods/vcmi/Sprites/stackWindow/level-3.png b/Mods/vcmi/Content/Sprites/stackWindow/level-3.png similarity index 100% rename from Mods/vcmi/Sprites/stackWindow/level-3.png rename to Mods/vcmi/Content/Sprites/stackWindow/level-3.png diff --git a/Mods/vcmi/Sprites/stackWindow/level-4.png b/Mods/vcmi/Content/Sprites/stackWindow/level-4.png similarity index 100% rename from Mods/vcmi/Sprites/stackWindow/level-4.png rename to Mods/vcmi/Content/Sprites/stackWindow/level-4.png diff --git a/Mods/vcmi/Sprites/stackWindow/level-5.png b/Mods/vcmi/Content/Sprites/stackWindow/level-5.png similarity index 100% rename from Mods/vcmi/Sprites/stackWindow/level-5.png rename to Mods/vcmi/Content/Sprites/stackWindow/level-5.png diff --git a/Mods/vcmi/Sprites/stackWindow/level-6.png b/Mods/vcmi/Content/Sprites/stackWindow/level-6.png similarity index 100% rename from Mods/vcmi/Sprites/stackWindow/level-6.png rename to Mods/vcmi/Content/Sprites/stackWindow/level-6.png diff --git a/Mods/vcmi/Sprites/stackWindow/level-7.png b/Mods/vcmi/Content/Sprites/stackWindow/level-7.png similarity index 100% rename from Mods/vcmi/Sprites/stackWindow/level-7.png rename to Mods/vcmi/Content/Sprites/stackWindow/level-7.png diff --git a/Mods/vcmi/Sprites/stackWindow/level-8.png b/Mods/vcmi/Content/Sprites/stackWindow/level-8.png similarity index 100% rename from Mods/vcmi/Sprites/stackWindow/level-8.png rename to Mods/vcmi/Content/Sprites/stackWindow/level-8.png diff --git a/Mods/vcmi/Sprites/stackWindow/level-9.png b/Mods/vcmi/Content/Sprites/stackWindow/level-9.png similarity index 100% rename from Mods/vcmi/Sprites/stackWindow/level-9.png rename to Mods/vcmi/Content/Sprites/stackWindow/level-9.png diff --git a/Mods/vcmi/Sprites/stackWindow/levels.json b/Mods/vcmi/Content/Sprites/stackWindow/levels.json similarity index 100% rename from Mods/vcmi/Sprites/stackWindow/levels.json rename to Mods/vcmi/Content/Sprites/stackWindow/levels.json diff --git a/Mods/vcmi/Data/stackWindow/spell-effects.png b/Mods/vcmi/Content/Sprites/stackWindow/spell-effects.png similarity index 100% rename from Mods/vcmi/Data/stackWindow/spell-effects.png rename to Mods/vcmi/Content/Sprites/stackWindow/spell-effects.png diff --git a/Mods/vcmi/Sprites/stackWindow/switchModeIcons.json b/Mods/vcmi/Content/Sprites/stackWindow/switchModeIcons.json similarity index 100% rename from Mods/vcmi/Sprites/stackWindow/switchModeIcons.json rename to Mods/vcmi/Content/Sprites/stackWindow/switchModeIcons.json diff --git a/Mods/vcmi/Sprites/stackWindow/upgrade-normal.png b/Mods/vcmi/Content/Sprites/stackWindow/upgrade-normal.png similarity index 100% rename from Mods/vcmi/Sprites/stackWindow/upgrade-normal.png rename to Mods/vcmi/Content/Sprites/stackWindow/upgrade-normal.png diff --git a/Mods/vcmi/Sprites/stackWindow/upgrade-pressed.png b/Mods/vcmi/Content/Sprites/stackWindow/upgrade-pressed.png similarity index 100% rename from Mods/vcmi/Sprites/stackWindow/upgrade-pressed.png rename to Mods/vcmi/Content/Sprites/stackWindow/upgrade-pressed.png diff --git a/Mods/vcmi/Sprites/stackWindow/upgradeButton.json b/Mods/vcmi/Content/Sprites/stackWindow/upgradeButton.json similarity index 100% rename from Mods/vcmi/Sprites/stackWindow/upgradeButton.json rename to Mods/vcmi/Content/Sprites/stackWindow/upgradeButton.json diff --git a/Mods/vcmi/Sprites/vcmi/creatureIcons/towerLarge.png b/Mods/vcmi/Content/Sprites/vcmi/creatureIcons/towerLarge.png similarity index 100% rename from Mods/vcmi/Sprites/vcmi/creatureIcons/towerLarge.png rename to Mods/vcmi/Content/Sprites/vcmi/creatureIcons/towerLarge.png diff --git a/Mods/vcmi/Sprites/vcmi/creatureIcons/towerSmall.png b/Mods/vcmi/Content/Sprites/vcmi/creatureIcons/towerSmall.png similarity index 100% rename from Mods/vcmi/Sprites/vcmi/creatureIcons/towerSmall.png rename to Mods/vcmi/Content/Sprites/vcmi/creatureIcons/towerSmall.png diff --git a/Mods/vcmi/Content/Sprites2x/battle/queueDefend.png b/Mods/vcmi/Content/Sprites2x/battle/queueDefend.png new file mode 100644 index 000000000..7ba9c0a58 Binary files /dev/null and b/Mods/vcmi/Content/Sprites2x/battle/queueDefend.png differ diff --git a/Mods/vcmi/Content/Sprites2x/battle/queueWait.png b/Mods/vcmi/Content/Sprites2x/battle/queueWait.png new file mode 100644 index 000000000..53e203dda Binary files /dev/null and b/Mods/vcmi/Content/Sprites2x/battle/queueWait.png differ diff --git a/Mods/vcmi/Content/Sprites2x/mapFormatIcons/vcmi1.png b/Mods/vcmi/Content/Sprites2x/mapFormatIcons/vcmi1.png new file mode 100644 index 000000000..1be03a460 Binary files /dev/null and b/Mods/vcmi/Content/Sprites2x/mapFormatIcons/vcmi1.png differ diff --git a/Mods/vcmi/Content/Sprites2x/settingsWindow/gear.png b/Mods/vcmi/Content/Sprites2x/settingsWindow/gear.png new file mode 100644 index 000000000..ff89fc0e6 Binary files /dev/null and b/Mods/vcmi/Content/Sprites2x/settingsWindow/gear.png differ diff --git a/Mods/vcmi/Content/Sprites2x/stackWindow/icons.png b/Mods/vcmi/Content/Sprites2x/stackWindow/icons.png new file mode 100644 index 000000000..b1cdf1c22 Binary files /dev/null and b/Mods/vcmi/Content/Sprites2x/stackWindow/icons.png differ diff --git a/Mods/vcmi/Content/Sprites3x/battle/queueDefend.png b/Mods/vcmi/Content/Sprites3x/battle/queueDefend.png new file mode 100644 index 000000000..1740d5b16 Binary files /dev/null and b/Mods/vcmi/Content/Sprites3x/battle/queueDefend.png differ diff --git a/Mods/vcmi/Content/Sprites3x/mapFormatIcons/vcmi1.png b/Mods/vcmi/Content/Sprites3x/mapFormatIcons/vcmi1.png new file mode 100644 index 000000000..d85607d13 Binary files /dev/null and b/Mods/vcmi/Content/Sprites3x/mapFormatIcons/vcmi1.png differ diff --git a/Mods/vcmi/Content/Sprites3x/settingsWindow/gear.png b/Mods/vcmi/Content/Sprites3x/settingsWindow/gear.png new file mode 100644 index 000000000..93cc18eeb Binary files /dev/null and b/Mods/vcmi/Content/Sprites3x/settingsWindow/gear.png differ diff --git a/Mods/vcmi/Content/Sprites3x/stackWindow/icons.png b/Mods/vcmi/Content/Sprites3x/stackWindow/icons.png new file mode 100644 index 000000000..3ec5a9e32 Binary files /dev/null and b/Mods/vcmi/Content/Sprites3x/stackWindow/icons.png differ diff --git a/Mods/vcmi/Content/Sprites4x/battle/queueDefend.png b/Mods/vcmi/Content/Sprites4x/battle/queueDefend.png new file mode 100644 index 000000000..df4a21235 Binary files /dev/null and b/Mods/vcmi/Content/Sprites4x/battle/queueDefend.png differ diff --git a/Mods/vcmi/Content/Sprites4x/mapFormatIcons/vcmi1.png b/Mods/vcmi/Content/Sprites4x/mapFormatIcons/vcmi1.png new file mode 100644 index 000000000..0193036be Binary files /dev/null and b/Mods/vcmi/Content/Sprites4x/mapFormatIcons/vcmi1.png differ diff --git a/Mods/vcmi/Content/Sprites4x/settingsWindow/gear.png b/Mods/vcmi/Content/Sprites4x/settingsWindow/gear.png new file mode 100644 index 000000000..83d451dbe Binary files /dev/null and b/Mods/vcmi/Content/Sprites4x/settingsWindow/gear.png differ diff --git a/Mods/vcmi/Content/Sprites4x/stackWindow/icons.png b/Mods/vcmi/Content/Sprites4x/stackWindow/icons.png new file mode 100644 index 000000000..95abe6c3c Binary files /dev/null and b/Mods/vcmi/Content/Sprites4x/stackWindow/icons.png differ diff --git a/Mods/vcmi/Video/tutorial/AbortSpell.webm b/Mods/vcmi/Content/Video/tutorial/AbortSpell.webm similarity index 100% rename from Mods/vcmi/Video/tutorial/AbortSpell.webm rename to Mods/vcmi/Content/Video/tutorial/AbortSpell.webm diff --git a/Mods/vcmi/Video/tutorial/BattleDirection.webm b/Mods/vcmi/Content/Video/tutorial/BattleDirection.webm similarity index 100% rename from Mods/vcmi/Video/tutorial/BattleDirection.webm rename to Mods/vcmi/Content/Video/tutorial/BattleDirection.webm diff --git a/Mods/vcmi/Video/tutorial/BattleDirectionAbort.webm b/Mods/vcmi/Content/Video/tutorial/BattleDirectionAbort.webm similarity index 100% rename from Mods/vcmi/Video/tutorial/BattleDirectionAbort.webm rename to Mods/vcmi/Content/Video/tutorial/BattleDirectionAbort.webm diff --git a/Mods/vcmi/Video/tutorial/MapPanning.webm b/Mods/vcmi/Content/Video/tutorial/MapPanning.webm similarity index 100% rename from Mods/vcmi/Video/tutorial/MapPanning.webm rename to Mods/vcmi/Content/Video/tutorial/MapPanning.webm diff --git a/Mods/vcmi/Video/tutorial/MapZooming.webm b/Mods/vcmi/Content/Video/tutorial/MapZooming.webm similarity index 100% rename from Mods/vcmi/Video/tutorial/MapZooming.webm rename to Mods/vcmi/Content/Video/tutorial/MapZooming.webm diff --git a/Mods/vcmi/Video/tutorial/RadialWheel.webm b/Mods/vcmi/Content/Video/tutorial/RadialWheel.webm similarity index 100% rename from Mods/vcmi/Video/tutorial/RadialWheel.webm rename to Mods/vcmi/Content/Video/tutorial/RadialWheel.webm diff --git a/Mods/vcmi/Video/tutorial/RightClick.webm b/Mods/vcmi/Content/Video/tutorial/RightClick.webm similarity index 100% rename from Mods/vcmi/Video/tutorial/RightClick.webm rename to Mods/vcmi/Content/Video/tutorial/RightClick.webm diff --git a/Mods/vcmi/config/vcmi/chinese.json b/Mods/vcmi/Content/config/chinese.json similarity index 90% rename from Mods/vcmi/config/vcmi/chinese.json rename to Mods/vcmi/Content/config/chinese.json index 9ac1d1952..ed93bb447 100644 --- a/Mods/vcmi/config/vcmi/chinese.json +++ b/Mods/vcmi/Content/config/chinese.json @@ -11,8 +11,12 @@ "vcmi.adventureMap.monsterThreat.levels.8" : "挑战性的", "vcmi.adventureMap.monsterThreat.levels.9" : "压倒性的", "vcmi.adventureMap.monsterThreat.levels.10" : "致命的", - "vcmi.adventureMap.monsterThreat.levels.11" : "无法取胜", - "vcmi.adventureMap.monsterLevel" : "\n\n%TOWN%LEVEL级生物", + "vcmi.adventureMap.monsterThreat.levels.11" : "无法取胜的", + "vcmi.adventureMap.monsterLevel" : "\n\n%TOWN%LEVEL级%ATTACK_TYPE生物", + "vcmi.adventureMap.monsterMeleeType" : "近战", + "vcmi.adventureMap.monsterRangedType" : "远程", + "vcmi.adventureMap.search.hover" : "搜索地图物体", + "vcmi.adventureMap.search.help" : "选择要再地图上搜索的物体。", "vcmi.adventureMap.confirmRestartGame" : "你想要重新开始游戏吗?", "vcmi.adventureMap.noTownWithMarket" : "没有足够的市场。", @@ -24,6 +28,13 @@ "vcmi.adventureMap.movementPointsHeroInfo" : "(移动点数: %REMAINING / %POINTS)", "vcmi.adventureMap.replayOpponentTurnNotImplemented" : "抱歉,重放对手行动功能目前暂未实现!", + "vcmi.bonusSource.artifact" : "宝物", + "vcmi.bonusSource.creature" : "技能", + "vcmi.bonusSource.spell" : "法术", + "vcmi.bonusSource.hero" : "英雄", + "vcmi.bonusSource.commander" : "指挥官", + "vcmi.bonusSource.other" : "其他", + "vcmi.capitalColors.0" : "红色", "vcmi.capitalColors.1" : "蓝色", "vcmi.capitalColors.2" : "褐色", @@ -38,6 +49,12 @@ "vcmi.heroOverview.secondarySkills" : "初始技能", "vcmi.heroOverview.spells" : "魔法", + "vcmi.quickExchange.moveUnit" : "移动生物", + "vcmi.quickExchange.moveAllUnits" : "移动所有生物", + "vcmi.quickExchange.swapAllUnits" : "交换生物", + "vcmi.quickExchange.moveAllArtifacts" : "移动所有宝物", + "vcmi.quickExchange.swapAllArtifacts" : "交换宝物", + "vcmi.radialWheel.mergeSameUnit" : "合并相同生物", "vcmi.radialWheel.fillSingleUnit" : "单个生物填充空格", "vcmi.radialWheel.splitSingleUnit" : "分割单个生物", @@ -57,8 +74,26 @@ "vcmi.radialWheel.moveDown" : "下移", "vcmi.radialWheel.moveBottom" : "移到底端", + "vcmi.randomMap.description" : "这是随机生成的地图。\\n模版为%s,大小为%dx%d,层数为%d,人类玩家数量为%d,电脑玩家数量为%d,水域面积%s,怪物等级%s,VCMI地图", + "vcmi.randomMap.description.isHuman" : ", %s为人类玩家", + "vcmi.randomMap.description.townChoice" : ", %s的城镇选择为%s", + "vcmi.randomMap.description.water.none" : "无", + "vcmi.randomMap.description.water.normal" : "普通", + "vcmi.randomMap.description.water.islands" : "岛屿", + "vcmi.randomMap.description.monster.weak" : "弱", + "vcmi.randomMap.description.monster.normal" : "普通", + "vcmi.randomMap.description.monster.strong" : "强", + "vcmi.spellBook.search" : "搜索中...", + "vcmi.spellResearch.canNotAfford" : "你没有足够的资源来将{%SPELL1}替换为{%SPELL2}。但你依然可以弃掉此法术,继续法术研究。", + "vcmi.spellResearch.comeAgain" : "今日已研究过法术,请明日再来。", + "vcmi.spellResearch.pay" : "你想将{%SPELL1}替换为{%SPELL2}吗?或者弃掉此法术,继续法术研究?", + "vcmi.spellResearch.research" : "研究此法术", + "vcmi.spellResearch.skip" : "跳过此法术", + "vcmi.spellResearch.abort" : "中止", + "vcmi.spellResearch.noMoreSpells" : "没有更多的法术可供研究。", + "vcmi.mainMenu.serverConnecting" : "连接中...", "vcmi.mainMenu.serverAddressEnter" : "使用地址:", "vcmi.mainMenu.serverConnectionFailed" : "连接失败", @@ -79,6 +114,12 @@ "vcmi.lobby.handicap.resource" : "给予玩家起始资源以外的更多资源,允许负值,但总量不会低于0(玩家永远不会能以负资源开始游戏)。", "vcmi.lobby.handicap.income" : "按百分比改变玩家的各种收入,向上取整。", "vcmi.lobby.handicap.growth" : "改变玩家拥有的城镇的生物增长率,向上取整。", + "vcmi.lobby.deleteUnsupportedSave" : "{检测到无法支持的存档}\n\nVCMI检测到%d个存档已不再受支持,这可能是由于 VCMI 版本不兼容导致的。\n\n你是否要删除这些存档?", + "vcmi.lobby.deleteSaveGameTitle" : "选择一个要删除的存档", + "vcmi.lobby.deleteMapTitle" : "选择一个要删除要删除的场景", + "vcmi.lobby.deleteFile" : "你确定要删除下列文件?", + "vcmi.lobby.deleteFolder" : "你确定要删除下列文件夹?", + "vcmi.lobby.deleteMode" : "切换删除模式并返回", "vcmi.lobby.login.title" : "VCMI大厅", "vcmi.lobby.login.username" : "用户名:", @@ -143,11 +184,10 @@ "vcmi.client.errors.invalidMap" : "{非法地图或战役}\n\n启动游戏失败,选择的地图或者战役,无效或被污染。原因:\n%s", "vcmi.client.errors.missingCampaigns" : "{找不到数据文件}\n\n没有找到战役数据文件!你可能使用了不完整或损坏的英雄无敌3数据文件,请重新安装数据文件。", "vcmi.server.errors.disconnected" : "{网络错误}\n\n与游戏服务器的连接已断开!", + "vcmi.server.errors.playerLeft" : "{玩家离开}\n\n%s玩家已断开游戏!", //%s -> player color "vcmi.server.errors.existingProcess" : "一个VCMI进程已经在运行,启动新进程前请结束它。", "vcmi.server.errors.modsToEnable" : "{需要启用的mod列表}", "vcmi.server.errors.modsToDisable" : "{需要禁用的mod列表}", - "vcmi.server.errors.modNoDependency" : "读取mod包 {'%s'}失败!\n 需要的mod {'%s'} 没有安装或无效!\n", - "vcmi.server.errors.modConflict" : "读取的mod包 {'%s'}无法运行!\n 与另一个mod {'%s'}冲突!\n", "vcmi.server.errors.unknownEntity" : "加载保存失败! 在保存的游戏中发现未知实体'%s'! 保存可能与当前安装的mod版本不兼容!", "vcmi.dimensionDoor.seaToLandError" : "无法在陆地与海洋之间使用异次元之门传送。", @@ -261,7 +301,7 @@ "vcmi.battleOptions.queueSizeNoneButton.help": "不显示回合顺序指示器", "vcmi.battleOptions.queueSizeAutoButton.help": "根据游戏的分辨率自动调整回合顺序指示器的大小(游戏处于高度低于700像素的分辨率时,使用小,否则使用大)", "vcmi.battleOptions.queueSizeSmallButton.help": "设置回合顺序指示器为小", - "vcmi.battleOptions.queueSizeBigButton.help": "设置次寻条为大尺寸(无法在游戏高度像素低于700时生效)", + "vcmi.battleOptions.queueSizeBigButton.help": "设置回合顺序指示器为大尺寸(无法在游戏高度像素低于700时生效)", "vcmi.battleOptions.animationsSpeed1.hover": "", "vcmi.battleOptions.animationsSpeed5.hover": "", "vcmi.battleOptions.animationsSpeed6.hover": "", @@ -340,6 +380,13 @@ "vcmi.heroWindow.openCommander.help" : "显示该英雄指挥官详细信息", "vcmi.heroWindow.openBackpack.hover" : "开启宝物背包界面", "vcmi.heroWindow.openBackpack.help" : "用更大的界面显示所有获得的宝物", + "vcmi.heroWindow.sortBackpackByCost.hover" : "按价格排序", + "vcmi.heroWindow.sortBackpackByCost.help" : "将行囊里的宝物按价格排序。", + "vcmi.heroWindow.sortBackpackBySlot.hover" : "按装备槽排序", + "vcmi.heroWindow.sortBackpackBySlot.help" : "将行囊里的宝物按装备槽排序。", + "vcmi.heroWindow.sortBackpackByClass.hover" : "按类型排序", + "vcmi.heroWindow.sortBackpackByClass.help" : "将行囊里的宝物按装备槽排序:低级宝物、中级宝物、高级宝物、圣物。", + "vcmi.heroWindow.fusingArtifact.fusing" : "你已拥有融合%s所需的全部组件,想现在进行融合吗?{所有组件在融合后将被消耗。}", "vcmi.tavernWindow.inviteHero" : "邀请英雄", @@ -517,6 +564,8 @@ "core.seerhut.quest.reachDate.visit.4" : "关门直到%s。", "core.seerhut.quest.reachDate.visit.5" : "关门直到%s。", + "mapObject.core.hillFort.object.description" : "升级生物,1-4级生物升级比城镇中更便宜。", + "core.bonus.ADDITIONAL_ATTACK.name": "双击", "core.bonus.ADDITIONAL_ATTACK.description": "生物可以攻击两次", "core.bonus.ADDITIONAL_RETALIATION.name": "额外反击", @@ -579,7 +628,7 @@ "core.bonus.GARGOYLE.description": "不能被复活或治疗", "core.bonus.GENERAL_DAMAGE_REDUCTION.name": "减少伤害 (${val}%)", "core.bonus.GENERAL_DAMAGE_REDUCTION.description": "减少从远程和近战中遭受的物理伤害", - "core.bonus.HATE.name": "${subtype.creature}的死敌", + "core.bonus.HATE.name": "憎恨${subtype.creature}", "core.bonus.HATE.description": "对${subtype.creature}造成额外${val}%伤害", "core.bonus.HEALER.name": "治疗者", "core.bonus.HEALER.description": "可以治疗友军单位", @@ -668,5 +717,31 @@ "core.bonus.DISINTEGRATE.name": "解体", "core.bonus.DISINTEGRATE.description": "死亡后不会留下尸体", "core.bonus.INVINCIBLE.name": "无敌", - "core.bonus.INVINCIBLE.description": "不受任何效果影响" + "core.bonus.INVINCIBLE.description": "不受任何效果影响", + "core.bonus.MECHANICAL.name": "机械", + "core.bonus.MECHANICAL.description": "免疫大多数效果,可修复", + "core.bonus.PRISM_HEX_ATTACK_BREATH.name": "棱光吐息", + "core.bonus.PRISM_HEX_ATTACK_BREATH.description": "攻击后向三方向扩散攻击", + + "spell.core.castleMoat.name" : "护城河", + "spell.core.castleMoatTrigger.name" : "护城河", + "spell.core.catapultShot.name" : "投石车射击", + "spell.core.cyclopsShot.name" : "攻城射击", + "spell.core.dungeonMoat.name" : "极热之油", + "spell.core.dungeonMoatTrigger.name" : "极热之油", + "spell.core.fireWallTrigger.name" : "烈火魔墙", + "spell.core.firstAid.name" : "急救术", + "spell.core.fortressMoat.name" : "焦油", + "spell.core.fortressMoatTrigger.name" : "焦油", + "spell.core.infernoMoat.name" : "熔岩", + "spell.core.infernoMoatTrigger.name" : "熔岩", + "spell.core.landMineTrigger.name" : "埋设地雷", + "spell.core.necropolisMoat.name" : "尸骨堆", + "spell.core.necropolisMoatTrigger.name" : "尸骨堆", + "spell.core.rampartMoat.name" : "护城河", + "spell.core.rampartMoatTrigger.name" : "护城河", + "spell.core.strongholdMoat.name" : "栅栏", + "spell.core.strongholdMoatTrigger.name" : "栅栏", + "spell.core.summonDemons.name" : "召唤恶鬼", + "spell.core.towerMoat.name" : "埋设地雷" } diff --git a/Mods/vcmi/Content/config/czech.json b/Mods/vcmi/Content/config/czech.json new file mode 100644 index 000000000..196a5405d --- /dev/null +++ b/Mods/vcmi/Content/config/czech.json @@ -0,0 +1,788 @@ +{ + "vcmi.adventureMap.monsterThreat.title" : "\n\nHrozba: ", + "vcmi.adventureMap.monsterThreat.levels.0" : "Bez námahy", + "vcmi.adventureMap.monsterThreat.levels.1" : "Velmi slabá", + "vcmi.adventureMap.monsterThreat.levels.2" : "Slabá", + "vcmi.adventureMap.monsterThreat.levels.3" : "O něco slabší", + "vcmi.adventureMap.monsterThreat.levels.4" : "Rovnocenná", + "vcmi.adventureMap.monsterThreat.levels.5" : "O něco silnější", + "vcmi.adventureMap.monsterThreat.levels.6" : "Silná", + "vcmi.adventureMap.monsterThreat.levels.7" : "Velmi silná", + "vcmi.adventureMap.monsterThreat.levels.8" : "Výzva", + "vcmi.adventureMap.monsterThreat.levels.9" : "Převaha", + "vcmi.adventureMap.monsterThreat.levels.10" : "Smrtící", + "vcmi.adventureMap.monsterThreat.levels.11" : "Nehratelná", + "vcmi.adventureMap.monsterLevel" : "\n\nÚroveň %LEVEL, %TOWN\nJednotka %ATTACK_TYPE", + "vcmi.adventureMap.monsterMeleeType" : "útočí zblízka", + "vcmi.adventureMap.monsterRangedType" : "útočí na dálku", + "vcmi.adventureMap.search.hover" : "Prohledat objekt", + "vcmi.adventureMap.search.help" : "Vyberte objekt na mapě k prohledání.", + + "vcmi.adventureMap.confirmRestartGame" : "Jste si jisti, že chcete restartovat hru?", + "vcmi.adventureMap.noTownWithMarket" : "Nejsou dostupné žádne tržnice!", + "vcmi.adventureMap.noTownWithTavern" : "Nejsou dostupná žádná města s putykou!", + "vcmi.adventureMap.spellUnknownProblem" : "Neznámý problém s tímto kouzlem! Další informace nejsou k dispozici.", + "vcmi.adventureMap.playerAttacked" : "Hráč byl napaden: %s", + "vcmi.adventureMap.moveCostDetails" : "Body pohybu - Cena: %TURNS tahů + %POINTS bodů, zbylé body: %REMAINING", + "vcmi.adventureMap.moveCostDetailsNoTurns" : "Body pohybu - Cena: %POINTS bodů, zbylé body: %REMAINING", + "vcmi.adventureMap.movementPointsHeroInfo" : "(Body pohybu: %REMAINING / %POINTS)", + "vcmi.adventureMap.replayOpponentTurnNotImplemented" : "Omlouváme se, přehrání tahu soupeře ještě není implementováno!", + + "vcmi.bonusSource.artifact" : "Artefakt", + "vcmi.bonusSource.creature" : "Schopnost", + "vcmi.bonusSource.spell" : "Kouzlo", + "vcmi.bonusSource.hero" : "Hrdina", + "vcmi.bonusSource.commander" : "Velitel", + "vcmi.bonusSource.other" : "Ostatní", + + "vcmi.capitalColors.0" : "Červený", + "vcmi.capitalColors.1" : "Modrý", + "vcmi.capitalColors.2" : "Hnědý", + "vcmi.capitalColors.3" : "Zelený", + "vcmi.capitalColors.4" : "Oranžový", + "vcmi.capitalColors.5" : "Fialový", + "vcmi.capitalColors.6" : "Tyrkysový", + "vcmi.capitalColors.7" : "Růžový", + + "vcmi.heroOverview.startingArmy" : "Počáteční jednotky", + "vcmi.heroOverview.warMachine" : "Bojové stroje", + "vcmi.heroOverview.secondarySkills" : "Druhotné schopnosti", + "vcmi.heroOverview.spells" : "Kouzla", + + "vcmi.quickExchange.moveUnit" : "Přesunout jednotku", + "vcmi.quickExchange.moveAllUnits" : "Přesunout všechny jednotky", + "vcmi.quickExchange.swapAllUnits" : "Vyměnit armády", + "vcmi.quickExchange.moveAllArtifacts" : "Přesunout všechny artefakty", + "vcmi.quickExchange.swapAllArtifacts" : "Vyměnit artefakt", + + "vcmi.radialWheel.mergeSameUnit" : "Sloučit stejné jednotky", + "vcmi.radialWheel.fillSingleUnit" : "Vyplnit jednou jednotkou", + "vcmi.radialWheel.splitSingleUnit" : "Rozdělit jedinou jednotku", + "vcmi.radialWheel.splitUnitEqually" : "Rozdělit jednotky rovnoměrně", + "vcmi.radialWheel.moveUnit" : "Přesunout jednotky do jiného oddílu", + "vcmi.radialWheel.splitUnit" : "Rozdělit jednotku do jiné pozice", + + "vcmi.radialWheel.heroGetArmy" : "Získat armádu jiného hrdiny", + "vcmi.radialWheel.heroSwapArmy" : "Vyměnit armádu s jiným hrdinou", + "vcmi.radialWheel.heroExchange" : "Otevřít výměnu hrdinů", + "vcmi.radialWheel.heroGetArtifacts" : "Získat artefakty od jiného hrdiny", + "vcmi.radialWheel.heroSwapArtifacts" : "Vyměnit artefakty s jiným hrdinou", + "vcmi.radialWheel.heroDismiss" : "Propustit hrdinu", + + "vcmi.radialWheel.moveTop" : "Přesunout nahoru", + "vcmi.radialWheel.moveUp" : "Posunout výše", + "vcmi.radialWheel.moveDown" : "Posunout níže", + "vcmi.radialWheel.moveBottom" : "Přesunout dolů", + + "vcmi.randomMap.description" : "Mapa vytvořená Generátorem náhodných map.\nŠablona: %s, rozměry: %dx%d, úroveň: %d, hráči: %d, AI hráči: %d, množství vody: %s, síla jednotek: %s, VCMI mapa", + "vcmi.randomMap.description.isHuman" : ", %s je lidský hráč", + "vcmi.randomMap.description.townChoice" : ", volba města pro %s je %s", + "vcmi.randomMap.description.water.none" : "žádná", + "vcmi.randomMap.description.water.normal" : "normální", + "vcmi.randomMap.description.water.islands" : "ostrovy", + "vcmi.randomMap.description.monster.weak" : "nízká", + "vcmi.randomMap.description.monster.normal" : "normální", + "vcmi.randomMap.description.monster.strong" : "vysoká", + + "vcmi.spellBook.search" : "Hledat", + + "vcmi.spellResearch.canNotAfford" : "Nemáte dostatek prostředků k nahrazení {%SPELL1} za {%SPELL2}. Stále však můžete toto kouzlo zrušit a pokračovat ve výzkumu dalších kouzel.", + "vcmi.spellResearch.comeAgain" : "Výzkum už byl dnes proveden. Vraťte se zítra.", + "vcmi.spellResearch.pay" : "Chcete nahradit {%SPELL1} za {%SPELL2}? Nebo zrušit toto kouzlo a pokračovat ve výzkumu dalších kouzel?", + "vcmi.spellResearch.research" : "Prozkoumat toto kouzlo", + "vcmi.spellResearch.skip" : "Přeskočit toto kouzlo", + "vcmi.spellResearch.abort" : "Přerušit", + "vcmi.spellResearch.noMoreSpells" : "Žádná další kouzla k výzkumu nejsou dostupná.", + + "vcmi.mainMenu.serverConnecting" : "Připojování...", + "vcmi.mainMenu.serverAddressEnter" : "Zadejte adresu:", + "vcmi.mainMenu.serverConnectionFailed" : "Připojování selhalo", + "vcmi.mainMenu.serverClosing" : "Zavírání...", + "vcmi.mainMenu.hostTCP" : "Pořádat hru TCP/IP", + "vcmi.mainMenu.joinTCP" : "Připojit se do hry TCP/IP", + + "vcmi.lobby.filepath" : "Název souboru", + "vcmi.lobby.creationDate" : "Datum vytvoření", + "vcmi.lobby.scenarioName" : "Název scénáře", + "vcmi.lobby.mapPreview" : "Náhled mapy", + "vcmi.lobby.noPreview" : "bez náhledu", + "vcmi.lobby.noUnderground" : "bez podzemí", + "vcmi.lobby.sortDate" : "Řadit mapy dle data změny", + "vcmi.lobby.backToLobby" : "Vrátit se do lobby", + "vcmi.lobby.author" : "Autor", + "vcmi.lobby.handicap" : "Postih", + "vcmi.lobby.handicap.resource" : "Dává hráčům odpovídající zdroje navíc k běžným startovním zdrojům. Jsou povoleny záporné hodnoty, ale jsou omezeny na celkovou hodnotu 0 (hráč nikdy nezačíná se zápornými zdroji).", + "vcmi.lobby.handicap.income" : "Mění různé příjmy hráče podle procent. Výsledek je zaokrouhlen nahoru.", + "vcmi.lobby.handicap.growth" : "Mění rychlost růstu jednotel v městech vlastněných hráčem. Výsledek je zaokrouhlen nahoru.", + "vcmi.lobby.deleteUnsupportedSave" : "Nalezeny nepodporované uložené hry.\n\nBylo nalezeno %d uložených her, které již nejsou podporovány, pravděpodobně kvůli rozdílům mezi verzemi VCMI.\n\nChcete je odstranit?", + "vcmi.lobby.deleteSaveGameTitle" : "Vyberte uloženou hru k odstranění", + "vcmi.lobby.deleteMapTitle" : "Vyberte scénář k odstranění", + "vcmi.lobby.deleteFile" : "Chcete odstranit následující soubor?", + "vcmi.lobby.deleteFolder" : "Chcete odstranit následující složku?", + "vcmi.lobby.deleteMode" : "Přepnout do režimu mazání a zpět", + + "vcmi.broadcast.failedLoadGame" : "Nepodařilo se načíst hru", + "vcmi.broadcast.command" : "Použijte '!help' pro zobrazení dostupných příkazů", + "vcmi.broadcast.simturn.end" : "Současné tahy byly ukončeny", + "vcmi.broadcast.simturn.endBetween" : "Současné tahy mezi hráči %s a %s byly ukončeny", + "vcmi.broadcast.serverProblem" : "Server narazil na problém", + "vcmi.broadcast.gameTerminated" : "Hra byla ukončena", + "vcmi.broadcast.gameSavedAs" : "Hra byla uložena jako", + "vcmi.broadcast.noCheater" : "Nejsou zaznamenáni žádní podvodníci!", + "vcmi.broadcast.playerCheater" : "Hráč %s je podvodník!", + "vcmi.broadcast.statisticFile" : "Soubory se statistikou lze nalézt v adresáři %s", + "vcmi.broadcast.help.commands" : "Dostupné příkazy pro hostitele:", + "vcmi.broadcast.help.exit" : "'!exit' - okamžitě ukončí aktuální hru", + "vcmi.broadcast.help.kick" : "'!kick ' - vyhodí vybraného hráče ze hry", + "vcmi.broadcast.help.save" : "'!save ' - uloží hru pod zadaným názvem", + "vcmi.broadcast.help.statistic" : "'!statistic' - uloží statistiky hry jako soubor CSV", + "vcmi.broadcast.help.commandsAll" : "Dostupné příkazy pro všechny hráče:", + "vcmi.broadcast.help.help" : "'!help' - zobrazí tuto nápovědu", + "vcmi.broadcast.help.cheaters" : "'!cheaters' - zobrazí seznam hráčů, kteří během hry použili cheaty", + "vcmi.broadcast.help.vote" : "'!vote' - umožňuje změnit některá nastavení hry, pokud všichni hráči souhlasí", + "vcmi.broadcast.vote.allow" : "'!vote simturns allow X' - povolí současné tahy na určený počet dní nebo dokud nenastane kontakt", + "vcmi.broadcast.vote.force" : "'!vote simturns force X' - vynutí současné tahy na určený počet dní s blokováním kontaktů hráčů", + "vcmi.broadcast.vote.abort" : "'!vote simturns abort' - ukončí současné tahy po skončení aktuálního tahu", + "vcmi.broadcast.vote.timer" : "'!vote timer prolong X' - prodlouží základní časovač pro všechny hráče o určený počet sekund", + "vcmi.broadcast.vote.noActive" : "Žádné aktivní hlasování!", + "vcmi.broadcast.vote.yes" : "ano", + "vcmi.broadcast.vote.no" : "ne", + "vcmi.broadcast.vote.notRecognized" : "Hlasovací příkaz nebyl rozpoznán!", + "vcmi.broadcast.vote.success.untilContacts" : "Hlasování bylo úspěšné. Současné tahy poběží ještě %s dní nebo dokud nenastane kontakt", + "vcmi.broadcast.vote.success.contactsBlocked" : "Hlasování bylo úspěšné. Současné tahy poběží ještě %s dní. Kontakty jsou blokovány", + "vcmi.broadcast.vote.success.nextDay" : "Hlasování bylo úspěšné. Současné tahy skončí následující den", + "vcmi.broadcast.vote.success.timer" : "Hlasování bylo úspěšné. Časovač pro všechny hráče byl prodloužen o %s sekund", + "vcmi.broadcast.vote.aborted" : "Hráč hlasoval proti změně. Hlasování bylo ukončeno", + "vcmi.broadcast.vote.start.untilContacts" : "Bylo zahájeno hlasování o povolení současných tahů na %s dní", + "vcmi.broadcast.vote.start.contactsBlocked" : "Bylo zahájeno hlasování o vynucení současných tahů na %s dní", + "vcmi.broadcast.vote.start.nextDay" : "Bylo zahájeno hlasování o ukončení současných tahů od následujícího dne", + "vcmi.broadcast.vote.start.timer" : "Bylo zahájeno hlasování o prodloužení časovače pro všechny hráče o %s sekund", + "vcmi.broadcast.vote.hint" : "Napište '!vote yes', pokud souhlasíte se změnou, nebo '!vote no', pokud jste proti", + + "vcmi.lobby.login.title" : "Online lobby VCMI", + "vcmi.lobby.login.username" : "Uživatelské jméno:", + "vcmi.lobby.login.connecting" : "Připojování...", + "vcmi.lobby.login.error" : "Chyba při připojování: %s", + "vcmi.lobby.login.create" : "Nový účet", + "vcmi.lobby.login.login" : "Přihlásit se", + "vcmi.lobby.login.as" : "Přihlásit se jako %s", + "vcmi.lobby.login.spectator" : "Divák", + "vcmi.lobby.header.rooms" : "Herní místnosti - %d", + "vcmi.lobby.header.channels" : "Kanály konverzace", + "vcmi.lobby.header.chat.global" : "Globální konverzace hry - %s", // %s -> language name + "vcmi.lobby.header.chat.match" : "Konverzace předchozí hry %s", // %s -> game start date & time + "vcmi.lobby.header.chat.player" : "Soukromá konverzace s %s", // %s -> nickname of another player + "vcmi.lobby.header.history" : "Vaše předchozí hry", + "vcmi.lobby.header.players" : "Online hráči - %d", + "vcmi.lobby.match.solo" : "Hra jednoho hráče", + "vcmi.lobby.match.duel" : "Hra s %s", // %s -> nickname of another player + "vcmi.lobby.match.multi" : "%d hráčů", + "vcmi.lobby.room.create" : "Vytvořit novou místnost", + "vcmi.lobby.room.players.limit" : "Omezení počtu hráčů", + "vcmi.lobby.room.description.public" : "Jakýkoliv hráč se může připojit do veřejné místnosti.", + "vcmi.lobby.room.description.private" : "Pouze pozvaní hráči se mohou připojit do soukromé místnosti.", + "vcmi.lobby.room.description.new" : "Pro start hry vyberte scénář, nebo nastavte náhodnou mapu.", + "vcmi.lobby.room.description.load" : "Pro start hry načtěte uloženou hru.", + "vcmi.lobby.room.description.limit" : "Až %d hráčů se může připojit do vaší místnosti (včetně vás).", + "vcmi.lobby.invite.header" : "Pozvat hráče", + "vcmi.lobby.invite.notification" : "Pozval vás hráč do jejich soukromé místnosti. Nyní se do ní můžete připojit.", + "vcmi.lobby.preview.title" : "Připojit se do herní místnosti", + "vcmi.lobby.preview.subtitle" : "Hra na %s, pořádána %s", //TL Note: 1) name of map or RMG template 2) nickname of game host + "vcmi.lobby.preview.version" : "Verze hry:", + "vcmi.lobby.preview.players" : "Hráči:", + "vcmi.lobby.preview.mods" : "Použité modifikace:", + "vcmi.lobby.preview.allowed" : "Připojit se do herní místnosti?", + "vcmi.lobby.preview.error.header" : "Nelze se připojit do této herní místnosti.", + "vcmi.lobby.preview.error.playing" : "Nejdříve musíte opustit vaši současnou hru.", + "vcmi.lobby.preview.error.full" : "Místnost je již plná.", + "vcmi.lobby.preview.error.busy" : "Místnost již nepřijímá nové hráče.", + "vcmi.lobby.preview.error.invite" : "Nebyl jste pozván do této mísnosti.", + "vcmi.lobby.preview.error.mods" : "Použváte jinou sadu modifikací.", + "vcmi.lobby.preview.error.version" : "Používáte jinou verzi VCMI.", + "vcmi.lobby.room.new" : "Nová hra", + "vcmi.lobby.room.load" : "Načíst hru", + "vcmi.lobby.room.type" : "Druh místnosti", + "vcmi.lobby.room.mode" : "Herní režim", + "vcmi.lobby.room.state.public" : "Veřejná", + "vcmi.lobby.room.state.private" : "Soukromá", + "vcmi.lobby.room.state.busy" : "Ve hře", + "vcmi.lobby.room.state.invited" : "Pozvaný", + "vcmi.lobby.mod.state.compatible" : "Kompatibilní", + "vcmi.lobby.mod.state.disabled" : "Musí být povolena", + "vcmi.lobby.mod.state.version" : "Neshoda verze", + "vcmi.lobby.mod.state.excessive" : "Musí být zakázána", + "vcmi.lobby.mod.state.missing" : "Není nainstalována", + "vcmi.lobby.pvp.coin.hover" : "Mince", + "vcmi.lobby.pvp.coin.help" : "Hodí mincí", + "vcmi.lobby.pvp.randomTown.hover" : "Náhodné město", + "vcmi.lobby.pvp.randomTown.help" : "Napsat náhodné město do konvezace", + "vcmi.lobby.pvp.randomTownVs.hover" : "Náhodné město vs.", + "vcmi.lobby.pvp.randomTownVs.help" : "Napsat 2 náhodná města do konvezace", + "vcmi.lobby.pvp.versus" : "vs.", + + "vcmi.client.errors.invalidMap" : "{Neplatná mapa nebo kampaň}\n\nChyba při startu hry! Vybraná mapa nebo kampaň může být neplatná nebo poškozená. Důvod:\n%s", + "vcmi.client.errors.missingCampaigns" : "{Chybějící datové soubory}\n\nDatové soubory kampaně nebyly nalezeny! Možná máte nekompletní nebo poškozené datové soubory Heroes 3. Prosíme, přeinstalujte hru.", + "vcmi.server.errors.disconnected" : "{Chyba sítě}\n\nPřipojení k hernímu serveru bylo ztraceno!", + "vcmi.server.errors.playerLeft" : "{Hráč opustil hru}\n\nHráč %s se odpojil ze hry!", //%s -> player color + "vcmi.server.errors.existingProcess" : "Již běží jiný server VCMI. Prosím, ukončete ho před startem nové hry.", + "vcmi.server.errors.modsToEnable" : "{Následující modifikace jsou nutné pro načtení hry}", + "vcmi.server.errors.modsToDisable" : "{Následující modifikace musí být zakázány}", + "vcmi.server.errors.unknownEntity" : "Nelze načíst uloženou pozici! Neznámá entita '%s' nalezena v uložené pozici! Uložná pozice nemusí být kompatibilní s aktuálními verzemi modifikací!", + "vcmi.server.errors.wrongIdentified" : "Byli jste identifikováni jako hráč %s, zatímco byl očekáván hráč %s.", + "vcmi.server.errors.notAllowed" : "Nemáte oprávnění provést tuto akci!", + + "vcmi.dimensionDoor.seaToLandError" : "Pomocí dimenzní brány není možné se teleportovat z moře na pevninu nebo naopak.", + + "vcmi.settingsMainWindow.generalTab.hover" : "Obecné", + "vcmi.settingsMainWindow.generalTab.help" : "Přepne na kartu obecných nastavení, která obsahuje nastavení související s obecným chováním klienta hry.", + "vcmi.settingsMainWindow.battleTab.hover" : "Bitva", + "vcmi.settingsMainWindow.battleTab.help" : "Přepne na kartu nastavení bitvy, která umožňuje konfiguraci chování hry v bitvách.", + "vcmi.settingsMainWindow.adventureTab.hover" : "Mapa světa", + "vcmi.settingsMainWindow.adventureTab.help" : "Přepne na kartu nastavení mapy světa (mapa světa je sekce hry, ve které hráči mohou ovládat pohyb hrdinů).", + + "vcmi.systemOptions.videoGroup" : "Nastavení obrazu", + "vcmi.systemOptions.audioGroup" : "Nastavení zvuku", + "vcmi.systemOptions.otherGroup" : "Ostatní nastavení", // unused right now + "vcmi.systemOptions.townsGroup" : "Obrazovka města", + + "vcmi.statisticWindow.statistics" : "Statistiky", + "vcmi.statisticWindow.tsvCopy" : "Zkopírovat data do schránky", + "vcmi.statisticWindow.selectView" : "Vybrat pohled", + "vcmi.statisticWindow.value" : "Hodnota", + "vcmi.statisticWindow.title.overview" : "Přehled", + "vcmi.statisticWindow.title.resources" : "Zdroje", + "vcmi.statisticWindow.title.income" : "Příjem", + "vcmi.statisticWindow.title.numberOfHeroes" : "Počet hrdinů", + "vcmi.statisticWindow.title.numberOfTowns" : "Počet měst", + "vcmi.statisticWindow.title.numberOfArtifacts" : "Počet artefaktů", + "vcmi.statisticWindow.title.numberOfDwellings" : "Počet obydlí", + "vcmi.statisticWindow.title.numberOfMines" : "Počet dolů", + "vcmi.statisticWindow.title.armyStrength" : "Síla armády", + "vcmi.statisticWindow.title.experience" : "Zkušenosti", + "vcmi.statisticWindow.title.resourcesSpentArmy" : "Náklady na armádu", + "vcmi.statisticWindow.title.resourcesSpentBuildings" : "Náklady na budovy", + "vcmi.statisticWindow.title.mapExplored" : "Prozkoumaná část mapy", + "vcmi.statisticWindow.param.playerName" : "Jméno hráče", + "vcmi.statisticWindow.param.daysSurvived" : "Počet přežitých dní", + "vcmi.statisticWindow.param.maxHeroLevel" : "Maximální úroveň hrdiny", + "vcmi.statisticWindow.param.battleWinRatioHero" : "Poměr výher (proti hrdinům)", + "vcmi.statisticWindow.param.battleWinRatioNeutral" : "Poměr výher (proti neutrálním jednotkám)", + "vcmi.statisticWindow.param.battlesHero" : "Bitev (proti hrdinům)", + "vcmi.statisticWindow.param.battlesNeutral" : "Bitej (proti neutrálním jednotkám)", + "vcmi.statisticWindow.param.maxArmyStrength" : "Maximální síla armády", + "vcmi.statisticWindow.param.tradeVolume" : "Objem obchodu", + "vcmi.statisticWindow.param.obeliskVisited" : "Navštívené obelisky", + "vcmi.statisticWindow.icon.townCaptured" : "Dobyto měst", + "vcmi.statisticWindow.icon.strongestHeroDefeated" : "Nejsilnější hrdina protivníka poražen", + "vcmi.statisticWindow.icon.grailFound" : "Nalezen Svatý grál", + "vcmi.statisticWindow.icon.defeated" : "Porážka", + + "vcmi.systemOptions.fullscreenBorderless.hover" : "Celá obrazovka (bez okrajů)", + "vcmi.systemOptions.fullscreenBorderless.help" : "{Celá obrazovka bez okrajů}\n\nPokud je vybráno, VCMI poběží v režimu celé obrazovky bez okrajů. V tomto režimu bude hra respektovat systémové rozlišení a ignorovat vybrané rozlišení ve hře.", + "vcmi.systemOptions.fullscreenExclusive.hover" : "Celá obrazovka (exkluzivní)", + "vcmi.systemOptions.fullscreenExclusive.help" : "{Celá obrazovka}\n\nPokud je vybráno, VCMI poběží v režimu exkluzivní celé obrazovky. V tomto režimu hra změní rozlišení na vybrané.", + "vcmi.systemOptions.resolutionButton.hover" : "Rozlišení: %wx%h", + "vcmi.systemOptions.resolutionButton.help" : "{Vybrat rozlišení}\n\nZmění rozlišení herní obrazovky.", + "vcmi.systemOptions.resolutionMenu.hover" : "Vybrat rozlišení", + "vcmi.systemOptions.resolutionMenu.help" : "Změní rozlišení herní obrazovky.", + "vcmi.systemOptions.scalingButton.hover" : "Škálování rozhraní: %p%", + "vcmi.systemOptions.scalingButton.help" : "{Škálování rozhraní}\n\nZmění škálování herního rozhraní", + "vcmi.systemOptions.scalingMenu.hover" : "Vybrat škálování rozhraní", + "vcmi.systemOptions.scalingMenu.help" : "Změní škálování herního rozhraní.", + "vcmi.systemOptions.longTouchButton.hover" : "Doba dlouhého podržení: %d ms", // Translation note: "ms" = "milliseconds" + "vcmi.systemOptions.longTouchButton.help" : "{Doba dlouhého podržení}\n\nPři používání dotykové obrazovky bude zobrazeno vyskakovací okno při podržení prstu na obrazovce, v milisekundách", + "vcmi.systemOptions.longTouchMenu.hover" : "Vybrat dobu dlouhého podržení", + "vcmi.systemOptions.longTouchMenu.help" : "Změnit dobu dlouhého podržení.", + "vcmi.systemOptions.longTouchMenu.entry" : "%d milisekund", + "vcmi.systemOptions.framerateButton.hover" : "Zobrazit FPS", + "vcmi.systemOptions.framerateButton.help" : "{Zobrazit FPS}\n\nPřepne viditelnost počítadla snímků za sekundu v rohu obrazovky hry.", + "vcmi.systemOptions.hapticFeedbackButton.hover" : "Vibrace", + "vcmi.systemOptions.hapticFeedbackButton.help" : "{Vibrace}\n\nPřepnout stav vibrací při dotykovém ovládání.", + "vcmi.systemOptions.enableUiEnhancementsButton.hover" : "Vylepšení rozhraní", + "vcmi.systemOptions.enableUiEnhancementsButton.help" : "{Vylepšení rozhraní}\n\nZapne různá vylepšení rozhraní, jako je tlačítko batohu atd. Zakažte pro zážitek klasické hry.", + "vcmi.systemOptions.enableLargeSpellbookButton.hover" : "Velká kniha kouzel", + "vcmi.systemOptions.enableLargeSpellbookButton.help" : "{Velká kniha kouzel}\n\nPovolí větší knihu kouzel, do které se vejde více kouzel na jednu stranu. Animace změny stránek s tímto nastavením nefunguje.", + "vcmi.systemOptions.audioMuteFocus.hover" : "Ztlumit při neaktivitě", + "vcmi.systemOptions.audioMuteFocus.help" : "{Ztlumit při neaktivitě}\n\nZtlumit zvuk, pokud je okno hry v pozadí. Výjimkou jsou zprávy ve hře a zvuk nového tahu.", + + "vcmi.adventureOptions.infoBarPick.hover" : "Zobrazit zprávy v panelu informací", + "vcmi.adventureOptions.infoBarPick.help" : "{Zobrazit zprávy v panelu informací}\n\nKdyž bude možné, herní zprávy z návštěv míst na mapě budou zobrazeny v panelu informací místo ve zvláštním okně.", + "vcmi.adventureOptions.numericQuantities.hover" : "Číselné množství jednotek", + "vcmi.adventureOptions.numericQuantities.help" : "{Číselné množství jednotek}\n\nZobrazit přibližné množství nepřátelských jednotek ve formátu A-B.", + "vcmi.adventureOptions.forceMovementInfo.hover" : "Vždy zobrazit cenu pohybu", + "vcmi.adventureOptions.forceMovementInfo.help" : "{Vždy zobrazit cenu pohybu}\n\nVždy zobrazit informace o bodech pohybu v panelu informací. (Místo zobrazení pouze při stisknuté klávese ALT).", + "vcmi.adventureOptions.showGrid.hover" : "Zobrazit mřížku", + "vcmi.adventureOptions.showGrid.help" : "{Zobrazit mřížku}\n\nZobrazit překrytí mřížkou, zvýrazňuje hranice mezi dlaždicemi mapy světa.", + "vcmi.adventureOptions.borderScroll.hover" : "Posouvání okrajem obrazovky", + "vcmi.adventureOptions.borderScroll.help" : "{Posouvání okrajem obrazovky}\n\nPosouvat mapu světa, když je kurzor na okraji obrazovky. Může být zakázáno držením klávesy CTRL.", + "vcmi.adventureOptions.infoBarCreatureManagement.hover" : "Správa jednotek v informačním panelu", + "vcmi.adventureOptions.infoBarCreatureManagement.help" : "{Správa jednotek v informačním panelu}\n\nUmožňuje přeskupovat jednotky v informačním panelu namísto procházení standardních informací.", + "vcmi.adventureOptions.leftButtonDrag.hover" : "Posun levým tlač.", + "vcmi.adventureOptions.leftButtonDrag.help" : "{Posun levým tlačítkem}\n\nPosouvání mapy tažením myši se stisknutým levým tlačítkem.", + "vcmi.adventureOptions.rightButtonDrag.hover" : "Posun pravým tlač.", + "vcmi.adventureOptions.rightButtonDrag.help" : "{Posun pravým tlačítkem}\n\nKdyž je povoleno, pohyb myší se stisknutým pravým tlačítkem bude posouvat pohled na mapě dobrodružství.", + "vcmi.adventureOptions.smoothDragging.hover" : "Plynulé posouvání mapy", + "vcmi.adventureOptions.smoothDragging.help" : "{Plynulé posouvání mapy}\n\nPokud je tato možnost aktivována, posouvání mapy bude plynulé.", + "vcmi.adventureOptions.skipAdventureMapAnimations.hover" : "Přeskočit efekty mizení", + "vcmi.adventureOptions.skipAdventureMapAnimations.help" : "{Přeskočit efekty mizení}\n\nKdyž je povoleno, přeskočí se efekty mizení objektů a podobné efekty (sběr surovin, nalodění atd.). V některých případech zrychlí uživatelské rozhraní na úkor estetiky. Obzvláště užitečné v PvP hrách. Pro maximální rychlost pohybu je toto nastavení aktivní bez ohledu na další volby.", + "vcmi.adventureOptions.mapScrollSpeed1.hover" : "", + "vcmi.adventureOptions.mapScrollSpeed5.hover" : "", + "vcmi.adventureOptions.mapScrollSpeed6.hover" : "", + "vcmi.adventureOptions.mapScrollSpeed1.help" : "Nastavit posouvání mapy na velmi pomalé", + "vcmi.adventureOptions.mapScrollSpeed5.help" : "Nastavit posouvání mapy na velmi rychlé", + "vcmi.adventureOptions.mapScrollSpeed6.help" : "Nastavit posouvání mapy na okamžité", + "vcmi.adventureOptions.hideBackground.hover" : "Skrýt pozadí", + "vcmi.adventureOptions.hideBackground.help" : "{Skrýt pozadí}\n\nSkryje mapu dobrodružství na pozadí a místo ní zobrazí texturu.", + + "vcmi.battleOptions.queueSizeLabel.hover" : "Zobrazit frontu pořadí tahů", + "vcmi.battleOptions.queueSizeNoneButton.hover" : "VYPNUTO", + "vcmi.battleOptions.queueSizeAutoButton.hover" : "AUTO", + "vcmi.battleOptions.queueSizeSmallButton.hover" : "MALÁ", + "vcmi.battleOptions.queueSizeBigButton.hover" : "VELKÁ", + "vcmi.battleOptions.queueSizeNoneButton.help" : "Nezobrazovat frontu pořadí tahů.", + "vcmi.battleOptions.queueSizeAutoButton.help" : "Nastavit automaticky velikost fronty pořadí tahů podle rozlišení obrazovky hry (Při výšce herního rozlišení menší než 700 pixelů je použita velikost MALÁ, jinak velikost VELKÁ)", + "vcmi.battleOptions.queueSizeSmallButton.help" : "Zobrazit MALOU frontu pořadí tahů.", + "vcmi.battleOptions.queueSizeBigButton.help" : "Zobrazit VELKOU frontu pořadí tahů (není podporováno, pokud výška rozlišení hry není alespoň 700 pixelů).", + "vcmi.battleOptions.animationsSpeed1.hover" : "", + "vcmi.battleOptions.animationsSpeed5.hover" : "", + "vcmi.battleOptions.animationsSpeed6.hover" : "", + "vcmi.battleOptions.animationsSpeed1.help" : "Nastavit rychlost animací na velmi pomalé.", + "vcmi.battleOptions.animationsSpeed5.help" : "Nastavit rychlost animací na velmi rychlé.", + "vcmi.battleOptions.animationsSpeed6.help" : "Nastavit rychlost animací na okamžité.", + "vcmi.battleOptions.movementHighlightOnHover.hover" : "Zvýraznění pohybu při najetí", + "vcmi.battleOptions.movementHighlightOnHover.help" : "{Zvýraznění pohybu při najetí}\n\nZvýraznit rozsah pohybu jednotky při najetí na něj.", + "vcmi.battleOptions.rangeLimitHighlightOnHover.hover" : "Zobrazit omezení dostřelu střelců", + "vcmi.battleOptions.rangeLimitHighlightOnHover.help" : "{Zobrazit omezení dostřelu střelců při najetí}\n\nZobrazit dostřel střelce při najetí na něj.", + "vcmi.battleOptions.showStickyHeroInfoWindows.hover" : "Zobrazit okno statistik hrdinů", + "vcmi.battleOptions.showStickyHeroInfoWindows.help" : "{Zobrazit okno statistik hrdinů}\n\nTrvale zapne okno statistiky hrdinů, které ukazuje hlavní schopnosti a magickou energii.", + "vcmi.battleOptions.skipBattleIntroMusic.hover" : "Přeskočit úvodní hudbu", + "vcmi.battleOptions.skipBattleIntroMusic.help" : "{Přeskočit úvodní hudbu}\n\nPovolí akce při úvodní hudbě přehrávané při začátku každé bitvy.", + "vcmi.battleOptions.endWithAutocombat.hover" : "Přeskočit bitvu", + "vcmi.battleOptions.endWithAutocombat.help" : "{Přeskočit bitvu}\n\nAutomatický boj okamžitě dohraje bitvu do konce.", + "vcmi.battleOptions.showQuickSpell.hover" : "Zobrazit rychlý panel kouzel", + "vcmi.battleOptions.showQuickSpell.help" : "{Zobrazit rychlý panel kouzel}\n\nZobrazí panel pro rychlý výběr kouzel.", + + "vcmi.adventureMap.revisitObject.hover" : "Znovu navštívit objekt", + "vcmi.adventureMap.revisitObject.help" : "{Znovu navštívit objekt}\n\nPokud hrdina právě stojí na objektu na mapě, může toto místo znovu navštívit.", + + "vcmi.battleWindow.pressKeyToSkipIntro" : "Stiskněte jakoukoliv klávesu pro okamžité zahájení bitvy", + "vcmi.battleWindow.damageEstimation.melee" : "Zaútočit na %CREATURE (%DAMAGE).", + "vcmi.battleWindow.damageEstimation.meleeKills" : "Zaútočit na %CREATURE (%DAMAGE, %KILLS).", + "vcmi.battleWindow.damageEstimation.ranged" : "Vystřelit na %CREATURE (%SHOTS, %DAMAGE).", + "vcmi.battleWindow.damageEstimation.rangedKills" : "Vystřelit na %CREATURE (%SHOTS, %DAMAGE, %KILLS).", + "vcmi.battleWindow.damageEstimation.shots" : "%d střel zbývá", + "vcmi.battleWindow.damageEstimation.shots.1" : "%d střela zbývá", + "vcmi.battleWindow.damageEstimation.damage" : "%d poškození", + "vcmi.battleWindow.damageEstimation.damage.1" : "%d poškození", + "vcmi.battleWindow.damageEstimation.kills" : "%d zahyne", + "vcmi.battleWindow.damageEstimation.kills.1" : "%d zahyne", + + "vcmi.battleWindow.damageRetaliation.will" : "Provede odvetu ", + "vcmi.battleWindow.damageRetaliation.may" : "Může provést odvetu", + "vcmi.battleWindow.damageRetaliation.never" : "Neprovede odvetu.", + "vcmi.battleWindow.damageRetaliation.damage" : "(%DAMAGE).", + "vcmi.battleWindow.damageRetaliation.damageKills" : "(%DAMAGE, %KILLS).", + + "vcmi.battleWindow.killed" : "Zabito", + "vcmi.battleWindow.accurateShot.resultDescription.0" : "%d %s bylo zabito přesnými zásahy!", + "vcmi.battleWindow.accurateShot.resultDescription.1" : "%d %s byl zabit přesným zásahem!", + "vcmi.battleWindow.accurateShot.resultDescription.2" : "%d %s bylo zabito přesnými zásahy!", + "vcmi.battleWindow.endWithAutocombat" : "Opravdu chcete dokončit bitvu automatickým bojem?", + + "vcmi.battleResultsWindow.applyResultsLabel" : "Použít výsledek bitvy", + + "vcmi.tutorialWindow.title" : "Úvod ovládání dotykem", + "vcmi.tutorialWindow.decription.RightClick" : "Klepněte a držte prvek, na který byste chtěli použít pravé tlačítko myši. Klepněte na volnou oblast pro zavření.", + "vcmi.tutorialWindow.decription.MapPanning" : "Klepněte a držte jedním prstem pro posouvání mapy.", + "vcmi.tutorialWindow.decription.MapZooming" : "Přibližte dva prsty k sobě pro přiblížení mapy.", + "vcmi.tutorialWindow.decription.RadialWheel" : "Přejetí otevře kruhovou nabídku pro různé akce, třeba správa hrdiny/jednotek a příkazy měst.", + "vcmi.tutorialWindow.decription.BattleDirection" : "Pro útok ze speifického úhlu, přejeďte směrem, ze kterého má být útok vykonán.", + "vcmi.tutorialWindow.decription.BattleDirectionAbort" : "Gesto útoku pod úhlem může být zrušeno, pokud, pokud je prst dostatečně daleko.", + "vcmi.tutorialWindow.decription.AbortSpell" : "Klepněte a držte pro zrušení kouzla.", + + "vcmi.otherOptions.availableCreaturesAsDwellingLabel.hover" : "Zobrazit dostupné jednotky", + "vcmi.otherOptions.availableCreaturesAsDwellingLabel.help" : "{Zobrazit dostupné jednotky}\n\nZobrazit počet jednotek dostupných ke koupení místo jejich týdenního přírůstku v přehledu města. (levý spodní okraj obrazovky města).", + "vcmi.otherOptions.creatureGrowthAsDwellingLabel.hover" : "Zobrazit týdenní přírůstek jednotek", + "vcmi.otherOptions.creatureGrowthAsDwellingLabel.help" : "{Zobrazit týdenní přírůstek jednotek}\n\nZobrazit týdenní přírůstek jednotek místo dostupného počtu ke koupení v přehledu města (levý spodní okraj obrazovky města).", + "vcmi.otherOptions.compactTownCreatureInfo.hover" : "Kompaktní informace o jednotkách", + "vcmi.otherOptions.compactTownCreatureInfo.help" : "{Kompaktní informace o jednotkách}\n\nZobrazit menší informace o jednotkách města v jeho přehledu (levý spodní okraj obrazovky města).", + + "vcmi.townHall.missingBase" : "Nejdříve musí být postavena základní budova %s", + "vcmi.townHall.noCreaturesToRecruit" : "Nejsou k dispozici žádné jednotky k najmutí!", + + "vcmi.townStructure.bank.borrow" : "Vstupujete do banky. Bankéř vás spatří a říká: \"Máme pro vás speciální nabídku. Můžete si vzít půjčku 2500 zlata na 5 dní. Každý den budete muset splácet 500 zlata.\"", + "vcmi.townStructure.bank.payBack" : "Vstupujete do banky. Bankéř vás spatří a říká: \"Již jste si vzali půjčku. Nejprve ji splaťte, než si vezmete další.\"", + + "vcmi.logicalExpressions.anyOf" : "Nějaké z následujících:", + "vcmi.logicalExpressions.allOf" : "Všechny následující:", + "vcmi.logicalExpressions.noneOf" : "Žádné z následujících:", + + "vcmi.heroWindow.openCommander.hover" : "Otevřít okno s informacemi o veliteli", + "vcmi.heroWindow.openCommander.help" : "Zobrazí podrobnosti o veliteli tohoto hrdiny.", + "vcmi.heroWindow.openBackpack.hover" : "Otevřít okno s artefakty", + "vcmi.heroWindow.openBackpack.help" : "Otevře okno, které umožňuje snadnější správu artefaktů v batohu.", + "vcmi.heroWindow.sortBackpackByCost.hover" : "Seřadit podle ceny", + "vcmi.heroWindow.sortBackpackByCost.help" : "Seřadí artefakty v batohu podle ceny.", + "vcmi.heroWindow.sortBackpackBySlot.hover" : "Seřadit podle slotu", + "vcmi.heroWindow.sortBackpackBySlot.help" : "Seřadí artefakty v batohu podle přiřazeného slotu.", + "vcmi.heroWindow.sortBackpackByClass.hover" : "Seřadit podle třídy", + "vcmi.heroWindow.sortBackpackByClass.help" : "Seřadí artefakty v batohu podle třídy artefaktu. Poklad, Menší, Větší, Relikvie.", + "vcmi.heroWindow.fusingArtifact.fusing" : "Máte všechny potřebné části k vytvoření %s. Chcete provést sloučení? {Při sloučení budou použity všechny části.}", + + "vcmi.tavernWindow.inviteHero" : "Pozvat hrdinu", + + "vcmi.commanderWindow.artifactMessage" : "Chcete tento artefakt vrátit hrdinovi?", + + "vcmi.creatureWindow.showBonuses.hover" : "Přepnout na zobrazení bonusů", + "vcmi.creatureWindow.showBonuses.help" : "Zobrazí všechny aktivní bonusy velitele.", + "vcmi.creatureWindow.showSkills.hover" : "Přepnout na zobrazení dovedností", + "vcmi.creatureWindow.showSkills.help" : "Zobrazí všechny naučené dovednosti velitele.", + "vcmi.creatureWindow.returnArtifact.hover" : "Vrátit artefakt", + "vcmi.creatureWindow.returnArtifact.help" : "Klikněte na toto tlačítko pro vrácení artefaktu do batohu hrdiny.", + + "vcmi.questLog.hideComplete.hover" : "Skrýt dokončené úkoly", + "vcmi.questLog.hideComplete.help" : "Skrýt všechny dokončené úkoly.", + + "vcmi.randomMapTab.widgets.randomTemplate" : "(Náhodná)", + "vcmi.randomMapTab.widgets.templateLabel" : "Šablona", + "vcmi.randomMapTab.widgets.teamAlignmentsButton" : "Nastavit...", + "vcmi.randomMapTab.widgets.teamAlignmentsLabel" : "Přiřazení týmů", + "vcmi.randomMapTab.widgets.roadTypesLabel" : "Druhy cest", + + "vcmi.optionsTab.turnOptions.hover" : "Možnosti tahu", + "vcmi.optionsTab.turnOptions.help" : "Vyberte odpočítávadlo a nastavení souběžných tahů", + + "vcmi.optionsTab.chessFieldBase.hover" : "Základní časovač", + "vcmi.optionsTab.chessFieldTurn.hover" : "Časovač tahu", + "vcmi.optionsTab.chessFieldBattle.hover" : "Časovač bitvy", + "vcmi.optionsTab.chessFieldUnit.hover" : "Časovač jednotky", + "vcmi.optionsTab.chessFieldBase.help" : "Použit při poklesnutí {Časovače bitvy} na 0. Nastaveno jednou při začátku hry. Při poklesu na nulu skončí tah. Jákákoliv trvající bitva skončí prohrou.", + "vcmi.optionsTab.chessFieldTurnAccumulate.help" : "Použit mimo bitvu nebo když {Časovač bitvy} vyprší. Resetuje se každý tah. Přebytečný čas je přidán do {Základního časovače} na konci tahu.", + "vcmi.optionsTab.chessFieldTurnDiscard.help" : "Použit mimo bitvu nebo když {Časovač bitvy} vyprší. Resetuje se každý tah. Jakýkoliv přebytečný čas je ztracen.", + "vcmi.optionsTab.chessFieldBattle.help" : "Použit v bitvách s AI nebo v pvp soubojích při vypršení {Časovače jednotky}. Resetuje se startu každé bitvy.", + "vcmi.optionsTab.chessFieldUnitAccumulate.help" : "Použit při vybírání úkonu jednotky. Přebytečný čas je přidán do {Časovače bitvy} na konci tahu jednotky.", + "vcmi.optionsTab.chessFieldUnitDiscard.help" : "Použit při vybírání úkonu jednotky. Resetuje se na začátku tahu každé jednotky. Jakýkoliv přebytečný čas je ztracen.", + + "vcmi.optionsTab.accumulate" : "Akumulovat", + + "vcmi.optionsTab.simturnsTitle" : "Souběžné tahy", + "vcmi.optionsTab.simturnsMin.hover" : "Alespoň po", + "vcmi.optionsTab.simturnsMax.hover" : "Nejvíce po", + "vcmi.optionsTab.simturnsAI.hover" : "(Experimentální) Souběžné tahy AI", + "vcmi.optionsTab.simturnsMin.help" : "Hrát souběžně po určený počet dní. Setkání mezi hráči je v této době zablokováno", + "vcmi.optionsTab.simturnsMax.help" : "Hrát souběžně po určený počet dní nebo do setkání s jiným hráčem", + "vcmi.optionsTab.simturnsAI.help" : "{Souběžné tahy AI}\nExperimentální volba. Dovoluje AI hráčům hrát souběžně s lidskými hráči, když jsou souběžné tahy povoleny.", + + "vcmi.optionsTab.turnTime.select" : "Šablona nastavení časovače", + "vcmi.optionsTab.turnTime.unlimited" : "Neomezený čas tahu", + "vcmi.optionsTab.turnTime.classic.1" : "Klasický časovač: 1 minuta", + "vcmi.optionsTab.turnTime.classic.2" : "Klasický časovač: 2 minuty", + "vcmi.optionsTab.turnTime.classic.5" : "Klasický časovač: 5 minut", + "vcmi.optionsTab.turnTime.classic.10" : "Klasický časovač: 10 minut", + "vcmi.optionsTab.turnTime.classic.20" : "Klasický časovač: 20 minut", + "vcmi.optionsTab.turnTime.classic.30" : "Klasický časovač: 30 minut", + "vcmi.optionsTab.turnTime.chess.20" : "Šachová: 20:00 + 10:00 + 02:00 + 00:00", + "vcmi.optionsTab.turnTime.chess.16" : "Šachová: 16:00 + 08:00 + 01:30 + 00:00", + "vcmi.optionsTab.turnTime.chess.8" : "Šachová: 08:00 + 04:00 + 01:00 + 00:00", + "vcmi.optionsTab.turnTime.chess.4" : "Šachová: 04:00 + 02:00 + 00:30 + 00:00", + "vcmi.optionsTab.turnTime.chess.2" : "Šachová: 02:00 + 01:00 + 00:15 + 00:00", + "vcmi.optionsTab.turnTime.chess.1" : "Šachová: 01:00 + 01:00 + 00:00 + 00:00", + + "vcmi.optionsTab.simturns.select" : "Šablona souběžných tahů", + "vcmi.optionsTab.simturns.none" : "Bez souběžných tahů", + "vcmi.optionsTab.simturns.tillContactMax" : "Souběžně: Do setkání", + "vcmi.optionsTab.simturns.tillContact1" : "Souběžně: 1 týden, přerušit při setkání", + "vcmi.optionsTab.simturns.tillContact2" : "Souběžně: 2 týdny, přerušit při setkání", + "vcmi.optionsTab.simturns.tillContact4" : "Souběžně: 1 mšsíc, přerušit při setkání", + "vcmi.optionsTab.simturns.blocked1" : "Souběžně: 1 týden, setkání zablokována", + "vcmi.optionsTab.simturns.blocked2" : "Souběžně: 2 týdny, setkání zablokována", + "vcmi.optionsTab.simturns.blocked4" : "Souběžně: 1 měsíc, setkání zablokována", + + // 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 dní", + "vcmi.optionsTab.simturns.days.1" : " %d den", + "vcmi.optionsTab.simturns.days.2" : " %d dny", + "vcmi.optionsTab.simturns.weeks.0" : " %d týdnů", + "vcmi.optionsTab.simturns.weeks.1" : " %d týden", + "vcmi.optionsTab.simturns.weeks.2" : " %d týdny", + "vcmi.optionsTab.simturns.months.0" : " %d měsíců", + "vcmi.optionsTab.simturns.months.1" : " %d měsíc", + "vcmi.optionsTab.simturns.months.2" : " %d měsíce", + + "vcmi.optionsTab.extraOptions.hover" : "Další možnosti", + "vcmi.optionsTab.extraOptions.help" : "Další herní možnosti", + + "vcmi.optionsTab.cheatAllowed.hover" : "Povolit cheaty", + "vcmi.optionsTab.unlimitedReplay.hover" : "Neomezené opakování bitvy", + "vcmi.optionsTab.cheatAllowed.help" : "{Povolit cheaty}\nPovolí zadávání cheatů během hry.", + "vcmi.optionsTab.unlimitedReplay.help" : "{Neomezené opakování bitvy}\nŽádné omezení pro opakování bitev.", + + // Custom victory conditions for H3 campaigns and HotA maps + "vcmi.map.victoryCondition.daysPassed.toOthers" : "Nepřítel zvládl přežít do této chvíle. Vítězství je jeho!", + "vcmi.map.victoryCondition.daysPassed.toSelf" : "Gratulace! Zvládli jste přežít. Vítězství je vaše!", + "vcmi.map.victoryCondition.eliminateMonsters.toOthers" : "Nepřítel porazil všechny jednotky zamořující tuto zemi a nárokuje si vítězství!", + "vcmi.map.victoryCondition.eliminateMonsters.toSelf" : "Gratulace! Porazili jste všechny nepřátele zamořující tuto zemi a můžete si nárokovat vítězství!", + "vcmi.map.victoryCondition.collectArtifacts.message" : "Získejte tři artefakty", + "vcmi.map.victoryCondition.angelicAlliance.toSelf" : "Gratulace! Všichni vaši nepřítelé byli poraženi a máte Andělskou alianci! Vítězství je vaše!", + "vcmi.map.victoryCondition.angelicAlliance.message" : "Porazte všechny nepřátele a utužte Andělskou alianci", + "vcmi.map.victoryCondition.angelicAlliancePartLost.toSelf" : "Bohužel, ztratili jste část Andělské aliance. Vše je ztraceno.", + + // few strings from WoG used by vcmi + "vcmi.stackExperience.description" : "» P o d r o b n o s t i z k u š e n o s t í o d d í l u «\n\nDruh jednotky ................... : %s\nÚroveň hodnosti ................. : %s (%i)\nBody zkušeností ............... : %i\nZkušenostních bodů do další úrovně hodnosti .. : %i\nMaximum zkušeností na bitvu ... : %i%% (%i)\nPočet jednotek v oddílu .... : %i\nMaximum nových rekrutů\n bez ztráty současné hodnosti .... : %i\nNásobič zkušeností ........... : %.2f\nNásobič vylepšení .............. : %.2f\nZkušnosti po 10. úrovně hodnosti ........ : %i\nMaximální počet nových rekrutů pro zachování\n 10. úrovně hodnosti s maximálními zkušenostmi: %i", + "vcmi.stackExperience.rank.0" : "Začátečník", + "vcmi.stackExperience.rank.1" : "Učeň", + "vcmi.stackExperience.rank.2" : "Trénovaný", + "vcmi.stackExperience.rank.3" : "Zručný", + "vcmi.stackExperience.rank.4" : "Prověřený", + "vcmi.stackExperience.rank.5" : "Veterán", + "vcmi.stackExperience.rank.6" : "Adept", + "vcmi.stackExperience.rank.7" : "Expert", + "vcmi.stackExperience.rank.8" : "Elitní", + "vcmi.stackExperience.rank.9" : "Mistr", + "vcmi.stackExperience.rank.10" : "Eso", + + // Strings for HotA Seer Hut / Quest Guards + "core.seerhut.quest.heroClass.complete.0" : "Ah, vy jste %s. Tady máte dárek. Přijmete ho?", + "core.seerhut.quest.heroClass.complete.1" : "Ah, vy jste %s. Tady máte dárek. Přijmete ho?", + "core.seerhut.quest.heroClass.complete.2" : "Ah, vy jste %s. Tady máte dárek. Přijmete ho?", + "core.seerhut.quest.heroClass.complete.3" : "Stráže si všimly, že jste %s a nabízejí vám průchod. Přijmete to?", + "core.seerhut.quest.heroClass.complete.4" : "Stráže si všimly, že jste %s a nabízejí vám průchod. Přijmete to?", + "core.seerhut.quest.heroClass.complete.5" : "Stráže si všimly, že jste %s a nabízejí vám průchod. Přijmete to?", + "core.seerhut.quest.heroClass.description.0" : "Pošlete %s do %s", + "core.seerhut.quest.heroClass.description.1" : "Pošlete %s do %s", + "core.seerhut.quest.heroClass.description.2" : "Pošlete %s do %s", + "core.seerhut.quest.heroClass.description.3" : "Pošlete %s, aby otevřel bránu", + "core.seerhut.quest.heroClass.description.4" : "Pošlete %s, aby otevřel bránu", + "core.seerhut.quest.heroClass.description.5" : "Pošlete %s, aby otevřel bránu", + "core.seerhut.quest.heroClass.hover.0" : "(hledá hrdinu třídy %s)", + "core.seerhut.quest.heroClass.hover.1" : "(hledá hrdinu třídy %s)", + "core.seerhut.quest.heroClass.hover.2" : "(hledá hrdinu třídy %s)", + "core.seerhut.quest.heroClass.hover.3" : "(hledá hrdinu třídy %s)", + "core.seerhut.quest.heroClass.hover.4" : "(hledá hrdinu třídy %s)", + "core.seerhut.quest.heroClass.hover.5" : "(hledá hrdinu třídy %s)", + "core.seerhut.quest.heroClass.receive.0" : "Mám dárek pro %s.", + "core.seerhut.quest.heroClass.receive.1" : "Mám dárek pro %s.", + "core.seerhut.quest.heroClass.receive.2" : "Mám dárek pro %s.", + "core.seerhut.quest.heroClass.receive.3" : "Stráže říkají, že průchod povolí pouze %s.", + "core.seerhut.quest.heroClass.receive.4" : "Stráže říkají, že průchod povolí pouze %s.", + "core.seerhut.quest.heroClass.receive.5" : "Stráže říkají, že průchod povolí pouze %s.", + "core.seerhut.quest.heroClass.visit.0" : "Nejste %s. Nemám pro vás nic. Zmizte!", + "core.seerhut.quest.heroClass.visit.1" : "Nejste %s. Nemám pro vás nic. Zmizte!", + "core.seerhut.quest.heroClass.visit.2" : "Nejste %s. Nemám pro vás nic. Zmizte!", + "core.seerhut.quest.heroClass.visit.3" : "Stráže zde povolí průchod pouze %s.", + "core.seerhut.quest.heroClass.visit.4" : "Stráže zde povolí průchod pouze %s.", + "core.seerhut.quest.heroClass.visit.5" : "Stráže zde povolí průchod pouze %s.", + + "core.seerhut.quest.reachDate.complete.0" : "Jsem nyní volný. Tady máte, co jsem pro vás měl. Přijmete to?", + "core.seerhut.quest.reachDate.complete.1" : "Jsem nyní volný. Tady máte, co jsem pro vás měl. Přijmete to?", + "core.seerhut.quest.reachDate.complete.2" : "Jsem nyní volný. Tady máte, co jsem pro vás měl. Přijmete to?", + "core.seerhut.quest.reachDate.complete.3" : "Nyní můžete projít. Chcete pokračovat?", + "core.seerhut.quest.reachDate.complete.4" : "Nyní můžete projít. Chcete pokračovat?", + "core.seerhut.quest.reachDate.complete.5" : "Nyní můžete projít. Chcete pokračovat?", + "core.seerhut.quest.reachDate.description.0" : "Čekejte do %s pro %s", + "core.seerhut.quest.reachDate.description.1" : "Čekejte do %s pro %s", + "core.seerhut.quest.reachDate.description.2" : "Čekejte do %s pro %s", + "core.seerhut.quest.reachDate.description.3" : "Čekejte do %s, abyste otevřeli bránu", + "core.seerhut.quest.reachDate.description.4" : "Čekejte do %s, abyste otevřeli bránu", + "core.seerhut.quest.reachDate.description.5" : "Čekejte do %s, abyste otevřeli bránu", + "core.seerhut.quest.reachDate.hover.0" : "(Vraťte se nejdříve %s)", + "core.seerhut.quest.reachDate.hover.1" : "(Vraťte se nejdříve %s)", + "core.seerhut.quest.reachDate.hover.2" : "(Vraťte se nejdříve %s)", + "core.seerhut.quest.reachDate.hover.3" : "(Vraťte se nejdříve %s)", + "core.seerhut.quest.reachDate.hover.4" : "(Vraťte se nejdříve %s)", + "core.seerhut.quest.reachDate.hover.5" : "(Vraťte se nejdříve %s)", + "core.seerhut.quest.reachDate.receive.0" : "Jsem zaneprázdněný. Vraťte se nejdříve %s", + "core.seerhut.quest.reachDate.receive.1" : "Jsem zaneprázdněný. Vraťte se nejdříve %s", + "core.seerhut.quest.reachDate.receive.2" : "Jsem zaneprázdněný. Vraťte se nejdříve %s", + "core.seerhut.quest.reachDate.receive.3" : "Zavřeno do %s.", + "core.seerhut.quest.reachDate.receive.4" : "Zavřeno do %s.", + "core.seerhut.quest.reachDate.receive.5" : "Zavřeno do %s.", + "core.seerhut.quest.reachDate.visit.0" : "Jsem zaneprázdněný. Vraťte se nejdříve %s.", + "core.seerhut.quest.reachDate.visit.1" : "Jsem zaneprázdněný. Vraťte se nejdříve %s.", + "core.seerhut.quest.reachDate.visit.2" : "Jsem zaneprázdněný. Vraťte se nejdříve %s.", + "core.seerhut.quest.reachDate.visit.3" : "Zavřeno do %s.", + "core.seerhut.quest.reachDate.visit.4" : "Zavřeno do %s.", + "core.seerhut.quest.reachDate.visit.5" : "Zavřeno do %s.", + + "mapObject.core.hillFort.object.description" : "Zde můžeš vylepšit jednotky. Vylepšení jednotek úrovně 1 až 4 je zde levnější než v jejich domovském městě.", + + "core.bonus.ADDITIONAL_ATTACK.name" : "Dvojitý útok", + "core.bonus.ADDITIONAL_ATTACK.description" : "Útočí dvakrát", + "core.bonus.ADDITIONAL_RETALIATION.name" : "Další odvetné útoky", + "core.bonus.ADDITIONAL_RETALIATION.description" : "Může odvetně zaútočit ${val} krát navíc", + "core.bonus.AIR_IMMUNITY.name" : "Odolnost vůči vzdušné magii", + "core.bonus.AIR_IMMUNITY.description" : "Imunní vůči všem kouzlům školy vzdušné magie", + "core.bonus.ATTACKS_ALL_ADJACENT.name" : "Útok na všechny kolem", + "core.bonus.ATTACKS_ALL_ADJACENT.description" : "Útočí na všechny sousední nepřátele", + "core.bonus.BLOCKS_RETALIATION.name" : "Žádná odveta", + "core.bonus.BLOCKS_RETALIATION.description" : "Nepřítel nemůže odvetně zaútočit", + "core.bonus.BLOCKS_RANGED_RETALIATION.name" : "Žádná střelecká odveta", + "core.bonus.BLOCKS_RANGED_RETALIATION.description" : "Nepřítel nemůže odvetně zaútočit střeleckým útokem", + "core.bonus.CATAPULT.name" : "Katapult", + "core.bonus.CATAPULT.description" : "Útočí na ochranné hradby", + "core.bonus.CHANGES_SPELL_COST_FOR_ALLY.name" : "Snížit cenu kouzel (${val})", + "core.bonus.CHANGES_SPELL_COST_FOR_ALLY.description" : "Snižuje náklady na kouzla pro hrdinu o ${val}", + "core.bonus.CHANGES_SPELL_COST_FOR_ENEMY.name" : "Tlumič magie (${val})", + "core.bonus.CHANGES_SPELL_COST_FOR_ENEMY.description" : "Zvyšuje náklady na kouzla nepřítele o ${val}", + "core.bonus.CHARGE_IMMUNITY.name" : "Odolnost vůči Nájezdu", + "core.bonus.CHARGE_IMMUNITY.description" : "Imunní vůči Nájezdu Jezdců a Šampionů", + "core.bonus.DARKNESS.name" : "Závoj temnoty", + "core.bonus.DARKNESS.description" : "Vytváří závoj temnoty s poloměrem ${val}", + "core.bonus.DEATH_STARE.name" : "Smrtící pohled (${val}%)", + "core.bonus.DEATH_STARE.description" : "Má ${val}% šanci zabít jednu jednotku", + "core.bonus.DEFENSIVE_STANCE.name" : "Obranný bonus", + "core.bonus.DEFENSIVE_STANCE.description" : "+${val} k obraně při bránění", + "core.bonus.DESTRUCTION.name" : "Zničení", + "core.bonus.DESTRUCTION.description" : "Má ${val}% šanci zabít další jednotky po útoku", + "core.bonus.DOUBLE_DAMAGE_CHANCE.name" : "Smrtelný úder", + "core.bonus.DOUBLE_DAMAGE_CHANCE.description" : "Má ${val}% šanci způsobit dvojnásobné základní poškození při útoku", + "core.bonus.DRAGON_NATURE.name" : "Dračí povaha", + "core.bonus.DRAGON_NATURE.description" : "Jednotka má Dračí povahu", + "core.bonus.EARTH_IMMUNITY.name" : "Odolnost vůči zemské magii", + "core.bonus.EARTH_IMMUNITY.description" : "Imunní vůči všem kouzlům školy zemské magie", + "core.bonus.ENCHANTER.name" : "Zaklínač", + "core.bonus.ENCHANTER.description" : "Může každé kolo sesílat masové kouzlo ${subtype.spell}", + "core.bonus.ENCHANTED.name" : "Očarovaný", + "core.bonus.ENCHANTED.description" : "Je pod trvalým účinkem kouzla ${subtype.spell}", + "core.bonus.ENEMY_ATTACK_REDUCTION.name" : "Ignorování útoku (${val}%)", + "core.bonus.ENEMY_ATTACK_REDUCTION.description" : "Při útoku je ignorováno ${val}% útočníkovy síly", + "core.bonus.ENEMY_DEFENCE_REDUCTION.name" : "Ignorování obrany (${val}%)", + "core.bonus.ENEMY_DEFENCE_REDUCTION.description" : "Pří útoku nebude bráno v potaz ${val}% bodů obrany obránce", + "core.bonus.FIRE_IMMUNITY.name" : "Odolnost vůči ohnivé magii", + "core.bonus.FIRE_IMMUNITY.description" : "Imunní vůči všem kouzlům školy ohnivé magie", + "core.bonus.FIRE_SHIELD.name" : "Ohnivý štít (${val}%)", + "core.bonus.FIRE_SHIELD.description" : "Odrazí část zranění při útoku z blízka", + "core.bonus.FIRST_STRIKE.name" : "První úder", + "core.bonus.FIRST_STRIKE.description" : "Tato jednotka útočí dříve, než je napadena", + "core.bonus.FEAR.name" : "Strach", + "core.bonus.FEAR.description" : "Vyvolává strach u nepřátelské jednotky", + "core.bonus.FEARLESS.name" : "Nebojácnost", + "core.bonus.FEARLESS.description" : "Imunní vůči schopnosti Strach", + "core.bonus.FEROCITY.name" : "Zuřivost", + "core.bonus.FEROCITY.description" : "Útočí ${val} krát navíc, pokud někoho zabije", + "core.bonus.FLYING.name" : "Létání", + "core.bonus.FLYING.description" : "Při pohybu létá (ignoruje překážky)", + "core.bonus.FREE_SHOOTING.name" : "Střelba zblízka", + "core.bonus.FREE_SHOOTING.description" : "Může použít výstřely i při útoku zblízka", + "core.bonus.GARGOYLE.name" : "Chrlič", + "core.bonus.GARGOYLE.description" : "Nemůže být oživen ani vyléčen", + "core.bonus.GENERAL_DAMAGE_REDUCTION.name" : "Snižuje poškození (${val}%)", + "core.bonus.GENERAL_DAMAGE_REDUCTION.description" : "Snižuje poškození od útoků z dálky a blízka", + "core.bonus.HATE.name" : "Nenávidí ${subtype.creature}", + "core.bonus.HATE.description" : "Způsobuje ${val}% více poškození vůči ${subtype.creature}", + "core.bonus.HEALER.name" : "Léčitel", + "core.bonus.HEALER.description" : "Léčí spojenecké jednotky", + "core.bonus.HP_REGENERATION.name" : "Regenerace", + "core.bonus.HP_REGENERATION.description" : "Každé kolo regeneruje ${val} bodů zdraví", + "core.bonus.JOUSTING.name" : "Nájezd šampionů", + "core.bonus.JOUSTING.description" : "+${val}% poškození za každé projité pole", + "core.bonus.KING.name" : "Král", + "core.bonus.KING.description" : "Zranitelný proti zabijákovi úrovně ${val} a vyšší", + "core.bonus.LEVEL_SPELL_IMMUNITY.name" : "Odolnost kouzel 1-${val}", + "core.bonus.LEVEL_SPELL_IMMUNITY.description" : "Odolnost vůči kouzlům úrovní 1-${val}", + "core.bonus.LIMITED_SHOOTING_RANGE.name" : "Omezený dostřel", + "core.bonus.LIMITED_SHOOTING_RANGE.description" : "Není schopen zasáhnout jednotky vzdálenější než ${val} polí", + "core.bonus.LIFE_DRAIN.name" : "Vysávání života (${val}%)", + "core.bonus.LIFE_DRAIN.description" : "Vysává ${val}% způsobeného poškození", + "core.bonus.MANA_CHANNELING.name" : "Kanál magie ${val}%", + "core.bonus.MANA_CHANNELING.description" : "Poskytuje vašemu hrdinovi ${val}% many použité nepřítelem", + "core.bonus.MANA_DRAIN.name" : "Vysávání many", + "core.bonus.MANA_DRAIN.description" : "Vysává ${val} many každý tah", + "core.bonus.MAGIC_MIRROR.name" : "Magické zrcadlo (${val}%)", + "core.bonus.MAGIC_MIRROR.description" : "Má ${val}% šanci odrazit útočné kouzlo na nepřátelskou jednotku", + "core.bonus.MAGIC_RESISTANCE.name" : "Magická odolnost (${val}%)", + "core.bonus.MAGIC_RESISTANCE.description" : "Má ${val}% šanci odolat nepřátelskému kouzlu", + "core.bonus.MIND_IMMUNITY.name" : "Imunita vůči kouzlům mysli", + "core.bonus.MIND_IMMUNITY.description" : "Imunní vůči kouzlům mysli", + "core.bonus.NO_DISTANCE_PENALTY.name" : "Žádná penalizace vzdálenosti", + "core.bonus.NO_DISTANCE_PENALTY.description" : "Způsobuje plné poškození na jakoukoliv vzdálenost", + "core.bonus.NO_MELEE_PENALTY.name" : "Bez penalizace útoku zblízka", + "core.bonus.NO_MELEE_PENALTY.description" : "Jednotka není penalizována za útok zblízka", + "core.bonus.NO_MORALE.name" : "Neutrální morálka", + "core.bonus.NO_MORALE.description" : "Jednotka je imunní vůči efektům morálky", + "core.bonus.NO_WALL_PENALTY.name" : "Bez penalizace hradbami", + "core.bonus.NO_WALL_PENALTY.description" : "Plné poškození během obléhání", + "core.bonus.NON_LIVING.name" : "Neživý", + "core.bonus.NON_LIVING.description" : "Imunní vůči mnohým efektům", + "core.bonus.RANDOM_SPELLCASTER.name" : "Náhodný kouzelník", + "core.bonus.RANDOM_SPELLCASTER.description" : "Může seslat náhodné kouzlo", + "core.bonus.RANGED_RETALIATION.name" : "Střelecká odveta", + "core.bonus.RANGED_RETALIATION.description" : "Může provést protiútok na dálku", + "core.bonus.RECEPTIVE.name" : "Vnímavý", + "core.bonus.RECEPTIVE.description" : "Nemá imunitu na přátelská kouzla", + "core.bonus.REBIRTH.name" : "Znovuzrození (${val}%)", + "core.bonus.REBIRTH.description" : "${val}% jednotek povstane po smrti", + "core.bonus.RETURN_AFTER_STRIKE.name" : "Útok a návrat", + "core.bonus.RETURN_AFTER_STRIKE.description" : "Navrátí se po útoku na zblízka", + "core.bonus.REVENGE.name" : "Pomsta", + "core.bonus.REVENGE.description" : "Způsobuje extra poškození na základě ztrát útočníka v bitvě", + "core.bonus.SHOOTER.name" : "Střelec", + "core.bonus.SHOOTER.description" : "Jednotka může střílet", + "core.bonus.SHOOTS_ALL_ADJACENT.name" : "Střílí všude kolem", + "core.bonus.SHOOTS_ALL_ADJACENT.description" : "Střelecký útok této jednotky zasáhne všechny cíle v malé oblasti", + "core.bonus.SOUL_STEAL.name" : "Zloděj duší", + "core.bonus.SOUL_STEAL.description" : "Získává ${val} nové jednotky za každého zabitého nepřítele", + "core.bonus.SPELLCASTER.name" : "Kouzelník", + "core.bonus.SPELLCASTER.description" : "Může seslat kouzlo ${subtype.spell}", + "core.bonus.SPELL_AFTER_ATTACK.name" : "Sesílá po útoku", + "core.bonus.SPELL_AFTER_ATTACK.description" : "Má ${val}% šanci seslat ${subtype.spell} po útoku", + "core.bonus.SPELL_BEFORE_ATTACK.name" : "Sesílá před útokem", + "core.bonus.SPELL_BEFORE_ATTACK.description" : "Má ${val}% šanci seslat ${subtype.spell} před útokem", + "core.bonus.SPELL_DAMAGE_REDUCTION.name" : "Magická odolnost", + "core.bonus.SPELL_DAMAGE_REDUCTION.description" : "Poškození kouzly sníženo o ${val}%.", + "core.bonus.SPELL_IMMUNITY.name" : "Imunita vůči kouzlům", + "core.bonus.SPELL_IMMUNITY.description" : "Imunní vůči ${subtype.spell}", + "core.bonus.SPELL_LIKE_ATTACK.name" : "Útok kouzlem", + "core.bonus.SPELL_LIKE_ATTACK.description" : "Útočí kouzlem ${subtype.spell}", + "core.bonus.SPELL_RESISTANCE_AURA.name" : "Aura odporu", + "core.bonus.SPELL_RESISTANCE_AURA.description" : "Jednotky poblíž získají ${val}% magickou odolnost", + "core.bonus.SUMMON_GUARDIANS.name" : "Přivolání ochránců", + "core.bonus.SUMMON_GUARDIANS.description" : "Na začátku bitvy přivolá ${subtype.creature} (${val}%)", + "core.bonus.SYNERGY_TARGET.name" : "Synergizovatelný", + "core.bonus.SYNERGY_TARGET.description" : "Tato jednotka je náchylná k synergickým efektům", + "core.bonus.TWO_HEX_ATTACK_BREATH.name" : "Dech", + "core.bonus.TWO_HEX_ATTACK_BREATH.description" : "Útok dechem (dosah 2 polí)", + "core.bonus.THREE_HEADED_ATTACK.name" : "Tříhlavý útok", + "core.bonus.THREE_HEADED_ATTACK.description" : "Útočí na tři sousední jednotky", + "core.bonus.TRANSMUTATION.name" : "Transmutace", + "core.bonus.TRANSMUTATION.description" : "${val}% šance na přeměnu napadené jednotky na jiný typ", + "core.bonus.UNDEAD.name" : "Nemrtvý", + "core.bonus.UNDEAD.description" : "Jednotka je nemrtvá", + "core.bonus.UNLIMITED_RETALIATIONS.name" : "Neomezené odvetné útoky", + "core.bonus.UNLIMITED_RETALIATIONS.description" : "Může provést neomezený počet odvetných útoků", + "core.bonus.WATER_IMMUNITY.name" : "Odolnost vůči vodní magii", + "core.bonus.WATER_IMMUNITY.description" : "Imunní vůči všem kouzlům školy vodní magie", + "core.bonus.WIDE_BREATH.name" : "Široký dech", + "core.bonus.WIDE_BREATH.description" : "Široký útok dechem (více polí)", + "core.bonus.DISINTEGRATE.name" : "Rozpad", + "core.bonus.DISINTEGRATE.description" : "Po smrti nezůstane žádné tělo", + "core.bonus.INVINCIBLE.name" : "Neporazitelný", + "core.bonus.INVINCIBLE.description" : "Nelze ovlivnit žádným efektem", + "core.bonus.MECHANICAL.description" : "Imunita vůči mnoha efektům, opravitelné", + "core.bonus.MECHANICAL.name" : "Mechanický", + "core.bonus.PRISM_HEX_ATTACK_BREATH.name" : "Trojitý dech", + "core.bonus.PRISM_HEX_ATTACK_BREATH.description" : "Útok trojitým dechem (útok přes 3 směry)", + + "spell.core.castleMoat.name" : "Hradní příkop", + "spell.core.castleMoatTrigger.name" : "Hradní příkop", + "spell.core.catapultShot.name" : "Výstřel z katapultu", + "spell.core.cyclopsShot.name" : "Obléhací střela", + "spell.core.dungeonMoat.name" : "Vařící olej", + "spell.core.dungeonMoatTrigger.name" : "Vařící olej", + "spell.core.fireWallTrigger.name" : "Ohnivá zeď", + "spell.core.firstAid.name" : "První pomoc", + "spell.core.fortressMoat.name" : "Vařící dehet", + "spell.core.fortressMoatTrigger.name" : "Vařící dehet", + "spell.core.infernoMoat.name" : "Láva", + "spell.core.infernoMoatTrigger.name" : "Láva", + "spell.core.landMineTrigger.name" : "Pozemní mina", + "spell.core.necropolisMoat.name" : "Hřbitov", + "spell.core.necropolisMoatTrigger.name" : "Hřbitov", + "spell.core.rampartMoat.name" : "Ostružiní", + "spell.core.rampartMoatTrigger.name" : "Ostružiní", + "spell.core.strongholdMoat.name" : "Dřevěné bodce", + "spell.core.strongholdMoatTrigger.name" : "Dřevěné bodce", + "spell.core.summonDemons.name" : "Přivolání démonů", + "spell.core.towerMoat.name" : "Pozemní mina" +} \ No newline at end of file diff --git a/Mods/vcmi/config/vcmi/english.json b/Mods/vcmi/Content/config/english.json similarity index 92% rename from Mods/vcmi/config/vcmi/english.json rename to Mods/vcmi/Content/config/english.json index b62ee25d2..4224afae0 100644 --- a/Mods/vcmi/config/vcmi/english.json +++ b/Mods/vcmi/Content/config/english.json @@ -12,7 +12,11 @@ "vcmi.adventureMap.monsterThreat.levels.9" : "Overpowering", "vcmi.adventureMap.monsterThreat.levels.10" : "Deadly", "vcmi.adventureMap.monsterThreat.levels.11" : "Impossible", - "vcmi.adventureMap.monsterLevel" : "\n\nLevel %LEVEL %TOWN unit", + "vcmi.adventureMap.monsterLevel" : "\n\nLevel %LEVEL %TOWN %ATTACK_TYPE unit", + "vcmi.adventureMap.monsterMeleeType" : "melee", + "vcmi.adventureMap.monsterRangedType" : "ranged", + "vcmi.adventureMap.search.hover" : "Search map object", + "vcmi.adventureMap.search.help" : "Select object to search on map.", "vcmi.adventureMap.confirmRestartGame" : "Are you sure you want to restart the game?", "vcmi.adventureMap.noTownWithMarket" : "There are no available marketplaces!", @@ -24,6 +28,13 @@ "vcmi.adventureMap.movementPointsHeroInfo" : "(Movement points: %REMAINING / %POINTS)", "vcmi.adventureMap.replayOpponentTurnNotImplemented" : "Sorry, replay opponent turn is not implemented yet!", + "vcmi.bonusSource.artifact" : "Artifact", + "vcmi.bonusSource.creature" : "Ability", + "vcmi.bonusSource.spell" : "Spell", + "vcmi.bonusSource.hero" : "Hero", + "vcmi.bonusSource.commander" : "Commander", + "vcmi.bonusSource.other" : "Other", + "vcmi.capitalColors.0" : "Red", "vcmi.capitalColors.1" : "Blue", "vcmi.capitalColors.2" : "Tan", @@ -38,6 +49,12 @@ "vcmi.heroOverview.secondarySkills" : "Secondary Skills", "vcmi.heroOverview.spells" : "Spells", + "vcmi.quickExchange.moveUnit" : "Move Unit", + "vcmi.quickExchange.moveAllUnits" : "Move All Units", + "vcmi.quickExchange.swapAllUnits" : "Swap Armies", + "vcmi.quickExchange.moveAllArtifacts" : "Move All Artifacts", + "vcmi.quickExchange.swapAllArtifacts" : "Swap Artifact", + "vcmi.radialWheel.mergeSameUnit" : "Merge same creatures", "vcmi.radialWheel.fillSingleUnit" : "Fill with single creatures", "vcmi.radialWheel.splitSingleUnit" : "Split off single creature", @@ -56,9 +73,27 @@ "vcmi.radialWheel.moveUp" : "Move up", "vcmi.radialWheel.moveDown" : "Move down", "vcmi.radialWheel.moveBottom" : "Move to bottom", + + "vcmi.randomMap.description" : "Map created by the Random Map Generator.\nTemplate was %s, size %dx%d, levels %d, players %d, computers %d, water %s, monster %s, VCMI map", + "vcmi.randomMap.description.isHuman" : ", %s is human", + "vcmi.randomMap.description.townChoice" : ", %s town choice is %s", + "vcmi.randomMap.description.water.none" : "none", + "vcmi.randomMap.description.water.normal" : "normal", + "vcmi.randomMap.description.water.islands" : "islands", + "vcmi.randomMap.description.monster.weak" : "weak", + "vcmi.randomMap.description.monster.normal" : "normal", + "vcmi.randomMap.description.monster.strong" : "strong", "vcmi.spellBook.search" : "search...", + "vcmi.spellResearch.canNotAfford" : "You can't afford to replace {%SPELL1} with {%SPELL2}. But you can still discard this spell and continue spell research.", + "vcmi.spellResearch.comeAgain" : "Research has already been done today. Come back tomorrow.", + "vcmi.spellResearch.pay" : "Would you like to replace {%SPELL1} with {%SPELL2}? Or discard this spell and continue spell research?", + "vcmi.spellResearch.research" : "Research this Spell", + "vcmi.spellResearch.skip" : "Skip this Spell", + "vcmi.spellResearch.abort" : "Abort", + "vcmi.spellResearch.noMoreSpells" : "There are no more spells available for research.", + "vcmi.mainMenu.serverConnecting" : "Connecting...", "vcmi.mainMenu.serverAddressEnter" : "Enter address:", "vcmi.mainMenu.serverConnectionFailed" : "Failed to connect", @@ -79,6 +114,12 @@ "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.lobby.login.title" : "VCMI Online Lobby", "vcmi.lobby.login.username" : "Username:", @@ -143,11 +184,10 @@ "vcmi.client.errors.invalidMap" : "{Invalid map or campaign}\n\nFailed to start game! Selected map or campaign might be invalid or corrupted. Reason:\n%s", "vcmi.client.errors.missingCampaigns" : "{Missing data files}\n\nCampaigns data files were not found! You may be using incomplete or corrupted Heroes 3 data files. Please reinstall game data.", "vcmi.server.errors.disconnected" : "{Network Error}\n\nConnection to game server has been lost!", + "vcmi.server.errors.playerLeft" : "{Player Left}\n\n%s player have disconnected from the game!", //%s -> player color "vcmi.server.errors.existingProcess" : "Another VCMI server process is running. Please terminate it before starting a new game.", "vcmi.server.errors.modsToEnable" : "{Following mods are required}", "vcmi.server.errors.modsToDisable" : "{Following mods must be disabled}", - "vcmi.server.errors.modNoDependency" : "Failed to load mod {'%s'}!\n It depends on mod {'%s'} which is not active!\n", - "vcmi.server.errors.modConflict" : "Failed to load mod {'%s'}!\n Conflicts with active mod {'%s'}!\n", "vcmi.server.errors.unknownEntity" : "Failed to load save! Unknown entity '%s' found in saved game! Save may not be compatible with currently installed version of mods!", "vcmi.dimensionDoor.seaToLandError" : "It's not possible to teleport from sea to land or vice versa with a Dimension Door.", @@ -340,6 +380,13 @@ "vcmi.heroWindow.openCommander.help" : "Shows details about the commander of this hero.", "vcmi.heroWindow.openBackpack.hover" : "Open artifact backpack window", "vcmi.heroWindow.openBackpack.help" : "Opens window that allows easier artifact backpack management.", + "vcmi.heroWindow.sortBackpackByCost.hover" : "Sort by cost", + "vcmi.heroWindow.sortBackpackByCost.help" : "Sort artifacts in backpack by cost.", + "vcmi.heroWindow.sortBackpackBySlot.hover" : "Sort by slot", + "vcmi.heroWindow.sortBackpackBySlot.help" : "Sort artifacts in backpack by equipped slot.", + "vcmi.heroWindow.sortBackpackByClass.hover" : "Sort by class", + "vcmi.heroWindow.sortBackpackByClass.help" : "Sort artifacts in backpack by artifact class. Treasure, Minor, Major, Relic", + "vcmi.heroWindow.fusingArtifact.fusing" : "You possess all of the components needed for the fusion of the %s. Do you wish to perform the fusion? {All components will be consumed upon fusion.}", "vcmi.tavernWindow.inviteHero" : "Invite hero", @@ -516,7 +563,9 @@ "core.seerhut.quest.reachDate.visit.3" : "Closed till %s.", "core.seerhut.quest.reachDate.visit.4" : "Closed till %s.", "core.seerhut.quest.reachDate.visit.5" : "Closed till %s.", - + + "mapObject.core.hillFort.object.description" : "Upgrades creatures. Levels 1 - 4 are less expensive than in associated town.", + "core.bonus.ADDITIONAL_ATTACK.name": "Double Strike", "core.bonus.ADDITIONAL_ATTACK.description": "Attacks twice", "core.bonus.ADDITIONAL_RETALIATION.name": "Additional retaliations", @@ -668,5 +717,9 @@ "core.bonus.DISINTEGRATE.name": "Disintegrate", "core.bonus.DISINTEGRATE.description": "No corpse remains after death", "core.bonus.INVINCIBLE.name": "Invincible", - "core.bonus.INVINCIBLE.description": "Cannot be affected by anything" + "core.bonus.INVINCIBLE.description": "Cannot be affected by anything", + "core.bonus.MECHANICAL.name": "Mechanical", + "core.bonus.MECHANICAL.description": "Immunity to many effects, repairable", + "core.bonus.PRISM_HEX_ATTACK_BREATH.name": "Prism Breath", + "core.bonus.PRISM_HEX_ATTACK_BREATH.description": "Prism Breath Attack (three directions)" } diff --git a/Mods/vcmi/config/vcmi/french.json b/Mods/vcmi/Content/config/french.json similarity index 100% rename from Mods/vcmi/config/vcmi/french.json rename to Mods/vcmi/Content/config/french.json diff --git a/Mods/vcmi/config/vcmi/german.json b/Mods/vcmi/Content/config/german.json similarity index 90% rename from Mods/vcmi/config/vcmi/german.json rename to Mods/vcmi/Content/config/german.json index efe786220..ff12badc0 100644 --- a/Mods/vcmi/config/vcmi/german.json +++ b/Mods/vcmi/Content/config/german.json @@ -12,7 +12,11 @@ "vcmi.adventureMap.monsterThreat.levels.9" : "Überwältigend", "vcmi.adventureMap.monsterThreat.levels.10" : "Tödlich", "vcmi.adventureMap.monsterThreat.levels.11" : "Unmöglich", - "vcmi.adventureMap.monsterLevel" : "\n\nStufe %LEVEL %TOWN-Einheit", + "vcmi.adventureMap.monsterLevel" : "\n\nStufe %LEVEL %TOWN-Einheit (%ATTACK_TYPE)", + "vcmi.adventureMap.monsterMeleeType" : "Nahkampf", + "vcmi.adventureMap.monsterRangedType" : "Fernkampf", + "vcmi.adventureMap.search.hover" : "Suche Kartenobjekt", + "vcmi.adventureMap.search.help" : "Wähle Objekt das gesucht werden soll.", "vcmi.adventureMap.confirmRestartGame" : "Seid Ihr sicher, dass Ihr das Spiel neu starten wollt?", "vcmi.adventureMap.noTownWithMarket" : "Kein Marktplatz verfügbar!", @@ -21,8 +25,16 @@ "vcmi.adventureMap.playerAttacked" : "Spieler wurde attackiert: %s", "vcmi.adventureMap.moveCostDetails" : "Bewegungspunkte - Kosten: %TURNS Runden + %POINTS Punkte, Verbleibende Punkte: %REMAINING", "vcmi.adventureMap.moveCostDetailsNoTurns" : "Bewegungspunkte - Kosten: %POINTS Punkte, Verbleibende Punkte: %REMAINING", + "vcmi.adventureMap.movementPointsHeroInfo" : "(Bewegungspunkte: %REMAINING / %POINTS)", "vcmi.adventureMap.replayOpponentTurnNotImplemented" : "Das Wiederholen des gegnerischen Zuges ist aktuell noch nicht implementiert!", + "vcmi.bonusSource.artifact" : "Artefakt", + "vcmi.bonusSource.creature" : "Fähigkeit", + "vcmi.bonusSource.spell" : "Zauber", + "vcmi.bonusSource.hero" : "Held", + "vcmi.bonusSource.commander" : "Commander", + "vcmi.bonusSource.other" : "Anderes", + "vcmi.capitalColors.0" : "Rot", "vcmi.capitalColors.1" : "Blau", "vcmi.capitalColors.2" : "Braun", @@ -37,6 +49,12 @@ "vcmi.heroOverview.secondarySkills" : "Sekundäre Skills", "vcmi.heroOverview.spells" : "Zaubersprüche", + "vcmi.quickExchange.moveUnit" : "Einheit bewegen", + "vcmi.quickExchange.moveAllUnits" : "Alle Einheiten bewegen", + "vcmi.quickExchange.swapAllUnits" : "Einheiten tauschen", + "vcmi.quickExchange.moveAllArtifacts" : "Alle Artefakte bewegen", + "vcmi.quickExchange.swapAllArtifacts" : "Artefakte tauschen", + "vcmi.radialWheel.mergeSameUnit" : "Gleiche Kreaturen zusammenführen", "vcmi.radialWheel.fillSingleUnit" : "Füllen mit einzelnen Kreaturen", "vcmi.radialWheel.splitSingleUnit" : "Wegtrennen einzelner Kreaturen", @@ -55,9 +73,27 @@ "vcmi.radialWheel.moveUp" : "Nach oben bewegen", "vcmi.radialWheel.moveDown" : "Nach unten bewegen", "vcmi.radialWheel.moveBottom" : "Ganz nach unten bewegen", + + "vcmi.randomMap.description" : "Die Karte wurde mit dem Zufallsgenerator erstellt.\nTemplate war %s, Größe %dx%d, Level %d, Spieler %d, Computer %d, Wasser %s, Monster %s, VCMI-Karte", + "vcmi.randomMap.description.isHuman" : ", %s ist Mensch", + "vcmi.randomMap.description.townChoice" : ", %s Stadt-Wahl ist %s", + "vcmi.randomMap.description.water.none" : "Kein", + "vcmi.randomMap.description.water.normal" : "Normal", + "vcmi.randomMap.description.water.islands" : "Inseln", + "vcmi.randomMap.description.monster.weak" : "Schwach", + "vcmi.randomMap.description.monster.normal" : "Normal", + "vcmi.randomMap.description.monster.strong" : "Stark", "vcmi.spellBook.search" : "suchen...", + "vcmi.spellResearch.canNotAfford" : "Ihr könnt es Euch nicht leisten, {%SPELL1} durch {%SPELL2} zu ersetzen. Aber Ihr könnt diesen Zauberspruch trotzdem verwerfen und die Zauberspruchforschung fortsetzen.", + "vcmi.spellResearch.comeAgain" : "Die Forschung wurde heute bereits abgeschlossen. Kommt morgen wieder.", + "vcmi.spellResearch.pay" : "Möchtet Ihr {%SPELL1} durch {%SPELL2} ersetzen? Oder diesen Zauberspruch verwerfen und die Zauberspruchforschung fortsetzen?", + "vcmi.spellResearch.research" : "Erforsche diesen Zauberspruch", + "vcmi.spellResearch.skip" : "Überspringe diesen Zauberspruch", + "vcmi.spellResearch.abort" : "Abbruch", + "vcmi.spellResearch.noMoreSpells" : "Es sind keine weiteren Zaubersprüche für die Forschung verfügbar.", + "vcmi.mainMenu.serverConnecting" : "Verbinde...", "vcmi.mainMenu.serverAddressEnter" : "Addresse eingeben:", "vcmi.mainMenu.serverConnectionFailed" : "Verbindung fehlgeschlagen", @@ -78,7 +114,13 @@ "vcmi.lobby.handicap.resource" : "Gibt den Spielern entsprechende Ressourcen zum Start zusätzlich zu den normalen Startressourcen. Negative Werte sind erlaubt, werden aber insgesamt auf 0 begrenzt (der Spieler beginnt nie mit negativen Ressourcen).", "vcmi.lobby.handicap.income" : "Verändert die verschiedenen Einkommen des Spielers um den Prozentsatz. Wird aufgerundet.", "vcmi.lobby.handicap.growth" : "Verändert die Wachstumsrate der Kreaturen in den Städten, die der Spieler besitzt. Wird aufgerundet.", - + "vcmi.lobby.deleteUnsupportedSave" : "{Nicht unterstützte Spielstände gefunden}\n\nVCMI hat %d gespeicherte Spiele gefunden, die nicht mehr unterstützt werden, möglicherweise aufgrund von Unterschieden in VCMI-Versionen.\n\nMöchtet Ihr sie löschen?", + "vcmi.lobby.deleteSaveGameTitle" : "Wählt gespeichertes Spiel zum Löschen aus", + "vcmi.lobby.deleteMapTitle" : "Wählt ein zu löschendes Szenario", + "vcmi.lobby.deleteFile" : "Möchtet Ihr folgende Datei löschen?", + "vcmi.lobby.deleteFolder" : "Möchtet Ihr folgenden Ordner löschen?", + "vcmi.lobby.deleteMode" : "In den Löschmodus wechseln und zurück", + "vcmi.lobby.login.title" : "VCMI Online Lobby", "vcmi.lobby.login.username" : "Benutzername:", "vcmi.lobby.login.connecting" : "Verbinde...", @@ -142,11 +184,11 @@ "vcmi.client.errors.invalidMap" : "{Ungültige Karte oder Kampagne}\n\nDas Spiel konnte nicht gestartet werden! Die ausgewählte Karte oder Kampagne ist möglicherweise ungültig oder beschädigt. Grund:\n%s", "vcmi.client.errors.missingCampaigns" : "{Fehlende Dateien}\n\nEs wurden keine Kampagnendateien gefunden! Möglicherweise verwendest du unvollständige oder beschädigte Heroes 3 Datendateien. Bitte installiere die Spieldaten neu.", "vcmi.server.errors.disconnected" : "{Netzwerkfehler}\n\nDie Verbindung zum Spielserver wurde unterbrochen!", + "vcmi.server.errors.playerLeft" : "{Verlassen eines Spielers}\n\n%s Spieler hat die Verbindung zum Spiel unterbrochen!", //%s -> player color "vcmi.server.errors.existingProcess" : "Es läuft ein weiterer vcmiserver-Prozess, bitte beendet diesen zuerst", "vcmi.server.errors.modsToEnable" : "{Erforderliche Mods um das Spiel zu laden}", "vcmi.server.errors.modsToDisable" : "{Folgende Mods müssen deaktiviert werden}", - "vcmi.server.errors.modNoDependency" : "Mod {'%s'} konnte nicht geladen werden!\n Sie hängt von Mod {'%s'} ab, die nicht aktiv ist!\n", - "vcmi.server.errors.modConflict" : "Mod {'%s'} konnte nicht geladen werden!\n Konflikte mit aktiver Mod {'%s'}!\n", + "vcmi.server.errors.modDependencyLoop" : "Mod {'%s'} konnte nicht geladen werden.!\n Möglicherweise befindet sie sich in einer (weichen) Abhängigkeitsschleife.", "vcmi.server.errors.unknownEntity" : "Spielstand konnte nicht geladen werden! Unbekannte Entität '%s' im gespeicherten Spiel gefunden! Der Spielstand ist möglicherweise nicht mit der aktuell installierten Version der Mods kompatibel!", "vcmi.dimensionDoor.seaToLandError" : "Es ist nicht möglich, mit einer Dimensionstür vom Meer zum Land oder umgekehrt zu teleportieren.", @@ -328,6 +370,9 @@ "vcmi.townHall.missingBase" : "Basis Gebäude %s muss als erstes gebaut werden", "vcmi.townHall.noCreaturesToRecruit" : "Es gibt keine Kreaturen zu rekrutieren!", + "vcmi.townStructure.bank.borrow" : "Ihr betretet die Bank. Ein Bankangestellter sieht Euch und sagt: \"Wir haben ein spezielles Angebot für Euch gemacht. Ihr könnt bei uns einen Kredit von 2500 Gold für 5 Tage aufnehmen. Ihr werdet jeden Tag 500 Gold zurückzahlen müssen.\"", + "vcmi.townStructure.bank.payBack" : "Ihr betretet die Bank. Ein Bankangestellter sieht Euch und sagt: \"Ihr habt Euren Kredit bereits erhalten. Zahlt Ihn ihn zurück, bevor Ihr einen neuen aufnehmt.\"", + "vcmi.logicalExpressions.anyOf" : "Eines der folgenden:", "vcmi.logicalExpressions.allOf" : "Alles der folgenden:", "vcmi.logicalExpressions.noneOf" : "Keines der folgenden:", @@ -336,6 +381,13 @@ "vcmi.heroWindow.openCommander.help" : "Zeige Informationen über Kommandanten dieses Helden", "vcmi.heroWindow.openBackpack.hover" : "Artefakt-Rucksack-Fenster öffnen", "vcmi.heroWindow.openBackpack.help" : "Öffnet ein Fenster, das die Verwaltung des Artefakt-Rucksacks erleichtert", + "vcmi.heroWindow.sortBackpackByCost.hover" : "Nach Kosten sortieren", + "vcmi.heroWindow.sortBackpackByCost.help" : "Artefakte im Rucksack nach Kosten sortieren.", + "vcmi.heroWindow.sortBackpackBySlot.hover" : "Nach Slot sortieren", + "vcmi.heroWindow.sortBackpackBySlot.help" : "Artefakte im Rucksack nach Ausrüstungsslot sortieren.", + "vcmi.heroWindow.sortBackpackByClass.hover" : "Nach Klasse sortieren", + "vcmi.heroWindow.sortBackpackByClass.help" : "Artefakte im Rucksack nach Artefaktklasse sortieren. Schatz, Klein, Groß, Relikt", + "vcmi.heroWindow.fusingArtifact.fusing" : "Ihr verfügt über alle Komponenten, die für die Fusion der %s benötigt werden. Möchtet Ihr die Verschmelzung durchführen? {Alle Komponenten werden bei der Fusion verbraucht.}", "vcmi.tavernWindow.inviteHero" : "Helden einladen", @@ -512,7 +564,9 @@ "core.seerhut.quest.reachDate.visit.3" : "Geschlossen bis %s.", "core.seerhut.quest.reachDate.visit.4" : "Geschlossen bis %s.", "core.seerhut.quest.reachDate.visit.5" : "Geschlossen bis %s.", - + + "mapObject.core.hillFort.object.description" : "Aufwertungen von Kreaturen. Die Stufen 1 - 4 sind billiger als in der zugehörigen Stadt.", + "core.bonus.ADDITIONAL_ATTACK.name": "Doppelschlag", "core.bonus.ADDITIONAL_ATTACK.description": "Greift zweimal an", "core.bonus.ADDITIONAL_RETALIATION.name": "Zusätzliche Vergeltungsmaßnahmen", @@ -660,5 +714,13 @@ "core.bonus.WATER_IMMUNITY.name": "Wasser-Immunität", "core.bonus.WATER_IMMUNITY.description": "Immun gegen alle Zauber der Wasserschule", "core.bonus.WIDE_BREATH.name": "Breiter Atem", - "core.bonus.WIDE_BREATH.description": "Breiter Atem-Angriff (mehrere Felder)" + "core.bonus.WIDE_BREATH.description": "Breiter Atem-Angriff (mehrere Felder)", + "core.bonus.DISINTEGRATE.name": "Auflösen", + "core.bonus.DISINTEGRATE.description": "Kein Leichnam bleibt nach dem Tod übrig", + "core.bonus.INVINCIBLE.name": "Unbesiegbar", + "core.bonus.INVINCIBLE.description": "Kann durch nichts beeinflusst werden", + "core.bonus.MECHANICAL.name": "Mechanisch", + "core.bonus.MECHANICAL.description": "Immunität gegen viele Effekte, reparierbar", + "core.bonus.PRISM_HEX_ATTACK_BREATH.name": "Prisma-Atem", + "core.bonus.PRISM_HEX_ATTACK_BREATH.description": "Prisma-Atem-Angriff (drei Richtungen)" } diff --git a/Mods/vcmi/config/vcmi/polish.json b/Mods/vcmi/Content/config/polish.json similarity index 92% rename from Mods/vcmi/config/vcmi/polish.json rename to Mods/vcmi/Content/config/polish.json index 2f8d0ce87..4a8eba77e 100644 --- a/Mods/vcmi/config/vcmi/polish.json +++ b/Mods/vcmi/Content/config/polish.json @@ -12,6 +12,11 @@ "vcmi.adventureMap.monsterThreat.levels.9" : "Przytłaczający", "vcmi.adventureMap.monsterThreat.levels.10" : "Śmiertelny", "vcmi.adventureMap.monsterThreat.levels.11" : "Nie do pokonania", + "vcmi.adventureMap.monsterLevel" : "\n\nJednostka %ATTACK_TYPE %LEVEL poziomu z miasta %TOWN", + "vcmi.adventureMap.monsterMeleeType" : "Walcząca wręcz", + "vcmi.adventureMap.monsterRangedType" : "Dystansowa", + "vcmi.adventureMap.search.hover" : "Wyszukiwarka obiektów", + "vcmi.adventureMap.search.help" : "Wybierz obiekt który chcesz znaleźć na mapie.", "vcmi.adventureMap.confirmRestartGame" : "Czy na pewno chcesz zrestartować grę?", "vcmi.adventureMap.noTownWithMarket" : "Brak dostępnego targowiska!", @@ -23,6 +28,13 @@ "vcmi.adventureMap.movementPointsHeroInfo" : "(Punkty ruchu: %REMAINING / %POINTS)", "vcmi.adventureMap.replayOpponentTurnNotImplemented" : "Wybacz, powtórka ruchu wroga nie została jeszcze zaimplementowana!", + "vcmi.bonusSource.artifact" : "Artefakt", + "vcmi.bonusSource.creature" : "Umiej.", + "vcmi.bonusSource.spell" : "Zaklęcie", + "vcmi.bonusSource.hero" : "Bohater", + "vcmi.bonusSource.commander" : "Dowódca", + "vcmi.bonusSource.other" : "Inne", + "vcmi.capitalColors.0" : "Czerwony", "vcmi.capitalColors.1" : "Niebieski", "vcmi.capitalColors.2" : "Brązowy", @@ -37,6 +49,12 @@ "vcmi.heroOverview.secondarySkills" : "Umiejętności drugorzędne", "vcmi.heroOverview.spells" : "Zaklęcia", + "vcmi.quickExchange.moveUnit" : "Przenieś jednostkę", + "vcmi.quickExchange.moveAllUnits" : "Przenieś wszystkie jednostki", + "vcmi.quickExchange.swapAllUnits" : "Zamień armię", + "vcmi.quickExchange.moveAllArtifacts" : "Przenieś wszystkie artefakty", + "vcmi.quickExchange.swapAllArtifacts" : "Zamień artefakty", + "vcmi.radialWheel.mergeSameUnit" : "Złącz takie same stworzenia", "vcmi.radialWheel.fillSingleUnit" : "Wypełnij pojedynczymi stworzeniami", "vcmi.radialWheel.splitSingleUnit" : "Wydziel pojedyncze stworzenie", @@ -56,8 +74,26 @@ "vcmi.radialWheel.moveDown" : "Przenieś w dół", "vcmi.radialWheel.moveBottom" : "Przenieś na spód", + "vcmi.randomMap.description" : "Mapa stworzona przez generator map losowych.\nSzablon: %s, rozmiar %dx%d, poziomów %d, graczy %d, komputerowych %d, woda %s, potwory %s, mapa VCMI", + "vcmi.randomMap.description.isHuman" : ", %s jest człowiekiem", + "vcmi.randomMap.description.townChoice" : ", %s wybiera %s", + "vcmi.randomMap.description.water.none" : "brak", + "vcmi.randomMap.description.water.normal" : "norm.", + "vcmi.randomMap.description.water.islands" : "wyspy", + "vcmi.randomMap.description.monster.weak" : "słabi", + "vcmi.randomMap.description.monster.normal" : "norm.", + "vcmi.randomMap.description.monster.strong" : "silni", + "vcmi.spellBook.search" : "szukaj...", + "vcmi.spellResearch.canNotAfford" : "Nie stać Cię na zastąpienie {%SPELL1} przez {%SPELL2}, ale za to możesz odrzucić ten czar i kontynuować badania.", + "vcmi.spellResearch.comeAgain" : "Badania zostały już przeprowadzone dzisiaj. Wróć jutro.", + "vcmi.spellResearch.pay" : "Czy chcesz zastąpić {%SPELL1} zaklęciem {%SPELL2}? Czy odrzucić ten czar i kontynuować badania?", + "vcmi.spellResearch.research" : "Zamień zaklęcia", + "vcmi.spellResearch.skip" : "Kontynuuj badania", + "vcmi.spellResearch.abort" : "Anuluj", + "vcmi.spellResearch.noMoreSpells" : "Nie ma już więcej zaklęć do zbadania.", + "vcmi.mainMenu.serverConnecting" : "Łączenie...", "vcmi.mainMenu.serverAddressEnter" : "Wprowadź adres:", "vcmi.mainMenu.serverConnectionFailed" : "Połączenie nie powiodło się", @@ -142,11 +178,11 @@ "vcmi.client.errors.invalidMap" : "{Błędna mapa lub kampania}\n\nNie udało się stworzyć gry! Wybrana mapa lub kampania jest niepoprawna lub uszkodzona. Powód:\n%s", "vcmi.client.errors.missingCampaigns" : "{Brakujące pliki gry}\n\nPliki kampanii nie zostały znalezione! Możliwe że używasz niekompletnych lub uszkodzonych plików Heroes 3. Spróbuj ponownej instalacji plików gry.", "vcmi.server.errors.disconnected" : "{Błąd sieciowy}\n\nUtracono połączenie z serwerem!", + "vcmi.server.errors.playerLeft" : "{Rozłączenie z graczem}\n\n%s opuścił rozgrywkę!", //%s -> player color "vcmi.server.errors.existingProcess" : "Inny proces 'vcmiserver' został już uruchomiony, zakończ go nim przejdziesz dalej", "vcmi.server.errors.modsToEnable" : "{Następujące mody są wymagane do wczytania gry}", "vcmi.server.errors.modsToDisable" : "{Następujące mody muszą zostać wyłączone}", - "vcmi.server.errors.modNoDependency" : "Nie udało się wczytać moda {'%s'}!\n Jest on zależny od moda {'%s'} który nie jest aktywny!\n", - "vcmi.server.errors.modConflict" : "Nie udało się wczytać moda {'%s'}!\n Konflikty z aktywnym modem {'%s'}!\n", + "vcmi.server.errors.modDependencyLoop" : "Nie udało się wczytać moda {'%s'}!\n Być może znajduje się w pętli zależności", "vcmi.server.errors.unknownEntity" : "Nie udało się wczytać zapisu! Nieznany element '%s' znaleziony w pliku zapisu! Zapis może nie być zgodny z aktualnie zainstalowaną wersją modów!", "vcmi.dimensionDoor.seaToLandError" : "Nie jest możliwa teleportacja przez drzwi wymiarów z wód na ląd i na odwrót.", @@ -235,8 +271,10 @@ "vcmi.adventureOptions.borderScroll.help" : "{Przewijanie na brzegu mapy}\n\nPrzewijanie mapy przygody gdy kursor najeżdża na brzeg okna gry. Może być wyłączone poprzez przytrzymanie klawisza CTRL.", "vcmi.adventureOptions.infoBarCreatureManagement.hover" : "Zarządzanie armią w panelu informacyjnym", "vcmi.adventureOptions.infoBarCreatureManagement.help" : "{Zarządzanie armią w panelu informacyjnym}\n\nPozwala zarządzać jednostkami w panelu informacyjnym, zamiast przełączać między domyślnymi informacjami.", - "vcmi.adventureOptions.leftButtonDrag.hover" : "Przeciąganie mapy lewym kliknięciem", + "vcmi.adventureOptions.leftButtonDrag.hover" : "Przeciąganie lewym", "vcmi.adventureOptions.leftButtonDrag.help" : "{Przeciąganie mapy lewym kliknięciem}\n\nUmożliwia przesuwanie mapy przygody poprzez przeciąganie myszy z wciśniętym lewym przyciskiem.", + "vcmi.adventureOptions.rightButtonDrag.hover" : "Przeciąganie prawym", + "vcmi.adventureOptions.rightButtonDrag.help" : "{Przeciąganie mapy prawym kliknięciem}\n\nUmożliwia przesuwanie mapy przygody poprzez przeciąganie myszy z wciśniętym prawym przyciskiem.", "vcmi.adventureOptions.smoothDragging.hover" : "'Pływające' przeciąganie mapy", "vcmi.adventureOptions.smoothDragging.help" : "{'Pływające' przeciąganie mapy}\n\nPrzeciąganie mapy następuje ze stopniowo zanikającym przyspieszeniem.", "vcmi.adventureOptions.skipAdventureMapAnimations.hover" : "Pomiń efekty zanikania", @@ -337,6 +375,13 @@ "vcmi.heroWindow.openCommander.help" : "Wyświetla informacje o dowódcy przynależącym do tego bohatera", "vcmi.heroWindow.openBackpack.hover" : "Otwórz okno sakwy", "vcmi.heroWindow.openBackpack.help" : "Otwiera okno pozwalające łatwiej zarządzać artefaktami w sakwie", + "vcmi.heroWindow.sortBackpackByCost.hover" : "Sortuj wg. wartości", + "vcmi.heroWindow.sortBackpackByCost.help" : "Sortuj artefakty w sakwie według wartości", + "vcmi.heroWindow.sortBackpackBySlot.hover" : "Sortuj wg. miejsc", + "vcmi.heroWindow.sortBackpackBySlot.help" : "Sortuj artefakty w sakwie według umiejscowienia na ciele", + "vcmi.heroWindow.sortBackpackByClass.hover" : "Sortuj wg. jakości", + "vcmi.heroWindow.sortBackpackByClass.help" : "Sortuj artefakty w sakwie według jakości: Skarb, Pomniejszy, Potężny, Relikt", + "vcmi.heroWindow.fusingArtifact.fusing" : "Posiadasz wszystkie niezbędne komponenty do stworzenia %s. Czy chcesz wykonać fuzję? {Wszystkie komponenty zostaną użyte}", "vcmi.tavernWindow.inviteHero" : "Zaproś bohatera", @@ -513,7 +558,9 @@ "core.seerhut.quest.reachDate.visit.3" : "Zamknięte do %s.", "core.seerhut.quest.reachDate.visit.4" : "Zamknięte do %s.", "core.seerhut.quest.reachDate.visit.5" : "Zamknięte do %s.", - + + "mapObject.core.hillFort.object.description" : "Ulepsza jednostki. Koszt ulepszenia dla poziomów 1 - 4 jest bardziej korzystny niż w mieście.", + "core.bonus.ADDITIONAL_ATTACK.name": "Podwójne Uderzenie", "core.bonus.ADDITIONAL_ATTACK.description": "Atakuje dwa razy", "core.bonus.ADDITIONAL_RETALIATION.name": "Dodatkowy odwet", @@ -663,5 +710,11 @@ "core.bonus.WIDE_BREATH.name": "Szerokie zionięcie", "core.bonus.WIDE_BREATH.description": "Szeroki atak zionięciem (wiele heksów)", "core.bonus.DISINTEGRATE.name": "Rozpadanie", - "core.bonus.DISINTEGRATE.description": "Po śmierci nie pozostaje żaden trup" + "core.bonus.DISINTEGRATE.description": "Po śmierci nie pozostaje żaden trup", + "core.bonus.INVINCIBLE.name": "Niezwyciężony", + "core.bonus.INVINCIBLE.description": "Nic nie może mieć na niego wpływu", + "core.bonus.MECHANICAL.name": "Mechaniczny", + "core.bonus.MECHANICAL.description": "Odporny na wiele efektów, naprawialny", + "core.bonus.PRISM_HEX_ATTACK_BREATH.name": "Pryzmatyczny oddech", + "core.bonus.PRISM_HEX_ATTACK_BREATH.description": "Atakuje pryzmatycznym zionięciem (trzy kierunki)" } diff --git a/Mods/vcmi/config/vcmi/portuguese.json b/Mods/vcmi/Content/config/portuguese.json similarity index 80% rename from Mods/vcmi/config/vcmi/portuguese.json rename to Mods/vcmi/Content/config/portuguese.json index f2b6e377e..025375d4c 100644 --- a/Mods/vcmi/config/vcmi/portuguese.json +++ b/Mods/vcmi/Content/config/portuguese.json @@ -12,7 +12,11 @@ "vcmi.adventureMap.monsterThreat.levels.9" : "Avassaladora", "vcmi.adventureMap.monsterThreat.levels.10" : "Mortal", "vcmi.adventureMap.monsterThreat.levels.11" : "Impossível", - "vcmi.adventureMap.monsterLevel" : "\n\nNível %LEVEL, unidade de %TOWN", + "vcmi.adventureMap.monsterLevel" : "\n\nNível %LEVEL, unidade %TOWN de ataque %ATTACK_TYPE", + "vcmi.adventureMap.monsterMeleeType" : "corpo a corpo", + "vcmi.adventureMap.monsterRangedType" : "à distância", + "vcmi.adventureMap.search.hover" : "Procurar objeto no mapa", + "vcmi.adventureMap.search.help" : "Selecione o objeto para procurar no mapa.", "vcmi.adventureMap.confirmRestartGame" : "Tem certeza de que deseja reiniciar o jogo?", "vcmi.adventureMap.noTownWithMarket" : "Não há mercados disponíveis!", @@ -24,6 +28,13 @@ "vcmi.adventureMap.movementPointsHeroInfo" : "(Pontos de movimento: %REMAINING / %POINTS)", "vcmi.adventureMap.replayOpponentTurnNotImplemented" : "Desculpe, a repetição do turno do oponente ainda não está implementada!", + "vcmi.bonusSource.artifact" : "Artefato", + "vcmi.bonusSource.creature" : "Habilidade", + "vcmi.bonusSource.spell" : "Feitiço", + "vcmi.bonusSource.hero" : "Herói", + "vcmi.bonusSource.commander" : "Comandante", + "vcmi.bonusSource.other" : "Outro", + "vcmi.capitalColors.0" : "Vermelho", "vcmi.capitalColors.1" : "Azul", "vcmi.capitalColors.2" : "Bege", @@ -38,6 +49,12 @@ "vcmi.heroOverview.secondarySkills" : "Habilid. Secundárias", "vcmi.heroOverview.spells" : "Feitiços", + "vcmi.quickExchange.moveUnit" : "Mover Unidade", + "vcmi.quickExchange.moveAllUnits" : "Mover Todas as Unidades", + "vcmi.quickExchange.swapAllUnits" : "Trocar Exércitos", + "vcmi.quickExchange.moveAllArtifacts" : "Mover Todos os Artefatos", + "vcmi.quickExchange.swapAllArtifacts" : "Trocar Artefato", + "vcmi.radialWheel.mergeSameUnit" : "Mesclar criaturas iguais", "vcmi.radialWheel.fillSingleUnit" : "Preencher com criaturas únicas", "vcmi.radialWheel.splitSingleUnit" : "Dividir uma criatura única", @@ -56,9 +73,27 @@ "vcmi.radialWheel.moveUp" : "Mover para cima", "vcmi.radialWheel.moveDown" : "Mover para baixo", "vcmi.radialWheel.moveBottom" : "Mover para o fundo", + + "vcmi.randomMap.description" : "Mapa criado pelo Gerador de Mapas Aleatórios.\nO modelo foi %s, tamanho %dx%d, níveis %d, jogadores %d, computadores %d, água %s, monstros %s, mapa VCMI", + "vcmi.randomMap.description.isHuman" : ", %s é humano", + "vcmi.randomMap.description.townChoice" : ", a escolha de cidade de %s é %s", + "vcmi.randomMap.description.water.none" : "nenhuma", + "vcmi.randomMap.description.water.normal" : "normal", + "vcmi.randomMap.description.water.islands" : "ilhas", + "vcmi.randomMap.description.monster.weak" : "fraco", + "vcmi.randomMap.description.monster.normal" : "normal", + "vcmi.randomMap.description.monster.strong" : "forte", "vcmi.spellBook.search" : "Procurar...", + "vcmi.spellResearch.canNotAfford" : "Você não pode se dar ao luxo de substituir {%SPELL1} por {%SPELL2}. Mas você ainda pode descartar este feitiço e continuar a pesquisa de feitiços.", + "vcmi.spellResearch.comeAgain" : "A pesquisa já foi realizada hoje. Volte amanhã.", + "vcmi.spellResearch.pay" : "Gostaria de substituir {%SPELL1} por {%SPELL2}? Ou descartar este feitiço e continuar a pesquisa de feitiços?", + "vcmi.spellResearch.research" : "Pesquisar este Feitiço", + "vcmi.spellResearch.skip" : "Pular este Feitiço", + "vcmi.spellResearch.abort" : "Abortar", + "vcmi.spellResearch.noMoreSpells" : "Não há mais feitiços disponíveis para pesquisa.", + "vcmi.mainMenu.serverConnecting" : "Conectando...", "vcmi.mainMenu.serverAddressEnter" : "Insira o endereço:", "vcmi.mainMenu.serverConnectionFailed" : "Falha ao conectar", @@ -69,24 +104,69 @@ "vcmi.lobby.filepath" : "Caminho do arquivo", "vcmi.lobby.creationDate" : "Data de criação", "vcmi.lobby.scenarioName" : "Nome do cenário", - "vcmi.lobby.mapPreview" : "Visualização do mapa", - "vcmi.lobby.noPreview" : "sem visualização", + "vcmi.lobby.mapPreview" : "Prévia do mapa", + "vcmi.lobby.noPreview" : "sem prévia", "vcmi.lobby.noUnderground" : "sem subterrâneo", - "vcmi.lobby.sortDate" : "Classifica mapas por data de alteração", + "vcmi.lobby.sortDate" : "Ordenar mapas por data de alteração", "vcmi.lobby.backToLobby" : "Voltar para a sala de espera", "vcmi.lobby.author" : "Autor", "vcmi.lobby.handicap" : "Desvant.", "vcmi.lobby.handicap.resource" : "Fornece aos jogadores recursos apropriados para começar, além dos recursos iniciais normais. Valores negativos são permitidos, mas são limitados a 0 no total (o jogador nunca começa com recursos negativos).", "vcmi.lobby.handicap.income" : "Altera as várias rendas do jogador em porcentagem. Arredondado para cima.", "vcmi.lobby.handicap.growth" : "Altera a taxa de produção das criaturas nas cidades possuídas pelo jogador. Arredondado para cima.", - + "vcmi.lobby.deleteUnsupportedSave" : "{Jogos salvos incompatíveis encontrados}\n\nO VCMI encontrou %d jogos salvos que não são mais compatíveis, possivelmente devido a diferenças nas versões do VCMI.\n\nVocê deseja excluí-los?", + "vcmi.lobby.deleteSaveGameTitle" : "Selecione um Jogo Salvo para excluir", + "vcmi.lobby.deleteMapTitle" : "Selecione um Cenário para excluir", + "vcmi.lobby.deleteFile" : "Deseja excluir o seguinte arquivo?", + "vcmi.lobby.deleteFolder" : "Deseja excluir a seguinte pasta?", + "vcmi.lobby.deleteMode" : "Alternar para o modo de exclusão e voltar", + + "vcmi.broadcast.failedLoadGame" : "Falha ao carregar o jogo", + "vcmi.broadcast.command" : "Use '!help' para listar os comandos disponíveis", + "vcmi.broadcast.simturn.end" : "Os turnos simultâneos terminaram", + "vcmi.broadcast.simturn.endBetween" : "Os turnos simultâneos entre os jogadores %s e %s terminaram", + "vcmi.broadcast.serverProblem" : "O servidor encontrou um problema", + "vcmi.broadcast.gameTerminated" : "o jogo foi encerrado", + "vcmi.broadcast.gameSavedAs" : "jogo salvo como", + "vcmi.broadcast.noCheater" : "Nenhum trapaçeiro registrado!", + "vcmi.broadcast.playerCheater" : "O jogador %s é um trapaçeiro!", + "vcmi.broadcast.statisticFile" : "Os arquivos de estatísticas podem ser encontrados no diretório %s", + "vcmi.broadcast.help.commands" : "Comandos disponíveis para o anfitrião:", + "vcmi.broadcast.help.exit" : "'!exit' - termina imediatamente o jogo atual", + "vcmi.broadcast.help.kick" : "'!kick ' - expulsa o jogador especificado do jogo", + "vcmi.broadcast.help.save" : "'!save ' - salva o jogo com o nome de arquivo especificado", + "vcmi.broadcast.help.statistic" : "'!statistic' - salva as estatísticas do jogo como arquivo csv", + "vcmi.broadcast.help.commandsAll" : "Comandos disponíveis para todos os jogadores:", + "vcmi.broadcast.help.help" : "'!help' - exibe esta ajuda", + "vcmi.broadcast.help.cheaters" : "'!cheaters' - lista os jogadores que usaram comandos de trapaça durante o jogo", + "vcmi.broadcast.help.vote" : "'!vote' - permite mudar algumas configurações do jogo se todos os jogadores votarem a favor", + "vcmi.broadcast.vote.allow" : "'!vote simturns allow X' - permite turnos simultâneos por um número determinado de dias, ou até o contato", + "vcmi.broadcast.vote.force" : "'!vote simturns force X' - força turnos simultâneos por um número determinado de dias, bloqueando os contatos dos jogadores", + "vcmi.broadcast.vote.abort" : "'!vote simturns abort' - aborta os turnos simultâneos assim que este turno terminar", + "vcmi.broadcast.vote.timer" : "'!vote timer prolong X' - prolonga o temporizador base para todos os jogadores por um número determinado de segundos", + "vcmi.broadcast.vote.noActive" : "Nenhuma votação ativa!", + "vcmi.broadcast.vote.yes" : "sim", + "vcmi.broadcast.vote.no" : "não", + "vcmi.broadcast.vote.notRecognized" : "Comando de votação não reconhecido!", + "vcmi.broadcast.vote.success.untilContacts" : "Votação bem-sucedida. Os turnos simultâneos ocorrerão por mais %s dias, ou até o contato", + "vcmi.broadcast.vote.success.contactsBlocked" : "Votação bem-sucedida. Os turnos simultâneos ocorrerão por mais %s dias. Os contatos estão bloqueados", + "vcmi.broadcast.vote.success.nextDay" : "Votação bem-sucedida. Os turnos simultâneos terminarão no próximo dia", + "vcmi.broadcast.vote.success.timer" : "Votação bem-sucedida. O temporizador para todos os jogadores foi prolongado por %s segundos", + "vcmi.broadcast.vote.aborted" : "O jogador votou contra a mudança. Votação abortada", + "vcmi.broadcast.vote.start.untilContacts" : "Iniciada votação para permitir turnos simultâneos por mais %s dias", + "vcmi.broadcast.vote.start.contactsBlocked" : "Iniciada votação para forçar turnos simultâneos por mais %s dias", + "vcmi.broadcast.vote.start.nextDay" : "Iniciada votação para terminar os turnos simultâneos a partir do próximo dia", + "vcmi.broadcast.vote.start.timer" : "Iniciada votação para prolongar o temporizador para todos os jogadores por %s segundos", + "vcmi.broadcast.vote.hint" : "Digite '!vote yes' para concordar com esta mudança ou '!vote no' para votar contra", + "vcmi.lobby.login.title" : "Sala de Espera Online do VCMI", "vcmi.lobby.login.username" : "Nome de usuário:", "vcmi.lobby.login.connecting" : "Conectando...", "vcmi.lobby.login.error" : "Erro de conexão: %s", "vcmi.lobby.login.create" : "Nova Conta", - "vcmi.lobby.login.login" : "Login", - "vcmi.lobby.login.as" : "Logar como %s", + "vcmi.lobby.login.login" : "Entrar", + "vcmi.lobby.login.as" : "Entrar como %s", + "vcmi.lobby.login.spectator" : "Espectador", "vcmi.lobby.header.rooms" : "Salas de Jogo - %d", "vcmi.lobby.header.channels" : "Canais de Bate-papo", "vcmi.lobby.header.chat.global" : "Bate-papo Global do Jogo - %s", // %s -> nome do idioma @@ -143,12 +223,13 @@ "vcmi.client.errors.invalidMap" : "{Mapa ou campanha inválido}\n\nFalha ao iniciar o jogo! O mapa ou campanha selecionado pode ser inválido ou corrompido. Motivo:\n%s", "vcmi.client.errors.missingCampaigns" : "{Arquivos de dados ausentes}\n\nOs arquivos de dados das campanhas não foram encontrados! Você pode estar usando arquivos de dados incompletos ou corrompidos do Heroes 3. Por favor, reinstale os dados do jogo.", "vcmi.server.errors.disconnected" : "{Erro de Rede}\n\nA conexão com o servidor do jogo foi perdida!", + "vcmi.server.errors.playerLeft" : "{Jogador Saiu}\n\nO jogador %s desconectou-se do jogo!", //%s -> player color "vcmi.server.errors.existingProcess" : "Outro processo do servidor VCMI está em execução. Por favor, termine-o antes de iniciar um novo jogo.", "vcmi.server.errors.modsToEnable" : "{Os seguintes mods são necessários}", "vcmi.server.errors.modsToDisable" : "{Os seguintes mods devem ser desativados}", - "vcmi.server.errors.modNoDependency" : "Falha ao carregar o mod {'%s'}!\n Ele depende do mod {'%s'} que não está ativo!\n", - "vcmi.server.errors.modConflict" : "Falha ao carregar o mod {'%s'}!\n Conflita com o mod ativo {'%s'}!\n", - "vcmi.server.errors.unknownEntity" : "Falha ao carregar o salvamento! Entidade desconhecida '%s' encontrada no jogo salvo! O salvamento pode não ser compatível com a versão atualmente instalada dos mods!", + "vcmi.server.errors.unknownEntity" : "Falha ao carregar o jogo salvo! Entidade desconhecida '%s' encontrada no jogo salvo! O jogo salvo pode não ser compatível com a versão atualmente instalada dos mods!", + "vcmi.server.errors.wrongIdentified" : "Você foi identificado como jogador %s, enquanto se espera %s", + "vcmi.server.errors.notAllowed" : "Você não tem permissão para realizar esta ação!", "vcmi.dimensionDoor.seaToLandError" : "Não é possível teleportar do mar para a terra ou vice-versa com uma Porta Dimensional.", @@ -201,11 +282,11 @@ "vcmi.systemOptions.fullscreenExclusive.hover" : "Tela Cheia (exclusiva)", "vcmi.systemOptions.fullscreenExclusive.help" : "{Tela Cheia}\n\nSe selecionado, o VCMI será executado em modo de tela cheia exclusiva. Neste modo, o jogo mudará a resolução do monitor para a resolução selecionada.", "vcmi.systemOptions.resolutionButton.hover" : "Resolução: %wx%h", - "vcmi.systemOptions.resolutionButton.help" : "{Selecionar Resolução}\n\nMuda a resolução da tela do jogo.", + "vcmi.systemOptions.resolutionButton.help" : "{Seleciona a Resolução}\n\nMuda a resolução da tela do jogo.", "vcmi.systemOptions.resolutionMenu.hover" : "Selecionar Resolução", "vcmi.systemOptions.resolutionMenu.help" : "Muda a resolução da tela do jogo.", "vcmi.systemOptions.scalingButton.hover" : "Escala da Interface: %p%", - "vcmi.systemOptions.scalingButton.help" : "{Escala da Interface}\n\nAlterar escala da interface do jogo.", + "vcmi.systemOptions.scalingButton.help" : "{Escala da Interface}\n\nAltera a escala da interface do jogo.", "vcmi.systemOptions.scalingMenu.hover" : "Selecionar Escala da Interface", "vcmi.systemOptions.scalingMenu.help" : "Altera a escala da interface do jogo.", "vcmi.systemOptions.longTouchButton.hover" : "Intervalo de Toque Longo: %d ms", // Translation note: "ms" = "milliseconds" @@ -214,15 +295,15 @@ "vcmi.systemOptions.longTouchMenu.help" : "Muda a duração do intervalo de toque longo.", "vcmi.systemOptions.longTouchMenu.entry" : "%d milissegundos", "vcmi.systemOptions.framerateButton.hover" : "Mostrar FPS", - "vcmi.systemOptions.framerateButton.help" : "{Mostrar FPS}\n\nAtiva ou desativa a visibilidade do contador de Quadros Por Segundo no canto da janela do jogo.", - "vcmi.systemOptions.hapticFeedbackButton.hover" : "Resposta tátil", - "vcmi.systemOptions.hapticFeedbackButton.help" : "{Resposta tátil}\n\nAtiva ou desativa a resposta tátil nos toques na tela.", + "vcmi.systemOptions.framerateButton.help" : "{Mostra os Quadros Por Segundo}\n\nAtiva ou desativa a visibilidade do contador de Quadros Por Segundo no canto da janela do jogo.", + "vcmi.systemOptions.hapticFeedbackButton.hover" : "Resposta Tátil", + "vcmi.systemOptions.hapticFeedbackButton.help" : "{Resposta Tátil}\n\nAtiva ou desativa a resposta tátil nos toques na tela.", "vcmi.systemOptions.enableUiEnhancementsButton.hover" : "Aprimoramentos da Interface", - "vcmi.systemOptions.enableUiEnhancementsButton.help" : "{Aprimoramentos da Interface}\n\nAtiva ou desativa várias melhorias de interface. Como um botão de mochila etc. Desative para ter uma experiência mais clássica.", + "vcmi.systemOptions.enableUiEnhancementsButton.help" : "{Aprimoramentos da Interface}\n\nAtiva ou desativa várias melhorias de interface, como um botão de mochila etc. Desative para ter uma experiência mais clássica.", "vcmi.systemOptions.enableLargeSpellbookButton.hover" : "Grimório Grande", "vcmi.systemOptions.enableLargeSpellbookButton.help" : "{Grimório Grande}\n\nAtiva um grimório maior que comporta mais feitiços por página. A animação de mudança de página do grimório não funciona com esta configuração ativada.", - "vcmi.systemOptions.audioMuteFocus.hover" : "Silenciar na inatividade", - "vcmi.systemOptions.audioMuteFocus.help" : "{Silenciar na inatividade}\n\nSilencia o áudio quando a janela está inativa. As exceções são mensagens no jogo e som de novo turno.", + "vcmi.systemOptions.audioMuteFocus.hover" : "Silenciar na Inatividade", + "vcmi.systemOptions.audioMuteFocus.help" : "{Silencia o Áudio na Inatividade}\n\nSilencia o áudio quando a janela está inativa. As exceções são mensagens no jogo e som de novo turno.", "vcmi.adventureOptions.infoBarPick.hover" : "Mensagens no Painel de Informações", "vcmi.adventureOptions.infoBarPick.help" : "{Mostra as Mensagens no Painel de Informações}\n\nSempre que possível, as mensagens do jogo provenientes de objetos no mapa serão mostradas no painel de informações, em vez de aparecerem em uma janela separada.", @@ -231,7 +312,7 @@ "vcmi.adventureOptions.forceMovementInfo.hover" : "Sempre Mostrar o Custo de Movimento", "vcmi.adventureOptions.forceMovementInfo.help" : "{Sempre Mostrar o Custo de Movimento}\n\nSempre mostra os dados de pontos de movimento na barra de status (em vez de apenas visualizá-los enquanto você mantém pressionada a tecla ALT).", "vcmi.adventureOptions.showGrid.hover" : "Mostrar Grade", - "vcmi.adventureOptions.showGrid.help" : "{Mostrar Grade}\n\nMostra a sobreposição da grade, destacando as fronteiras entre as telhas do mapa de aventura.", + "vcmi.adventureOptions.showGrid.help" : "{Mostra a Grade}\n\nMostra a sobreposição da grade, destacando as fronteiras entre os hexágonos do mapa de aventura.", "vcmi.adventureOptions.borderScroll.hover" : "Rolagem de Borda", "vcmi.adventureOptions.borderScroll.help" : "{Rolagem de Borda}\n\nFaz o mapa de aventura rolar quando o cursor está adjacente à borda da janela. Pode ser desativado mantendo pressionada a tecla CTRL.", "vcmi.adventureOptions.infoBarCreatureManagement.hover" : "Gerenciar Criaturas no Painel de Info.", @@ -243,7 +324,7 @@ "vcmi.adventureOptions.smoothDragging.hover" : "Arrastar Suavemente o Mapa", "vcmi.adventureOptions.smoothDragging.help" : "{Arrasta o Mapa Suavemente}\n\nQuando ativado, o arrasto do mapa tem um efeito de movimento moderno.", "vcmi.adventureOptions.skipAdventureMapAnimations.hover" : "Omitir Efeitos de Desvanecimento", - "vcmi.adventureOptions.skipAdventureMapAnimations.help" : "{Omitir Efeitos de Desvanecimento}\n\nQuando ativado, omite o desvanecimento de objetos e efeitos semelhantes (coleta de recursos, embarque em navios etc). Torna a interface do usuário mais reativa em alguns casos em detrimento da estética. Especialmente útil em jogos PvP. Para obter velocidade de movimento máxima, o pulo está ativo independentemente desta configuração.", + "vcmi.adventureOptions.skipAdventureMapAnimations.help" : "{Omite os Efeitos de Desvanecimento}\n\nQuando ativado, omite o desvanecimento de objetos e efeitos semelhantes (coleta de recursos, embarque em navios etc.). Torna a interface do usuário mais reativa em alguns casos, em detrimento da estética. Especialmente útil em jogos PvP. Para obter velocidade de movimento máxima, o pulo está ativo independentemente desta configuração.", "vcmi.adventureOptions.mapScrollSpeed1.hover": "", "vcmi.adventureOptions.mapScrollSpeed5.hover": "", "vcmi.adventureOptions.mapScrollSpeed6.hover": "", @@ -251,7 +332,7 @@ "vcmi.adventureOptions.mapScrollSpeed5.help" : "Define a velocidade de rolagem do mapa como muito rápida.", "vcmi.adventureOptions.mapScrollSpeed6.help" : "Define a velocidade de rolagem do mapa como instantânea.", "vcmi.adventureOptions.hideBackground.hover" : "Ocultar Fundo", - "vcmi.adventureOptions.hideBackground.help" : "{Ocultar Fundo}\n\nOculta o mapa de aventura no fundo e mostra uma textura em vez disso.", + "vcmi.adventureOptions.hideBackground.help" : "{Oculta o Fundo}\n\nOculta o mapa de aventura no fundo e mostra uma textura em vez disso.", "vcmi.battleOptions.queueSizeLabel.hover": "Mostrar Fila de Ordem de Turno", "vcmi.battleOptions.queueSizeNoneButton.hover": "DESL.", @@ -259,7 +340,7 @@ "vcmi.battleOptions.queueSizeSmallButton.hover": "PEQU.", "vcmi.battleOptions.queueSizeBigButton.hover": "GRAN.", "vcmi.battleOptions.queueSizeNoneButton.help": "Não exibir Fila de Ordem de Turno.", - "vcmi.battleOptions.queueSizeAutoButton.help": "Ajusta automaticamente o tamanho da fila de ordem de turno com base na resolução do jogo (o tamanho PEQUENO é usado ao jogar o jogo em uma resolução com altura inferior a 700 pixels, o tamanho GRANDE é usado caso contrário).", + "vcmi.battleOptions.queueSizeAutoButton.help": "Ajusta automaticamente o tamanho da fila de ordem de turno com base na resolução do jogo (o tamanho PEQUENO é usado ao jogar em uma resolução com altura inferior a 700 pixels; o tamanho GRANDE é usado caso contrário).", "vcmi.battleOptions.queueSizeSmallButton.help": "Define o tamanho da fila de ordem de turno como PEQUENO.", "vcmi.battleOptions.queueSizeBigButton.help": "Define o tamanho da fila de ordem de turno como GRANDE (não suportado se a altura da resolução do jogo for inferior a 700 pixels).", "vcmi.battleOptions.animationsSpeed1.hover": "", @@ -271,7 +352,7 @@ "vcmi.battleOptions.movementHighlightOnHover.hover": "Destacar Movimento ao Passar o Mouse", "vcmi.battleOptions.movementHighlightOnHover.help": "{Destaca o Movimento ao Passar o Mouse}\n\nDestaca o alcance de movimento da unidade quando você passa o mouse sobre ela.", "vcmi.battleOptions.rangeLimitHighlightOnHover.hover": "Mostrar Limites de Alcance de Atiradores", - "vcmi.battleOptions.rangeLimitHighlightOnHover.help": "{Mostra o Limites de Alcance dos Atiradores ao Passar o Mouse}\n\nMostra os limites de alcance do atirador quando você passa o mouse sobre ele.", + "vcmi.battleOptions.rangeLimitHighlightOnHover.help": "{Mostra os Limites de Alcance dos Atiradores ao Passar o Mouse}\n\nMostra os limites de alcance do atirador quando você passa o mouse sobre ele.", "vcmi.battleOptions.showStickyHeroInfoWindows.hover": "Mostrar Janelas de Estatísticas de Heróis", "vcmi.battleOptions.showStickyHeroInfoWindows.help": "{Mostra as Janelas de Estatísticas de Heróis}\n\nAlterna permanentemente as janelas de estatísticas dos heróis que mostram estatísticas primárias e pontos de feitiço.", "vcmi.battleOptions.skipBattleIntroMusic.hover": "Pular Música de Introdução", @@ -279,7 +360,7 @@ "vcmi.battleOptions.endWithAutocombat.hover": "Terminar a batalha", "vcmi.battleOptions.endWithAutocombat.help": "{Termina a batalha}\n\nO Combate Automático reproduz a batalha até o final instantâneo.", "vcmi.battleOptions.showQuickSpell.hover": "Mostrar Painel de Feitiço Rápido", - "vcmi.battleOptions.showQuickSpell.help": "{Mostrar Painel de Feitiço Rápido}\n\nMostra um painel para seleção rápida de feitiços", + "vcmi.battleOptions.showQuickSpell.help": "{Mostra o Painel de Feitiço Rápido}\n\nMostra um painel para seleção rápida de feitiços.", "vcmi.adventureMap.revisitObject.hover" : "Revisitar Objeto", "vcmi.adventureMap.revisitObject.help" : "{Revisitar Objeto}\n\nSe um herói estiver atualmente em um Objeto do Mapa, ele pode revisitar o local.", @@ -324,9 +405,9 @@ "vcmi.otherOptions.creatureGrowthAsDwellingLabel.hover" : "Mostrar Produção Semanal de Criaturas", "vcmi.otherOptions.creatureGrowthAsDwellingLabel.help" : "{Mostrar Produção Semanal de Criaturas}\n\nMostra a produção semanal das criaturas em vez da quantidade disponível no resumo da cidade (canto inferior esquerdo da tela da cidade).", "vcmi.otherOptions.compactTownCreatureInfo.hover" : "Informações Compactas de Criaturas", - "vcmi.otherOptions.compactTownCreatureInfo.help" : "{Informações Compactas de Criaturas}\n\nMostra informações menores para criaturas da cidade no resumo da cidade (canto inferior esquerdo da tela da cidade).", + "vcmi.otherOptions.compactTownCreatureInfo.help" : "{Informações Compactas de Criaturas}\n\nMostra informações reduzidas para criaturas da cidade no resumo da cidade (canto inferior esquerdo da tela da cidade).", - "vcmi.townHall.missingBase" : "A construção base %s deve ser construída primeiro", + "vcmi.townHall.missingBase" : "A construção base %s deve ser feita primeiro.", "vcmi.townHall.noCreaturesToRecruit" : "Não há criaturas para recrutar!", "vcmi.townStructure.bank.borrow" : "Você entra no banco. Um banqueiro o vê e diz: \"Temos uma oferta especial para você. Você pode pegar um empréstimo de 2500 de ouro por 5 dias. Você terá que pagar 500 de ouro todos os dias.\"", @@ -340,6 +421,13 @@ "vcmi.heroWindow.openCommander.help" : "Mostra detalhes sobre o comandante deste herói.", "vcmi.heroWindow.openBackpack.hover" : "Abrir janela da mochila de artefatos", "vcmi.heroWindow.openBackpack.help" : "Abre a janela que facilita o gerenciamento da mochila de artefatos.", + "vcmi.heroWindow.sortBackpackByCost.hover" : "Ordenar por custo", + "vcmi.heroWindow.sortBackpackByCost.help" : "Ordena artefatos na mochila por custo.", + "vcmi.heroWindow.sortBackpackBySlot.hover" : "Ordenar por espaço", + "vcmi.heroWindow.sortBackpackBySlot.help" : "Ordena artefatos na mochila por espaço equipado.", + "vcmi.heroWindow.sortBackpackByClass.hover" : "Ordenar por classe", + "vcmi.heroWindow.sortBackpackByClass.help" : "Ordena artefatos na mochila por classe de artefato. Tesouro, Menor, Maior, Relíquia.", + "vcmi.heroWindow.fusingArtifact.fusing" : "Você possui todos os componentes necessários para a fusão de %s. Deseja realizar a fusão? {Todos os componentes serão consumidos após a fusão.}", "vcmi.tavernWindow.inviteHero" : "Convidar herói", @@ -350,7 +438,7 @@ "vcmi.creatureWindow.showSkills.hover" : "Alternar para visualização de habilidades", "vcmi.creatureWindow.showSkills.help" : "Exibe todas as habilidades aprendidas do comandante.", "vcmi.creatureWindow.returnArtifact.hover" : "Devolver artefato", - "vcmi.creatureWindow.returnArtifact.help" : "Clique neste botão para devolver o artefato para a mochila do herói.", + "vcmi.creatureWindow.returnArtifact.help" : "Clique neste botão para devolver o artefato à mochila do herói.", "vcmi.questLog.hideComplete.hover" : "Ocultar missões completas", "vcmi.questLog.hideComplete.help" : "Oculta todas as missões completas.", @@ -362,7 +450,7 @@ "vcmi.randomMapTab.widgets.roadTypesLabel" : "Tipos de Estrada", "vcmi.optionsTab.turnOptions.hover" : "Opções de Turno", - "vcmi.optionsTab.turnOptions.help" : "Selecione as opções de cronômetro do turno e turnos simultâneos", + "vcmi.optionsTab.turnOptions.help" : "Selecione as opções de cronômetro do turno e turnos simultâneos.", "vcmi.optionsTab.chessFieldBase.hover" : "Cronômetro Base", "vcmi.optionsTab.chessFieldTurn.hover" : "Cronôm. Turno", @@ -372,8 +460,8 @@ "vcmi.optionsTab.chessFieldTurnAccumulate.help" : "Usado fora de combate ou quando o {Cronômetro da Batalha} se esgota. Restaurado a cada turno. O tempo restante é adicionado ao {Tempo Base} no final do turno.", "vcmi.optionsTab.chessFieldTurnDiscard.help" : "Usado fora de combate ou quando o {Cronômetro da Batalha} se esgota. Restaurado a cada turno. Qualquer tempo não utilizado é perdido.", "vcmi.optionsTab.chessFieldBattle.help" : "Usado em batalhas com a IA ou em combates PvP quando o {Cronômetro da Unidade} se esgota. Restaurado no início de cada combate.", - "vcmi.optionsTab.chessFieldUnitAccumulate.help" : "Usado ao selecionar ação da unidade em combates PvP. O tempo restante é adicionado ao {Cronômetro da Batalha} no final do turno da unidade.", - "vcmi.optionsTab.chessFieldUnitDiscard.help" : "Usado ao selecionar ação da unidade em combates PvP. Restaurado no início do turno de cada unidade. Qualquer tempo não utilizado é perdido.", + "vcmi.optionsTab.chessFieldUnitAccumulate.help" : "Usado ao selecionar a ação da unidade em combates PvP. O tempo restante é adicionado ao {Cronômetro da Batalha} no final do turno da unidade.", + "vcmi.optionsTab.chessFieldUnitDiscard.help" : "Usado ao selecionar a ação da unidade em combates PvP. Restaurado no início do turno de cada unidade. Qualquer tempo não utilizado é perdido.", "vcmi.optionsTab.accumulate" : "Acumular", @@ -504,9 +592,9 @@ "core.seerhut.quest.reachDate.hover.3" : "(Não retorne antes de %s)", "core.seerhut.quest.reachDate.hover.4" : "(Não retorne antes de %s)", "core.seerhut.quest.reachDate.hover.5" : "(Não retorne antes de %s)", - "core.seerhut.quest.reachDate.receive.0" : "Estou ocupado. Não volte antes de %s", - "core.seerhut.quest.reachDate.receive.1" : "Estou ocupado. Não volte antes de %s", - "core.seerhut.quest.reachDate.receive.2" : "Estou ocupado. Não volte antes de %s", + "core.seerhut.quest.reachDate.receive.0" : "Estou ocupado. Não volte antes de %s.", + "core.seerhut.quest.reachDate.receive.1" : "Estou ocupado. Não volte antes de %s.", + "core.seerhut.quest.reachDate.receive.2" : "Estou ocupado. Não volte antes de %s.", "core.seerhut.quest.reachDate.receive.3" : "Fechado até %s.", "core.seerhut.quest.reachDate.receive.4" : "Fechado até %s.", "core.seerhut.quest.reachDate.receive.5" : "Fechado até %s.", @@ -516,7 +604,9 @@ "core.seerhut.quest.reachDate.visit.3" : "Fechado até %s.", "core.seerhut.quest.reachDate.visit.4" : "Fechado até %s.", "core.seerhut.quest.reachDate.visit.5" : "Fechado até %s.", - + + "mapObject.core.hillFort.object.description" : "Atualiza criaturas. O custo de atualização para os níveis 1 a 4 é mais vantajoso do que na cidade associada.", + "core.bonus.ADDITIONAL_ATTACK.name" : "Ataque Duplo", "core.bonus.ADDITIONAL_ATTACK.description" : "Ataca duas vezes", "core.bonus.ADDITIONAL_RETALIATION.name" : "Contra-ataques Adicionais", @@ -554,7 +644,7 @@ "core.bonus.ENCHANTER.name" : "Encantador", "core.bonus.ENCHANTER.description" : "Pode lançar ${subtype.spell} em massa a cada turno", "core.bonus.ENCHANTED.name" : "Encantado", - "core.bonus.ENCHANTED.description" : "Afetado por ${subtype.spell} permanente", + "core.bonus.ENCHANTED.description" : "Afetado por ${subtype.spell} permanentemente", "core.bonus.ENEMY_ATTACK_REDUCTION.name" : "Ignorar Ataque (${val}%)", "core.bonus.ENEMY_ATTACK_REDUCTION.description" : "Ao ser atacado, ${val}% do ataque do agressor é ignorado", "core.bonus.ENEMY_DEFENCE_REDUCTION.name" : "Ignorar Defesa (${val}%)", @@ -573,7 +663,7 @@ "core.bonus.FEROCITY.description" : "Ataca ${val} vezes adicionais se matar alguém", "core.bonus.FLYING.name" : "Voo", "core.bonus.FLYING.description" : "Voa ao se mover (ignora obstáculos)", - "core.bonus.FREE_SHOOTING.name" : "Tiro Livre", + "core.bonus.FREE_SHOOTING.name" : "Tiro Curto", "core.bonus.FREE_SHOOTING.description" : "Pode usar ataques à distância em combate corpo a corpo", "core.bonus.GARGOYLE.name" : "Gárgula", "core.bonus.GARGOYLE.description" : "Não pode ser levantado ou curado", @@ -592,7 +682,7 @@ "core.bonus.LEVEL_SPELL_IMMUNITY.name" : "Imune a Feitiços 1-${val}", "core.bonus.LEVEL_SPELL_IMMUNITY.description" : "Imunidade a feitiços dos níveis 1-${val}", "core.bonus.LIMITED_SHOOTING_RANGE.name" : "Alcance de Tiro Limitado", - "core.bonus.LIMITED_SHOOTING_RANGE.description" : "Incapaz de mirar unidades a uma distância maior que ${val} hexágonos", + "core.bonus.LIMITED_SHOOTING_RANGE.description" : "Incapaz de mirar unidades a mais de ${val} hexágonos de distância", "core.bonus.LIFE_DRAIN.name" : "Drenar Vida (${val}%)", "core.bonus.LIFE_DRAIN.description" : "Drena ${val}% do dano causado", "core.bonus.MANA_CHANNELING.name" : "Canalização Mágica ${val}%", @@ -646,7 +736,7 @@ "core.bonus.SPELL_LIKE_ATTACK.name" : "Ataque Similar a Feitiço", "core.bonus.SPELL_LIKE_ATTACK.description" : "Ataques com ${subtype.spell}", "core.bonus.SPELL_RESISTANCE_AURA.name" : "Aura de Resistência", - "core.bonus.SPELL_RESISTANCE_AURA.description" : "Pilhas próximas ganham ${val}% de resistência a magia", + "core.bonus.SPELL_RESISTANCE_AURA.description" : "Pilhas próximas ganham ${val}% de resistência à magia", "core.bonus.SUMMON_GUARDIANS.name" : "Invocar Guardas", "core.bonus.SUMMON_GUARDIANS.description" : "No início da batalha, invoca ${subtype.creature} (${val}%)", "core.bonus.SYNERGY_TARGET.name" : "Alvo Sinergizável", @@ -659,8 +749,8 @@ "core.bonus.TRANSMUTATION.description" : "${val}% de chance de transformar a unidade atacada em um tipo diferente", "core.bonus.UNDEAD.name" : "Morto-vivo", "core.bonus.UNDEAD.description" : "A criatura é um Morto-vivo", - "core.bonus.UNLIMITED_RETALIATIONS.name" : "Contra-ataques Ilimitadas", - "core.bonus.UNLIMITED_RETALIATIONS.description" : "Pode contra-atacar contra um número ilimitado de ataques", + "core.bonus.UNLIMITED_RETALIATIONS.name" : "Contra-ataques Ilimitados", + "core.bonus.UNLIMITED_RETALIATIONS.description" : "Pode contra-atacar um número ilimitado de vezes", "core.bonus.WATER_IMMUNITY.name" : "Imunidade à Água", "core.bonus.WATER_IMMUNITY.description" : "Imune a todos os feitiços da escola de magia da Água", "core.bonus.WIDE_BREATH.name" : "Sopro Amplo", @@ -668,5 +758,31 @@ "core.bonus.DISINTEGRATE.name": "Desintegrar", "core.bonus.DISINTEGRATE.description": "Nenhum corpo permanece após a morte", "core.bonus.INVINCIBLE.name": "Invencível", - "core.bonus.INVINCIBLE.description": "Não pode ser afetado por nada" + "core.bonus.INVINCIBLE.description": "Não pode ser afetado por nada", + "core.bonus.MECHANICAL.name": "Mecânico", + "core.bonus.MECHANICAL.description": "Imunidade a muitos efeitos, reparável", + "core.bonus.PRISM_HEX_ATTACK_BREATH.name" : "Sopro Prismático", + "core.bonus.PRISM_HEX_ATTACK_BREATH.description" : "Ataque de Sopro Prismático (três direções)", + + "spell.core.castleMoat.name": "Fosso", + "spell.core.castleMoatTrigger.name": "Fosso", + "spell.core.catapultShot.name": "Disparo de Catapulta", + "spell.core.cyclopsShot.name": "Tiro de Cerco", + "spell.core.dungeonMoat.name": "Óleo Fervente", + "spell.core.dungeonMoatTrigger.name": "Óleo Fervente", + "spell.core.fireWallTrigger.name": "Parede de Fogo", + "spell.core.firstAid.name": "Primeiros Socorros", + "spell.core.fortressMoat.name": "Alcatrão Fervente", + "spell.core.fortressMoatTrigger.name": "Alcatrão Fervente", + "spell.core.infernoMoat.name": "Lava", + "spell.core.infernoMoatTrigger.name": "Lava", + "spell.core.landMineTrigger.name": "Mina Terrestre", + "spell.core.necropolisMoat.name": "Cemitério", + "spell.core.necropolisMoatTrigger.name": "Cemitério", + "spell.core.rampartMoat.name": "Espraiamento", + "spell.core.rampartMoatTrigger.name": "Espraiamento", + "spell.core.strongholdMoat.name": "Estacas de Madeira", + "spell.core.strongholdMoatTrigger.name": "Estacas de Madeira", + "spell.core.summonDemons.name": "Invocar Demônios", + "spell.core.towerMoat.name": "Mina Terrestre" } diff --git a/Mods/vcmi/config/vcmi/rmg/hdmod/aroundamarsh.JSON b/Mods/vcmi/Content/config/rmg/hdmod/aroundamarsh.json similarity index 100% rename from Mods/vcmi/config/vcmi/rmg/hdmod/aroundamarsh.JSON rename to Mods/vcmi/Content/config/rmg/hdmod/aroundamarsh.json diff --git a/Mods/vcmi/config/vcmi/rmg/hdmod/balance.JSON b/Mods/vcmi/Content/config/rmg/hdmod/balance.json similarity index 100% rename from Mods/vcmi/config/vcmi/rmg/hdmod/balance.JSON rename to Mods/vcmi/Content/config/rmg/hdmod/balance.json diff --git a/Mods/vcmi/config/vcmi/rmg/hdmod/blockbuster.JSON b/Mods/vcmi/Content/config/rmg/hdmod/blockbuster.json similarity index 100% rename from Mods/vcmi/config/vcmi/rmg/hdmod/blockbuster.JSON rename to Mods/vcmi/Content/config/rmg/hdmod/blockbuster.json diff --git a/Mods/vcmi/config/vcmi/rmg/hdmod/clashOfDragons.json b/Mods/vcmi/Content/config/rmg/hdmod/clashOfDragons.json similarity index 100% rename from Mods/vcmi/config/vcmi/rmg/hdmod/clashOfDragons.json rename to Mods/vcmi/Content/config/rmg/hdmod/clashOfDragons.json diff --git a/Mods/vcmi/config/vcmi/rmg/hdmod/coldshadowsFantasy.json b/Mods/vcmi/Content/config/rmg/hdmod/coldshadowsFantasy.json similarity index 100% rename from Mods/vcmi/config/vcmi/rmg/hdmod/coldshadowsFantasy.json rename to Mods/vcmi/Content/config/rmg/hdmod/coldshadowsFantasy.json diff --git a/Mods/vcmi/config/vcmi/rmg/hdmod/cube.JSON b/Mods/vcmi/Content/config/rmg/hdmod/cube.json similarity index 100% rename from Mods/vcmi/config/vcmi/rmg/hdmod/cube.JSON rename to Mods/vcmi/Content/config/rmg/hdmod/cube.json diff --git a/Mods/vcmi/config/vcmi/rmg/hdmod/diamond.JSON b/Mods/vcmi/Content/config/rmg/hdmod/diamond.json similarity index 100% rename from Mods/vcmi/config/vcmi/rmg/hdmod/diamond.JSON rename to Mods/vcmi/Content/config/rmg/hdmod/diamond.json diff --git a/Mods/vcmi/config/vcmi/rmg/hdmod/extreme.JSON b/Mods/vcmi/Content/config/rmg/hdmod/extreme.json similarity index 100% rename from Mods/vcmi/config/vcmi/rmg/hdmod/extreme.JSON rename to Mods/vcmi/Content/config/rmg/hdmod/extreme.json diff --git a/Mods/vcmi/config/vcmi/rmg/hdmod/extreme2.JSON b/Mods/vcmi/Content/config/rmg/hdmod/extreme2.json similarity index 100% rename from Mods/vcmi/config/vcmi/rmg/hdmod/extreme2.JSON rename to Mods/vcmi/Content/config/rmg/hdmod/extreme2.json diff --git a/Mods/vcmi/config/vcmi/rmg/hdmod/fear.JSON b/Mods/vcmi/Content/config/rmg/hdmod/fear.json similarity index 100% rename from Mods/vcmi/config/vcmi/rmg/hdmod/fear.JSON rename to Mods/vcmi/Content/config/rmg/hdmod/fear.json diff --git a/Mods/vcmi/config/vcmi/rmg/hdmod/frozenDragons.JSON b/Mods/vcmi/Content/config/rmg/hdmod/frozenDragons.json similarity index 100% rename from Mods/vcmi/config/vcmi/rmg/hdmod/frozenDragons.JSON rename to Mods/vcmi/Content/config/rmg/hdmod/frozenDragons.json diff --git a/Mods/vcmi/config/vcmi/rmg/hdmod/gimlisRevenge.JSON b/Mods/vcmi/Content/config/rmg/hdmod/gimlisRevenge.json similarity index 100% rename from Mods/vcmi/config/vcmi/rmg/hdmod/gimlisRevenge.JSON rename to Mods/vcmi/Content/config/rmg/hdmod/gimlisRevenge.json diff --git a/Mods/vcmi/config/vcmi/rmg/hdmod/guerilla.JSON b/Mods/vcmi/Content/config/rmg/hdmod/guerilla.json similarity index 100% rename from Mods/vcmi/config/vcmi/rmg/hdmod/guerilla.JSON rename to Mods/vcmi/Content/config/rmg/hdmod/guerilla.json diff --git a/Mods/vcmi/config/vcmi/rmg/hdmod/headquarters.JSON b/Mods/vcmi/Content/config/rmg/hdmod/headquarters.json similarity index 100% rename from Mods/vcmi/config/vcmi/rmg/hdmod/headquarters.JSON rename to Mods/vcmi/Content/config/rmg/hdmod/headquarters.json diff --git a/Mods/vcmi/config/vcmi/rmg/hdmod/hypercube.JSON b/Mods/vcmi/Content/config/rmg/hdmod/hypercube.json similarity index 100% rename from Mods/vcmi/config/vcmi/rmg/hdmod/hypercube.JSON rename to Mods/vcmi/Content/config/rmg/hdmod/hypercube.json diff --git a/Mods/vcmi/config/vcmi/rmg/hdmod/jebusCross.json b/Mods/vcmi/Content/config/rmg/hdmod/jebusCross.json similarity index 100% rename from Mods/vcmi/config/vcmi/rmg/hdmod/jebusCross.json rename to Mods/vcmi/Content/config/rmg/hdmod/jebusCross.json diff --git a/Mods/vcmi/config/vcmi/rmg/hdmod/longRun.JSON b/Mods/vcmi/Content/config/rmg/hdmod/longRun.json similarity index 100% rename from Mods/vcmi/config/vcmi/rmg/hdmod/longRun.JSON rename to Mods/vcmi/Content/config/rmg/hdmod/longRun.json diff --git a/Mods/vcmi/config/vcmi/rmg/hdmod/marathon.JSON b/Mods/vcmi/Content/config/rmg/hdmod/marathon.json similarity index 100% rename from Mods/vcmi/config/vcmi/rmg/hdmod/marathon.JSON rename to Mods/vcmi/Content/config/rmg/hdmod/marathon.json diff --git a/Mods/vcmi/config/vcmi/rmg/hdmod/miniNostalgia.JSON b/Mods/vcmi/Content/config/rmg/hdmod/miniNostalgia.json similarity index 100% rename from Mods/vcmi/config/vcmi/rmg/hdmod/miniNostalgia.JSON rename to Mods/vcmi/Content/config/rmg/hdmod/miniNostalgia.json diff --git a/Mods/vcmi/config/vcmi/rmg/hdmod/nostalgia.JSON b/Mods/vcmi/Content/config/rmg/hdmod/nostalgia.json similarity index 100% rename from Mods/vcmi/config/vcmi/rmg/hdmod/nostalgia.JSON rename to Mods/vcmi/Content/config/rmg/hdmod/nostalgia.json diff --git a/Mods/vcmi/config/vcmi/rmg/hdmod/oceansEleven.JSON b/Mods/vcmi/Content/config/rmg/hdmod/oceansEleven.json similarity index 100% rename from Mods/vcmi/config/vcmi/rmg/hdmod/oceansEleven.JSON rename to Mods/vcmi/Content/config/rmg/hdmod/oceansEleven.json diff --git a/Mods/vcmi/config/vcmi/rmg/hdmod/panic.JSON b/Mods/vcmi/Content/config/rmg/hdmod/panic.json similarity index 100% rename from Mods/vcmi/config/vcmi/rmg/hdmod/panic.JSON rename to Mods/vcmi/Content/config/rmg/hdmod/panic.json diff --git a/Mods/vcmi/config/vcmi/rmg/hdmod/poorJebus.JSON b/Mods/vcmi/Content/config/rmg/hdmod/poorJebus.json similarity index 100% rename from Mods/vcmi/config/vcmi/rmg/hdmod/poorJebus.JSON rename to Mods/vcmi/Content/config/rmg/hdmod/poorJebus.json diff --git a/Mods/vcmi/config/vcmi/rmg/hdmod/reckless.JSON b/Mods/vcmi/Content/config/rmg/hdmod/reckless.json similarity index 100% rename from Mods/vcmi/config/vcmi/rmg/hdmod/reckless.JSON rename to Mods/vcmi/Content/config/rmg/hdmod/reckless.json diff --git a/Mods/vcmi/config/vcmi/rmg/hdmod/roadrunner.JSON b/Mods/vcmi/Content/config/rmg/hdmod/roadrunner.json similarity index 100% rename from Mods/vcmi/config/vcmi/rmg/hdmod/roadrunner.JSON rename to Mods/vcmi/Content/config/rmg/hdmod/roadrunner.json diff --git a/Mods/vcmi/config/vcmi/rmg/hdmod/shaaafworld.JSON b/Mods/vcmi/Content/config/rmg/hdmod/shaaafworld.json similarity index 100% rename from Mods/vcmi/config/vcmi/rmg/hdmod/shaaafworld.JSON rename to Mods/vcmi/Content/config/rmg/hdmod/shaaafworld.json diff --git a/Mods/vcmi/config/vcmi/rmg/hdmod/skirmish.JSON b/Mods/vcmi/Content/config/rmg/hdmod/skirmish.json similarity index 100% rename from Mods/vcmi/config/vcmi/rmg/hdmod/skirmish.JSON rename to Mods/vcmi/Content/config/rmg/hdmod/skirmish.json diff --git a/Mods/vcmi/config/vcmi/rmg/hdmod/speed1.JSON b/Mods/vcmi/Content/config/rmg/hdmod/speed1.json similarity index 100% rename from Mods/vcmi/config/vcmi/rmg/hdmod/speed1.JSON rename to Mods/vcmi/Content/config/rmg/hdmod/speed1.json diff --git a/Mods/vcmi/config/vcmi/rmg/hdmod/speed2.JSON b/Mods/vcmi/Content/config/rmg/hdmod/speed2.json similarity index 100% rename from Mods/vcmi/config/vcmi/rmg/hdmod/speed2.JSON rename to Mods/vcmi/Content/config/rmg/hdmod/speed2.json diff --git a/Mods/vcmi/config/vcmi/rmg/hdmod/spider.JSON b/Mods/vcmi/Content/config/rmg/hdmod/spider.json similarity index 100% rename from Mods/vcmi/config/vcmi/rmg/hdmod/spider.JSON rename to Mods/vcmi/Content/config/rmg/hdmod/spider.json diff --git a/Mods/vcmi/config/vcmi/rmg/hdmod/superslam.JSON b/Mods/vcmi/Content/config/rmg/hdmod/superslam.json similarity index 100% rename from Mods/vcmi/config/vcmi/rmg/hdmod/superslam.JSON rename to Mods/vcmi/Content/config/rmg/hdmod/superslam.json diff --git a/Mods/vcmi/config/vcmi/rmg/hdmod/triad.JSON b/Mods/vcmi/Content/config/rmg/hdmod/triad.json similarity index 100% rename from Mods/vcmi/config/vcmi/rmg/hdmod/triad.JSON rename to Mods/vcmi/Content/config/rmg/hdmod/triad.json diff --git a/Mods/vcmi/config/vcmi/rmg/hdmod/vortex.JSON b/Mods/vcmi/Content/config/rmg/hdmod/vortex.json similarity index 100% rename from Mods/vcmi/config/vcmi/rmg/hdmod/vortex.JSON rename to Mods/vcmi/Content/config/rmg/hdmod/vortex.json diff --git a/Mods/vcmi/config/vcmi/rmg/hdmodUnused/anarchy.JSON b/Mods/vcmi/Content/config/rmg/hdmodUnused/anarchy.json similarity index 100% rename from Mods/vcmi/config/vcmi/rmg/hdmodUnused/anarchy.JSON rename to Mods/vcmi/Content/config/rmg/hdmodUnused/anarchy.json diff --git a/Mods/vcmi/config/vcmi/rmg/hdmodUnused/balance m+u 200%.JSON b/Mods/vcmi/Content/config/rmg/hdmodUnused/balance m+u 200%.json similarity index 100% rename from Mods/vcmi/config/vcmi/rmg/hdmodUnused/balance m+u 200%.JSON rename to Mods/vcmi/Content/config/rmg/hdmodUnused/balance m+u 200%.json diff --git a/Mods/vcmi/config/vcmi/rmg/hdmodUnused/midnightMix.JSON b/Mods/vcmi/Content/config/rmg/hdmodUnused/midnightMix.json similarity index 100% rename from Mods/vcmi/config/vcmi/rmg/hdmodUnused/midnightMix.JSON rename to Mods/vcmi/Content/config/rmg/hdmodUnused/midnightMix.json diff --git a/Mods/vcmi/config/vcmi/rmg/hdmodUnused/skirmish m-u 200%.JSON b/Mods/vcmi/Content/config/rmg/hdmodUnused/skirmish m-u 200%.json similarity index 100% rename from Mods/vcmi/config/vcmi/rmg/hdmodUnused/skirmish m-u 200%.JSON rename to Mods/vcmi/Content/config/rmg/hdmodUnused/skirmish m-u 200%.json diff --git a/Mods/vcmi/config/vcmi/rmg/hdmodUnused/true random.JSON b/Mods/vcmi/Content/config/rmg/hdmodUnused/true random.json similarity index 100% rename from Mods/vcmi/config/vcmi/rmg/hdmodUnused/true random.JSON rename to Mods/vcmi/Content/config/rmg/hdmodUnused/true random.json diff --git a/Mods/vcmi/config/vcmi/rmg/heroes3/dwarvenTunnels.JSON b/Mods/vcmi/Content/config/rmg/heroes3/dwarvenTunnels.json similarity index 100% rename from Mods/vcmi/config/vcmi/rmg/heroes3/dwarvenTunnels.JSON rename to Mods/vcmi/Content/config/rmg/heroes3/dwarvenTunnels.json diff --git a/Mods/vcmi/config/vcmi/rmg/heroes3/golemsAplenty.JSON b/Mods/vcmi/Content/config/rmg/heroes3/golemsAplenty.json similarity index 100% rename from Mods/vcmi/config/vcmi/rmg/heroes3/golemsAplenty.JSON rename to Mods/vcmi/Content/config/rmg/heroes3/golemsAplenty.json diff --git a/Mods/vcmi/config/vcmi/rmg/heroes3/meetingInMuzgob.JSON b/Mods/vcmi/Content/config/rmg/heroes3/meetingInMuzgob.json similarity index 100% rename from Mods/vcmi/config/vcmi/rmg/heroes3/meetingInMuzgob.JSON rename to Mods/vcmi/Content/config/rmg/heroes3/meetingInMuzgob.json diff --git a/Mods/vcmi/config/vcmi/rmg/heroes3/monksRetreat.JSON b/Mods/vcmi/Content/config/rmg/heroes3/monksRetreat.json similarity index 100% rename from Mods/vcmi/config/vcmi/rmg/heroes3/monksRetreat.JSON rename to Mods/vcmi/Content/config/rmg/heroes3/monksRetreat.json diff --git a/Mods/vcmi/config/vcmi/rmg/heroes3/newcomers.JSON b/Mods/vcmi/Content/config/rmg/heroes3/newcomers.json similarity index 100% rename from Mods/vcmi/config/vcmi/rmg/heroes3/newcomers.JSON rename to Mods/vcmi/Content/config/rmg/heroes3/newcomers.json diff --git a/Mods/vcmi/config/vcmi/rmg/heroes3/readyOrNot.JSON b/Mods/vcmi/Content/config/rmg/heroes3/readyOrNot.json similarity index 100% rename from Mods/vcmi/config/vcmi/rmg/heroes3/readyOrNot.JSON rename to Mods/vcmi/Content/config/rmg/heroes3/readyOrNot.json diff --git a/Mods/vcmi/config/vcmi/rmg/heroes3/smallRing.JSON b/Mods/vcmi/Content/config/rmg/heroes3/smallRing.json similarity index 100% rename from Mods/vcmi/config/vcmi/rmg/heroes3/smallRing.JSON rename to Mods/vcmi/Content/config/rmg/heroes3/smallRing.json diff --git a/Mods/vcmi/config/vcmi/rmg/heroes3/southOfHell.JSON b/Mods/vcmi/Content/config/rmg/heroes3/southOfHell.json similarity index 100% rename from Mods/vcmi/config/vcmi/rmg/heroes3/southOfHell.JSON rename to Mods/vcmi/Content/config/rmg/heroes3/southOfHell.json diff --git a/Mods/vcmi/config/vcmi/rmg/heroes3/worldsAtWar.JSON b/Mods/vcmi/Content/config/rmg/heroes3/worldsAtWar.json similarity index 100% rename from Mods/vcmi/config/vcmi/rmg/heroes3/worldsAtWar.JSON rename to Mods/vcmi/Content/config/rmg/heroes3/worldsAtWar.json diff --git a/Mods/vcmi/config/vcmi/rmg/heroes3unused/dragon.json b/Mods/vcmi/Content/config/rmg/heroes3unused/dragon.json similarity index 100% rename from Mods/vcmi/config/vcmi/rmg/heroes3unused/dragon.json rename to Mods/vcmi/Content/config/rmg/heroes3unused/dragon.json diff --git a/Mods/vcmi/config/vcmi/rmg/heroes3unused/gauntlet.JSON b/Mods/vcmi/Content/config/rmg/heroes3unused/gauntlet.json similarity index 100% rename from Mods/vcmi/config/vcmi/rmg/heroes3unused/gauntlet.JSON rename to Mods/vcmi/Content/config/rmg/heroes3unused/gauntlet.json diff --git a/Mods/vcmi/config/vcmi/rmg/heroes3unused/ring.JSON b/Mods/vcmi/Content/config/rmg/heroes3unused/ring.json similarity index 100% rename from Mods/vcmi/config/vcmi/rmg/heroes3unused/ring.JSON rename to Mods/vcmi/Content/config/rmg/heroes3unused/ring.json diff --git a/Mods/vcmi/config/vcmi/rmg/heroes3unused/riseOfPhoenix.JSON b/Mods/vcmi/Content/config/rmg/heroes3unused/riseOfPhoenix.json similarity index 100% rename from Mods/vcmi/config/vcmi/rmg/heroes3unused/riseOfPhoenix.JSON rename to Mods/vcmi/Content/config/rmg/heroes3unused/riseOfPhoenix.json diff --git a/Mods/vcmi/config/vcmi/rmg/symmetric/2sm0k.JSON b/Mods/vcmi/Content/config/rmg/symmetric/2sm0k.json similarity index 100% rename from Mods/vcmi/config/vcmi/rmg/symmetric/2sm0k.JSON rename to Mods/vcmi/Content/config/rmg/symmetric/2sm0k.json diff --git a/Mods/vcmi/config/vcmi/rmg/symmetric/2sm2a.JSON b/Mods/vcmi/Content/config/rmg/symmetric/2sm2a.json similarity index 100% rename from Mods/vcmi/config/vcmi/rmg/symmetric/2sm2a.JSON rename to Mods/vcmi/Content/config/rmg/symmetric/2sm2a.json diff --git a/Mods/vcmi/config/vcmi/rmg/symmetric/2sm2b(2).JSON b/Mods/vcmi/Content/config/rmg/symmetric/2sm2b(2).json similarity index 100% rename from Mods/vcmi/config/vcmi/rmg/symmetric/2sm2b(2).JSON rename to Mods/vcmi/Content/config/rmg/symmetric/2sm2b(2).json diff --git a/Mods/vcmi/config/vcmi/rmg/symmetric/2sm2b.JSON b/Mods/vcmi/Content/config/rmg/symmetric/2sm2b.json similarity index 100% rename from Mods/vcmi/config/vcmi/rmg/symmetric/2sm2b.JSON rename to Mods/vcmi/Content/config/rmg/symmetric/2sm2b.json diff --git a/Mods/vcmi/config/vcmi/rmg/symmetric/2sm2c.JSON b/Mods/vcmi/Content/config/rmg/symmetric/2sm2c.json similarity index 100% rename from Mods/vcmi/config/vcmi/rmg/symmetric/2sm2c.JSON rename to Mods/vcmi/Content/config/rmg/symmetric/2sm2c.json diff --git a/Mods/vcmi/config/vcmi/rmg/symmetric/2sm2f(2).JSON b/Mods/vcmi/Content/config/rmg/symmetric/2sm2f(2).json similarity index 100% rename from Mods/vcmi/config/vcmi/rmg/symmetric/2sm2f(2).JSON rename to Mods/vcmi/Content/config/rmg/symmetric/2sm2f(2).json diff --git a/Mods/vcmi/config/vcmi/rmg/symmetric/2sm2f.JSON b/Mods/vcmi/Content/config/rmg/symmetric/2sm2f.json similarity index 100% rename from Mods/vcmi/config/vcmi/rmg/symmetric/2sm2f.JSON rename to Mods/vcmi/Content/config/rmg/symmetric/2sm2f.json diff --git a/Mods/vcmi/config/vcmi/rmg/symmetric/2sm2h(2).JSON b/Mods/vcmi/Content/config/rmg/symmetric/2sm2h(2).json similarity index 100% rename from Mods/vcmi/config/vcmi/rmg/symmetric/2sm2h(2).JSON rename to Mods/vcmi/Content/config/rmg/symmetric/2sm2h(2).json diff --git a/Mods/vcmi/config/vcmi/rmg/symmetric/2sm2h.JSON b/Mods/vcmi/Content/config/rmg/symmetric/2sm2h.json similarity index 100% rename from Mods/vcmi/config/vcmi/rmg/symmetric/2sm2h.JSON rename to Mods/vcmi/Content/config/rmg/symmetric/2sm2h.json diff --git a/Mods/vcmi/config/vcmi/rmg/symmetric/2sm2i(2).JSON b/Mods/vcmi/Content/config/rmg/symmetric/2sm2i(2).json similarity index 100% rename from Mods/vcmi/config/vcmi/rmg/symmetric/2sm2i(2).JSON rename to Mods/vcmi/Content/config/rmg/symmetric/2sm2i(2).json diff --git a/Mods/vcmi/config/vcmi/rmg/symmetric/2sm2i.JSON b/Mods/vcmi/Content/config/rmg/symmetric/2sm2i.json similarity index 100% rename from Mods/vcmi/config/vcmi/rmg/symmetric/2sm2i.JSON rename to Mods/vcmi/Content/config/rmg/symmetric/2sm2i.json diff --git a/Mods/vcmi/config/vcmi/rmg/symmetric/2sm4d(2).JSON b/Mods/vcmi/Content/config/rmg/symmetric/2sm4d(2).json similarity index 100% rename from Mods/vcmi/config/vcmi/rmg/symmetric/2sm4d(2).JSON rename to Mods/vcmi/Content/config/rmg/symmetric/2sm4d(2).json diff --git a/Mods/vcmi/config/vcmi/rmg/symmetric/2sm4d(3).JSON b/Mods/vcmi/Content/config/rmg/symmetric/2sm4d(3).json similarity index 100% rename from Mods/vcmi/config/vcmi/rmg/symmetric/2sm4d(3).JSON rename to Mods/vcmi/Content/config/rmg/symmetric/2sm4d(3).json diff --git a/Mods/vcmi/config/vcmi/rmg/symmetric/2sm4d.JSON b/Mods/vcmi/Content/config/rmg/symmetric/2sm4d.json similarity index 100% rename from Mods/vcmi/config/vcmi/rmg/symmetric/2sm4d.JSON rename to Mods/vcmi/Content/config/rmg/symmetric/2sm4d.json diff --git a/Mods/vcmi/config/vcmi/rmg/symmetric/3sb0b.JSON b/Mods/vcmi/Content/config/rmg/symmetric/3sb0b.json similarity index 100% rename from Mods/vcmi/config/vcmi/rmg/symmetric/3sb0b.JSON rename to Mods/vcmi/Content/config/rmg/symmetric/3sb0b.json diff --git a/Mods/vcmi/config/vcmi/rmg/symmetric/3sb0c.JSON b/Mods/vcmi/Content/config/rmg/symmetric/3sb0c.json similarity index 100% rename from Mods/vcmi/config/vcmi/rmg/symmetric/3sb0c.JSON rename to Mods/vcmi/Content/config/rmg/symmetric/3sb0c.json diff --git a/Mods/vcmi/config/vcmi/rmg/symmetric/3sm3d.JSON b/Mods/vcmi/Content/config/rmg/symmetric/3sm3d.json similarity index 100% rename from Mods/vcmi/config/vcmi/rmg/symmetric/3sm3d.JSON rename to Mods/vcmi/Content/config/rmg/symmetric/3sm3d.json diff --git a/Mods/vcmi/config/vcmi/rmg/symmetric/4sm0d.JSON b/Mods/vcmi/Content/config/rmg/symmetric/4sm0d.json similarity index 100% rename from Mods/vcmi/config/vcmi/rmg/symmetric/4sm0d.JSON rename to Mods/vcmi/Content/config/rmg/symmetric/4sm0d.json diff --git a/Mods/vcmi/config/vcmi/rmg/symmetric/4sm0f.JSON b/Mods/vcmi/Content/config/rmg/symmetric/4sm0f.json similarity index 100% rename from Mods/vcmi/config/vcmi/rmg/symmetric/4sm0f.JSON rename to Mods/vcmi/Content/config/rmg/symmetric/4sm0f.json diff --git a/Mods/vcmi/config/vcmi/rmg/symmetric/4sm0g.JSON b/Mods/vcmi/Content/config/rmg/symmetric/4sm0g.json similarity index 100% rename from Mods/vcmi/config/vcmi/rmg/symmetric/4sm0g.JSON rename to Mods/vcmi/Content/config/rmg/symmetric/4sm0g.json diff --git a/Mods/vcmi/config/vcmi/rmg/symmetric/4sm4e.JSON b/Mods/vcmi/Content/config/rmg/symmetric/4sm4e.json similarity index 100% rename from Mods/vcmi/config/vcmi/rmg/symmetric/4sm4e.JSON rename to Mods/vcmi/Content/config/rmg/symmetric/4sm4e.json diff --git a/Mods/vcmi/config/vcmi/rmg/symmetric/5sb0a.JSON b/Mods/vcmi/Content/config/rmg/symmetric/5sb0a.json similarity index 100% rename from Mods/vcmi/config/vcmi/rmg/symmetric/5sb0a.JSON rename to Mods/vcmi/Content/config/rmg/symmetric/5sb0a.json diff --git a/Mods/vcmi/config/vcmi/rmg/symmetric/5sb0b.JSON b/Mods/vcmi/Content/config/rmg/symmetric/5sb0b.json similarity index 100% rename from Mods/vcmi/config/vcmi/rmg/symmetric/5sb0b.JSON rename to Mods/vcmi/Content/config/rmg/symmetric/5sb0b.json diff --git a/Mods/vcmi/config/vcmi/rmg/symmetric/6lm10.JSON b/Mods/vcmi/Content/config/rmg/symmetric/6lm10.json similarity index 100% rename from Mods/vcmi/config/vcmi/rmg/symmetric/6lm10.JSON rename to Mods/vcmi/Content/config/rmg/symmetric/6lm10.json diff --git a/Mods/vcmi/config/vcmi/rmg/symmetric/6lm10a.JSON b/Mods/vcmi/Content/config/rmg/symmetric/6lm10a.json similarity index 100% rename from Mods/vcmi/config/vcmi/rmg/symmetric/6lm10a.JSON rename to Mods/vcmi/Content/config/rmg/symmetric/6lm10a.json diff --git a/Mods/vcmi/config/vcmi/rmg/symmetric/6sm0b.JSON b/Mods/vcmi/Content/config/rmg/symmetric/6sm0b.json similarity index 100% rename from Mods/vcmi/config/vcmi/rmg/symmetric/6sm0b.JSON rename to Mods/vcmi/Content/config/rmg/symmetric/6sm0b.json diff --git a/Mods/vcmi/config/vcmi/rmg/symmetric/6sm0d.JSON b/Mods/vcmi/Content/config/rmg/symmetric/6sm0d.json similarity index 100% rename from Mods/vcmi/config/vcmi/rmg/symmetric/6sm0d.JSON rename to Mods/vcmi/Content/config/rmg/symmetric/6sm0d.json diff --git a/Mods/vcmi/config/vcmi/rmg/symmetric/6sm0e.JSON b/Mods/vcmi/Content/config/rmg/symmetric/6sm0e.json similarity index 100% rename from Mods/vcmi/config/vcmi/rmg/symmetric/6sm0e.JSON rename to Mods/vcmi/Content/config/rmg/symmetric/6sm0e.json diff --git a/Mods/vcmi/config/vcmi/rmg/symmetric/7sb0b.JSON b/Mods/vcmi/Content/config/rmg/symmetric/7sb0b.json similarity index 100% rename from Mods/vcmi/config/vcmi/rmg/symmetric/7sb0b.JSON rename to Mods/vcmi/Content/config/rmg/symmetric/7sb0b.json diff --git a/Mods/vcmi/config/vcmi/rmg/symmetric/7sb0c.JSON b/Mods/vcmi/Content/config/rmg/symmetric/7sb0c.json similarity index 100% rename from Mods/vcmi/config/vcmi/rmg/symmetric/7sb0c.JSON rename to Mods/vcmi/Content/config/rmg/symmetric/7sb0c.json diff --git a/Mods/vcmi/config/vcmi/rmg/symmetric/8mm0e.JSON b/Mods/vcmi/Content/config/rmg/symmetric/8mm0e.json similarity index 100% rename from Mods/vcmi/config/vcmi/rmg/symmetric/8mm0e.JSON rename to Mods/vcmi/Content/config/rmg/symmetric/8mm0e.json diff --git a/Mods/vcmi/config/vcmi/rmg/symmetric/8mm6.JSON b/Mods/vcmi/Content/config/rmg/symmetric/8mm6.json similarity index 100% rename from Mods/vcmi/config/vcmi/rmg/symmetric/8mm6.JSON rename to Mods/vcmi/Content/config/rmg/symmetric/8mm6.json diff --git a/Mods/vcmi/config/vcmi/rmg/symmetric/8mm6a.JSON b/Mods/vcmi/Content/config/rmg/symmetric/8mm6a.json similarity index 100% rename from Mods/vcmi/config/vcmi/rmg/symmetric/8mm6a.JSON rename to Mods/vcmi/Content/config/rmg/symmetric/8mm6a.json diff --git a/Mods/vcmi/config/vcmi/rmg/symmetric/8sm0c.JSON b/Mods/vcmi/Content/config/rmg/symmetric/8sm0c.json similarity index 100% rename from Mods/vcmi/config/vcmi/rmg/symmetric/8sm0c.JSON rename to Mods/vcmi/Content/config/rmg/symmetric/8sm0c.json diff --git a/Mods/vcmi/config/vcmi/rmg/symmetric/8sm0f.JSON b/Mods/vcmi/Content/config/rmg/symmetric/8sm0f.json similarity index 100% rename from Mods/vcmi/config/vcmi/rmg/symmetric/8sm0f.JSON rename to Mods/vcmi/Content/config/rmg/symmetric/8sm0f.json diff --git a/Mods/vcmi/config/vcmi/rmg/symmetric/8xm12.JSON b/Mods/vcmi/Content/config/rmg/symmetric/8xm12.json similarity index 100% rename from Mods/vcmi/config/vcmi/rmg/symmetric/8xm12.JSON rename to Mods/vcmi/Content/config/rmg/symmetric/8xm12.json diff --git a/Mods/vcmi/config/vcmi/rmg/symmetric/8xm12a.JSON b/Mods/vcmi/Content/config/rmg/symmetric/8xm12a.json similarity index 100% rename from Mods/vcmi/config/vcmi/rmg/symmetric/8xm12a.JSON rename to Mods/vcmi/Content/config/rmg/symmetric/8xm12a.json diff --git a/Mods/vcmi/config/vcmi/rmg/symmetric/8xm8.JSON b/Mods/vcmi/Content/config/rmg/symmetric/8xm8.json similarity index 100% rename from Mods/vcmi/config/vcmi/rmg/symmetric/8xm8.JSON rename to Mods/vcmi/Content/config/rmg/symmetric/8xm8.json diff --git a/Mods/vcmi/config/vcmi/rmg/unknownUnused/2mm2h.JSON b/Mods/vcmi/Content/config/rmg/unknownUnused/2mm2h.json similarity index 100% rename from Mods/vcmi/config/vcmi/rmg/unknownUnused/2mm2h.JSON rename to Mods/vcmi/Content/config/rmg/unknownUnused/2mm2h.json diff --git a/Mods/vcmi/config/vcmi/rmg/unknownUnused/2x2sm4d(3).JSON b/Mods/vcmi/Content/config/rmg/unknownUnused/2x2sm4d(3).json similarity index 100% rename from Mods/vcmi/config/vcmi/rmg/unknownUnused/2x2sm4d(3).JSON rename to Mods/vcmi/Content/config/rmg/unknownUnused/2x2sm4d(3).json diff --git a/Mods/vcmi/config/vcmi/rmg/unknownUnused/4mm2h.JSON b/Mods/vcmi/Content/config/rmg/unknownUnused/4mm2h.json similarity index 100% rename from Mods/vcmi/config/vcmi/rmg/unknownUnused/4mm2h.JSON rename to Mods/vcmi/Content/config/rmg/unknownUnused/4mm2h.json diff --git a/Mods/vcmi/config/vcmi/rmg/unknownUnused/4sm3i.JSON b/Mods/vcmi/Content/config/rmg/unknownUnused/4sm3i.json similarity index 100% rename from Mods/vcmi/config/vcmi/rmg/unknownUnused/4sm3i.JSON rename to Mods/vcmi/Content/config/rmg/unknownUnused/4sm3i.json diff --git a/Mods/vcmi/config/vcmi/rmg/unknownUnused/6lm10a.JSON b/Mods/vcmi/Content/config/rmg/unknownUnused/6lm10a.json similarity index 100% rename from Mods/vcmi/config/vcmi/rmg/unknownUnused/6lm10a.JSON rename to Mods/vcmi/Content/config/rmg/unknownUnused/6lm10a.json diff --git a/Mods/vcmi/config/vcmi/rmg/unknownUnused/8xm12 huge.JSON b/Mods/vcmi/Content/config/rmg/unknownUnused/8xm12 huge.json similarity index 100% rename from Mods/vcmi/config/vcmi/rmg/unknownUnused/8xm12 huge.JSON rename to Mods/vcmi/Content/config/rmg/unknownUnused/8xm12 huge.json diff --git a/Mods/vcmi/config/vcmi/rmg/unknownUnused/8xm8 huge.JSON b/Mods/vcmi/Content/config/rmg/unknownUnused/8xm8 huge.json similarity index 100% rename from Mods/vcmi/config/vcmi/rmg/unknownUnused/8xm8 huge.JSON rename to Mods/vcmi/Content/config/rmg/unknownUnused/8xm8 huge.json diff --git a/Mods/vcmi/config/vcmi/rmg/unknownUnused/analogy.json b/Mods/vcmi/Content/config/rmg/unknownUnused/analogy.json similarity index 100% rename from Mods/vcmi/config/vcmi/rmg/unknownUnused/analogy.json rename to Mods/vcmi/Content/config/rmg/unknownUnused/analogy.json diff --git a/Mods/vcmi/config/vcmi/rmg/unknownUnused/cross.JSON b/Mods/vcmi/Content/config/rmg/unknownUnused/cross.json similarity index 100% rename from Mods/vcmi/config/vcmi/rmg/unknownUnused/cross.JSON rename to Mods/vcmi/Content/config/rmg/unknownUnused/cross.json diff --git a/Mods/vcmi/config/vcmi/rmg/unknownUnused/cross2.JSON b/Mods/vcmi/Content/config/rmg/unknownUnused/cross2.json similarity index 100% rename from Mods/vcmi/config/vcmi/rmg/unknownUnused/cross2.JSON rename to Mods/vcmi/Content/config/rmg/unknownUnused/cross2.json diff --git a/Mods/vcmi/config/vcmi/rmg/unknownUnused/cross3.JSON b/Mods/vcmi/Content/config/rmg/unknownUnused/cross3.json similarity index 100% rename from Mods/vcmi/config/vcmi/rmg/unknownUnused/cross3.JSON rename to Mods/vcmi/Content/config/rmg/unknownUnused/cross3.json diff --git a/Mods/vcmi/config/vcmi/rmg/unknownUnused/deux paires.JSON b/Mods/vcmi/Content/config/rmg/unknownUnused/deux paires.json similarity index 100% rename from Mods/vcmi/config/vcmi/rmg/unknownUnused/deux paires.JSON rename to Mods/vcmi/Content/config/rmg/unknownUnused/deux paires.json diff --git a/Mods/vcmi/config/vcmi/rmg/unknownUnused/doubled 8mm6.JSON b/Mods/vcmi/Content/config/rmg/unknownUnused/doubled 8mm6.json similarity index 100% rename from Mods/vcmi/config/vcmi/rmg/unknownUnused/doubled 8mm6.JSON rename to Mods/vcmi/Content/config/rmg/unknownUnused/doubled 8mm6.json diff --git a/Mods/vcmi/config/vcmi/rmg/unknownUnused/elka.JSON b/Mods/vcmi/Content/config/rmg/unknownUnused/elka.json similarity index 100% rename from Mods/vcmi/config/vcmi/rmg/unknownUnused/elka.JSON rename to Mods/vcmi/Content/config/rmg/unknownUnused/elka.json diff --git a/Mods/vcmi/config/vcmi/rmg/unknownUnused/goldenRing.json b/Mods/vcmi/Content/config/rmg/unknownUnused/goldenRing.json similarity index 100% rename from Mods/vcmi/config/vcmi/rmg/unknownUnused/goldenRing.json rename to Mods/vcmi/Content/config/rmg/unknownUnused/goldenRing.json diff --git a/Mods/vcmi/config/vcmi/rmg/unknownUnused/greatSands.JSON b/Mods/vcmi/Content/config/rmg/unknownUnused/greatSands.json similarity index 100% rename from Mods/vcmi/config/vcmi/rmg/unknownUnused/greatSands.JSON rename to Mods/vcmi/Content/config/rmg/unknownUnused/greatSands.json diff --git a/Mods/vcmi/config/vcmi/rmg/unknownUnused/kite.JSON b/Mods/vcmi/Content/config/rmg/unknownUnused/kite.json similarity index 100% rename from Mods/vcmi/config/vcmi/rmg/unknownUnused/kite.JSON rename to Mods/vcmi/Content/config/rmg/unknownUnused/kite.json diff --git a/Mods/vcmi/config/vcmi/rmg/unknownUnused/upgrade.json b/Mods/vcmi/Content/config/rmg/unknownUnused/upgrade.json similarity index 100% rename from Mods/vcmi/config/vcmi/rmg/unknownUnused/upgrade.json rename to Mods/vcmi/Content/config/rmg/unknownUnused/upgrade.json diff --git a/Mods/vcmi/config/vcmi/rmg/unknownUnused/wheel.JSON b/Mods/vcmi/Content/config/rmg/unknownUnused/wheel.json similarity index 100% rename from Mods/vcmi/config/vcmi/rmg/unknownUnused/wheel.JSON rename to Mods/vcmi/Content/config/rmg/unknownUnused/wheel.json diff --git a/Mods/vcmi/config/vcmi/russian.json b/Mods/vcmi/Content/config/russian.json similarity index 99% rename from Mods/vcmi/config/vcmi/russian.json rename to Mods/vcmi/Content/config/russian.json index ff088d60d..8417255fd 100644 --- a/Mods/vcmi/config/vcmi/russian.json +++ b/Mods/vcmi/Content/config/russian.json @@ -203,7 +203,7 @@ "mapObject.core.creatureBank.dragonFlyHive.name" : "Улей летучих змиев", "mapObject.core.creatureBank.dwarvenTreasury.name" : "Сокровищница гномов", "mapObject.core.creatureBank.griffinConservatory.name" : "Консерватория грифонов", - "mapObject.core.creatureBank.inpCache.name" : "Яма бесов", + "mapObject.core.creatureBank.impCache.name" : "Яма бесов", "mapObject.core.creatureBank.medusaStore.name" : "Склады медуз", "mapObject.core.creatureBank.nagaBank.name" : "Хранилище наг", "mapObject.core.crypt.crypt.name" : "Склеп", diff --git a/Mods/vcmi/config/vcmi/spanish.json b/Mods/vcmi/Content/config/spanish.json similarity index 99% rename from Mods/vcmi/config/vcmi/spanish.json rename to Mods/vcmi/Content/config/spanish.json index c4f4c5215..f1e82d286 100644 --- a/Mods/vcmi/config/vcmi/spanish.json +++ b/Mods/vcmi/Content/config/spanish.json @@ -79,8 +79,6 @@ "vcmi.server.errors.modsToEnable" : "{Se requieren los siguientes mods}", "vcmi.server.errors.modsToDisable" : "{Deben desactivarse los siguientes mods}", "vcmi.server.confirmReconnect" : "¿Quieres reconectar a la última sesión?", - "vcmi.server.errors.modNoDependency" : "Error al cargar el mod {'%s'}.\n Depende del mod {'%s'}, que no está activo.\n", - "vcmi.server.errors.modConflict" : "Error al cargar el mod {'%s'}.\n Conflicto con el mod activo {'%s'}.\n", "vcmi.server.errors.unknownEntity" : "Error al cargar la partida guardada. ¡Se encontró una entidad desconocida '%s' en la partida guardada! Es posible que la partida no sea compatible con la versión actualmente instalada de los mods.", "vcmi.settingsMainWindow.generalTab.hover" : "General", diff --git a/Mods/vcmi/Content/config/spells.json b/Mods/vcmi/Content/config/spells.json new file mode 100644 index 000000000..d8b7daa26 --- /dev/null +++ b/Mods/vcmi/Content/config/spells.json @@ -0,0 +1,66 @@ +{ + "core:summonDemons" : { + "name": "Summon Demons" + }, + "core:firstAid" : { + "name": "First Aid" + }, + "core:catapultShot" : { + "name": "Catapult shot" + }, + "core:cyclopsShot" : { + "name": "Siege shot" + }, + + "core:fireWallTrigger" : { + "name" : "Fire Wall" + }, + "core:landMineTrigger" : { + "name" : "Land Mine", + }, + "core:castleMoatTrigger" : { + "name": "Moat" + }, + "core:castleMoat": { + "name": "Moat" + }, + "core:rampartMoatTrigger" : { + "name": "Brambles" + }, + "core:rampartMoat": { + "name": "Brambles" + }, + "core:towerMoat": { + "name": "Land Mine" + }, + "core:infernoMoatTrigger" : { + "name": "Lava" + }, + "core:infernoMoat": { + "name": "Lava" + }, + "core:necropolisMoatTrigger" : { + "name": "Boneyard" + }, + "core:necropolisMoat": { + "name": "Boneyard" + }, + "core:dungeonMoatTrigger" : { + "name": "Boiling Oil" + }, + "core:dungeonMoat": { + "name": "Boiling Oil" + }, + "core:strongholdMoatTrigger" : { + "name": "Wooden Spikes" + }, + "core:strongholdMoat": { + "name": "Wooden Spikes" + }, + "core:fortressMoatTrigger" : { + "name": "Boiling Tar" + }, + "core:fortressMoat": { + "name": "Boiling Tar" + } +} diff --git a/Mods/vcmi/Content/config/swedish.json b/Mods/vcmi/Content/config/swedish.json new file mode 100644 index 000000000..9f5b68046 --- /dev/null +++ b/Mods/vcmi/Content/config/swedish.json @@ -0,0 +1,726 @@ +{ + "vcmi.adventureMap.monsterThreat.title" : "\n\nHotnivå: ", + "vcmi.adventureMap.monsterThreat.levels.0" : "Utan ansträngning", + "vcmi.adventureMap.monsterThreat.levels.1" : "Väldigt svag", + "vcmi.adventureMap.monsterThreat.levels.2" : "Svag", + "vcmi.adventureMap.monsterThreat.levels.3" : "Lite svagare", + "vcmi.adventureMap.monsterThreat.levels.4" : "Jämbördig", + "vcmi.adventureMap.monsterThreat.levels.5" : "Lite starkare", + "vcmi.adventureMap.monsterThreat.levels.6" : "Stark", + "vcmi.adventureMap.monsterThreat.levels.7" : "Väldigt stark", + "vcmi.adventureMap.monsterThreat.levels.8" : "Utmanande", + "vcmi.adventureMap.monsterThreat.levels.9" : "Överväldigande", + "vcmi.adventureMap.monsterThreat.levels.10" : "Dödlig", + "vcmi.adventureMap.monsterThreat.levels.11" : "Omöjlig", + "vcmi.adventureMap.monsterLevel" : "\n\nNivå: %LEVEL - Faktion: %TOWN", + "vcmi.adventureMap.monsterMeleeType" : "närstrid", + "vcmi.adventureMap.monsterRangedType" : "fjärrstrid", + "vcmi.adventureMap.search.hover" : "Sök kartobjekt", + "vcmi.adventureMap.search.help" : "Välj objekt för att söka på kartan.", + + "vcmi.adventureMap.confirmRestartGame" : "Är du säker på att du vill starta om spelet?", + "vcmi.adventureMap.noTownWithMarket" : "Det finns inga tillgängliga marknadsplatser!", + "vcmi.adventureMap.noTownWithTavern" : "Det finns inga tillgängliga städer med värdshus!", + "vcmi.adventureMap.spellUnknownProblem" : "Det finns ett okänt problem med den här formeln! Ingen mer information är tillgänglig.", + "vcmi.adventureMap.playerAttacked" : "Spelare har blivit attackerad: %s", + "vcmi.adventureMap.moveCostDetails" : "Förflyttningspoängs-kostnad: %TURNS tur(er) + %POINTS poäng - Återstående poäng: %REMAINING", + "vcmi.adventureMap.moveCostDetailsNoTurns" : "Förflyttningspoängs-kostnad: %POINTS poäng - Återstående poäng: %REMAINING", + "vcmi.adventureMap.movementPointsHeroInfo" : "(Förflyttningspoäng: %REMAINING / %POINTS)", + "vcmi.adventureMap.replayOpponentTurnNotImplemented" : "Tyvärr, att spela om motståndarens tur är inte implementerat ännu!", + + "vcmi.bonusSource.artifact" : "Artefakt", + "vcmi.bonusSource.creature" : "Förmåga", + "vcmi.bonusSource.spell" : "Trollformel", + "vcmi.bonusSource.hero" : "Hjälte", + "vcmi.bonusSource.commander": "Befälhavare", + "vcmi.bonusSource.other" : "Annan", + + "vcmi.capitalColors.0" : "Röd", + "vcmi.capitalColors.1" : "Blå", + "vcmi.capitalColors.2" : "Ljusbrun", + "vcmi.capitalColors.3" : "Grön", + "vcmi.capitalColors.4" : "Orange", + "vcmi.capitalColors.5" : "Lila", + "vcmi.capitalColors.6" : "Grönblå", + "vcmi.capitalColors.7" : "Rosa", + + "vcmi.heroOverview.startingArmy" : "Startarmé", + "vcmi.heroOverview.warMachine" : "Krigsmaskiner", + "vcmi.heroOverview.secondarySkills" : "Sekundärförmågor", + "vcmi.heroOverview.spells" : "Trollformler", + + "vcmi.quickExchange.moveUnit" : "Flytta enhet", + "vcmi.quickExchange.moveAllUnits" : "Flytta alla enheter", + "vcmi.quickExchange.swapAllUnits" : "Byt arméer", + "vcmi.quickExchange.moveAllArtifacts": "Flytta alla artefakter", + "vcmi.quickExchange.swapAllArtifacts": "Byt artefakter", + + "vcmi.radialWheel.mergeSameUnit" : "Slå samman varelser av samma sort", + "vcmi.radialWheel.fillSingleUnit" : "Fyll på med enstaka varelser", + "vcmi.radialWheel.splitSingleUnit" : "Dela av en enda varelse", + "vcmi.radialWheel.splitUnitEqually" : "Dela upp varelser lika", + "vcmi.radialWheel.moveUnit" : "Flytta varelser till en annan armé", + "vcmi.radialWheel.splitUnit" : "Dela upp varelseförband till en annan ruta", + + "vcmi.radialWheel.heroGetArmy" : "Hämta armé från annan hjälte", + "vcmi.radialWheel.heroSwapArmy" : "Byt armé med annan hjälte", + "vcmi.radialWheel.heroExchange" : "Öppna hjälteutbyte", + "vcmi.radialWheel.heroGetArtifacts" : "Hämta artefakter från annan hjälte", + "vcmi.radialWheel.heroSwapArtifacts" : "Byt artefakter med annan hjälte", + "vcmi.radialWheel.heroDismiss" : "Avfärda hjälten", + + "vcmi.radialWheel.moveTop" : "Flytta längst upp", + "vcmi.radialWheel.moveUp" : "Flytta upp", + "vcmi.radialWheel.moveDown" : "Flytta nedåt", + "vcmi.radialWheel.moveBottom" : "Flytta längst ner", + + "vcmi.randomMap.description" : "Kartan skapades av den slumpmässiga kartgeneratorn.\nMallen var %s, storlek %dx%d, nivåer %d, spelare %d, datorspelare %d, vatten %s, monster %s, VCMI karta", + "vcmi.randomMap.description.isHuman" : ", %s är människa", + "vcmi.randomMap.description.townChoice" : ", %s valde stadstyp: %s", + "vcmi.randomMap.description.water.none" : "inget", + "vcmi.randomMap.description.water.normal" : "normalt", + "vcmi.randomMap.description.water.islands" : "öar", + "vcmi.randomMap.description.monster.weak" : "svaga", + "vcmi.randomMap.description.monster.normal": "normala", + "vcmi.randomMap.description.monster.strong": "starka", + + "vcmi.spellBook.search" : "sök...", + + "vcmi.spellResearch.canNotAfford" : "Du har inte råd att byta ut '{%SPELL1}' med '{%SPELL2}'. Du kan fortfarande göra dig av med den här trollformeln och forska vidare.", + "vcmi.spellResearch.comeAgain" : "Forskningen har redan gjorts idag. Kom tillbaka imorgon.", + "vcmi.spellResearch.pay" : "Vill du byta ut '{%SPELL1}' med '{%SPELL2}'? Eller vill du göra dig av med den valda trollformeln och forska vidare?", + "vcmi.spellResearch.research" : "Forska fram denna trollformel", + "vcmi.spellResearch.skip" : "Strunta i denna trollformel", + "vcmi.spellResearch.abort" : "Avbryt", + "vcmi.spellResearch.noMoreSpells" : "Det finns inga fler trollformler tillgängliga för forskning.", + + "vcmi.mainMenu.serverConnecting" : "Ansluter...", + "vcmi.mainMenu.serverAddressEnter" : "Ange adress:", + "vcmi.mainMenu.serverConnectionFailed" : "Misslyckades med att ansluta", + "vcmi.mainMenu.serverClosing" : "Avslutar...", + "vcmi.mainMenu.hostTCP" : "Spela som värd (TCP/IP)", + "vcmi.mainMenu.joinTCP" : "Anslut till värd (TCP/IP)", + + "vcmi.lobby.filepath" : "Filsökväg", + "vcmi.lobby.creationDate" : "Skapelsedatum", + "vcmi.lobby.scenarioName" : "Namn på scenariot", + "vcmi.lobby.mapPreview" : "Förhandsgranskning av karta", + "vcmi.lobby.noPreview" : "ingen förhandsgranskning", + "vcmi.lobby.noUnderground" : "ingen underjord", + "vcmi.lobby.sortDate" : "Sorterar kartor efter ändringsdatum", + "vcmi.lobby.backToLobby" : "Återgå till lobbyn", + "vcmi.lobby.author" : "Skaparen av lobbyn", + "vcmi.lobby.handicap" : "Handikapp", + "vcmi.lobby.handicap.resource" : "Ger spelarna lämpliga resurser att börja med utöver de normala startresurserna. Negativa värden är tillåtna men är begränsade till 0 totalt (spelaren börjar aldrig med negativa resurser).", + "vcmi.lobby.handicap.income" : "Ändrar spelarens olika inkomster i procent (resultaten avrundas uppåt).", + "vcmi.lobby.handicap.growth" : "Ändrar tillväxttakten för varelser i de städer som ägs av spelaren (resultaten avrundas uppåt).", + "vcmi.lobby.deleteUnsupportedSave": "{Ostödda sparningar av spel hittades}\n\nVCMI har hittat %d sparade spelfiler som inte längre stöds, möjligen på grund av skillnader i VCMI-versioner.\n\nVill du ta bort dem?", + "vcmi.lobby.deleteSaveGameTitle" : "Välj ett sparat spel som ska raderas", + "vcmi.lobby.deleteMapTitle" : "Välj ett scenario som ska raderas", + "vcmi.lobby.deleteFile" : "Vill du radera följande fil?", + "vcmi.lobby.deleteFolder" : "Vill du radera följande mapp?", + "vcmi.lobby.deleteMode" : "Växla till raderingsläge och tillbaka", + + "vcmi.lobby.login.title" : "VCMI Online Lobby", + "vcmi.lobby.login.username" : "Användarnamn:", + "vcmi.lobby.login.connecting" : "Ansluter...", + "vcmi.lobby.login.error" : "Anslutningsfel: %s", + "vcmi.lobby.login.create" : "Nytt konto", + "vcmi.lobby.login.login" : "Logga in", + "vcmi.lobby.login.as" : "Logga in som %s", + "vcmi.lobby.header.rooms" : "Spelrum - %d", + "vcmi.lobby.header.channels" : "Chattkanaler", + "vcmi.lobby.header.chat.global" : "Global spelchatt - %s", // %s -> språknamn + "vcmi.lobby.header.chat.match" : "Chatt från föregående spel på %s", // %s -> datum och tid för spelstart + "vcmi.lobby.header.chat.player" : "Privat chatt med %s", // %s -> smeknamn på en annan spelare + "vcmi.lobby.header.history" : "Dina tidigare spel", + "vcmi.lobby.header.players" : "Spelare online - %d", + "vcmi.lobby.match.solo" : "Spel för en spelare", + "vcmi.lobby.match.duel" : "Spel med %s", // %s -> smeknamn på en annan spelare + "vcmi.lobby.match.multi" : "%d spelare", + "vcmi.lobby.room.create" : "Skapa nytt rum", + "vcmi.lobby.room.players.limit" : "Begränsning av spelare", + "vcmi.lobby.room.description.public" : "Alla spelare kan gå med i det offentliga rummet.", + "vcmi.lobby.room.description.private" : "Endast inbjudna spelare kan gå med i ett privat rum.", + "vcmi.lobby.room.description.new" : "För att starta spelet, välj ett scenario eller skapa en slumpmässig karta.", + "vcmi.lobby.room.description.load" : "Använd ett av dina sparade spel för att starta spelet.", + "vcmi.lobby.room.description.limit" : "Upp till %d spelare kan komma in i ditt rum (dig inkluderad).", + "vcmi.lobby.invite.header" : "Bjud in spelare", + "vcmi.lobby.invite.notification" : "Spelaren har bjudit in dig till sitt spelrum. Du kan nu gå med i deras privata rum.", + "vcmi.lobby.preview.title" : "Gå med i spelrummet", + "vcmi.lobby.preview.subtitle" : "Spel på karta/RMG-mall: %s - Värdens smeknamn: %s", //TL Notering: 1) namn på karta eller RMG-mall 2) smeknamn på spelvärden + "vcmi.lobby.preview.version" : "Spelversion:", + "vcmi.lobby.preview.players" : "Spelare:", + "vcmi.lobby.preview.mods" : "Moddar som används:", + "vcmi.lobby.preview.allowed" : "Gå med i spelrummet?", + "vcmi.lobby.preview.error.header" : "Det går inte att gå med i det här rummet.", + "vcmi.lobby.preview.error.playing" : "Du måste lämna ditt nuvarande spel först.", + "vcmi.lobby.preview.error.full" : "Rummet är redan fullt.", + "vcmi.lobby.preview.error.busy" : "Rummet tar inte längre emot nya spelare.", + "vcmi.lobby.preview.error.invite" : "Du blev inte inbjuden till det här rummet.", + "vcmi.lobby.preview.error.mods" : "Du använder en annan uppsättning moddar.", + "vcmi.lobby.preview.error.version" : "Du använder en annan version av VCMI.", + "vcmi.lobby.room.new" : "Nytt spel", + "vcmi.lobby.room.load" : "Ladda spel", + "vcmi.lobby.room.type" : "Rumstyp", + "vcmi.lobby.room.mode" : "Spelläge", + "vcmi.lobby.room.state.public" : "Offentligt", + "vcmi.lobby.room.state.private" : "Privat", + "vcmi.lobby.room.state.busy" : "I spel", + "vcmi.lobby.room.state.invited" : "Inbjuden", + "vcmi.lobby.mod.state.compatible" : "Kompatibel", + "vcmi.lobby.mod.state.disabled" : "Måste vara aktiverat", + "vcmi.lobby.mod.state.version" : "Versioner matchar inte", + "vcmi.lobby.mod.state.excessive" : "Måste vara inaktiverat", + "vcmi.lobby.mod.state.missing" : "Ej installerad", + "vcmi.lobby.pvp.coin.hover" : "Mynt", + "vcmi.lobby.pvp.coin.help" : "Singla slant", + "vcmi.lobby.pvp.randomTown.hover" : "Slumpmässig stad", + "vcmi.lobby.pvp.randomTown.help" : "Skriv en slumpad stad i chatten", + "vcmi.lobby.pvp.randomTownVs.hover" : "Slumpad stad vs.", + "vcmi.lobby.pvp.randomTownVs.help" : "Skriv två slumpade städer i chatten", + "vcmi.lobby.pvp.versus" : "vs.", + + "vcmi.client.errors.invalidMap" : "{Ogiltig karta eller kampanj}\n\nStartade inte spelet! Vald karta eller kampanj kan vara ogiltig eller skadad. Orsak:\n%s", + "vcmi.client.errors.missingCampaigns" : "{Saknade datafiler}\n\nKampanjernas datafiler hittades inte! Du kanske använder ofullständiga eller skadade Heroes 3-datafiler. Vänligen installera om speldata.", + "vcmi.server.errors.disconnected" : "{Nätverksfel}\n\nAnslutningen till spelservern har förlorats!", + "vcmi.server.errors.playerLeft" : "{Spelare har lämnat}\n\n%s spelaren har kopplat bort sig från spelet!", //%s -> spelarens färg + "vcmi.server.errors.existingProcess" : "En annan VCMI-serverprocess är igång. Vänligen avsluta den innan du startar ett nytt spel.", + "vcmi.server.errors.modsToEnable" : "{Följande modd(ar) krävs}", + "vcmi.server.errors.modsToDisable" : "{Följande modd(ar) måste inaktiveras}", + "vcmi.server.errors.modDependencyLoop": "Misslyckades med att ladda modd {'%s'}!\n Den kanske är i en (mjuk) beroendeloop.", + "vcmi.server.errors.unknownEntity" : "Misslyckades med att ladda sparat spel! Okänd enhet '%s' hittades i sparat spel! Sparningen kanske inte är kompatibel med den aktuella versionen av moddarna!", + + "vcmi.dimensionDoor.seaToLandError" : "Det går inte att teleportera sig från hav till land eller tvärtom med trollformeln 'Dimensionsdörr'.", + + "vcmi.settingsMainWindow.generalTab.hover" : "Allmänt", + "vcmi.settingsMainWindow.generalTab.help" : "Växlar till fliken/menyn med allmänna spelklients-inställningar relaterade till allmänt beteende för spelklienten.", + "vcmi.settingsMainWindow.battleTab.hover" : "Strid", + "vcmi.settingsMainWindow.battleTab.help" : "Växlar till fliken/menyn med strids-inställningar där man kan konfigurera spelets beteende under strider.", + "vcmi.settingsMainWindow.adventureTab.hover" : "Äventyrskarta", + "vcmi.settingsMainWindow.adventureTab.help" : "Växlar till fliken/menyn med inställningar som har med äventyrskartan att göra (äventyrskartan är den del av spelet där spelarna kan styra sina hjältars förflyttning på land, vatten och nere i underjorden).", + + "vcmi.systemOptions.videoGroup" : "Bild-inställningar", + "vcmi.systemOptions.audioGroup" : "Ljud-inställningar", + "vcmi.systemOptions.otherGroup" : "Andra inställningar", // unused right now + "vcmi.systemOptions.townsGroup" : "By-/Stads-skärm", + + "vcmi.statisticWindow.statistics" : "Statistik", + "vcmi.statisticWindow.tsvCopy" : "Statistik-data till urklipp", + "vcmi.statisticWindow.selectView" : "Välj vy", + "vcmi.statisticWindow.value" : "Värde", + "vcmi.statisticWindow.title.overview" : "Översikt", + "vcmi.statisticWindow.title.resources" : "Resurser", + "vcmi.statisticWindow.title.income" : "Inkomst", + "vcmi.statisticWindow.title.numberOfHeroes" : "Antal hjältar", + "vcmi.statisticWindow.title.numberOfTowns" : "Antal städer/byar", + "vcmi.statisticWindow.title.numberOfArtifacts" : "Antal artefakter", + "vcmi.statisticWindow.title.numberOfDwellings" : "Antal varelse-bon", + "vcmi.statisticWindow.title.numberOfMines" : "Antal gruvor", + "vcmi.statisticWindow.title.armyStrength" : "Arméns styrka", + "vcmi.statisticWindow.title.experience" : "Erfarenhet", + "vcmi.statisticWindow.title.resourcesSpentArmy" : "Armé-kostnader", + "vcmi.statisticWindow.title.resourcesSpentBuildings" : "Byggnadskostnader", + "vcmi.statisticWindow.title.mapExplored" : "Kart-utforskningsratio", + "vcmi.statisticWindow.param.playerName" : "Spelarens namn", + "vcmi.statisticWindow.param.daysSurvived" : "Överlevda dagar", + "vcmi.statisticWindow.param.maxHeroLevel" : "Den högsta hjältenivån", + "vcmi.statisticWindow.param.battleWinRatioHero" : "Vinstkvot (gentemot hjältar)", + "vcmi.statisticWindow.param.battleWinRatioNeutral" : "Vinstkvot (gentemot neutrala)", + "vcmi.statisticWindow.param.battlesHero" : "Strider (gentemot hjältar)", + "vcmi.statisticWindow.param.battlesNeutral" : "Strider (gentemot neutrala)", + "vcmi.statisticWindow.param.maxArmyStrength" : "Största totala arméstyrkan", + "vcmi.statisticWindow.param.tradeVolume" : "Handelsvolym", + "vcmi.statisticWindow.param.obeliskVisited" : "Obelisker besökta", + "vcmi.statisticWindow.icon.townCaptured" : "Städer/byar erövrade", + "vcmi.statisticWindow.icon.strongestHeroDefeated" : "Den starkaste motståndarhjälten som blivit besegrad", + "vcmi.statisticWindow.icon.grailFound" : "Graal funnen", + "vcmi.statisticWindow.icon.defeated" : "Besegrad", + + "vcmi.systemOptions.fullscreenBorderless.hover" : "Helskärm (kantlös)", + "vcmi.systemOptions.fullscreenBorderless.help" : "{Kantlös helskärm}\n\nI kantlöst helskärmsläge kommer spelet alltid att använda samma bildskärmsupplösning som valts i operativsystemet (bildskärmsupplösningen som valts i VCMI kommer ignoreras).", + "vcmi.systemOptions.fullscreenExclusive.hover" : "Exklusivt helskärmsläge", + "vcmi.systemOptions.fullscreenExclusive.help" : "{Helskärm}\n\nI exklusivt helskärmsläge kommer spelet att ändra bildskärmsupplösningen till det som valts i VCMI.", + "vcmi.systemOptions.resolutionButton.hover" : "Bildskärmsupplösning: %wx%h", + "vcmi.systemOptions.resolutionButton.help" : "{Bildskärmsupplösning}\n\nÄndrar bildskärmens upplösning i spelet.", + "vcmi.systemOptions.resolutionMenu.hover" : "Välj bildskärmsupplösningen i spelet", + "vcmi.systemOptions.resolutionMenu.help" : "Ändrar bildskärmsupplösning i spelet.", + "vcmi.systemOptions.scalingButton.hover" : "Gränssnittsskalning: %p%", + "vcmi.systemOptions.scalingButton.help" : "{Gränssnittsskalning}\n\nÄndrar storleken av de olika gränssnitten som finns i spelet.", + "vcmi.systemOptions.scalingMenu.hover" : "Välj gränssnittsskalning", + "vcmi.systemOptions.scalingMenu.help" : "Förstorar eller förminskar olika gränssnitt i spelet.", + "vcmi.systemOptions.longTouchButton.hover" : "Fördröjt tryckintervall: %d ms", // Översättningsnot: ’ms’ = "millisekunder" + "vcmi.systemOptions.longTouchButton.help" : "{Fördröjt tryckintervall}\n\nNär du använder dig av en pekskärm och vill komma åt spelalternativ behöver du göra en fördröjd pekskärmsberöring under en specifikt angiven tid (i millisekunder) för att en popup-meny skall synas.", + "vcmi.systemOptions.longTouchMenu.hover" : "Välj tidsintervall för fördröjd pekskärmsberöringsmeny", + "vcmi.systemOptions.longTouchMenu.help" : "Ändra varaktighetsintervallet för fördröjd beröring.", + "vcmi.systemOptions.longTouchMenu.entry" : "%d millisekunder", + "vcmi.systemOptions.framerateButton.hover" : "Visar FPS (skärmbilder per sekund)", + "vcmi.systemOptions.framerateButton.help" : "{Visa FPS}\n\nVisar räknaren för bildrutor per sekund i hörnet av spelfönstret.", + "vcmi.systemOptions.hapticFeedbackButton.hover" : "Haptisk återkoppling", + "vcmi.systemOptions.hapticFeedbackButton.help" : "{Haptisk feedback}\n\nÄndrar den haptiska feedbacken för berörings-input.", + "vcmi.systemOptions.enableUiEnhancementsButton.hover" : "Förbättringar av användargränssnittet", + "vcmi.systemOptions.enableUiEnhancementsButton.help" : "{Gränssnittsförbättringar}\n\nVälj mellan olika förbättringar av användargränssnittet. Till exempel en lättåtkomlig ryggsäcksknapp med mera. Avaktivera för att få en mer klassisk spelupplevelse.", + "vcmi.systemOptions.enableLargeSpellbookButton.hover" : "Stor trollformelsbok", + "vcmi.systemOptions.enableLargeSpellbookButton.help" : "{Stor trollformelsbok}\n\nAktiverar en större trollformelsbok som rymmer fler trollformler per sida (animeringen av sidbyte i den större trollformelsboken fungerar inte).", + "vcmi.systemOptions.audioMuteFocus.hover" : "Tyst vid inaktivitet", + "vcmi.systemOptions.audioMuteFocus.help" : "{Tyst vid inaktivitet}\n\nStänger av ljudet i spelet vid inaktivitet. Undantag är meddelanden i spelet och ljudet för ny turomgång.", + + "vcmi.adventureOptions.infoBarPick.hover" : "Visar textmeddelanden i infopanelen", + "vcmi.adventureOptions.infoBarPick.help" : "{Infopanelsmeddelanden}\n\nNär det är möjligt kommer spelmeddelanden från besökande kartobjekt att visas i infopanelen istället för att dyka upp i ett separat fönster.", + "vcmi.adventureOptions.numericQuantities.hover" : "Numeriska antal varelser", + "vcmi.adventureOptions.numericQuantities.help" : "{Numerisk varelsemängd}\n\nVisa ungefärliga mängder av fiendevarelser i det numeriska A-B-formatet.", + "vcmi.adventureOptions.forceMovementInfo.hover" : "Visa alltid förflyttningskostnad", + "vcmi.adventureOptions.forceMovementInfo.help" : "{Visa alltid förflyttningskostnad}\n\nVisar alltid förflyttningspoäng i statusfältet (istället för att bara visa dem när du håller ned ALT-tangenten).", + "vcmi.adventureOptions.showGrid.hover" : "Visa rutnät", + "vcmi.adventureOptions.showGrid.help" : "{Visa rutnät}\n\nVisa rutnätsöverlägget som markerar gränserna mellan äventyrskartans brickor/rutor.", + "vcmi.adventureOptions.borderScroll.hover" : "Kantrullning", + "vcmi.adventureOptions.borderScroll.help" : "{Kantrullning}\n\nRullar äventyrskartan när markören är angränsande till fönsterkanten. Kan inaktiveras genom att hålla ned CTRL-tangenten.", + "vcmi.adventureOptions.infoBarCreatureManagement.hover" : "Hantera armén i nedre högra hörnet", + "vcmi.adventureOptions.infoBarCreatureManagement.help" : "{Varelsehantering i infopanelen}\n\nTillåter omarrangering av varelser i infopanelen längst ner till höger på äventyrskartan istället för att bläddra mellan olika infopaneler.", + "vcmi.adventureOptions.leftButtonDrag.hover" : "V.klicksdragning", + "vcmi.adventureOptions.leftButtonDrag.help" : "{Vänsterklicksdragning}\n\nVid aktivering kan äventyrskartans kartvy dras genom att flytta musen med vänster musknapp nedtryckt.", + "vcmi.adventureOptions.rightButtonDrag.hover" : "H.klicksdragning", + "vcmi.adventureOptions.rightButtonDrag.help" : "{Högerklicksdragning}\n\nVid aktivering kan äventyrskartans kartvy dras genom att flytta musen med höger musknapp nedtryckt.", + "vcmi.adventureOptions.smoothDragging.hover" : "Mjuk kartdragning", + "vcmi.adventureOptions.smoothDragging.help" : "{Mjuk kartdragning}\n\nVid aktivering så har kartdragningen en modern rullningseffekt.", + "vcmi.adventureOptions.skipAdventureMapAnimations.hover" : "Skippar övertoningseffekter", + "vcmi.adventureOptions.skipAdventureMapAnimations.help" : "{Skippa övertoningseffekter}\n\nHoppar över ut- och intoningseffekten och liknande effekter av kartobjekt (resursinsamling, ombordning och avbordning av skepp osv.). Gör användargränssnittet mer reaktivt i vissa fall på bekostnad av estetiken. Speciellt användbart i PvP-spel. När maximal förflyttningshastighet är valt så är skippning av effekter aktiverat oavsett vad du har valt här.", + "vcmi.adventureOptions.mapScrollSpeed1.hover" : "", + "vcmi.adventureOptions.mapScrollSpeed5.hover" : "", + "vcmi.adventureOptions.mapScrollSpeed6.hover" : "", + "vcmi.adventureOptions.mapScrollSpeed1.help" : "Ställ in kartans rullningshastighet på 'Mycket långsam'.", + "vcmi.adventureOptions.mapScrollSpeed5.help" : "Ställ in kartans rullningshastighet på 'Mycket snabb'.", + "vcmi.adventureOptions.mapScrollSpeed6.help" : "Ställ in kartans rullningshastighet till 'Omedelbar'.", + "vcmi.adventureOptions.hideBackground.hover" : "Dölj bakgrund", + "vcmi.adventureOptions.hideBackground.help" : "{Dölj bakgrund}\n\nDöljer äventyrskartan i bakgrunden och visar en textur istället.", + + "vcmi.battleOptions.queueSizeLabel.hover" : "Visa turordningskö", + "vcmi.battleOptions.queueSizeNoneButton.hover" : "AV", + "vcmi.battleOptions.queueSizeAutoButton.hover" : "AUTO", + "vcmi.battleOptions.queueSizeSmallButton.hover" : "LITEN", + "vcmi.battleOptions.queueSizeBigButton.hover" : "STOR", + "vcmi.battleOptions.queueSizeNoneButton.help" : "Visa inte turordningskö.", + "vcmi.battleOptions.queueSizeAutoButton.help" : "Justera automatiskt storleken på turordningskön baserat på spelets skärmbildsupplösning ('LITEN' används när det är mindre än 700 pixlar i höjd, 'STOR' används annars).", + "vcmi.battleOptions.queueSizeSmallButton.help" : "Ställer in storleksinställningen på turordningskön till 'LITEN'.", + "vcmi.battleOptions.queueSizeBigButton.help" : "Ställer in storleksinställningen på turordningskön till 'STOR' (bildskärmsupplösningen måste överstiga 700 pixlar i höjd).", + "vcmi.battleOptions.animationsSpeed1.hover" : "", + "vcmi.battleOptions.animationsSpeed5.hover" : "", + "vcmi.battleOptions.animationsSpeed6.hover" : "", + "vcmi.battleOptions.animationsSpeed1.help" : "Ställ in animationshastigheten till mycket långsam.", + "vcmi.battleOptions.animationsSpeed5.help" : "Ställ in animationshastigheten till mycket snabb.", + "vcmi.battleOptions.animationsSpeed6.help" : "Ställ in animationshastigheten till omedelbar.", + "vcmi.battleOptions.movementHighlightOnHover.hover" : "Avslöja förflyttningsräckvidd", + "vcmi.battleOptions.movementHighlightOnHover.help" : "{Muspeka (hovra) för att avslöja förflyttningsräckvidd}\n\nVisar enheters potentiella förflyttningsräckvidd över slagfältet när du håller muspekaren över dem.", + "vcmi.battleOptions.rangeLimitHighlightOnHover.hover": "Avslöja skyttars räckvidd", + "vcmi.battleOptions.rangeLimitHighlightOnHover.help" : "{Muspeka för att avslöja skyttars räckvidd}\n\nVisar hur långt en enhets distansattack sträcker sig över slagfältet när du håller muspekaren över dem.", + "vcmi.battleOptions.showStickyHeroInfoWindows.hover" : "Visa fönster med hjältars primärförmågor", + "vcmi.battleOptions.showStickyHeroInfoWindows.help" : "{Visa fönster med hjältars primärförmågor}\n\nKommer alltid att visa ett fönster där du kan se dina hjältars primärförmågor (anfall, försvar, trollkonst, kunskap och trollformelpoäng).", + "vcmi.battleOptions.skipBattleIntroMusic.hover" : "Hoppa över intromusik", + "vcmi.battleOptions.skipBattleIntroMusic.help" : "{Hoppa över intromusik}\n\nTillåt åtgärder under intromusiken som spelas i början av varje strid.", + "vcmi.battleOptions.endWithAutocombat.hover" : "Snabbstrid (AI)", + "vcmi.battleOptions.endWithAutocombat.help" : "{Slutför striden så fort som möjligt}\n\nAI för auto-strid spelar striden åt dig för att striden ska slutföras så fort som möjligt.", + "vcmi.battleOptions.showQuickSpell.hover" : "Snabb åtkomst till dina trollformler", + "vcmi.battleOptions.showQuickSpell.help" : "{Visa snabbtrollformels-panelen}\n\nVisar en snabbvalspanel vid sidan av stridsfönstret där du har snabb åtkomst till några av dina trollformler", + + "vcmi.adventureMap.revisitObject.hover" : "Gör ett återbesök", + "vcmi.adventureMap.revisitObject.help" : "{Återbesök kartobjekt}\n\nEn hjälte som för närvarande står på ett kartobjekt kan göra ett återbesök (utan att först behöva avlägsna sig från kartobjektet för att sedan göra ett återbesök).", + + "vcmi.battleWindow.pressKeyToSkipIntro" : "Tryck på valfri tangent för att starta striden omedelbart", + "vcmi.battleWindow.damageEstimation.melee" : "Attackera %CREATURE (%DAMAGE).", + "vcmi.battleWindow.damageEstimation.meleeKills" : "Attackera %CREATURE (%DAMAGE, %KILLS).", + "vcmi.battleWindow.damageEstimation.ranged" : "Skjut %CREATURE (%SHOTS, %DAMAGE).", + "vcmi.battleWindow.damageEstimation.rangedKills" : "Skjut %CREATURE (%SHOTS, %DAMAGE, %KILLS).", + "vcmi.battleWindow.damageEstimation.shots" : "%d skott kvar", + "vcmi.battleWindow.damageEstimation.shots.1" : "%d skott kvar", + "vcmi.battleWindow.damageEstimation.damage" : "%d skada", + "vcmi.battleWindow.damageEstimation.damage.1" : "%d skada", + "vcmi.battleWindow.damageEstimation.kills" : "%d kommer att förgås", + "vcmi.battleWindow.damageEstimation.kills.1" : "%d kommer att förgås", + + "vcmi.battleWindow.damageRetaliation.will" : "Kommer att retaliera ", + "vcmi.battleWindow.damageRetaliation.may" : "Kommer kanske att retaliera ", + "vcmi.battleWindow.damageRetaliation.never" : "Kommer inte att retaliera.", + "vcmi.battleWindow.damageRetaliation.damage" : "(%DAMAGE).", + "vcmi.battleWindow.damageRetaliation.damageKills" : "(%DAMAGE, %KILLS).", + + "vcmi.battleWindow.killed" : "Dödad", + "vcmi.battleWindow.accurateShot.resultDescription.0" : "%d %s dödades av träffsäkra skott!", + "vcmi.battleWindow.accurateShot.resultDescription.1" : "%d %s dödades med ett träffsäkert skott!", + "vcmi.battleWindow.accurateShot.resultDescription.2" : "%d %s dödades av träffsäkra skott!", + "vcmi.battleWindow.endWithAutocombat" : "Är du säker på att du vill slutföra striden med auto-strid?", + + "vcmi.battleResultsWindow.applyResultsLabel" : "Acceptera stridsresultat?", + + "vcmi.tutorialWindow.title" : "Pekskärmsintroduktion", + "vcmi.tutorialWindow.decription.RightClick" : "Rör vid och håll kvar det element som du vill högerklicka på. Tryck på det fria området för att stänga.", + "vcmi.tutorialWindow.decription.MapPanning" : "Tryck och dra med ett finger för att flytta kartan.", + "vcmi.tutorialWindow.decription.MapZooming" : "Nyp med två fingrar för att ändra kartans zoom.", + "vcmi.tutorialWindow.decription.RadialWheel" : "Genom att svepa öppnas ett radiellt hjul för olika åtgärder t.ex. hantering av varelser/hjältar och stadsåtgärder.", + "vcmi.tutorialWindow.decription.BattleDirection" : "För att attackera från en viss riktning sveper du i den riktning från vilken attacken ska göras.", + "vcmi.tutorialWindow.decription.BattleDirectionAbort" : "Gesten för attackriktning kan avbrytas om du sveper tillräckligt långt bort.", + "vcmi.tutorialWindow.decription.AbortSpell" : "Tryck och håll för att avbryta en vald trollformel.", + + "vcmi.otherOptions.availableCreaturesAsDwellingLabel.hover" : "Visar tillgängliga varelser att rekrytera", + "vcmi.otherOptions.availableCreaturesAsDwellingLabel.help" : "{Visa tillgängliga varelser}\n\nVisa antalet varelser som finns tillgängliga att rekrytera istället för deras veckovisa förökning i stadsöversikten (nedre vänstra hörnet av stadsskärmen).", + "vcmi.otherOptions.creatureGrowthAsDwellingLabel.hover" : "Visar den veckovisa varelseförökningen", + "vcmi.otherOptions.creatureGrowthAsDwellingLabel.help" : "{Visa veckovis varelseförökning}\n\nVisa varelsers veckovisa förökning istället för antalet tillgängliga varelser i stadsöversikten (nedre vänstra hörnet av stadsskärmen).", + "vcmi.otherOptions.compactTownCreatureInfo.hover" : "Visa mindre varelse-info", + "vcmi.otherOptions.compactTownCreatureInfo.help" : "{Kompakt varelse-info}\n\nVisa mindre information för stadens varelser i stadsöversikten (nedre vänstra hörnet av stadsskärmen).", + + "vcmi.townHall.missingBase" : "Basbyggnaden '%s' måste byggas först", + "vcmi.townHall.noCreaturesToRecruit" : "Det finns inga varelser att rekrytera!", + + "vcmi.townStructure.bank.borrow" : "Du går in i banken. En bankman ser dig och säger: \"Vi har gjort ett specialerbjudande till dig. Du kan ta ett lån på 2500 guld från oss i 5 dagar. Du måste återbetala 500 guld varje dag.\"", + "vcmi.townStructure.bank.payBack" : "Du går in i banken. En bankman ser dig och säger: \"Du har redan fått ditt lån. Betala tillbaka det innan du tar ett nytt.\"", + + "vcmi.logicalExpressions.anyOf" : "Något av följande:", + "vcmi.logicalExpressions.allOf" : "Alla följande:", + "vcmi.logicalExpressions.noneOf" : "Inget av följande:", + + "vcmi.heroWindow.openCommander.hover" : "Öppna befälhavarens informationsfönster", + "vcmi.heroWindow.openCommander.help" : "Visar detaljer om befälhavaren för den här hjälten.", + "vcmi.heroWindow.openBackpack.hover" : "Öppna artefaktryggsäcksfönster", + "vcmi.heroWindow.openBackpack.help" : "Öppnar fönster som gör det enklare att hantera artefaktryggsäcken.", + "vcmi.heroWindow.sortBackpackByCost.hover" : "Sortera efter kostnad", + "vcmi.heroWindow.sortBackpackByCost.help" : "Sorterar artefakter i ryggsäcken efter kostnad.", + "vcmi.heroWindow.sortBackpackBySlot.hover" : "Sortera efter plats", + "vcmi.heroWindow.sortBackpackBySlot.help" : "Sorterar artefakter i ryggsäcken efter utrustad plats.", + "vcmi.heroWindow.sortBackpackByClass.hover" : "Sortera efter klass", + "vcmi.heroWindow.sortBackpackByClass.help" : "Sorterar artefakter i ryggsäcken efter artefaktklass (skatt, mindre, större, relik)", + "vcmi.heroWindow.fusingArtifact.fusing" : "Du har alla komponenterna som behövs för en sammanslagning av %s. Vill du utföra sammanslagningen? {Alla komponenter kommer att förbrukas vid sammanslagningen.}", + + "vcmi.tavernWindow.inviteHero" : "Bjud in hjälte", + + "vcmi.commanderWindow.artifactMessage" : "Vill du återlämna denna artefakt till hjälten?", + + "vcmi.creatureWindow.showBonuses.hover" : "Byt till bonusvy", + "vcmi.creatureWindow.showBonuses.help" : "Visa befälhavarens aktiva bonusar.", + "vcmi.creatureWindow.showSkills.hover" : "Byt till färdighetsvy", + "vcmi.creatureWindow.showSkills.help" : "Visa befälhavarens inlärda färdigheter.", + "vcmi.creatureWindow.returnArtifact.hover" : "Återlämna artefakt", + "vcmi.creatureWindow.returnArtifact.help" : "Klicka på den här knappen för att lägga tillbaka artefakten i hjältens ryggsäck.", + + "vcmi.questLog.hideComplete.hover" : "Dölj alla slutförda uppdrag", + "vcmi.questLog.hideComplete.help" : "Gömmer undan alla slutförda uppdrag.", + + "vcmi.randomMapTab.widgets.randomTemplate" : "(Slumpmässig)", + "vcmi.randomMapTab.widgets.templateLabel" : "Mall", + "vcmi.randomMapTab.widgets.teamAlignmentsButton" : "Ställ in...", + "vcmi.randomMapTab.widgets.teamAlignmentsLabel" : "Laggruppering", + "vcmi.randomMapTab.widgets.roadTypesLabel" : "Vägtyper", + + "vcmi.optionsTab.turnOptions.hover" : "Turomgångsalternativ", + "vcmi.optionsTab.turnOptions.help" : "Turomgångs-timer och samtidiga turomgångar (förinställningar)", + + "vcmi.optionsTab.chessFieldBase.hover" : "Bas-timern", + "vcmi.optionsTab.chessFieldTurn.hover" : "Tur-timern", + "vcmi.optionsTab.chessFieldBattle.hover" : "Strids-timern", + "vcmi.optionsTab.chessFieldUnit.hover" : "Enhets-timern", + "vcmi.optionsTab.chessFieldBase.help" : "Används när {Tur-timern} når 0. Ställs in i början av spelet. Vid 0 avslutas turomgången (pågående strid avslutas med förlust).", + "vcmi.optionsTab.chessFieldTurnAccumulate.help" : "Används utanför strid eller när {Strids-timern} tar slut. Återställs varje turomgång. Outnyttjad tid läggs till i {Bas-timern}.", + "vcmi.optionsTab.chessFieldTurnDiscard.help" : "Används utanför strid eller när {Strids-timern} tar slut. Återställs varje turomgång. Outnyttjad tid går förlorad.", + "vcmi.optionsTab.chessFieldBattle.help" : "Används i strider med AI eller i PvP-strid när {Enhets-timern} tar slut. Återställs i början av varje strid.", + "vcmi.optionsTab.chessFieldUnitAccumulate.help" : "Används när du styr dina enheter i PvP-strid. Outnyttjad tid läggs till i {Strids-timern} när enheten har avslutat sin turomgång.", + "vcmi.optionsTab.chessFieldUnitDiscard.help" : "Används när du styr dina enheter i PvP-strid. Återställs i början av varje enhets turomgång. Outnyttjad tid går förlorad.", + + "vcmi.optionsTab.accumulate" : "Ackumulera", + + "vcmi.optionsTab.simturnsTitle" : "Samtidiga turomgångar", + "vcmi.optionsTab.simturnsMin.hover" : "Åtminstone i", + "vcmi.optionsTab.simturnsMax.hover" : "Som mest i", + "vcmi.optionsTab.simturnsAI.hover" : "(Experimentell) Simultana AI-turomgångar", + "vcmi.optionsTab.simturnsMin.help" : "Spela samtidigt som andra spelare under ett angivet antal dagar. Kontakter mellan spelare under denna period är blockerade", + "vcmi.optionsTab.simturnsMax.help" : "Spela samtidigt som andra spelare under ett angivet antal dagar eller tills en tillräckligt nära kontakt inträffar med en annan spelare", + "vcmi.optionsTab.simturnsAI.help" : "{Simultana AI-turomgångar}\nExperimentellt alternativ. Tillåter AI-spelare att agera samtidigt som den mänskliga spelaren när simultana turomgångar är aktiverade.", + + "vcmi.optionsTab.turnTime.select" : "Ställ in turomgångs-timer", + "vcmi.optionsTab.turnTime.unlimited" : "Obegränsat med tid", + "vcmi.optionsTab.turnTime.classic.1" : "Klassisk timer: 1 minut", + "vcmi.optionsTab.turnTime.classic.2" : "Klassisk timer: 2 minuter", + "vcmi.optionsTab.turnTime.classic.5" : "Klassisk timer: 5 minuter", + "vcmi.optionsTab.turnTime.classic.10" : "Klassisk timer: 10 minuter", + "vcmi.optionsTab.turnTime.classic.20" : "Klassisk timer: 20 minuter", + "vcmi.optionsTab.turnTime.classic.30" : "Klassisk timer: 30 minuter", + "vcmi.optionsTab.turnTime.chess.20" : "Schack: 20:00 + 10:00 + 02:00 + 00:00", + "vcmi.optionsTab.turnTime.chess.16" : "Schack: 16:00 + 08:00 + 01:30 + 00:00", + "vcmi.optionsTab.turnTime.chess.8" : "Schack: 08:00 + 04:00 + 01:00 + 00:00", + "vcmi.optionsTab.turnTime.chess.4" : "Schack: 04:00 + 02:00 + 00:30 + 00:00", + "vcmi.optionsTab.turnTime.chess.2" : "Schack: 02:00 + 01:00 + 00:15 + 00:00", + "vcmi.optionsTab.turnTime.chess.1" : "Schack: 01:00 + 01:00 + 00:00 + 00:00", + + "vcmi.optionsTab.simturns.select" : "Ställ in samtidiga turomgångar", + "vcmi.optionsTab.simturns.none" : "Inga samtidiga turomgångar", + "vcmi.optionsTab.simturns.tillContactMax" : "Samtur: Fram till närkontakt", + "vcmi.optionsTab.simturns.tillContact1" : "Samtur: 1 vecka (bryts vid närkontakt)", + "vcmi.optionsTab.simturns.tillContact2" : "Samtur: 2 veckor (bryts vid närkontakt)", + "vcmi.optionsTab.simturns.tillContact4" : "Samtur: 1 månad (bryts vid närkontakt)", + "vcmi.optionsTab.simturns.blocked1" : "Samtur: 1 vecka (närkontakt blockerad)", + "vcmi.optionsTab.simturns.blocked2" : "Samtur: 2 veckor (närkontakt blockerad)", + "vcmi.optionsTab.simturns.blocked4" : "Samtur: 1 månad (närkontakt blockerad)", + + // 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 dagar", + "vcmi.optionsTab.simturns.days.1" : " %d dag", + "vcmi.optionsTab.simturns.days.2" : " %d dagar", + "vcmi.optionsTab.simturns.weeks.0" : " %d veckor", + "vcmi.optionsTab.simturns.weeks.1" : " %d vecka", + "vcmi.optionsTab.simturns.weeks.2" : " %d veckor", + "vcmi.optionsTab.simturns.months.0" : " %d månader", + "vcmi.optionsTab.simturns.months.1" : " %d månad", + "vcmi.optionsTab.simturns.months.2" : " %d månader", + + "vcmi.optionsTab.extraOptions.hover" : "Extra-inställningar", + "vcmi.optionsTab.extraOptions.help" : "Ytterligare spelinställningar", + + "vcmi.optionsTab.cheatAllowed.hover" : "Tillåter fusk i spelet", + "vcmi.optionsTab.unlimitedReplay.hover" : "Obegränsade omspelningar av strider", + "vcmi.optionsTab.cheatAllowed.help" : "{Tillåt fusk}\nTillåter inmatning av fuskkoder under spelets gång.", + "vcmi.optionsTab.unlimitedReplay.help" : "{Obegränsade stridsomspelningar}\nIngen begränsning för hur många gånger man kan spela om sina strider.", + + // Custom victory conditions for H3 campaigns and HotA maps + "vcmi.map.victoryCondition.daysPassed.toOthers" : "Fienden har lyckats överleva fram till denna dag. Segern är deras!", + "vcmi.map.victoryCondition.daysPassed.toSelf" : "Gratulerar! Du har lyckats överleva. Segern är er!", + "vcmi.map.victoryCondition.eliminateMonsters.toOthers" : "Fienden har besegrat alla monster som plågade detta land och utropar seger!", + "vcmi.map.victoryCondition.eliminateMonsters.toSelf" : "Gratulerar! Du har besegrat alla monster som plågade detta land och kan utropa seger!", + "vcmi.map.victoryCondition.collectArtifacts.message" : "Förvärva tre artefakter", + "vcmi.map.victoryCondition.angelicAlliance.toSelf" : "Gratulerar! Alla dina fiender har besegrats och du har 'Änglaalliansen'! Segern är din!", + "vcmi.map.victoryCondition.angelicAlliance.message" : "Besegra alla fiender och sätt ihop 'Änglaalliansen'", + "vcmi.map.victoryCondition.angelicAlliancePartLost.toSelf" : "Ack, du har förlorat en del av 'Änglaalliansen'. Allt är förlorat.", + + // few strings from WoG used by vcmi + "vcmi.stackExperience.description" : "» V a r e l s e f ö r b a n d s d e t a l j e r «\n\nVarelsetyp ................... : %s\nErfarenhetsrank ................. : %s (%i)\nErfarenhetspoäng ............... : %i\nErfarenhetspoäng till nästa rank .. : %i\nMaximal erfarenhet per strid ... : %i%% (%i)\nAntal varelser i förbandet .... : %i\nMaximalt antal nya rekryter\n utan att förlora nuvarande rank .... : %i\nErfarenhetsmultiplikator ........... : %.2f\nUppgradera erfarenhetsmultiplikator .............. : %.2f\nErfarenhet efter rank 10 ........ : %i\nMaximalt antal nyrekryteringar för att stanna kvar på\n rank 10 vid maximal erfarenhet : %i", + "vcmi.stackExperience.rank.0" : "Grundläggande", + "vcmi.stackExperience.rank.1" : "Novis", + "vcmi.stackExperience.rank.2" : "Tränad", + "vcmi.stackExperience.rank.3" : "Erfaren", + "vcmi.stackExperience.rank.4" : "Beprövad", + "vcmi.stackExperience.rank.5" : "Veteran", + "vcmi.stackExperience.rank.6" : "Adept", + "vcmi.stackExperience.rank.7" : "Expert", + "vcmi.stackExperience.rank.8" : "Elit", + "vcmi.stackExperience.rank.9" : "Mästare", + "vcmi.stackExperience.rank.10" : "Äss", + + // Strings for HotA Seer Hut / Quest Guards + "core.seerhut.quest.heroClass.complete.0" : "Ah, du är %s. Här är en gåva till dig. Tar du emot den?", + "core.seerhut.quest.heroClass.complete.1" : "Ah, du är %s. Här är en gåva till dig. Tar du emot den?", + "core.seerhut.quest.heroClass.complete.2" : "Ah, du är %s. Här är en gåva till dig. Tar du emot den?", + "core.seerhut.quest.heroClass.complete.3" : "Vakterna ser att du är %s och erbjuder att låta dig passera. Accepterar du?", + "core.seerhut.quest.heroClass.complete.4" : "Vakterna ser att du är %s och erbjuder att låta dig passera. Accepterar du?", + "core.seerhut.quest.heroClass.complete.5" : "Vakterna ser att du är %s och erbjuder att låta dig passera. Accepterar du?", + "core.seerhut.quest.heroClass.description.0" : "Skicka %s till %s", + "core.seerhut.quest.heroClass.description.1" : "Skicka %s till %s", + "core.seerhut.quest.heroClass.description.2" : "Skicka %s till %s", + "core.seerhut.quest.heroClass.description.3" : "Skicka %s för att öppna grinden", + "core.seerhut.quest.heroClass.description.4" : "Skicka %s till öppen grind", + "core.seerhut.quest.heroClass.description.5" : "Skicka %s till öppen grind", + "core.seerhut.quest.heroClass.hover.0" : "(söker hjälte i %s klass)", + "core.seerhut.quest.heroClass.hover.1" : "(söker hjälte i %s klass)", + "core.seerhut.quest.heroClass.hover.2" : "(söker hjälte i %s klass)", + "core.seerhut.quest.heroClass.hover.3" : "(söker hjälte i %s klass)", + "core.seerhut.quest.heroClass.hover.4" : "(söker hjälte i %s klass)", + "core.seerhut.quest.heroClass.hover.5" : "(söker hjälte i %s klass)", + "core.seerhut.quest.heroClass.receive.0" : "Jag har en gåva till %s.", + "core.seerhut.quest.heroClass.receive.1" : "Jag har en gåva till %s.", + "core.seerhut.quest.heroClass.receive.2" : "Jag har en gåva till %s.", + "core.seerhut.quest.heroClass.receive.3" : "Vakterna här säger att de bara kommer att låta %s passera.", + "core.seerhut.quest.heroClass.receive.4" : "Vakterna här säger att de bara kommer att låta %s passera.", + "core.seerhut.quest.heroClass.receive.5" : "Vakterna här säger att de bara kommer att låta %s passera.", + "core.seerhut.quest.heroClass.visit.0" : "Du är inte %s. Det finns inget här för dig att hämta. Försvinn!", + "core.seerhut.quest.heroClass.visit.1" : "Du är inte %s. Det finns inget här för dig att hämta. Försvinn!", + "core.seerhut.quest.heroClass.visit.2" : "Du är inte %s. Det finns inget här för dig att hämta. Försvinn!", + "core.seerhut.quest.heroClass.visit.3" : "Vakterna här kommer bara att låta %s passera.", + "core.seerhut.quest.heroClass.visit.4" : "Vakterna här kommer bara att låta %s passera.", + "core.seerhut.quest.heroClass.visit.5" : "Vakterna här kommer bara att låta %s passera.", + + "core.seerhut.quest.reachDate.complete.0" : "Jag är fri nu. Det här är vad jag har att erbjuda dig. Accepterar du?", + "core.seerhut.quest.reachDate.complete.1" : "Jag är fri nu. Det här är vad jag har att erbjuda dig. Accepterar du?", + "core.seerhut.quest.reachDate.complete.2" : "Jag är fri nu. Det här är vad jag har att erbjuda dig. Accepterar du?", + "core.seerhut.quest.reachDate.complete.3" : "Du är fri att gå igenom nu. Önskar ni att passera?", + "core.seerhut.quest.reachDate.complete.4" : "Du är fri att gå igenom nu. Önskar ni att passera?", + "core.seerhut.quest.reachDate.complete.5" : "Du är fri att gå igenom nu. Önskar ni att passera?", + "core.seerhut.quest.reachDate.description.0" : "Vänta tills %s för %s", + "core.seerhut.quest.reachDate.description.1" : "Vänta tills %s för %s", + "core.seerhut.quest.reachDate.description.2" : "Vänta tills %s för %s", + "core.seerhut.quest.reachDate.description.3" : "Vänta tills %s öppnar grinden", + "core.seerhut.quest.reachDate.description.4" : "Vänta tills %s öppnar grinden", + "core.seerhut.quest.reachDate.description.5" : "Vänta tills %s öppnar grinden", + "core.seerhut.quest.reachDate.hover.0" : "(Återvänd inte före %s)", + "core.seerhut.quest.reachDate.hover.1" : "(Återvänd inte före %s)", + "core.seerhut.quest.reachDate.hover.2" : "(Återvänd inte före %s)", + "core.seerhut.quest.reachDate.hover.3" : "(Återvänd inte före %s)", + "core.seerhut.quest.reachDate.hover.4" : "(Återvänd inte före %s)", + "core.seerhut.quest.reachDate.hover.5" : "(Återvänd inte före %s)", + "core.seerhut.quest.reachDate.receive.0" : "Jag är upptagen. Kom inte tillbaka före %s", + "core.seerhut.quest.reachDate.receive.1" : "Jag är upptagen. Kom inte tillbaka före %s", + "core.seerhut.quest.reachDate.receive.2" : "Jag är upptagen. Kom inte tillbaka före %s", + "core.seerhut.quest.reachDate.receive.3" : "Stängt fram till %s.", + "core.seerhut.quest.reachDate.receive.4" : "Stängt fram till %s.", + "core.seerhut.quest.reachDate.receive.5" : "Stängt fram till %s.", + "core.seerhut.quest.reachDate.visit.0" : "Jag är upptagen. Kom inte tillbaka före %s.", + "core.seerhut.quest.reachDate.visit.1" : "Jag är upptagen. Kom inte tillbaka före %s.", + "core.seerhut.quest.reachDate.visit.2" : "Jag är upptagen. Kom inte tillbaka före %s.", + "core.seerhut.quest.reachDate.visit.3" : "Stängt fram till %s.", + "core.seerhut.quest.reachDate.visit.4" : "Stängt fram till %s.", + "core.seerhut.quest.reachDate.visit.5" : "Stängt fram till %s.", + + "mapObject.core.hillFort.object.description" : "Uppgraderar varelser. Nivåerna 1 - 4 är billigare än i associerad stad.", + + "core.bonus.ADDITIONAL_ATTACK.name" : "Dubbelslag", + "core.bonus.ADDITIONAL_ATTACK.description" : "Attackerar två gånger.", + "core.bonus.ADDITIONAL_RETALIATION.name" : "Ytterligare motattacker", + "core.bonus.ADDITIONAL_RETALIATION.description" : "Kan slå tillbaka ${val} extra gång(er).", + "core.bonus.AIR_IMMUNITY.name" : "Luft-immunitet", + "core.bonus.AIR_IMMUNITY.description" : "Immun mot alla luftmagi-trollformler.", + "core.bonus.ATTACKS_ALL_ADJACENT.name" : "Attackera runtomkring", + "core.bonus.ATTACKS_ALL_ADJACENT.description" : "Attackerar alla angränsande fiender.", + "core.bonus.BLOCKS_RETALIATION.name" : "Retaliera ej i närstrid", + "core.bonus.BLOCKS_RETALIATION.description" : "Fienden kan inte slå tillbaka/retaliera.", + "core.bonus.BLOCKS_RANGED_RETALIATION.name" : "Retaliera ej på avstånd", + "core.bonus.BLOCKS_RANGED_RETALIATION.description" : "Fienden kan inte retaliera på avstånd.", + "core.bonus.CATAPULT.name" : "Katapult-attack", + "core.bonus.CATAPULT.description" : "Attackerar belägringsmurar.", + "core.bonus.CHANGES_SPELL_COST_FOR_ALLY.name" : "Minska magikostnad (${val})", + "core.bonus.CHANGES_SPELL_COST_FOR_ALLY.description" : "Minskar magikostnaden för hjälten med ${val}.", + "core.bonus.CHANGES_SPELL_COST_FOR_ENEMY.name" : "Magisk dämpare (${val})", + "core.bonus.CHANGES_SPELL_COST_FOR_ENEMY.description": "Ökar fiendens magikostnad med ${val}.", + "core.bonus.CHARGE_IMMUNITY.name" : "Galoppanfalls-immunitet", + "core.bonus.CHARGE_IMMUNITY.description" : "Immun mot ryttares galopperande ridanfall.", + "core.bonus.DARKNESS.name" : "I skydd av mörkret", + "core.bonus.DARKNESS.description" : "Skapar ett mörkerhölje med rutradien ${val}.", + "core.bonus.DEATH_STARE.name" : "Dödsblick (${val}%)", + "core.bonus.DEATH_STARE.description" : "Varje dödsblick har ${val}% chans att döda.", + "core.bonus.DEFENSIVE_STANCE.name" : "Försvarshållning", + "core.bonus.DEFENSIVE_STANCE.description" : "+${val} extra försvar när du försvarar dig.", + "core.bonus.DESTRUCTION.name" : "Förintelse", + "core.bonus.DESTRUCTION.description" : "${val}% chans att ta död på fler efter attack.", + "core.bonus.DOUBLE_DAMAGE_CHANCE.name" : "Dödsstöt", + "core.bonus.DOUBLE_DAMAGE_CHANCE.description" : "${val}% chans till dubbel basskada vid attack.", + "core.bonus.DRAGON_NATURE.name" : "Drake", + "core.bonus.DRAGON_NATURE.description" : "Varelsen har en draknatur.", + "core.bonus.EARTH_IMMUNITY.name" : "Jord-immunitet", + "core.bonus.EARTH_IMMUNITY.description" : "Immun mot alla jordmagi-trollformler.", + "core.bonus.ENCHANTER.name" : "Förtrollare", + "core.bonus.ENCHANTER.description" : "Kastar mass-${subtype.spell} varje turomgång.", + "core.bonus.ENCHANTED.name" : "Förtrollad", + "core.bonus.ENCHANTED.description" : "Påverkas av permanent ${subtype.spell}.", + "core.bonus.ENEMY_ATTACK_REDUCTION.name" : "Avfärda attack (${val}%)", + "core.bonus.ENEMY_ATTACK_REDUCTION.description" : "Ignorerar ${val}% av angriparens attack.", + "core.bonus.ENEMY_DEFENCE_REDUCTION.name" : "Förbigå försvar (${val}%)", + "core.bonus.ENEMY_DEFENCE_REDUCTION.description" : "Attacker ignorerar ${val}% av fiendens försvar.", + "core.bonus.FIRE_IMMUNITY.name" : "Eld-immunitet", + "core.bonus.FIRE_IMMUNITY.description" : "Immun mot alla eldmagi-trollformler.", + "core.bonus.FIRE_SHIELD.name" : "Eldsköld (${val}%)", + "core.bonus.FIRE_SHIELD.description" : "Reflekterar en del av närstridsskadorna.", + "core.bonus.FIRST_STRIKE.name" : "Första slaget", + "core.bonus.FIRST_STRIKE.description" : "Retalierar innan den blir attackerad.", + "core.bonus.FEAR.name" : "Rädsla", + "core.bonus.FEAR.description" : "Orsakar rädsla på ett fiendeförband.", + "core.bonus.FEARLESS.name" : "Orädd", + "core.bonus.FEARLESS.description" : "Immun mot rädsla.", + "core.bonus.FEROCITY.name" : "Vildsint", + "core.bonus.FEROCITY.description" : "+${val} extra attack(er) om någon dödas.", + "core.bonus.FLYING.name" : "Flygande", + "core.bonus.FLYING.description" : "Flyger vid förflyttning (ignorerar hinder).", + "core.bonus.FREE_SHOOTING.name" : "Skjut på nära håll", + "core.bonus.FREE_SHOOTING.description" : "Använd distansattacker på närstridsavstånd.", + "core.bonus.GARGOYLE.name" : "Stenfigur", + "core.bonus.GARGOYLE.description" : "Kan varken upplivas eller läkas.", + "core.bonus.GENERAL_DAMAGE_REDUCTION.name" : "Minska skada (${val}%)", + "core.bonus.GENERAL_DAMAGE_REDUCTION.description" : "Reducerar skadan från fiendens attacker.", + "core.bonus.HATE.name" : "Hatar: ${subtype.creature}", + "core.bonus.HATE.description" : "Gör ${val}% mer skada mot ${subtype.creature}.", + "core.bonus.HEALER.name" : "Helare", + "core.bonus.HEALER.description" : "Helar/läker allierade enheter.", + "core.bonus.HP_REGENERATION.name" : "Självläkande", + "core.bonus.HP_REGENERATION.description" : "Återfår ${val} hälsa (träffpoäng) varje runda.", + "core.bonus.JOUSTING.name" : "Galopperande ridanfall", + "core.bonus.JOUSTING.description" : "+${val}% skada per rutförflyttning före attack.", + "core.bonus.KING.name" : "Kung", + "core.bonus.KING.description" : "Sårbar för Dräpar-nivå ${val} eller högre.", + "core.bonus.LEVEL_SPELL_IMMUNITY.name" : "Trolldomsimmunitet 1-${val}", + "core.bonus.LEVEL_SPELL_IMMUNITY.description" : "Immun mot nivå 1-${val}-trollformler.", + "core.bonus.LIMITED_SHOOTING_RANGE.name" : "Begränsad skjuträckvidd", + "core.bonus.LIMITED_SHOOTING_RANGE.description" : "Skjuträckvidd: ${val} rutor.", + "core.bonus.LIFE_DRAIN.name" : "Dränera livskraft (${val}%)", + "core.bonus.LIFE_DRAIN.description" : "Dränera ${val}% hälsa av den vållade skadan.", + "core.bonus.MANA_CHANNELING.name" : "Kanalisera magi (${val}%)", + "core.bonus.MANA_CHANNELING.description" : "Ger din hjälte ${val}% av fiendens spenderade mana.", + "core.bonus.MANA_DRAIN.name" : "Dränera mana", + "core.bonus.MANA_DRAIN.description" : "Dränerar ${val} mana från fienden varje tur.", + "core.bonus.MAGIC_MIRROR.name" : "Magisk spegel (${val}%)", + "core.bonus.MAGIC_MIRROR.description" : "${val}% chans att reflektera skadliga trollformler.", + "core.bonus.MAGIC_RESISTANCE.name" : "Magiskt motstånd (${val}%)", + "core.bonus.MAGIC_RESISTANCE.description" : "${val}% chans att motstå en skadlig trollformel.", + "core.bonus.MIND_IMMUNITY.name" : "Immun mot sinnesmagi", + "core.bonus.MIND_IMMUNITY.description" : "Immun mot magi som påverkar dina sinnen.", + "core.bonus.NO_DISTANCE_PENALTY.name" : "Långdistansskytt", + "core.bonus.NO_DISTANCE_PENALTY.description" : "Gör full skada på alla avstånd i strid.", + "core.bonus.NO_MELEE_PENALTY.name" : "Närstridsspecialist", + "core.bonus.NO_MELEE_PENALTY.description" : "Ingen närstridsbestraffning.", + "core.bonus.NO_MORALE.name" : "Ingen moralpåverkan", + "core.bonus.NO_MORALE.description" : "Immun mot moral-effekter (neutral moral).", + "core.bonus.NO_WALL_PENALTY.name" : "Ingen murbestraffning", + "core.bonus.NO_WALL_PENALTY.description" : "Gör full skada mot fiender bakom en mur.", + "core.bonus.NON_LIVING.name" : "Icke levande", + "core.bonus.NON_LIVING.description" : "Immunitet mot många effekter.", + "core.bonus.RANDOM_SPELLCASTER.name" : "Slumpmässig besvärjare", + "core.bonus.RANDOM_SPELLCASTER.description" : "Kastar trollformler som väljs slumpmässigt.", + "core.bonus.RANGED_RETALIATION.name" : "Motattacker på avstånd", + "core.bonus.RANGED_RETALIATION.description" : "Kan retaliera/motattackera på avstånd.", + "core.bonus.RECEPTIVE.name" : "Magiskt mottaglig", + "core.bonus.RECEPTIVE.description" : "Ingen immunitet mot vänliga trollformler.", + "core.bonus.REBIRTH.name" : "Återfödelse (${val}%)", + "core.bonus.REBIRTH.description" : "${val}% återuppväcks efter döden.", + "core.bonus.RETURN_AFTER_STRIKE.name" : "Återvänder efter närstrid", + "core.bonus.RETURN_AFTER_STRIKE.description" : "Återvänder till sin ruta efter attack.", + "core.bonus.REVENGE.name" : "Hämndlysten", + "core.bonus.REVENGE.description" : "Vållar mer skada om den själv blivit skadad.", + "core.bonus.SHOOTER.name" : "Distans-attack", + "core.bonus.SHOOTER.description" : "Skjuter/attackerar på avstånd.", + "core.bonus.SHOOTS_ALL_ADJACENT.name" : "Skjuter alla i närheten", + "core.bonus.SHOOTS_ALL_ADJACENT.description" : "Distans-attack drabbar alla inom räckhåll.", + "core.bonus.SOUL_STEAL.name" : "Själtjuv", + "core.bonus.SOUL_STEAL.description" : "För varje dödad fiende återuppväcks: ${val}.", + "core.bonus.SPELLCASTER.name" : "Besvärjare", + "core.bonus.SPELLCASTER.description" : "Kan kasta: ${subtype.spell}.", + "core.bonus.SPELL_AFTER_ATTACK.name" : "Besvärja efter attack", + "core.bonus.SPELL_AFTER_ATTACK.description" : "${val}% chans för '${subtype.spell}' efter attack.", + "core.bonus.SPELL_BEFORE_ATTACK.name" : "Besvärja före attack", + "core.bonus.SPELL_BEFORE_ATTACK.description" : "${val}% chans för '${subtype.spell}' före attack.", + "core.bonus.SPELL_DAMAGE_REDUCTION.name" : "Trolldoms-resistens", + "core.bonus.SPELL_DAMAGE_REDUCTION.description" : "Reducerar magisk-skada med ${val}%.", + "core.bonus.SPELL_IMMUNITY.name" : "Trolldoms-immunitet", + "core.bonus.SPELL_IMMUNITY.description" : "Immun mot '${subtype.spell}'.", + "core.bonus.SPELL_LIKE_ATTACK.name" : "Magisk attack", + "core.bonus.SPELL_LIKE_ATTACK.description" : "Attackerar med '${subtype.spell}'.", + "core.bonus.SPELL_RESISTANCE_AURA.name" : "Motståndsaura", + "core.bonus.SPELL_RESISTANCE_AURA.description" : "Angränsande förband får ${val}% magi-resistens.", + "core.bonus.SUMMON_GUARDIANS.name" : "Åkalla väktare", + "core.bonus.SUMMON_GUARDIANS.description" : "Vid strid åkallas: ${subtype.creature} ${val}%.", + "core.bonus.SYNERGY_TARGET.name" : "Synergibar", + "core.bonus.SYNERGY_TARGET.description" : "Denna varelse är sårbar för synergieffekt.", + "core.bonus.TWO_HEX_ATTACK_BREATH.name" : "Dödlig andedräkt", + "core.bonus.TWO_HEX_ATTACK_BREATH.description" : "Andningsattack (2 rutors räckvidd).", + "core.bonus.THREE_HEADED_ATTACK.name" : "Trehövdad attack", + "core.bonus.THREE_HEADED_ATTACK.description" : "Attackerar upp till tre enheter framför sig.", + "core.bonus.TRANSMUTATION.name" : "Transmutation", + "core.bonus.TRANSMUTATION.description" : "${val}% chans att förvandla angripen enhet.", + "core.bonus.UNDEAD.name" : "Odöd", + "core.bonus.UNDEAD.description" : "Varelsen är odöd.", + "core.bonus.UNLIMITED_RETALIATIONS.name" : "Slår tillbaka varje gång", + "core.bonus.UNLIMITED_RETALIATIONS.description" : "Obegränsat antal motattacker.", + "core.bonus.WATER_IMMUNITY.name" : "Vatten-immunitet", + "core.bonus.WATER_IMMUNITY.description" : "Immun mot alla vattenmagi-trollformler.", + "core.bonus.WIDE_BREATH.name" : "Bred dödlig andedräkt", + "core.bonus.WIDE_BREATH.description" : "Bred andningsattack (flera rutor).", + "core.bonus.DISINTEGRATE.name" : "Desintegrerar", + "core.bonus.DISINTEGRATE.description" : "Ingen kropp lämnas kvar på slagfältet.", + "core.bonus.INVINCIBLE.name" : "Oövervinnerlig", + "core.bonus.INVINCIBLE.description" : "Kan inte påverkas av någonting.", + "core.bonus.MECHANICAL.name" : "Mekanisk", + "core.bonus.MECHANICAL.description" : "Immun mot många effekter, reparerbar.", + "core.bonus.PRISM_HEX_ATTACK_BREATH.name" : "Prism-andedräkt", + "core.bonus.PRISM_HEX_ATTACK_BREATH.description" : "Treriktad andedräkt." +} diff --git a/Mods/vcmi/config/vcmi/towerCreature.json b/Mods/vcmi/Content/config/towerCreature.json similarity index 100% rename from Mods/vcmi/config/vcmi/towerCreature.json rename to Mods/vcmi/Content/config/towerCreature.json diff --git a/Mods/vcmi/config/vcmi/towerFactions.json b/Mods/vcmi/Content/config/towerFactions.json similarity index 100% rename from Mods/vcmi/config/vcmi/towerFactions.json rename to Mods/vcmi/Content/config/towerFactions.json diff --git a/Mods/vcmi/config/vcmi/ukrainian.json b/Mods/vcmi/Content/config/ukrainian.json similarity index 97% rename from Mods/vcmi/config/vcmi/ukrainian.json rename to Mods/vcmi/Content/config/ukrainian.json index 9ce7fa79c..6dfde5bfe 100644 --- a/Mods/vcmi/config/vcmi/ukrainian.json +++ b/Mods/vcmi/Content/config/ukrainian.json @@ -139,8 +139,6 @@ "vcmi.server.errors.modsToEnable" : "{Потрібні модифікації для завантаження гри}", "vcmi.server.errors.modsToDisable" : "{Модифікації що мають бути вимкнені}", "vcmi.server.confirmReconnect" : "Підключитися до минулої сесії?", - "vcmi.server.errors.modNoDependency" : "Не вдалося увімкнути мод {'%s'}!\n Модифікація потребує мод {'%s'} який зараз не активний!\n", - "vcmi.server.errors.modConflict" : "Не вдалося увімкнути мод {'%s'}!\n Конфліктує з активним модом {'%s'}!\n", "vcmi.server.errors.unknownEntity" : "Не вдалося завантажити гру! У збереженій грі знайдено невідомий об'єкт '%s'! Це збереження може бути несумісним зі встановленою версією модифікацій!", "vcmi.dimensionDoor.seaToLandError" : "Неможливо телепортуватися з моря на сушу або навпаки за допомогою просторової брами", @@ -612,18 +610,5 @@ "core.bonus.WIDE_BREATH.name" : "Широкий подих", "core.bonus.WIDE_BREATH.description" : "Атака широким подихом", "core.bonus.LIMITED_SHOOTING_RANGE.name" : "Обмежена дальність стрільби", - "core.bonus.LIMITED_SHOOTING_RANGE.description" : "Не може стріляти по цілях на відстані більше ${val} гексів", - - "vcmi.stackExperience.description" : "» S t a c k E x p e r i e n c e D e t a i l s «\n\nCreature Type ................... : %s\nExperience Rank ................. : %s (%i)\nExperience Points ............... : %i\nExperience Points to Next Rank .. : %i\nMaximum Experience per Battle ... : %i%% (%i)\nNumber of Creatures in stack .... : %i\nMaximum New Recruits\n without losing current Rank .... : %i\nExperience Multiplier ........... : %.2f\nUpgrade Multiplier .............. : %.2f\nExperience after Rank 10 ........ : %i\nMaximum New Recruits to remain at\n Rank 10 if at Maximum Experience : %i", - "vcmi.stackExperience.rank.0" : "Початковий", - "vcmi.stackExperience.rank.1" : "Новачок", - "vcmi.stackExperience.rank.2" : "Підготовлений", - "vcmi.stackExperience.rank.3" : "Досвідчений", - "vcmi.stackExperience.rank.4" : "Випробуваний", - "vcmi.stackExperience.rank.5" : "Ветеран", - "vcmi.stackExperience.rank.6" : "Адепт", - "vcmi.stackExperience.rank.7" : "Експерт", - "vcmi.stackExperience.rank.8" : "Еліта", - "vcmi.stackExperience.rank.9" : "Майстер", - "vcmi.stackExperience.rank.10" : "Профі" + "core.bonus.LIMITED_SHOOTING_RANGE.description" : "Не може стріляти по цілях на відстані більше ${val} гексів" } diff --git a/Mods/vcmi/config/vcmi/vietnamese.json b/Mods/vcmi/Content/config/vietnamese.json similarity index 100% rename from Mods/vcmi/config/vcmi/vietnamese.json rename to Mods/vcmi/Content/config/vietnamese.json diff --git a/Mods/vcmi/Data/settingsWindow/gear.png b/Mods/vcmi/Data/settingsWindow/gear.png deleted file mode 100644 index c5974983e..000000000 Binary files a/Mods/vcmi/Data/settingsWindow/gear.png and /dev/null differ diff --git a/Mods/vcmi/Data/stackWindow/bonus-effects.png b/Mods/vcmi/Data/stackWindow/bonus-effects.png deleted file mode 100644 index aed22ccdf..000000000 Binary files a/Mods/vcmi/Data/stackWindow/bonus-effects.png and /dev/null differ diff --git a/Mods/vcmi/Sprites/QuickRecruitmentWindow/CreaturePurchaseCard.png b/Mods/vcmi/Sprites/QuickRecruitmentWindow/CreaturePurchaseCard.png deleted file mode 100644 index 5d7bfbfef..000000000 Binary files a/Mods/vcmi/Sprites/QuickRecruitmentWindow/CreaturePurchaseCard.png and /dev/null differ diff --git a/Mods/vcmi/Sprites/vcmi/battleQueue/defendBig.png b/Mods/vcmi/Sprites/vcmi/battleQueue/defendBig.png deleted file mode 100644 index ef9022ca6..000000000 Binary files a/Mods/vcmi/Sprites/vcmi/battleQueue/defendBig.png and /dev/null differ diff --git a/Mods/vcmi/Sprites/vcmi/battleQueue/defendSmall.png b/Mods/vcmi/Sprites/vcmi/battleQueue/defendSmall.png deleted file mode 100644 index b22a1b5d6..000000000 Binary files a/Mods/vcmi/Sprites/vcmi/battleQueue/defendSmall.png and /dev/null differ diff --git a/Mods/vcmi/Sprites/vcmi/battleQueue/statesBig.json b/Mods/vcmi/Sprites/vcmi/battleQueue/statesBig.json deleted file mode 100644 index e8383883c..000000000 --- a/Mods/vcmi/Sprites/vcmi/battleQueue/statesBig.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "basepath": "vcmi/battleQueue/", - "images" : - [ - { "frame" : 0, "file" : "defendBig"}, - { "frame" : 1, "file" : "waitBig"} - ] -} diff --git a/Mods/vcmi/Sprites/vcmi/battleQueue/statesSmall.json b/Mods/vcmi/Sprites/vcmi/battleQueue/statesSmall.json deleted file mode 100644 index 796657130..000000000 --- a/Mods/vcmi/Sprites/vcmi/battleQueue/statesSmall.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "basepath": "vcmi/battleQueue/", - "images" : - [ - { "frame" : 0, "file" : "defendSmall"}, - { "frame" : 1, "file" : "waitSmall"} - ] -} diff --git a/Mods/vcmi/Sprites/vcmi/battleQueue/waitBig.png b/Mods/vcmi/Sprites/vcmi/battleQueue/waitBig.png deleted file mode 100644 index ed0b70ae6..000000000 Binary files a/Mods/vcmi/Sprites/vcmi/battleQueue/waitBig.png and /dev/null differ diff --git a/Mods/vcmi/Sprites/vcmi/battleQueue/waitSmall.png b/Mods/vcmi/Sprites/vcmi/battleQueue/waitSmall.png deleted file mode 100644 index da6ebed50..000000000 Binary files a/Mods/vcmi/Sprites/vcmi/battleQueue/waitSmall.png and /dev/null differ diff --git a/Mods/vcmi/config/vcmi/czech.json b/Mods/vcmi/config/vcmi/czech.json deleted file mode 100644 index 57625bd43..000000000 --- a/Mods/vcmi/config/vcmi/czech.json +++ /dev/null @@ -1,548 +0,0 @@ -{ - "vcmi.adventureMap.monsterThreat.title" : "\n\nHrozba: ", - "vcmi.adventureMap.monsterThreat.levels.0" : "Bez námahy", - "vcmi.adventureMap.monsterThreat.levels.1" : "Velmi slabá", - "vcmi.adventureMap.monsterThreat.levels.2" : "Slabá", - "vcmi.adventureMap.monsterThreat.levels.3" : "Trochu slabší", - "vcmi.adventureMap.monsterThreat.levels.4" : "Podobná", - "vcmi.adventureMap.monsterThreat.levels.5" : "Trochu silnější", - "vcmi.adventureMap.monsterThreat.levels.6" : "Silná", - "vcmi.adventureMap.monsterThreat.levels.7" : "Velmi silná", - "vcmi.adventureMap.monsterThreat.levels.8" : "Výzva", - "vcmi.adventureMap.monsterThreat.levels.9" : "Převažující", - "vcmi.adventureMap.monsterThreat.levels.10" : "Smrtelná", - "vcmi.adventureMap.monsterThreat.levels.11" : "Nemožná", - - "vcmi.adventureMap.confirmRestartGame" : "Jste si jisti, že chcete restartovat hru?", - "vcmi.adventureMap.noTownWithMarket" : "Nejsou dostupná jakákoliv tržiště!", - "vcmi.adventureMap.noTownWithTavern" : "Nejsou dostupná jakákoliv města s krčmou!", - "vcmi.adventureMap.spellUnknownProblem" : "Neznámý problém s tímto kouzlem! Další informace nejsou k dispozici.", - "vcmi.adventureMap.playerAttacked" : "Hráč byl napaden: %s", - "vcmi.adventureMap.moveCostDetails" : "Body pohybu - Cena: %TURNS tahů + %POINTS bodů, zbylé body: %REMAINING", - "vcmi.adventureMap.moveCostDetailsNoTurns" : "Body pohybu - Cena: %POINTS bodů, zbylé body: %REMAINING", - - "vcmi.capitalColors.0" : "Červený", - "vcmi.capitalColors.1" : "Modrý", - "vcmi.capitalColors.2" : "Hnědý", - "vcmi.capitalColors.3" : "Zelený", - "vcmi.capitalColors.4" : "Oranžový", - "vcmi.capitalColors.5" : "Fialový", - "vcmi.capitalColors.6" : "Tyrkysový", - "vcmi.capitalColors.7" : "Růžový", - - "vcmi.heroOverview.startingArmy" : "Počáteční jednotky", - "vcmi.heroOverview.warMachine" : "Bojové stroje", - "vcmi.heroOverview.secondarySkills" : "Druhotné schopnosti", - "vcmi.heroOverview.spells" : "Kouzla", - - "vcmi.radialWheel.mergeSameUnit" : "Sloučit stejné jednotky", - "vcmi.radialWheel.fillSingleUnit" : "Vyplnit jednou jednotkou", - "vcmi.radialWheel.splitSingleUnit" : "Rozdělit jedinou jednotku", - "vcmi.radialWheel.splitUnitEqually" : "Rozdělit jednotky rovnoměrně", - "vcmi.radialWheel.moveUnit" : "Přesunout jednotky do jiného oddílu", - "vcmi.radialWheel.splitUnit" : "Rozdělit jednotku do jiné pozice", - - "vcmi.radialWheel.heroGetArmy" : "Získat armádu jiného hrdiny", - "vcmi.radialWheel.heroSwapArmy" : "Vyměnit armádu s jiným hrdinou", - "vcmi.radialWheel.heroExchange" : "Otevřít výměnu hrdinů", - "vcmi.radialWheel.heroGetArtifacts" : "Získat artefakty od jiního hrdiny", - "vcmi.radialWheel.heroSwapArtifacts" : "Vyměnit artefakty s jiným hrdinou", - "vcmi.radialWheel.heroDismiss" : "Propustit hrdinu", - - "vcmi.radialWheel.moveTop" : "Přesunout nahoru", - "vcmi.radialWheel.moveUp" : "Posunout výše", - "vcmi.radialWheel.moveDown" : "Posunout níže", - "vcmi.radialWheel.moveBottom" : "Přesunout dolů", - - "vcmi.spellBook.search" : "hledat...", - - "vcmi.mainMenu.serverConnecting" : "Připojování...", - "vcmi.mainMenu.serverAddressEnter" : "Zadejte adresu:", - "vcmi.mainMenu.serverConnectionFailed" : "Připojování selhalo", - "vcmi.mainMenu.serverClosing" : "Zavírání...", - "vcmi.mainMenu.hostTCP" : "Pořádat hru TCP/IP", - "vcmi.mainMenu.joinTCP" : "Připojit se do hry TCP/IP", - - "vcmi.lobby.filepath" : "Název souboru", - "vcmi.lobby.creationDate" : "Datum vytvoření", - "vcmi.lobby.scenarioName" : "Název scénáře", - "vcmi.lobby.mapPreview" : "Náhled mapy", - "vcmi.lobby.noPreview" : "bez náhledu", - "vcmi.lobby.noUnderground" : "bez podzemí", - "vcmi.lobby.sortDate" : "Řadit mapy dle data změny", - "vcmi.lobby.backToLobby" : "Vrátit se do předsíně", - - "vcmi.lobby.login.title" : "Online předsíň VCMI", - "vcmi.lobby.login.username" : "Uživatelské jméno:", - "vcmi.lobby.login.connecting" : "Připojování...", - "vcmi.lobby.login.error" : "Chyba při připojování: %s", - "vcmi.lobby.login.create" : "Nový účet", - "vcmi.lobby.login.login" : "Přihlásit se", - "vcmi.lobby.login.as" : "Přilásit se jako %s", - "vcmi.lobby.header.rooms" : "Herní místnosti - %d", - "vcmi.lobby.header.channels" : "Kanály konverzace", - "vcmi.lobby.header.chat.global" : "Globální konverzace hry - %s", // %s -> language name - "vcmi.lobby.header.chat.match" : "Konverzace předchozí hry %s", // %s -> game start date & time - "vcmi.lobby.header.chat.player" : "Soukromá konverzace s %s", // %s -> nickname of another player - "vcmi.lobby.header.history" : "Vaše předchozí hry", - "vcmi.lobby.header.players" : "Online hráči - %d", - "vcmi.lobby.match.solo" : "Hra jednoho hráče", - "vcmi.lobby.match.duel" : "Hra s %s", // %s -> nickname of another player - "vcmi.lobby.match.multi" : "%d hráčů", - "vcmi.lobby.room.create" : "Vytvořit novou místnost", - "vcmi.lobby.room.players.limit" : "Omezení počtu hráčů", - "vcmi.lobby.room.description.public" : "Jakýkoliv hráč se může připojit do veřejné místnosti.", - "vcmi.lobby.room.description.private" : "Pouze pozvaní hráči se mohou připojit do soukromé místnosti.", - "vcmi.lobby.room.description.new" : "Pro start hry vyberte scénář, nebo nastavte náhodnou mapu.", - "vcmi.lobby.room.description.load" : "Pro start hry načtěte uloženou hru.", - "vcmi.lobby.room.description.limit" : "Až %d hráčů se může připojit do vaší místnosti (včetně vás).", - "vcmi.lobby.invite.header" : "Pozvat hráče", - "vcmi.lobby.invite.notification" : "Pozval vás hráč do jejich soukromé místnosti. Nyní se do ní můžete připojit.", - "vcmi.lobby.preview.title" : "Připojit se do herní místnosti", - "vcmi.lobby.preview.subtitle" : "Hra na %s, pořádána %s", //TL Note: 1) name of map or RMG template 2) nickname of game host - "vcmi.lobby.preview.version" : "Verze hry:", - "vcmi.lobby.preview.players" : "Hráči:", - "vcmi.lobby.preview.mods" : "Použité modifikace:", - "vcmi.lobby.preview.allowed" : "Připojit se do herní místnosti?", - "vcmi.lobby.preview.error.header" : "Nelze se připojit do této herní místnosti.", - "vcmi.lobby.preview.error.playing" : "Nejdříve musíte opustit vaši současnou hru.", - "vcmi.lobby.preview.error.full" : "Místnost je již plná.", - "vcmi.lobby.preview.error.busy" : "Místnost již nepřijímá nové hráče.", - "vcmi.lobby.preview.error.invite" : "Nebyl jste pozván do této mísnosti.", - "vcmi.lobby.preview.error.mods" : "Použváte jinou sadu modifikací.", - "vcmi.lobby.preview.error.version" : "Používáte jinou verzi VCMI.", - "vcmi.lobby.room.new" : "Nová hra", - "vcmi.lobby.room.load" : "Načíst hru", - "vcmi.lobby.room.type" : "Druh místnosti", - "vcmi.lobby.room.mode" : "Herní režim", - "vcmi.lobby.room.state.public" : "Veřejná", - "vcmi.lobby.room.state.private" : "Soukromá", - "vcmi.lobby.room.state.busy" : "Ve hře", - "vcmi.lobby.room.state.invited" : "Pozvaný", - "vcmi.lobby.mod.state.compatible" : "Kompatibilní", - "vcmi.lobby.mod.state.disabled" : "Musí být povolena", - "vcmi.lobby.mod.state.version" : "Neshoda verze", - "vcmi.lobby.mod.state.excessive" : "Musí být zakázána", - "vcmi.lobby.mod.state.missing" : "Není nainstalována", - "vcmi.lobby.pvp.coin.hover" : "Mince", - "vcmi.lobby.pvp.coin.help" : "Hodí mincí", - "vcmi.lobby.pvp.randomTown.hover" : "Náhodné město", - "vcmi.lobby.pvp.randomTown.help" : "Napsat náhodné město do konvezace", - "vcmi.lobby.pvp.randomTownVs.hover" : "Náhodné město vs.", - "vcmi.lobby.pvp.randomTownVs.help" : "Napsat 2 náhodná města do konvezace", - "vcmi.lobby.pvp.versus" : "vs.", - - "vcmi.client.errors.invalidMap" : "{Neplatná mapa nebo kampaň}\n\nChyba při startu hry! Vybraná mapa nebo kampaň může být neplatná nebo poškozená. Důvod:\n%s", - "vcmi.client.errors.missingCampaigns" : "{Chybějící datové soubory}\n\nDatové soubory kampaně nebyly nalezeny! Možná máte nekompletní nebo poškozené datové soubory Heroes 3. Prosíme, přeinstalujte hru.", - "vcmi.server.errors.disconnected" : "{Chyba sítě}\n\nPřipojení k hernímu serveru bylo ztraceno!", - "vcmi.server.errors.existingProcess" : "Již běží jiný server VCMI. Prosím, ukončete ho před startem nové hry.", - "vcmi.server.errors.modsToEnable" : "{Následující modifikace jsou nutné pro načtení hry}", - "vcmi.server.errors.modsToDisable" : "{Následující modifikace musí být zakázány}", - "vcmi.server.errors.modNoDependency" : "Nelze načíst modifikaci {'%s'}!\n Závisí na modifikaci {'%s'}, která není aktivní!\n", - "vcmi.server.errors.modConflict" : "Nelze načíst modifikaci {'%s'}!\n Je v kolizi s aktivní modifikací {'%s'}!\n", - "vcmi.server.errors.unknownEntity" : "Nelze načíst uloženou pozici! Neznámá entita '%s' nalezena v uložené pozici! Uložná pozice nemusí být kompatibilní s aktuálními verzemi modifikací!", - - "vcmi.dimensionDoor.seaToLandError" : "It's not possible to teleport from sea to land or vice versa with a Dimension Door.", //TODO - - "vcmi.settingsMainWindow.generalTab.hover" : "Obecné", - "vcmi.settingsMainWindow.generalTab.help" : "Přepne na kartu obecných nastavení, která obsahuje nastavení související s obecným chováním klienta hry.", - "vcmi.settingsMainWindow.battleTab.hover" : "Bitva", - "vcmi.settingsMainWindow.battleTab.help" : "Přepne na kartu nastavení bitvy, která umožňuje konfiguraci chování hry v bitvách.", - "vcmi.settingsMainWindow.adventureTab.hover" : "Mapa světa", - "vcmi.settingsMainWindow.adventureTab.help" : "Přepne na kartu nastavení mapy světa (mapa světa je sekce hry, ve které hráči mohou ovládat pohyb hrdinů).", - - "vcmi.systemOptions.videoGroup" : "Nastavení obrazu", - "vcmi.systemOptions.audioGroup" : "Nastavení zvuku", - "vcmi.systemOptions.otherGroup" : "Ostatní nastavení", // unused right now - "vcmi.systemOptions.townsGroup" : "Obrazovka města", - - "vcmi.systemOptions.fullscreenBorderless.hover" : "Celá obrazovka (bez okrajů)", - "vcmi.systemOptions.fullscreenBorderless.help" : "{Celá obrazovka bez okrajů}\n\nPokud je vybráno, VCMI poběží v režimu celé obrazovky bez okrajů. V tomto režimu bude hra respektovat systémové rozlišení a ignorovat vybrané rozlišení ve hře.", - "vcmi.systemOptions.fullscreenExclusive.hover" : "Celá obrazovka (exkluzivní)", - "vcmi.systemOptions.fullscreenExclusive.help" : "{Celá obrazovka}\n\nPokud je vybráno, VCMI poběží v režimu exkluzivní celé obrazovky. V tomto režimu hra změní rozlišení na vybrané.", - "vcmi.systemOptions.resolutionButton.hover" : "Rozlišení: %wx%h", - "vcmi.systemOptions.resolutionButton.help" : "{Vybrat rozlišení}\n\nZmění rozlišení herní obrazovky.", - "vcmi.systemOptions.resolutionMenu.hover" : "Vybrat rozlišení", - "vcmi.systemOptions.resolutionMenu.help" : "Změnit rozlišení herní obrazovky.", - "vcmi.systemOptions.scalingButton.hover" : "Škálování rozhraní: %p%", - "vcmi.systemOptions.scalingButton.help" : "{Škálování rozhraní}\n\nZmění škálování herního rozhraní", - "vcmi.systemOptions.scalingMenu.hover" : "Vybrat škálování rozhraní", - "vcmi.systemOptions.scalingMenu.help" : "Změní škálování herního rozhraní.", - "vcmi.systemOptions.longTouchButton.hover" : "Doba dlouhého podržení: %d ms", // Translation note: "ms" = "milliseconds" - "vcmi.systemOptions.longTouchButton.help" : "{Doba dlouhého podržení}\n\nPři používání dotykové obrazovky budou zobrazeno vyskakovací okno při podržení prstu na obrazovce, v milisekundách", - "vcmi.systemOptions.longTouchMenu.hover" : "Vybrat dobu dlouhého podržení", - "vcmi.systemOptions.longTouchMenu.help" : "Změnit dobu dlouhého podržení.", - "vcmi.systemOptions.longTouchMenu.entry" : "%d milisekund", - "vcmi.systemOptions.framerateButton.hover" : "Zobrazit FPS", - "vcmi.systemOptions.framerateButton.help" : "{Zobrazit FPS}\n\nPřepne viditelnost počitadla snímků za sekundu v rohu obrazovky hry.", - "vcmi.systemOptions.hapticFeedbackButton.hover" : "Vibrace", - "vcmi.systemOptions.hapticFeedbackButton.help" : "{Vibrace}\n\nPřepnout stav vibrací při dotykovém ovládání.", - "vcmi.systemOptions.enableUiEnhancementsButton.hover" : "Vylepšení rozhraní", - "vcmi.systemOptions.enableUiEnhancementsButton.help" : "{Vylepšení rozhraní}\n\nZapne různá vylepšení rozhraní, jako je tlačítko batohu atd. Zakažte pro zážitek klasické hry.", - "vcmi.systemOptions.enableLargeSpellbookButton.hover" : "Velká kniha kouzel", - "vcmi.systemOptions.enableLargeSpellbookButton.help" : "{Velká kniha kouzel}\n\nPovolí větší knihu kouzel, do které se jich více vleze na jednu stranu. Animace změny stránek s tímto nastavením nefunguje.", - "vcmi.systemOptions.audioMuteFocus.hover" : "Ztlumit při neaktivitě", - "vcmi.systemOptions.audioMuteFocus.help" : "{Ztlumit při neaktivitě}\n\nZtlumit zvuk, pokud je okno hry v pozadí. Výjimkou jsou zprávy ve hře a zvuk nového tahu.", - - "vcmi.adventureOptions.infoBarPick.hover" : "Zobrazit zprávy v panelu informací", - "vcmi.adventureOptions.infoBarPick.help" : "{Zobrazit zprávy v panelu informací}\n\nKdyž bude možné, herní zprávy z návštěv míst na mapě budou zobrazeny v panelu informací místo ve zvláštním okně.", - "vcmi.adventureOptions.numericQuantities.hover" : "Číselné množství jednotek", - "vcmi.adventureOptions.numericQuantities.help" : "{Číselné množství jednotek}\n\nZobrazit přibližné množství nepřátelských jednotek ve formátu A-B.", - "vcmi.adventureOptions.forceMovementInfo.hover" : "Vždy zobrazit cenu pohybu", - "vcmi.adventureOptions.forceMovementInfo.help" : "{Vždy zobrazit cenu pohybu}\n\nVždy zobrazit informace o bodech pohybu v panelu informací. (Místo zobrazení pouze při stisknuté klávese ALT).", - "vcmi.adventureOptions.showGrid.hover" : "Zobrazit mřížku", - "vcmi.adventureOptions.showGrid.help" : "{Zobrazit mřížku}\n\nZobrazit překrytí mřížkou, zvýrazňuje hranice mezi dlaždicemi mapy světa.", - "vcmi.adventureOptions.borderScroll.hover" : "Posouvání okraji", - "vcmi.adventureOptions.borderScroll.help" : "{Posouvání okraji}\n\nPosouvat mapu světa, když je kurzor na okraji obrazovky. Může být zakázáno držením klávesy CTRL.", - "vcmi.adventureOptions.infoBarCreatureManagement.hover" : "Info Panel Creature Management", //TODO - "vcmi.adventureOptions.infoBarCreatureManagement.help" : "{Info Panel Creature Management}\n\nAllows rearranging creatures in info panel instead of cycling between default components.", - "vcmi.adventureOptions.leftButtonDrag.hover" : "Posouvání mapy levým kliknutím", - "vcmi.adventureOptions.leftButtonDrag.help" : "{Posouvání mapy levým kliknutím}\n\nPosouvání mapy tažením myši se stisknutým levým tlačítkem.", - "vcmi.adventureOptions.smoothDragging.hover" : "Plynulé posouvání mapy", - "vcmi.adventureOptions.smoothDragging.help" : "{Plynulé posouvání mapy}\n\nWhen enabled, map dragging has a modern run out effect.", // TODO - "vcmi.adventureOptions.mapScrollSpeed1.hover": "", - "vcmi.adventureOptions.mapScrollSpeed5.hover": "", - "vcmi.adventureOptions.mapScrollSpeed6.hover": "", - "vcmi.adventureOptions.mapScrollSpeed1.help": "Nastavit posouvání mapy na velmi pomalé", - "vcmi.adventureOptions.mapScrollSpeed5.help": "Nastavit posouvání mapy na velmi rychlé", - "vcmi.adventureOptions.mapScrollSpeed6.help": "Nastavit posouvání mapy na okamžité", - - "vcmi.battleOptions.queueSizeLabel.hover": "Zobrazit frontu pořadí tahů", - "vcmi.battleOptions.queueSizeNoneButton.hover": "VYPNUTO", - "vcmi.battleOptions.queueSizeAutoButton.hover": "AUTO", - "vcmi.battleOptions.queueSizeSmallButton.hover": "MALÁ", - "vcmi.battleOptions.queueSizeBigButton.hover": "VELKÁ", - "vcmi.battleOptions.queueSizeNoneButton.help": "Nezobrazovat frontu pořadí tahů.", - "vcmi.battleOptions.queueSizeAutoButton.help": "Nastavit automaticky velikost fronty pořadí tahů podle rozlišení obrazovky hry (Při výšce herního rozlišení menší než 700 pixelů je použita velikost MALÁ, jinak velikost VELKÁ)", - "vcmi.battleOptions.queueSizeSmallButton.help": "Zobrazit MALOU frontu pořadí tahů.", - "vcmi.battleOptions.queueSizeBigButton.help": "Zobrazit VELKOU frontu pořadí tahů (není podporováno, pokud výška rozlišení hry není alespoň 700 pixelů).", - "vcmi.battleOptions.animationsSpeed1.hover": "", - "vcmi.battleOptions.animationsSpeed5.hover": "", - "vcmi.battleOptions.animationsSpeed6.hover": "", - "vcmi.battleOptions.animationsSpeed1.help": "Nastavit rychlost animací na velmi pomalé.", - "vcmi.battleOptions.animationsSpeed5.help": "Nastavit rychlost animací na velmi rychlé.", - "vcmi.battleOptions.animationsSpeed6.help": "Nastavit rychlost animací na okamžité.", - "vcmi.battleOptions.movementHighlightOnHover.hover": "Zvýraznění pohybu při najetí", - "vcmi.battleOptions.movementHighlightOnHover.help": "{Zvýraznění pohybu při najetí}\n\nZvýraznit rozsah pohybu jednotky při najetí na něj.", - "vcmi.battleOptions.rangeLimitHighlightOnHover.hover": "Zobrazit omezení dostřelu střelců", - "vcmi.battleOptions.rangeLimitHighlightOnHover.help": "{Zobrazit omezení dostřelu střelců při najetí}\n\nZobrazit dostřel střelce při najetí na něj.", - "vcmi.battleOptions.showStickyHeroInfoWindows.hover": "Zobrazit okno statistik hrdinů", - "vcmi.battleOptions.showStickyHeroInfoWindows.help": "{Zobrazit okno statistik hrdinů}\n\nTrvale zapne okno statistiky hrdinů, které ukazuje hlavní schopnosti a magickou energii.", - "vcmi.battleOptions.skipBattleIntroMusic.hover": "Přeskočit úvodní hudbu", - "vcmi.battleOptions.skipBattleIntroMusic.help": "{Přeskočit úvodní hudbu}\n\nPovolí akce při úvodní hudbě přehrávané při začátku každé bitvy.", - - "vcmi.adventureMap.revisitObject.hover" : "Znovu navštívit místo", - "vcmi.adventureMap.revisitObject.help" : "{Znovu navštívit místo}\n\nPokud se hrdina nachází na nějakém místě mapy, může jej znovu navštívit.", - - "vcmi.battleWindow.pressKeyToSkipIntro" : "Stiskněte jakoukoliv klávesu pro okamžité zahájení bitvy", - "vcmi.battleWindow.damageEstimation.melee" : "Zaútočit na %CREATURE (%DAMAGE).", - "vcmi.battleWindow.damageEstimation.meleeKills" : "Zaútočit na %CREATURE (%DAMAGE, %KILLS).", - "vcmi.battleWindow.damageEstimation.ranged" : "Vystřelit na %CREATURE (%SHOTS, %DAMAGE).", - "vcmi.battleWindow.damageEstimation.rangedKills" : "Vystřelit na %CREATURE (%SHOTS, %DAMAGE, %KILLS).", - "vcmi.battleWindow.damageEstimation.shots" : "%d střel zbývá", - "vcmi.battleWindow.damageEstimation.shots.1" : "%d střela zbývá", - "vcmi.battleWindow.damageEstimation.damage" : "%d poškození", - "vcmi.battleWindow.damageEstimation.damage.1" : "%d poškození", - "vcmi.battleWindow.damageEstimation.kills" : "%d zahyne", - "vcmi.battleWindow.damageEstimation.kills.1" : "%d zahyne", - - "vcmi.battleWindow.damageRetaliation.will" : "Zahyne ", - "vcmi.battleWindow.damageRetaliation.may" : "Možná zahyne ", - "vcmi.battleWindow.damageRetaliation.never" : "Nezahyne.", - "vcmi.battleWindow.damageRetaliation.damage" : "(%DAMAGE).", - "vcmi.battleWindow.damageRetaliation.damageKills" : "(%DAMAGE, %KILLS).", - - "vcmi.battleWindow.killed" : "Zabito", //TODO - "vcmi.battleWindow.accurateShot.resultDescription.0" : "%d %s were killed by accurate shots!", - "vcmi.battleWindow.accurateShot.resultDescription.1" : "%d %s was killed with an accurate shot!", - "vcmi.battleWindow.accurateShot.resultDescription.2" : "%d %s were killed by accurate shots!", - "vcmi.battleWindow.endWithAutocombat" : "Are you sure you wish to end the battle with auto combat?", - - "vcmi.battleResultsWindow.applyResultsLabel" : "Použít výsledek bitvy", - - "vcmi.tutorialWindow.title" : "Úvod ovládání dotykem", - "vcmi.tutorialWindow.decription.RightClick" : "Klepněte a držte prvek, na který byste chtěli použít pravé tlačítko myši. Klepněte na volnou oblast pro zavření.", - "vcmi.tutorialWindow.decription.MapPanning" : "Klepněte a držte jedním prstem pro posouvání mapy.", - "vcmi.tutorialWindow.decription.MapZooming" : "Přibližte dva prsty k sobě pro přiblížení mapy.", - "vcmi.tutorialWindow.decription.RadialWheel" : "Přejetí otevře kruhovou nabídku pro různé akce, třeba správa hrdiny/bojovnínků a příkazy měst.", - "vcmi.tutorialWindow.decription.BattleDirection" : "Pro útok ze speifického úhlu, přejeďte směrem, ze kterého má být útok vykonán.", - "vcmi.tutorialWindow.decription.BattleDirectionAbort" : "Gesto útoku pod úhlem může být zrušeno, pokud, pokud je prst dostatečně daleko.", - "vcmi.tutorialWindow.decription.AbortSpell" : "Klepněte a držte pro zrušení kouzla.", - - "vcmi.otherOptions.availableCreaturesAsDwellingLabel.hover" : "Zobrazit dostupné jednotky", - "vcmi.otherOptions.availableCreaturesAsDwellingLabel.help" : "{Zobrazit dostupné jednotky}\n\nZobrazit počet jednotek dostupných ke koupení místo jejich týdenního přírůstku v přehledu města. (levý spodní okraj obrazovky města).", - "vcmi.otherOptions.creatureGrowthAsDwellingLabel.hover" : "Zobrazit týdenní přírůstek jednotek", - "vcmi.otherOptions.creatureGrowthAsDwellingLabel.help" : "{Zobrazit týdenní přírůstek jednotek}\n\nZobrazit týdenní přírůstek jednotek místo dostupného počtu ke koupení v přehledu města (levý spodní okraj obrazovky města).", - "vcmi.otherOptions.compactTownCreatureInfo.hover": "Kompaktní informace o jednotkách", - "vcmi.otherOptions.compactTownCreatureInfo.help": "{Kompaktní informace o jednotkách}\n\nZobrazit menší informace o jednotkách města v jeho přehledu (levý spodní okraj obrazovky města).", - - "vcmi.townHall.missingBase" : "Základní budova %s musí být postavena jako první", - "vcmi.townHall.noCreaturesToRecruit" : "Žádné jednotky k vycvičení!", - - "vcmi.logicalExpressions.anyOf" : "Něco z následujících:", - "vcmi.logicalExpressions.allOf" : "Všechny následující:", - "vcmi.logicalExpressions.noneOf" : "Žádné z následujících:", - - "vcmi.heroWindow.openCommander.hover" : "Open commander info window", - "vcmi.heroWindow.openCommander.help" : "Shows details about the commander of this hero.", - "vcmi.heroWindow.openBackpack.hover" : "Open artifact backpack window", - "vcmi.heroWindow.openBackpack.help" : "Opens window that allows easier artifact backpack management.", - - "vcmi.commanderWindow.artifactMessage" : "Chcete navrátit tento artefakt hrdinovi?", - - "vcmi.creatureWindow.showBonuses.hover" : "Přepnout na zobrazení bonusů", - "vcmi.creatureWindow.showBonuses.help" : "Display all active bonuses of the commander.", - "vcmi.creatureWindow.showSkills.hover" : "Přepnout na zobrazení schoostí", - "vcmi.creatureWindow.showSkills.help" : "Display all learned skills of the commander.", - "vcmi.creatureWindow.returnArtifact.hover" : "Vrátit artefakt", - "vcmi.creatureWindow.returnArtifact.help" : "Klikněte na toto tlačítko pro navrácení artefaktů do hrdinova batohu.", - - "vcmi.questLog.hideComplete.hover" : "Skrýt dokončené úkoly", - "vcmi.questLog.hideComplete.help" : "Skrýt všechny dokončené úkoly.", - - "vcmi.randomMapTab.widgets.randomTemplate" : "(Náhodná)", - "vcmi.randomMapTab.widgets.templateLabel" : "Šablona", - "vcmi.randomMapTab.widgets.teamAlignmentsButton" : "Nastavit...", - "vcmi.randomMapTab.widgets.teamAlignmentsLabel" : "Přiřazení týmů", - "vcmi.randomMapTab.widgets.roadTypesLabel" : "Druhy cest", - - "vcmi.optionsTab.turnOptions.hover" : "Možnosti tahu", - "vcmi.optionsTab.turnOptions.help" : "Vyberte odpočítávadlo tahů a nastavení souběžných tahů", - - "vcmi.optionsTab.chessFieldBase.hover" : "Základní časovač", - "vcmi.optionsTab.chessFieldTurn.hover" : "Časovač tahu", - "vcmi.optionsTab.chessFieldBattle.hover" : "Časovač bitvy", - "vcmi.optionsTab.chessFieldUnit.hover" : "Časovač jednotky", - "vcmi.optionsTab.chessFieldBase.help" : "Použit při poklesnutí {Časovače bitvy} na 0. Nastaveno jednou při začátku hry. Při poklesu na nulu skončí tah. Jákákoliv trvající bitva skončí prohrou.", - "vcmi.optionsTab.chessFieldTurnAccumulate.help" : "Použit mimo bitvu nebo když {Časovač bitvy} vyprší. Resetuje se každý tah. Přebytečný čas je přidán do {Základního časovače} na konci tahu.", - "vcmi.optionsTab.chessFieldTurnDiscard.help" : "Použit mimo bitvu nebo když {Časovač bitvy} vyprší. Resetuje se každý tah. Jakýkoliv přebytečný čas je ztracen.", - "vcmi.optionsTab.chessFieldBattle.help" : "Použit v bitvách s AI nebo v pvp soubojích při vypršení {Časovače jednotky}. Resetuje se startu každé bitvy.", - "vcmi.optionsTab.chessFieldUnitAccumulate.help" : "Použit při vybírání úkonu jednotky. Přebytečný čas je přidán do {Časovače bitvy} na konci tahu jednotky.", - "vcmi.optionsTab.chessFieldUnitDiscard.help" : "Použit při vybírání úkonu jednotky. Resetuje se na začátku tahu každé jednotky. Jakýkoliv přebytečný čas je ztracen.", - - "vcmi.optionsTab.accumulate" : "Akumulovat", - - "vcmi.optionsTab.simturnsTitle" : "Souběžné tahy", - "vcmi.optionsTab.simturnsMin.hover" : "Alespoň po", - "vcmi.optionsTab.simturnsMax.hover" : "Nejvíce po", - "vcmi.optionsTab.simturnsAI.hover" : "(Experimentální) Souběžné tahy AI", - "vcmi.optionsTab.simturnsMin.help" : "Hrát souběžně po určený počet dní. Setkání mezi hráči je v této době zablokováno", - "vcmi.optionsTab.simturnsMax.help" : "Hrát souběžně po určený počet dní nebo do setkání s jiným hráčem", - "vcmi.optionsTab.simturnsAI.help" : "{Souběžné tahy AI}\nExperimentální volba. Dovoluje AI hráčům hrát souběžně s lidskými hráči, když jsou souběžné tahy povoleny.", - - "vcmi.optionsTab.turnTime.select" : "Vyberte šablonu nastavení časovače", - "vcmi.optionsTab.turnTime.unlimited" : "Neomezený čas tahu", - "vcmi.optionsTab.turnTime.classic.1" : "Klasický časovač: 1 minuta", - "vcmi.optionsTab.turnTime.classic.2" : "Klasický časovač: 2 minuty", - "vcmi.optionsTab.turnTime.classic.5" : "Klasický časovač: 5 minut", - "vcmi.optionsTab.turnTime.classic.10" : "Klasický časovač: 10 minut", - "vcmi.optionsTab.turnTime.classic.20" : "Klasický časovač: 20 minut", - "vcmi.optionsTab.turnTime.classic.30" : "Klasický časovač: 30 minut", - "vcmi.optionsTab.turnTime.chess.20" : "Šachová: 20:00 + 10:00 + 02:00 + 00:00", - "vcmi.optionsTab.turnTime.chess.16" : "Šachová: 16:00 + 08:00 + 01:30 + 00:00", - "vcmi.optionsTab.turnTime.chess.8" : "Šachová: 08:00 + 04:00 + 01:00 + 00:00", - "vcmi.optionsTab.turnTime.chess.4" : "Šachová: 04:00 + 02:00 + 00:30 + 00:00", - "vcmi.optionsTab.turnTime.chess.2" : "Šachová: 02:00 + 01:00 + 00:15 + 00:00", - "vcmi.optionsTab.turnTime.chess.1" : "Šachová: 01:00 + 01:00 + 00:00 + 00:00", - - "vcmi.optionsTab.simturns.select" : "Vyberte šablonu souběžných tahů", - "vcmi.optionsTab.simturns.none" : "Bez souběžných tahů", - "vcmi.optionsTab.simturns.tillContactMax" : "Souběžně: Do setkání", - "vcmi.optionsTab.simturns.tillContact1" : "Souběžně: 1 týden, přerušit při setkání", - "vcmi.optionsTab.simturns.tillContact2" : "Souběžně: 2 týdny, přerušit při setkání", - "vcmi.optionsTab.simturns.tillContact4" : "Souběžně: 1 mšsíc, přerušit při setkání", - "vcmi.optionsTab.simturns.blocked1" : "Souběžně: 1 týden, setkání zablokována", - "vcmi.optionsTab.simturns.blocked2" : "Souběžně: 2 týdny, setkání zablokována", - "vcmi.optionsTab.simturns.blocked4" : "Souběžně: 1 měsíc, setkání zablokována", - - // 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 dní", - "vcmi.optionsTab.simturns.days.1" : " %d den", - "vcmi.optionsTab.simturns.days.2" : " %d dny", - "vcmi.optionsTab.simturns.weeks.0" : " %d týdnů", - "vcmi.optionsTab.simturns.weeks.1" : " %d týden", - "vcmi.optionsTab.simturns.weeks.2" : " %d týdny", - "vcmi.optionsTab.simturns.months.0" : " %d měsíců", - "vcmi.optionsTab.simturns.months.1" : " %d měsíc", - "vcmi.optionsTab.simturns.months.2" : " %d měsíce", - - "vcmi.optionsTab.extraOptions.hover" : "Další možnosti", - "vcmi.optionsTab.extraOptions.help" : "Další herní možnosti", - - "vcmi.optionsTab.cheatAllowed.hover" : "Povolit cheaty", - "vcmi.optionsTab.unlimitedReplay.hover" : "Unlimited battle replay", - "vcmi.optionsTab.cheatAllowed.help" : "{Povolit cheaty}\nPovolí zadávání cheatů během hry.", - "vcmi.optionsTab.unlimitedReplay.help" : "{Unlimited battle replay}\nNo limit of replaying battles.", - - // Custom victory conditions for H3 campaigns and HotA maps - "vcmi.map.victoryCondition.daysPassed.toOthers" : "Nepřítel zvládl přežít do této chvíle. Vítězství je jeho!", - "vcmi.map.victoryCondition.daysPassed.toSelf" : "Gratulace! Zvládli jste přežít. Vítězství je vaše!", - "vcmi.map.victoryCondition.eliminateMonsters.toOthers" : "Nepřítel porazil všechny bojovníky zamořující tuto zemi a nárokuje si vítězství!", - "vcmi.map.victoryCondition.eliminateMonsters.toSelf" : "Gratulace! Porazili jste všechny nepřátele zamořující tuto zemi a můžete si nárokovat vítězství!", - "vcmi.map.victoryCondition.collectArtifacts.message" : "Získejte tři artefakty", - "vcmi.map.victoryCondition.angelicAlliance.toSelf" : "Gratulace! Všichni vaši nepřítelé byli poraženi a máte Andělskou alianci! Vítězství je vaše!", - "vcmi.map.victoryCondition.angelicAlliance.message" : "Porazte všechny nepřátele a utužte Andělskou alianci", - - // few strings from WoG used by vcmi - "vcmi.stackExperience.description" : "» P o d r o b n o s t i z k u š e n o s t í o d d í l u «\n\nDruh bojovníka ................... : %s\nÚroveň hodnosti ................. : %s (%i)\nBody zkušeností ............... : %i\nZkušenostních bodů do další úrovně hodnosti .. : %i\nMaximum zkušeností na bitvu ... : %i%% (%i)\nPočet bojovníků v oddílu .... : %i\nMaximum nových rekrutů\n bez ztráty současné hodnosti .... : %i\nNásobič zkušeností ........... : %.2f\nNásobič vylepšení .............. : %.2f\nZkušnosti po 10. úrovně hodnosti ........ : %i\nMaximální počet nových rekrutů pro zachování\n 10. úrovně hodnosti s maximálními zkušenostmi: %i", - "vcmi.stackExperience.rank.0" : "Začátečník", - "vcmi.stackExperience.rank.1" : "Učeň", - "vcmi.stackExperience.rank.2" : "Trénovaný", - "vcmi.stackExperience.rank.3" : "Zručný", - "vcmi.stackExperience.rank.4" : "Prověřený", - "vcmi.stackExperience.rank.5" : "Veterán", - "vcmi.stackExperience.rank.6" : "Adept", - "vcmi.stackExperience.rank.7" : "Expert", - "vcmi.stackExperience.rank.8" : "Elitní", - "vcmi.stackExperience.rank.9" : "Mistr", - "vcmi.stackExperience.rank.10" : "Eso", - - "core.bonus.ADDITIONAL_ATTACK.name": "Dvojitý úder", - "core.bonus.ADDITIONAL_ATTACK.description": "Útočí dvakrát", - "core.bonus.ADDITIONAL_RETALIATION.name": "Další odveta", - "core.bonus.ADDITIONAL_RETALIATION.description": "Může zaútočit zpět navíc ${val}x", - "core.bonus.AIR_IMMUNITY.name": "Vzdušná odolnost", - "core.bonus.AIR_IMMUNITY.description": "Imunní všem kouzlům školy vzdušné magie", - "core.bonus.ATTACKS_ALL_ADJACENT.name": "Útok okolo", - "core.bonus.ATTACKS_ALL_ADJACENT.description": "Útočí na všechny sousední jednotky", - "core.bonus.BLOCKS_RETALIATION.name": "Žádná odveta", - "core.bonus.BLOCKS_RETALIATION.description": "Nepřítel nemůže zaútočit zpět", - "core.bonus.BLOCKS_RANGED_RETALIATION.name": "Žádná odveta na dálku", - "core.bonus.BLOCKS_RANGED_RETALIATION.description": "Nepřítel nemůže zaútočit zpět útokem na dálku", - "core.bonus.CATAPULT.name": "Katapult", - "core.bonus.CATAPULT.description": "Útočí na ochranné hradby", - "core.bonus.CHANGES_SPELL_COST_FOR_ALLY.name": "Snížit cenu kouzel (${val})", - "core.bonus.CHANGES_SPELL_COST_FOR_ALLY.description": "Snižuje cenu energie hrdiny o ${val}", - "core.bonus.CHANGES_SPELL_COST_FOR_ENEMY.name": "Tlumič magie (${val})", - "core.bonus.CHANGES_SPELL_COST_FOR_ENEMY.description": "Zvyšuje cenu energie kouzlení nepřítele o ${val}", - "core.bonus.CHARGE_IMMUNITY.name": "Immune to Charge", // TODO - "core.bonus.CHARGE_IMMUNITY.description": "Immune to Cavalier's and Champion's Charge", - "core.bonus.DARKNESS.name": "Závoj temnoty", - "core.bonus.DARKNESS.description": "Vytvoří clonu temnoty v oblasti ${val} polí", - "core.bonus.DEATH_STARE.name": "Smrtící pohled (${val}%)", - "core.bonus.DEATH_STARE.description": "Má ${val}% šanci zabít jednu creature", - "core.bonus.DEFENSIVE_STANCE.name": "Obranný bonus", - "core.bonus.DEFENSIVE_STANCE.description": "+${val} obranné síly při obraně", - "core.bonus.DESTRUCTION.name": "Zničení", - "core.bonus.DESTRUCTION.description": "Má ${val}% šanci zabít další jednotky po útoku", - "core.bonus.DOUBLE_DAMAGE_CHANCE.name": "Smrtící rána", - "core.bonus.DOUBLE_DAMAGE_CHANCE.description": "Má ${val}% šanci na udělení dvojnásobného základního poškození při útoku", - "core.bonus.DRAGON_NATURE.name": "Drak", - "core.bonus.DRAGON_NATURE.description": "Jednotka má povahu draka", - "core.bonus.EARTH_IMMUNITY.name": "Zemní odolnost", - "core.bonus.EARTH_IMMUNITY.description": "Imunní všem kouzlům školy zemské magie", - "core.bonus.ENCHANTER.name": "Zaklínač", - "core.bonus.ENCHANTER.description": "Může masově seslat ${subtype.spell} každý tah", - "core.bonus.ENCHANTED.name": "Očarovaný", - "core.bonus.ENCHANTED.description": "Trvale ovlivněm kouzlem ${subtype.spell}", - "core.bonus.ENEMY_DEFENCE_REDUCTION.name": "Nevšímá si ${val} % bodů obrany", - "core.bonus.ENEMY_ATTACK_REDUCTION.description": "When being attacked, ${val}% of the attacker's attack is ignored", - "core.bonus.ENEMY_DEFENCE_REDUCTION.description": "Pří útoku nebude brát v potaz ${val}% bodů obrany obránce", - "core.bonus.FIRE_IMMUNITY.name": "Ohnivá odolnost", - "core.bonus.FIRE_IMMUNITY.description": "Imunní všem kouzlům školy ohnivé magie", - "core.bonus.FIRE_SHIELD.name": "Ohnivý štít (${val}%)", - "core.bonus.FIRE_SHIELD.description": "Odrazí část zranení útoku zblízka", - "core.bonus.FIRST_STRIKE.name": "První úder", - "core.bonus.FIRST_STRIKE.description": "Tato jednotka provede odvetu ještě než je na ni zaútočeno", - "core.bonus.FEAR.name": "Strach", - "core.bonus.FEAR.description": "Způsobí strach nepřátelskému oddílu", - "core.bonus.FEARLESS.name": "Nebojácnost", - "core.bonus.FEARLESS.description": "Odolnost proti strachu", - "core.bonus.FEROCITY.name": "Ferocity", //TODO - "core.bonus.FEROCITY.description": "Attacks ${val} additional times if killed anybody", - "core.bonus.FLYING.name": "Letec", - "core.bonus.FLYING.description": "Při pohybu létá (přes překážky)", - "core.bonus.FREE_SHOOTING.name": "Blízké výstřely", - "core.bonus.FREE_SHOOTING.description": "Může použít výstřely při útoku zblízka", - "core.bonus.GARGOYLE.name": "Chrlič", - "core.bonus.GARGOYLE.description": "Cannot be raised or healed", // TODO - "core.bonus.GENERAL_DAMAGE_REDUCTION.name": "Snižuje poškození (${val}%)", - "core.bonus.GENERAL_DAMAGE_REDUCTION.description": "Snižuje poškození od útoků z dálky a blízka", - "core.bonus.HATE.name": "Nesnáší ${subtype.creature}", - "core.bonus.HATE.description": "Dává o ${val} % větší zranění jednotce ${subtype.creature}", - "core.bonus.HEALER.name": "Léčitel", - "core.bonus.HEALER.description": "Léčí spojenecké jednotky", - "core.bonus.HP_REGENERATION.name": "Regenerace", - "core.bonus.HP_REGENERATION.description": "Každé kolo léčí ${val} životů", - "core.bonus.JOUSTING.name": "Nabití šampiona", - "core.bonus.JOUSTING.description": "+${val}% poškození za každé projité pole", - "core.bonus.KING.name": "Král", - "core.bonus.KING.description": "Zranitelný zabijákovi úrovně ${val} a vyšší", - "core.bonus.LEVEL_SPELL_IMMUNITY.name": "Odolnost kouzel 1-${val}", - "core.bonus.LEVEL_SPELL_IMMUNITY.description": "Odolnost vůči kouzlům úrovní 1-${val}", - "core.bonus.LIMITED_SHOOTING_RANGE.name" : "Omezený dosah střelby", - "core.bonus.LIMITED_SHOOTING_RANGE.description" : "Nevystřelí na jednotky dále než ${val} polí", - "core.bonus.LIFE_DRAIN.name": "Vysátí životů (${val}%)", - "core.bonus.LIFE_DRAIN.description": "Vysaje ${val}% uděleného poškození", - "core.bonus.MANA_CHANNELING.name": "${val}% kouzelný kanál", - "core.bonus.MANA_CHANNELING.description": "Dá vašemu hrdinovi ${val} % many využité nepřítelem", - "core.bonus.MANA_DRAIN.name": "Vysátí many", - "core.bonus.MANA_DRAIN.description": "Každé kolo vysaje ${val} many", - "core.bonus.MAGIC_MIRROR.name": "Kouzelné zrcadlo (${val}%)", - "core.bonus.MAGIC_MIRROR.description": "Má ${val}% šanci odrazit útočné kouzlo na nepřátelskou jednotku", - "core.bonus.MAGIC_RESISTANCE.name": "Magická odolnost (${val}%)", - "core.bonus.MAGIC_RESISTANCE.description": "Má ${val}% šanci ustát nepřátelské kouzlo", - "core.bonus.MIND_IMMUNITY.name": "Imunita kouzel mysli", - "core.bonus.MIND_IMMUNITY.description": "Imunní vůči kouzlům cílícím na mysl", - "core.bonus.NO_DISTANCE_PENALTY.name": "Bez penalizace vzdálenosti", - "core.bonus.NO_DISTANCE_PENALTY.description": "Plné poškození na jakoukoliv vzdálenost", - "core.bonus.NO_MELEE_PENALTY.name": "Bez penalizace útoku zblízka", - "core.bonus.NO_MELEE_PENALTY.description": "Jednotka není penalizována za útok zblízka", - "core.bonus.NO_MORALE.name": "Neutrální morálka", - "core.bonus.NO_MORALE.description": "Jednotka je imunní vůči efektu morálky", - "core.bonus.NO_WALL_PENALTY.name": "Bez penalizace hradbami", - "core.bonus.NO_WALL_PENALTY.description": "Plné poškození při obléhání", - "core.bonus.NON_LIVING.name": "Neživoucí", - "core.bonus.NON_LIVING.description": "Imunní vůči mnohým efektům", - "core.bonus.RANDOM_SPELLCASTER.name": "Náhodný kouzelník", - "core.bonus.RANDOM_SPELLCASTER.description": "Může seslat náhodné kouzlo", - "core.bonus.RANGED_RETALIATION.name": "Vzdálená odveta", - "core.bonus.RANGED_RETALIATION.description": "Může provést protiútok na dálku", - "core.bonus.RECEPTIVE.name": "Přijímavý", - "core.bonus.RECEPTIVE.description": "Není imunní vůči přátelským kouzlům", - "core.bonus.REBIRTH.name": "Znovuzrození (${val}%)", - "core.bonus.REBIRTH.description": "${val}% oddílu se po smrti znovu narodí", - "core.bonus.RETURN_AFTER_STRIKE.name": "Útok a návrat", - "core.bonus.RETURN_AFTER_STRIKE.description": "Navrátí se po útoku na blízko", - "core.bonus.REVENGE.name": "Msta", - "core.bonus.REVENGE.description": "Deals extra damage based on attacker's lost health in battle", //TODO - "core.bonus.SHOOTER.name": "Střelec", - "core.bonus.SHOOTER.description": "Jednotka může střílet", - "core.bonus.SHOOTS_ALL_ADJACENT.name": "Střílí okolo", - "core.bonus.SHOOTS_ALL_ADJACENT.description": "Vzdálené útoky této jednotky zasáhnou všechny cíle v malé oblasti", - "core.bonus.SOUL_STEAL.name": "Zloděj duší", - "core.bonus.SOUL_STEAL.description": "Získá ${val} nových jednotek za každého zabitého nepřítele", - "core.bonus.SPELLCASTER.name": "Kouzelník", - "core.bonus.SPELLCASTER.description": "Může seslat ${subtype.spell}", - "core.bonus.SPELL_AFTER_ATTACK.name": "Kouzlení po útoku", - "core.bonus.SPELL_AFTER_ATTACK.description": "Má ${val}% šanci seslat ${subtype.spell} po zaútočení", - "core.bonus.SPELL_BEFORE_ATTACK.name": "Kouzlení před útokem", - "core.bonus.SPELL_BEFORE_ATTACK.description": "Má ${val}% šanci seslat ${subtype.spell} před zaútočením", - "core.bonus.SPELL_DAMAGE_REDUCTION.name": "Magická odolnost", - "core.bonus.SPELL_DAMAGE_REDUCTION.description": "Zranění od kouzel sníženo o ${val}%.", - "core.bonus.SPELL_IMMUNITY.name": "Odolnost vůči kouzlům", - "core.bonus.SPELL_IMMUNITY.description": "Odolnost proti ${subtype.spell}", - "core.bonus.SPELL_LIKE_ATTACK.name": "Útok kouzlem", - "core.bonus.SPELL_LIKE_ATTACK.description": "Útočí kouzlem ${subtype.spell}", - "core.bonus.SPELL_RESISTANCE_AURA.name": "Aura odolnosti", - "core.bonus.SPELL_RESISTANCE_AURA.description": "Oddíly poblíž získají ${val}% magickou odolnost", - "core.bonus.SUMMON_GUARDIANS.name": "Povolat strážce", - "core.bonus.SUMMON_GUARDIANS.description": "Na začátku bitvy povolá ${subtype.creature} (${val}%)", - "core.bonus.SYNERGY_TARGET.name": "Synergizable", // TODO - "core.bonus.SYNERGY_TARGET.description": "This creature is vulnerable to synergy effect", //TODO - "core.bonus.TWO_HEX_ATTACK_BREATH.name": "Dech", - "core.bonus.TWO_HEX_ATTACK_BREATH.description": "Dechový útok (dosah do dvou polí)", - "core.bonus.THREE_HEADED_ATTACK.name": "Tříhlavý útok", - "core.bonus.THREE_HEADED_ATTACK.description": "Útočí na tři sousední jednotky", - "core.bonus.TRANSMUTATION.name": "Transmutace", - "core.bonus.TRANSMUTATION.description": "${val}% šance na přeměnu útočené jednotky na jiný druh", - "core.bonus.UNDEAD.name": "Nemrtvý", - "core.bonus.UNDEAD.description": "Jednotka je nemrtvá", - "core.bonus.UNLIMITED_RETALIATIONS.name": "Neomezené odvety", - "core.bonus.UNLIMITED_RETALIATIONS.description": "Může provést odvetu za neomezený počet útoků", - "core.bonus.WATER_IMMUNITY.name": "Vodní odolnost", - "core.bonus.WATER_IMMUNITY.description": "Imunní všem kouzlům školy vodní magie", - "core.bonus.WIDE_BREATH.name": "Široký dech", - "core.bonus.WIDE_BREATH.description": "Útočí širokým dechem (více polí)" -} diff --git a/Mods/vcmi/mod.json b/Mods/vcmi/mod.json index baf74106d..0d1d6b65d 100644 --- a/Mods/vcmi/mod.json +++ b/Mods/vcmi/mod.json @@ -8,7 +8,7 @@ "skipValidation" : true, "translations" : [ - "config/vcmi/chinese.json" + "config/chinese.json" ] }, @@ -18,7 +18,7 @@ "skipValidation" : true, "translations" : [ - "config/vcmi/czech.json" + "config/czech.json" ] }, @@ -29,7 +29,7 @@ "skipValidation" : true, "translations" : [ - "config/vcmi/french.json" + "config/french.json" ] }, @@ -40,7 +40,7 @@ "skipValidation" : true, "translations" : [ - "config/vcmi/german.json" + "config/german.json" ] }, @@ -51,7 +51,7 @@ "skipValidation" : true, "translations" : [ - "config/vcmi/polish.json" + "config/polish.json" ] }, @@ -62,7 +62,7 @@ "skipValidation" : true, "translations" : [ - "config/vcmi/portuguese.json" + "config/portuguese.json" ] }, @@ -73,18 +73,7 @@ "skipValidation" : true, "translations" : [ - "config/vcmi/russian.json" - ] - }, - - "ukrainian" : { - "name" : "VCMI - ключові файли", - "description" : "Ключові файли необхідні для повноцінної роботи VCMI", - "author" : "Команда VCMI", - - "skipValidation" : true, - "translations" : [ - "config/vcmi/ukrainian.json" + "config/russian.json" ] }, @@ -95,7 +84,29 @@ "skipValidation" : true, "translations" : [ - "config/vcmi/spanish.json" + "config/spanish.json" + ] + }, + + "swedish" : { + "name" : "Nödvändiga VCMI-filer", + "description" : "Filer som behövs för att köra VCMI korrekt", + "author" : "Maurycy (XCOM-HUB on GitHub)", + + "skipValidation" : true, + "translations" : [ + "config/swedish.json" + ] + }, + + "ukrainian" : { + "name" : "VCMI - ключові файли", + "description" : "Ключові файли необхідні для повноцінної роботи VCMI", + "author" : "Команда VCMI", + + "skipValidation" : true, + "translations" : [ + "config/ukrainian.json" ] }, @@ -105,7 +116,7 @@ "author": "Vũ Đắc Hoàng Ân", "skipValidation": true, "translations": [ - "config/vcmi/vietnamese.json" + "config/vietnamese.json" ] }, @@ -114,121 +125,94 @@ "contact" : "http://forum.vcmi.eu/index.php", "modType" : "Graphical", - "factions" : [ "config/vcmi/towerFactions" ], - "creatures" : [ "config/vcmi/towerCreature" ], + "factions" : [ "config/towerFactions" ], + "creatures" : [ "config/towerCreature" ], + "spells" : [ "config/spells" ], "translations" : [ - "config/vcmi/english.json" + "config/english.json" ], "templates" : [ - "config/vcmi/rmg/hdmod/aroundamarsh.JSON", - "config/vcmi/rmg/hdmod/balance.JSON", - "config/vcmi/rmg/hdmod/blockbuster.JSON", - "config/vcmi/rmg/hdmod/clashOfDragons.json", - "config/vcmi/rmg/hdmod/coldshadowsFantasy.json", - "config/vcmi/rmg/hdmod/cube.JSON", - "config/vcmi/rmg/hdmod/diamond.JSON", - "config/vcmi/rmg/hdmod/extreme.JSON", - "config/vcmi/rmg/hdmod/extreme2.JSON", - "config/vcmi/rmg/hdmod/fear.JSON", - "config/vcmi/rmg/hdmod/frozenDragons.JSON", - "config/vcmi/rmg/hdmod/gimlisRevenge.JSON", - "config/vcmi/rmg/hdmod/guerilla.JSON", - "config/vcmi/rmg/hdmod/headquarters.JSON", - "config/vcmi/rmg/hdmod/hypercube.JSON", - "config/vcmi/rmg/hdmod/jebusCross.json", - "config/vcmi/rmg/hdmod/longRun.JSON", - "config/vcmi/rmg/hdmod/marathon.JSON", - "config/vcmi/rmg/hdmod/miniNostalgia.JSON", - "config/vcmi/rmg/hdmod/nostalgia.JSON", - "config/vcmi/rmg/hdmod/oceansEleven.JSON", - "config/vcmi/rmg/hdmod/panic.JSON", - "config/vcmi/rmg/hdmod/poorJebus.JSON", - "config/vcmi/rmg/hdmod/reckless.JSON", - "config/vcmi/rmg/hdmod/roadrunner.JSON", - "config/vcmi/rmg/hdmod/shaaafworld.JSON", - "config/vcmi/rmg/hdmod/skirmish.JSON", - "config/vcmi/rmg/hdmod/speed1.JSON", - "config/vcmi/rmg/hdmod/speed2.JSON", - "config/vcmi/rmg/hdmod/spider.JSON", - "config/vcmi/rmg/hdmod/superslam.JSON", - "config/vcmi/rmg/hdmod/triad.JSON", - "config/vcmi/rmg/hdmod/vortex.JSON", - "config/vcmi/rmg/heroes3/dwarvenTunnels.JSON", - "config/vcmi/rmg/heroes3/golemsAplenty.JSON", - "config/vcmi/rmg/heroes3/meetingInMuzgob.JSON", - "config/vcmi/rmg/heroes3/monksRetreat.JSON", - "config/vcmi/rmg/heroes3/newcomers.JSON", - "config/vcmi/rmg/heroes3/readyOrNot.JSON", - "config/vcmi/rmg/heroes3/smallRing.JSON", - "config/vcmi/rmg/heroes3/southOfHell.JSON", - "config/vcmi/rmg/heroes3/worldsAtWar.JSON", - "config/vcmi/rmg/symmetric/2sm0k.JSON", - "config/vcmi/rmg/symmetric/2sm2a.JSON", - "config/vcmi/rmg/symmetric/2sm2b.JSON", - "config/vcmi/rmg/symmetric/2sm2b(2).JSON", - "config/vcmi/rmg/symmetric/2sm2c.JSON", - "config/vcmi/rmg/symmetric/2sm2f.JSON", - "config/vcmi/rmg/symmetric/2sm2f(2).JSON", - "config/vcmi/rmg/symmetric/2sm2h.JSON", - "config/vcmi/rmg/symmetric/2sm2h(2).JSON", - "config/vcmi/rmg/symmetric/2sm2i.JSON", - "config/vcmi/rmg/symmetric/2sm2i(2).JSON", - "config/vcmi/rmg/symmetric/2sm4d.JSON", - "config/vcmi/rmg/symmetric/2sm4d(2).JSON", - "config/vcmi/rmg/symmetric/2sm4d(3).JSON", - "config/vcmi/rmg/symmetric/3sb0b.JSON", - "config/vcmi/rmg/symmetric/3sb0c.JSON", - "config/vcmi/rmg/symmetric/3sm3d.JSON", - "config/vcmi/rmg/symmetric/4sm0d.JSON", - "config/vcmi/rmg/symmetric/4sm0f.JSON", - "config/vcmi/rmg/symmetric/4sm0g.JSON", - "config/vcmi/rmg/symmetric/4sm4e.JSON", - "config/vcmi/rmg/symmetric/5sb0a.JSON", - "config/vcmi/rmg/symmetric/5sb0b.JSON", - "config/vcmi/rmg/symmetric/6lm10.JSON", - "config/vcmi/rmg/symmetric/6lm10a.JSON", - "config/vcmi/rmg/symmetric/6sm0b.JSON", - "config/vcmi/rmg/symmetric/6sm0d.JSON", - "config/vcmi/rmg/symmetric/6sm0e.JSON", - "config/vcmi/rmg/symmetric/7sb0b.JSON", - "config/vcmi/rmg/symmetric/7sb0c.JSON", - "config/vcmi/rmg/symmetric/8mm0e.JSON", - "config/vcmi/rmg/symmetric/8mm6.JSON", - "config/vcmi/rmg/symmetric/8mm6a.JSON", - "config/vcmi/rmg/symmetric/8sm0c.JSON", - "config/vcmi/rmg/symmetric/8sm0f.JSON", - "config/vcmi/rmg/symmetric/8xm12.JSON", - "config/vcmi/rmg/symmetric/8xm12a.JSON", - "config/vcmi/rmg/symmetric/8xm8.JSON" - ], - - "filesystem": - { - "CONFIG/" : - [ - {"type" : "dir", "path" : "/Config"} - ], - "DATA/" : - [ - {"type" : "dir", "path" : "/Data"} - ], - "SPRITES/": - [ - {"type" : "dir", "path" : "/Sprites"} - ], - "MAPS/": - [ - {"type" : "dir", "path" : "/Maps"} - ], - "SOUNDS/": - [ - {"type" : "dir", "path" : "/Sounds"} - ], - "VIDEO/": - [ - {"type" : "dir", "path" : "/Video"} - ] - } + "config/rmg/hdmod/aroundamarsh.JSON", + "config/rmg/hdmod/balance.JSON", + "config/rmg/hdmod/blockbuster.JSON", + "config/rmg/hdmod/clashOfDragons.json", + "config/rmg/hdmod/coldshadowsFantasy.json", + "config/rmg/hdmod/cube.JSON", + "config/rmg/hdmod/diamond.JSON", + "config/rmg/hdmod/extreme.JSON", + "config/rmg/hdmod/extreme2.JSON", + "config/rmg/hdmod/fear.JSON", + "config/rmg/hdmod/frozenDragons.JSON", + "config/rmg/hdmod/gimlisRevenge.JSON", + "config/rmg/hdmod/guerilla.JSON", + "config/rmg/hdmod/headquarters.JSON", + "config/rmg/hdmod/hypercube.JSON", + "config/rmg/hdmod/jebusCross.json", + "config/rmg/hdmod/longRun.JSON", + "config/rmg/hdmod/marathon.JSON", + "config/rmg/hdmod/miniNostalgia.JSON", + "config/rmg/hdmod/nostalgia.JSON", + "config/rmg/hdmod/oceansEleven.JSON", + "config/rmg/hdmod/panic.JSON", + "config/rmg/hdmod/poorJebus.JSON", + "config/rmg/hdmod/reckless.JSON", + "config/rmg/hdmod/roadrunner.JSON", + "config/rmg/hdmod/shaaafworld.JSON", + "config/rmg/hdmod/skirmish.JSON", + "config/rmg/hdmod/speed1.JSON", + "config/rmg/hdmod/speed2.JSON", + "config/rmg/hdmod/spider.JSON", + "config/rmg/hdmod/superslam.JSON", + "config/rmg/hdmod/triad.JSON", + "config/rmg/hdmod/vortex.JSON", + "config/rmg/heroes3/dwarvenTunnels.JSON", + "config/rmg/heroes3/golemsAplenty.JSON", + "config/rmg/heroes3/meetingInMuzgob.JSON", + "config/rmg/heroes3/monksRetreat.JSON", + "config/rmg/heroes3/newcomers.JSON", + "config/rmg/heroes3/readyOrNot.JSON", + "config/rmg/heroes3/smallRing.JSON", + "config/rmg/heroes3/southOfHell.JSON", + "config/rmg/heroes3/worldsAtWar.JSON", + "config/rmg/symmetric/2sm0k.JSON", + "config/rmg/symmetric/2sm2a.JSON", + "config/rmg/symmetric/2sm2b.JSON", + "config/rmg/symmetric/2sm2b(2).JSON", + "config/rmg/symmetric/2sm2c.JSON", + "config/rmg/symmetric/2sm2f.JSON", + "config/rmg/symmetric/2sm2f(2).JSON", + "config/rmg/symmetric/2sm2h.JSON", + "config/rmg/symmetric/2sm2h(2).JSON", + "config/rmg/symmetric/2sm2i.JSON", + "config/rmg/symmetric/2sm2i(2).JSON", + "config/rmg/symmetric/2sm4d.JSON", + "config/rmg/symmetric/2sm4d(2).JSON", + "config/rmg/symmetric/2sm4d(3).JSON", + "config/rmg/symmetric/3sb0b.JSON", + "config/rmg/symmetric/3sb0c.JSON", + "config/rmg/symmetric/3sm3d.JSON", + "config/rmg/symmetric/4sm0d.JSON", + "config/rmg/symmetric/4sm0f.JSON", + "config/rmg/symmetric/4sm0g.JSON", + "config/rmg/symmetric/4sm4e.JSON", + "config/rmg/symmetric/5sb0a.JSON", + "config/rmg/symmetric/5sb0b.JSON", + "config/rmg/symmetric/6lm10.JSON", + "config/rmg/symmetric/6lm10a.JSON", + "config/rmg/symmetric/6sm0b.JSON", + "config/rmg/symmetric/6sm0d.JSON", + "config/rmg/symmetric/6sm0e.JSON", + "config/rmg/symmetric/7sb0b.JSON", + "config/rmg/symmetric/7sb0c.JSON", + "config/rmg/symmetric/8mm0e.JSON", + "config/rmg/symmetric/8mm6.JSON", + "config/rmg/symmetric/8mm6a.JSON", + "config/rmg/symmetric/8sm0c.JSON", + "config/rmg/symmetric/8sm0f.JSON", + "config/rmg/symmetric/8xm12.JSON", + "config/rmg/symmetric/8xm12a.JSON", + "config/rmg/symmetric/8xm8.JSON" + ] } diff --git a/client/ArtifactsUIController.cpp b/client/ArtifactsUIController.cpp index 73461a58a..7f018c208 100644 --- a/client/ArtifactsUIController.cpp +++ b/client/ArtifactsUIController.cpp @@ -26,6 +26,7 @@ ArtifactsUIController::ArtifactsUIController() { numOfMovedArts = 0; + numOfArtsAskAssembleSession = 0; } bool ArtifactsUIController::askToAssemble(const ArtifactLocation & al, const bool onlyEquipped, const bool checkIgnored) @@ -71,10 +72,13 @@ bool ArtifactsUIController::askToAssemble(const CGHeroInstance * hero, const Art } bool assembleConfirmed = false; - MetaString message = MetaString::createFromTextID(art->artType->getDescriptionTextID()); + MetaString message = MetaString::createFromTextID(art->getType()->getDescriptionTextID()); message.appendEOL(); message.appendEOL(); - message.appendRawString(CGI->generaltexth->allTexts[732]); // You possess all of the components needed to assemble the + if(combinedArt->isFused()) + message.appendRawString(CGI->generaltexth->translate("vcmi.heroWindow.fusingArtifact.fusing")); + else + message.appendRawString(CGI->generaltexth->allTexts[732]); // You possess all of the components needed to assemble the message.replaceName(ArtifactID(combinedArt->getId())); LOCPLINT->showYesNoDialog(message.toString(), [&assembleConfirmed, hero, slot, combinedArt]() { @@ -102,12 +106,12 @@ bool ArtifactsUIController::askToDisassemble(const CGHeroInstance * hero, const if(hero->tempOwner != LOCPLINT->playerID) return false; - if(art->isCombined()) + if(art->hasParts()) { - if(ArtifactUtils::isSlotBackpack(slot) && !ArtifactUtils::isBackpackFreeSlots(hero, art->artType->getConstituents().size() - 1)) + if(ArtifactUtils::isSlotBackpack(slot) && !ArtifactUtils::isBackpackFreeSlots(hero, art->getType()->getConstituents().size() - 1)) return false; - MetaString message = MetaString::createFromTextID(art->artType->getDescriptionTextID()); + MetaString message = MetaString::createFromTextID(art->getType()->getDescriptionTextID()); message.appendEOL(); message.appendEOL(); message.appendRawString(CGI->generaltexth->allTexts[733]); // Do you wish to disassemble this artifact? diff --git a/client/CMakeLists.txt b/client/CMakeLists.txt index 991aba090..5de831a28 100644 --- a/client/CMakeLists.txt +++ b/client/CMakeLists.txt @@ -116,8 +116,8 @@ set(vcmiclientcommon_SRCS globalLobby/GlobalLobbyWindow.cpp widgets/Buttons.cpp - widgets/CArtPlace.cpp widgets/CComponent.cpp + widgets/CComponentHolder.cpp widgets/CExchangeController.cpp widgets/CGarrisonInt.cpp widgets/CreatureCostBox.cpp @@ -327,8 +327,8 @@ set(vcmiclientcommon_HEADERS globalLobby/GlobalLobbyWindow.h widgets/Buttons.h - widgets/CArtPlace.h widgets/CComponent.h + widgets/CComponentHolder.h widgets/CExchangeController.h widgets/CGarrisonInt.h widgets/CreatureCostBox.h diff --git a/client/CPlayerInterface.cpp b/client/CPlayerInterface.cpp index 05faf07a0..1462e7977 100644 --- a/client/CPlayerInterface.cpp +++ b/client/CPlayerInterface.cpp @@ -67,7 +67,6 @@ #include "../lib/CConfigHandler.h" #include "../lib/texts/CGeneralTextHandler.h" -#include "../lib/CHeroHandler.h" #include "../lib/CPlayerState.h" #include "../lib/CRandomGenerator.h" #include "../lib/CStack.h" @@ -239,7 +238,7 @@ void CPlayerInterface::performAutosave() std::string name = cb->getMapHeader()->name.toString(); int txtlen = TextOperations::getUnicodeCharactersCount(name); - TextOperations::trimRightUnicode(name, std::max(0, txtlen - 15)); + TextOperations::trimRightUnicode(name, std::max(0, txtlen - 14)); auto const & isSymbolIllegal = [&](char c) { static const std::string forbiddenChars("\\/:*?\"<>| "); @@ -250,7 +249,7 @@ void CPlayerInterface::performAutosave() }; std::replace_if(name.begin(), name.end(), isSymbolIllegal, '_' ); - prefix = name + "_" + cb->getStartInfo()->startTimeIso8601 + "/"; + prefix = vstd::getFormattedDateTime(cb->getStartInfo()->startTime, "%Y-%m-%d_%H-%M") + "_" + name + "/"; } } @@ -432,6 +431,8 @@ void CPlayerInterface::heroCreated(const CGHeroInstance * hero) EVENT_HANDLER_CALLED_BY_CLIENT; localState->addWanderingHero(hero); adventureInt->onHeroChanged(hero); + if(castleInt) + CCS->soundh->playSound(soundBase::newBuilding); } void CPlayerInterface::openTownWindow(const CGTownInstance * town) { @@ -1148,7 +1149,7 @@ void CPlayerInterface::showMapObjectSelectDialog(QueryID askID, const Component const CGTownInstance * t = dynamic_cast(cb->getObj(obj)); if(t) { - auto image = GH.renderHandler().loadImage(AnimationPath::builtin("ITPA"), t->town->clientInfo.icons[t->hasFort()][false] + 2, 0, EImageBlitMode::OPAQUE); + auto image = GH.renderHandler().loadImage(AnimationPath::builtin("ITPA"), t->getTown()->clientInfo.icons[t->hasFort()][false] + 2, 0, EImageBlitMode::OPAQUE); image->scaleTo(Point(35, 23)); images.push_back(image); } @@ -1343,6 +1344,8 @@ void CPlayerInterface::initializeHeroTownList() localState->addOwnedTown(town); } + localState->deserialize(*cb->getPlayerState(playerID)->playerLocalSettings); + if(adventureInt) adventureInt->onHeroChanged(nullptr); } diff --git a/client/CServerHandler.cpp b/client/CServerHandler.cpp index 373bbec3e..4a3dcd690 100644 --- a/client/CServerHandler.cpp +++ b/client/CServerHandler.cpp @@ -141,7 +141,12 @@ void CServerHandler::resetStateForLobby(EStartMode mode, ESelectionScreen screen if(!playerNames.empty()) //if have custom set of player names - use it localPlayerNames = playerNames; else - localPlayerNames.push_back(settings["general"]["playerName"].String()); + { + std::string playerName = settings["general"]["playerName"].String(); + if(playerName == "Player") + playerName = CGI->generaltexth->translate("core.genrltxt.434"); + localPlayerNames.push_back(playerName); + } gameChat->resetMatchState(); lobbyClient->resetMatchState(); @@ -853,7 +858,7 @@ void CServerHandler::onPacketReceived(const std::shared_ptr if(getState() == EClientState::DISCONNECTING) return; - CPack * pack = logicConnection->retrievePack(message); + auto pack = logicConnection->retrievePack(message); ServerHandlerCPackVisitor visitor(*this); pack->visit(visitor); } @@ -938,14 +943,14 @@ void CServerHandler::visitForLobby(CPackForLobby & lobbyPack) void CServerHandler::visitForClient(CPackForClient & clientPack) { - client->handlePack(&clientPack); + client->handlePack(clientPack); } void CServerHandler::sendLobbyPack(const CPackForLobby & pack) const { if(getState() != EClientState::STARTING) - logicConnection->sendPack(&pack); + logicConnection->sendPack(pack); } bool CServerHandler::inLobbyRoom() const diff --git a/client/CServerHandler.h b/client/CServerHandler.h index 1ef583e0c..a62aa8a45 100644 --- a/client/CServerHandler.h +++ b/client/CServerHandler.h @@ -25,7 +25,6 @@ struct TurnTimerInfo; class CMapInfo; class CGameState; struct ClientPlayer; -struct CPack; struct CPackForLobby; struct CPackForClient; diff --git a/client/Client.cpp b/client/Client.cpp index 2d57375ca..0f55cbd52 100644 --- a/client/Client.cpp +++ b/client/Client.cpp @@ -163,7 +163,7 @@ void CClient::save(const std::string & fname) } SaveGame save_game(fname); - sendRequest(&save_game, PlayerColor::NEUTRAL); + sendRequest(save_game, PlayerColor::NEUTRAL); } void CClient::endNetwork() @@ -348,37 +348,35 @@ void CClient::installNewBattleInterface(std::shared_ptr ba } } -void CClient::handlePack(CPackForClient * pack) +void CClient::handlePack(CPackForClient & pack) { ApplyClientNetPackVisitor afterVisitor(*this, *gameState()); ApplyFirstClientNetPackVisitor beforeVisitor(*this, *gameState()); - pack->visit(beforeVisitor); - logNetwork->trace("\tMade first apply on cl: %s", typeid(*pack).name()); + pack.visit(beforeVisitor); + logNetwork->trace("\tMade first apply on cl: %s", typeid(pack).name()); { boost::unique_lock lock(CGameState::mutex); gs->apply(pack); } - logNetwork->trace("\tApplied on gs: %s", typeid(*pack).name()); - pack->visit(afterVisitor); - logNetwork->trace("\tMade second apply on cl: %s", typeid(*pack).name()); - - delete pack; + logNetwork->trace("\tApplied on gs: %s", typeid(pack).name()); + pack.visit(afterVisitor); + logNetwork->trace("\tMade second apply on cl: %s", typeid(pack).name()); } -int CClient::sendRequest(const CPackForServer * request, PlayerColor player) +int CClient::sendRequest(const CPackForServer & request, PlayerColor player) { static ui32 requestCounter = 1; ui32 requestID = requestCounter++; - logNetwork->trace("Sending a request \"%s\". It'll have an ID=%d.", typeid(*request).name(), requestID); + logNetwork->trace("Sending a request \"%s\". It'll have an ID=%d.", typeid(request).name(), requestID); waitingRequest.pushBack(requestID); - request->requestID = requestID; - request->player = player; + request.requestID = requestID; + request.player = player; CSH->logicConnection->sendPack(request); if(vstd::contains(playerint, player)) - playerint[player]->requestSent(request, requestID); + playerint[player]->requestSent(&request, requestID); return requestID; } diff --git a/client/Client.h b/client/Client.h index d63f70578..ce1276b06 100644 --- a/client/Client.h +++ b/client/Client.h @@ -16,7 +16,6 @@ VCMI_LIB_NAMESPACE_BEGIN -struct CPack; struct CPackForServer; class IBattleEventsReceiver; class CBattleGameInterface; @@ -143,8 +142,8 @@ public: static ThreadSafeVector waitingRequest; //FIXME: make this normal field (need to join all threads before client destruction) - void handlePack(CPackForClient * pack); //applies the given pack and deletes it - int sendRequest(const CPackForServer * request, PlayerColor player); //returns ID given to that request + void handlePack(CPackForClient & pack); //applies the given pack and deletes it + int sendRequest(const CPackForServer & request, PlayerColor player); //returns ID given to that request void battleStarted(const BattleInfo * info); void battleFinished(const BattleID & battleID); @@ -159,6 +158,7 @@ public: friend class CBattleCallback; //handling players actions void changeSpells(const CGHeroInstance * hero, bool give, const std::set & spells) override {}; + void setResearchedSpells(const CGTownInstance * town, int level, const std::vector & spells, bool accepted) override {}; bool removeObject(const CGObjectInstance * obj, const PlayerColor & initiator) override {return false;}; void createBoat(const int3 & visitablePosition, BoatId type, PlayerColor initiator) override {}; void setOwner(const CGObjectInstance * obj, PlayerColor owner) override {}; @@ -188,7 +188,7 @@ public: bool swapGarrisonOnSiege(ObjectInstanceID tid) override {return false;}; bool giveHeroNewArtifact(const CGHeroInstance * h, const ArtifactID & artId, const ArtifactPosition & pos) override {return false;}; bool giveHeroNewScroll(const CGHeroInstance * h, const SpellID & spellId, const ArtifactPosition & pos) override {return false;}; - bool putArtifact(const ArtifactLocation & al, const CArtifactInstance * art, std::optional askAssemble) override {return false;}; + bool putArtifact(const ArtifactLocation & al, const ArtifactInstanceID & id, std::optional askAssemble) override {return false;}; void removeArtifact(const ArtifactLocation & al) override {}; bool moveArtifact(const PlayerColor & player, const ArtifactLocation & al1, const ArtifactLocation & al2) override {return false;}; @@ -204,7 +204,7 @@ public: void setManaPoints(ObjectInstanceID hid, int val) override {}; void giveHero(ObjectInstanceID id, PlayerColor player, ObjectInstanceID boatId = ObjectInstanceID()) override {}; void changeObjPos(ObjectInstanceID objid, int3 newPos, const PlayerColor & initiator) override {}; - void sendAndApply(CPackForClient * pack) override {}; + void sendAndApply(CPackForClient & pack) override {}; void heroExchange(ObjectInstanceID hero1, ObjectInstanceID hero2) override {}; void castSpell(const spells::Caster * caster, SpellID spellID, const int3 &pos) override {}; diff --git a/client/ClientCommandManager.cpp b/client/ClientCommandManager.cpp index a2a7176ef..fc41ffab1 100644 --- a/client/ClientCommandManager.cpp +++ b/client/ClientCommandManager.cpp @@ -36,7 +36,6 @@ #include "../lib/modding/CModHandler.h" #include "../lib/modding/ContentTypeHandler.h" #include "../lib/modding/ModUtility.h" -#include "../lib/CHeroHandler.h" #include "../lib/VCMIDirs.h" #include "../lib/logging/VisualLogger.h" #include "../lib/serializer/Connection.h" @@ -186,12 +185,12 @@ void ClientCommandManager::handleRedrawCommand() GH.windows().totalRedraw(); } -void ClientCommandManager::handleTranslateGameCommand() +void ClientCommandManager::handleTranslateGameCommand(bool onlyMissing) { std::map> textsByMod; - VLC->generaltexth->exportAllTexts(textsByMod); + VLC->generaltexth->exportAllTexts(textsByMod, onlyMissing); - const boost::filesystem::path outPath = VCMIDirs::get().userExtractedPath() / "translation"; + const boost::filesystem::path outPath = VCMIDirs::get().userExtractedPath() / ( onlyMissing ? "translationMissing" : "translation"); boost::filesystem::create_directories(outPath); for(const auto & modEntry : textsByMod) @@ -255,13 +254,20 @@ void ClientCommandManager::handleTranslateMapsCommand() logGlobal->info("Loading campaigns for export"); for (auto const & campaignName : campaignList) { - loadedCampaigns.push_back(CampaignHandler::getCampaign(campaignName.getName())); - for (auto const & part : loadedCampaigns.back()->allScenarios()) - loadedCampaigns.back()->getMap(part, nullptr); + try + { + loadedCampaigns.push_back(CampaignHandler::getCampaign(campaignName.getName())); + for (auto const & part : loadedCampaigns.back()->allScenarios()) + loadedCampaigns.back()->getMap(part, nullptr); + } + catch(std::exception & e) + { + logGlobal->warn("Campaign %s is invalid. Message: %s", campaignName.getName(), e.what()); + } } std::map> textsByMod; - VLC->generaltexth->exportAllTexts(textsByMod); + VLC->generaltexth->exportAllTexts(textsByMod, false); const boost::filesystem::path outPath = VCMIDirs::get().userExtractedPath() / "translation"; boost::filesystem::create_directories(outPath); @@ -388,7 +394,7 @@ void ClientCommandManager::handleDef2bmpCommand(std::istringstream& singleWordBu { std::string URI; singleWordBuffer >> URI; - auto anim = GH.renderHandler().loadAnimation(AnimationPath::builtin(URI), EImageBlitMode::ALPHA); + auto anim = GH.renderHandler().loadAnimation(AnimationPath::builtin(URI), EImageBlitMode::SIMPLE); anim->exportBitmaps(VCMIDirs::get().userExtractedPath()); } @@ -453,7 +459,7 @@ void ClientCommandManager::handleTellCommand(std::istringstream& singleWordBuffe if(what == "hs") { for(const CGHeroInstance* h : LOCPLINT->cb->getHeroesInfo()) - if(h->type->getIndex() == id1) + if(h->getHeroTypeID().getNum() == id1) if(const CArtifactInstance* a = h->getArt(ArtifactPosition(id2))) printCommandMessage(a->nodeName()); } @@ -592,7 +598,10 @@ void ClientCommandManager::processCommand(const std::string & message, bool call handleRedrawCommand(); else if(message=="translate" || message=="translate game") - handleTranslateGameCommand(); + handleTranslateGameCommand(false); + + else if(message=="translate missing") + handleTranslateGameCommand(true); else if(message=="translate maps") handleTranslateMapsCommand(); diff --git a/client/ClientCommandManager.h b/client/ClientCommandManager.h index 8a4caeacf..45d027075 100644 --- a/client/ClientCommandManager.h +++ b/client/ClientCommandManager.h @@ -46,7 +46,7 @@ class ClientCommandManager //take mantis #2292 issue about account if thinking a void handleRedrawCommand(); // Extracts all translateable game texts into Translation directory, separating files on per-mod basis - void handleTranslateGameCommand(); + void handleTranslateGameCommand(bool onlyMissing); // Extracts all translateable texts from maps and campaigns into Translation directory, separating files on per-mod basis void handleTranslateMapsCommand(); diff --git a/client/ClientNetPackVisitors.h b/client/ClientNetPackVisitors.h index 82cb72d53..3198f74fe 100644 --- a/client/ClientNetPackVisitors.h +++ b/client/ClientNetPackVisitors.h @@ -37,6 +37,7 @@ public: void visitHeroVisitCastle(HeroVisitCastle & pack) override; void visitSetMana(SetMana & pack) override; void visitSetMovePoints(SetMovePoints & pack) override; + void visitSetResearchedSpells(SetResearchedSpells & pack) override; void visitFoWChange(FoWChange & pack) override; void visitChangeStackCount(ChangeStackCount & pack) override; void visitSetStackType(SetStackType & pack) override; diff --git a/client/HeroMovementController.cpp b/client/HeroMovementController.cpp index d402413e5..8584ddb44 100644 --- a/client/HeroMovementController.cpp +++ b/client/HeroMovementController.cpp @@ -291,14 +291,12 @@ AudioPath HeroMovementController::getMovementSoundFor(const CGHeroInstance * her auto prevTile = LOCPLINT->cb->getTile(posPrev); auto nextTile = LOCPLINT->cb->getTile(posNext); - auto prevRoad = prevTile->roadType; - auto nextRoad = nextTile->roadType; - bool movingOnRoad = prevRoad->getId() != Road::NO_ROAD && nextRoad->getId() != Road::NO_ROAD; + bool movingOnRoad = prevTile->hasRoad() && nextTile->hasRoad(); if(movingOnRoad) - return nextTile->terType->horseSound; + return nextTile->getTerrain()->horseSound; else - return nextTile->terType->horseSoundPenalty; + return nextTile->getTerrain()->horseSoundPenalty; }; void HeroMovementController::updateMovementSound(const CGHeroInstance * h, int3 posPrev, int3 nextCoord, EPathNodeAction moveType) @@ -375,7 +373,7 @@ void HeroMovementController::sendMovementRequest(const CGHeroInstance * h, const { updateMovementSound(h, currNode.coord, nextNode.coord, nextNode.action); - assert(h->pos.z == nextNode.coord.z); // Z should change only if it's movement via teleporter and in this case this code shouldn't be executed at all + assert(h->anchorPos().z == nextNode.coord.z); // Z should change only if it's movement via teleporter and in this case this code shouldn't be executed at all logGlobal->trace("Requesting hero movement to %s", nextNode.coord.toString()); diff --git a/client/NetPacksClient.cpp b/client/NetPacksClient.cpp index cda16c4b3..1c94e0d7d 100644 --- a/client/NetPacksClient.cpp +++ b/client/NetPacksClient.cpp @@ -14,6 +14,7 @@ #include "CPlayerInterface.h" #include "CGameInfo.h" #include "windows/GUIClasses.h" +#include "windows/CCastleInterface.h" #include "mapView/mapHandler.h" #include "adventureMap/AdventureMapInterface.h" #include "adventureMap/CInGameConsole.h" @@ -31,7 +32,6 @@ #include "../lib/filesystem/FileInfo.h" #include "../lib/serializer/Connection.h" #include "../lib/texts/CGeneralTextHandler.h" -#include "../lib/CHeroHandler.h" #include "../lib/VCMI_Lib.h" #include "../lib/mapping/CMap.h" #include "../lib/VCMIDirs.h" @@ -172,6 +172,12 @@ void ApplyClientNetPackVisitor::visitSetMovePoints(SetMovePoints & pack) callInterfaceIfPresent(cl, h->tempOwner, &IGameEventsReceiver::heroMovePointsChanged, h); } +void ApplyClientNetPackVisitor::visitSetResearchedSpells(SetResearchedSpells & pack) +{ + for(const auto & win : GH.windows().findWindows()) + win->updateSpells(pack.tid); +} + void ApplyClientNetPackVisitor::visitFoWChange(FoWChange & pack) { for(auto &i : cl.playerint) @@ -664,7 +670,7 @@ void ApplyClientNetPackVisitor::visitSetHeroesInTown(SetHeroesInTown & pack) void ApplyClientNetPackVisitor::visitHeroRecruited(HeroRecruited & pack) { CGHeroInstance *h = gs.map->heroesOnMap.back(); - if(h->getHeroType() != pack.hid) + if(h->getHeroTypeID() != pack.hid) { logNetwork->error("Something wrong with hero recruited!"); } diff --git a/client/NetPacksLobbyClient.cpp b/client/NetPacksLobbyClient.cpp index 4689d8d57..49c86f11d 100644 --- a/client/NetPacksLobbyClient.cpp +++ b/client/NetPacksLobbyClient.cpp @@ -226,7 +226,7 @@ void ApplyOnLobbyScreenNetPackVisitor::visitLobbyUpdateState(LobbyUpdateState & else lobby->updateAfterStateChange(); - if(pack.hostChanged) + if(pack.hostChanged || pack.refreshList) lobby->toggleMode(handler.isHost()); } diff --git a/client/PlayerLocalState.cpp b/client/PlayerLocalState.cpp index 541b9ff80..29e664498 100644 --- a/client/PlayerLocalState.cpp +++ b/client/PlayerLocalState.cpp @@ -11,6 +11,7 @@ #include "PlayerLocalState.h" #include "../CCallback.h" +#include "../lib/json/JsonNode.h" #include "../lib/mapObjects/CGHeroInstance.h" #include "../lib/mapObjects/CGTownInstance.h" #include "../lib/pathfinder/CGPathNode.h" @@ -23,34 +24,20 @@ PlayerLocalState::PlayerLocalState(CPlayerInterface & owner) { } -void PlayerLocalState::saveHeroPaths(std::map & pathsMap) +const PlayerSpellbookSetting & PlayerLocalState::getSpellbookSettings() const { - for(auto & p : paths) - { - if(p.second.nodes.size()) - pathsMap[p.first] = p.second.endPos(); - else - logGlobal->debug("%s has assigned an empty path! Ignoring it...", p.first->getNameTranslated()); - } + return spellbookSettings; } -void PlayerLocalState::loadHeroPaths(std::map & pathsMap) +void PlayerLocalState::setSpellbookSettings(const PlayerSpellbookSetting & newSettings) { - if(owner.cb) - { - for(auto & p : pathsMap) - { - CGPath path; - owner.cb->getPathsInfo(p.first)->getPath(path, p.second); - paths[p.first] = path; - logGlobal->trace("Restored path for hero %s leading to %s with %d nodes", p.first->nodeName(), p.second.toString(), path.nodes.size()); - } - } + spellbookSettings = newSettings; } void PlayerLocalState::setPath(const CGHeroInstance * h, const CGPath & path) { paths[h] = path; + syncronizeState(); } const CGPath & PlayerLocalState::getPath(const CGHeroInstance * h) const @@ -70,6 +57,7 @@ bool PlayerLocalState::setPath(const CGHeroInstance * h, const int3 & destinatio if(!owner.cb->getPathsInfo(h)->getPath(path, destination)) { paths.erase(h); //invalidate previously possible path if selected (before other hero blocked only path / fly spell expired) + syncronizeState(); return false; } @@ -93,6 +81,7 @@ void PlayerLocalState::erasePath(const CGHeroInstance * h) { paths.erase(h); adventureInt->onHeroChanged(h); + syncronizeState(); } void PlayerLocalState::verifyPath(const CGHeroInstance * h) @@ -170,6 +159,7 @@ void PlayerLocalState::setSelection(const CArmedInstance * selection) if (adventureInt && selection) adventureInt->onSelectionChanged(selection); + syncronizeState(); } bool PlayerLocalState::isHeroSleeping(const CGHeroInstance * hero) const @@ -184,6 +174,7 @@ void PlayerLocalState::setHeroAsleep(const CGHeroInstance * hero) assert(!vstd::contains(sleepingHeroes, hero)); sleepingHeroes.push_back(hero); + syncronizeState(); } void PlayerLocalState::setHeroAwaken(const CGHeroInstance * hero) @@ -193,6 +184,7 @@ void PlayerLocalState::setHeroAwaken(const CGHeroInstance * hero) assert(vstd::contains(sleepingHeroes, hero)); vstd::erase(sleepingHeroes, hero); + syncronizeState(); } const std::vector & PlayerLocalState::getWanderingHeroes() @@ -215,6 +207,8 @@ void PlayerLocalState::addWanderingHero(const CGHeroInstance * hero) if (currentSelection == nullptr) setSelection(hero); + + syncronizeState(); } void PlayerLocalState::removeWanderingHero(const CGHeroInstance * hero) @@ -225,7 +219,12 @@ void PlayerLocalState::removeWanderingHero(const CGHeroInstance * hero) if (hero == currentSelection) { auto const * nextHero = getNextWanderingHero(hero); - setSelection(nextHero); + if (nextHero) + setSelection(nextHero); + else if (!ownedTowns.empty()) + setSelection(ownedTowns.front()); + else + setSelection(nullptr); } vstd::erase(wanderingHeroes, hero); @@ -236,6 +235,8 @@ void PlayerLocalState::removeWanderingHero(const CGHeroInstance * hero) if (currentSelection == nullptr && !ownedTowns.empty()) setSelection(ownedTowns.front()); + + syncronizeState(); } void PlayerLocalState::swapWanderingHero(size_t pos1, size_t pos2) @@ -244,6 +245,8 @@ void PlayerLocalState::swapWanderingHero(size_t pos1, size_t pos2) std::swap(wanderingHeroes.at(pos1), wanderingHeroes.at(pos2)); adventureInt->onHeroOrderChanged(); + + syncronizeState(); } const std::vector & PlayerLocalState::getOwnedTowns() @@ -266,6 +269,8 @@ void PlayerLocalState::addOwnedTown(const CGTownInstance * town) if (currentSelection == nullptr) setSelection(town); + + syncronizeState(); } void PlayerLocalState::removeOwnedTown(const CGTownInstance * town) @@ -282,6 +287,8 @@ void PlayerLocalState::removeOwnedTown(const CGTownInstance * town) if (currentSelection == nullptr && !ownedTowns.empty()) setSelection(ownedTowns.front()); + + syncronizeState(); } void PlayerLocalState::swapOwnedTowns(size_t pos1, size_t pos2) @@ -289,5 +296,123 @@ void PlayerLocalState::swapOwnedTowns(size_t pos1, size_t pos2) assert(ownedTowns[pos1] && ownedTowns[pos2]); std::swap(ownedTowns.at(pos1), ownedTowns.at(pos2)); + syncronizeState(); + adventureInt->onTownOrderChanged(); } + +void PlayerLocalState::syncronizeState() +{ + JsonNode data; + serialize(data); + owner.cb->saveLocalState(data); +} + +void PlayerLocalState::serialize(JsonNode & dest) const +{ + dest.clear(); + + for (auto const * town : ownedTowns) + { + JsonNode record; + record["id"].Integer() = town->id; + dest["towns"].Vector().push_back(record); + } + + for (auto const * hero : wanderingHeroes) + { + JsonNode record; + record["id"].Integer() = hero->id; + if (vstd::contains(sleepingHeroes, hero)) + record["sleeping"].Bool() = true; + + if (paths.count(hero)) + { + record["path"]["x"].Integer() = paths.at(hero).lastNode().coord.x; + record["path"]["y"].Integer() = paths.at(hero).lastNode().coord.y; + record["path"]["z"].Integer() = paths.at(hero).lastNode().coord.z; + } + dest["heroes"].Vector().push_back(record); + } + dest["spellbook"]["pageBattle"].Integer() = spellbookSettings.spellbookLastPageBattle; + dest["spellbook"]["pageAdvmap"].Integer() = spellbookSettings.spellbookLastPageAdvmap; + dest["spellbook"]["tabBattle"].Integer() = spellbookSettings.spellbookLastTabBattle; + dest["spellbook"]["tabAdvmap"].Integer() = spellbookSettings.spellbookLastTabAdvmap; + + if (currentSelection) + dest["currentSelection"].Integer() = currentSelection->id; +} + +void PlayerLocalState::deserialize(const JsonNode & source) +{ + // this method must be called after player state has been initialized + assert(currentSelection != nullptr); + assert(!ownedTowns.empty() || !wanderingHeroes.empty()); + + auto oldHeroes = wanderingHeroes; + auto oldTowns = ownedTowns; + + paths.clear(); + sleepingHeroes.clear(); + wanderingHeroes.clear(); + ownedTowns.clear(); + + for (auto const & town : source["towns"].Vector()) + { + ObjectInstanceID objID(town["id"].Integer()); + const CGTownInstance * townPtr = owner.cb->getTown(objID); + + if (!townPtr) + continue; + + if (!vstd::contains(oldTowns, townPtr)) + continue; + + ownedTowns.push_back(townPtr); + vstd::erase(oldTowns, townPtr); + } + + for (auto const & hero : source["heroes"].Vector()) + { + ObjectInstanceID objID(hero["id"].Integer()); + const CGHeroInstance * heroPtr = owner.cb->getHero(objID); + + if (!heroPtr) + continue; + + if (!vstd::contains(oldHeroes, heroPtr)) + continue; + + wanderingHeroes.push_back(heroPtr); + vstd::erase(oldHeroes, heroPtr); + + if (hero["sleeping"].Bool()) + sleepingHeroes.push_back(heroPtr); + + if (hero["path"]["x"].isNumber() && hero["path"]["y"].isNumber() && hero["path"]["z"].isNumber()) + { + int3 pathTarget(hero["path"]["x"].Integer(), hero["path"]["y"].Integer(), hero["path"]["z"].Integer()); + setPath(heroPtr, pathTarget); + } + } + + if (!source["spellbook"].isNull()) + { + spellbookSettings.spellbookLastPageBattle = source["spellbook"]["pageBattle"].Integer(); + spellbookSettings.spellbookLastPageAdvmap = source["spellbook"]["pageAdvmap"].Integer(); + spellbookSettings.spellbookLastTabBattle = source["spellbook"]["tabBattle"].Integer(); + spellbookSettings.spellbookLastTabAdvmap = source["spellbook"]["tabAdvmap"].Integer(); + } + + // append any owned heroes / towns that were not present in loaded state + wanderingHeroes.insert(wanderingHeroes.end(), oldHeroes.begin(), oldHeroes.end()); + ownedTowns.insert(ownedTowns.end(), oldTowns.begin(), oldTowns.end()); + +//FIXME: broken, anything that is selected in here will be overwritten on PlayerStartsTurn pack +// ObjectInstanceID selectedObjectID(source["currentSelection"].Integer()); +// const CGObjectInstance * objectPtr = owner.cb->getObjInstance(selectedObjectID); +// const CArmedInstance * armyPtr = dynamic_cast(objectPtr); +// +// if (armyPtr) +// setSelection(armyPtr); +} diff --git a/client/PlayerLocalState.h b/client/PlayerLocalState.h index c6029ff1b..3372b6052 100644 --- a/client/PlayerLocalState.h +++ b/client/PlayerLocalState.h @@ -14,6 +14,7 @@ VCMI_LIB_NAMESPACE_BEGIN class CGHeroInstance; class CGTownInstance; class CArmedInstance; +class JsonNode; struct CGPath; class int3; @@ -21,6 +22,15 @@ VCMI_LIB_NAMESPACE_END class CPlayerInterface; +struct PlayerSpellbookSetting +{ + //on which page we left spellbook + int spellbookLastPageBattle = 0; + int spellbookLastPageAdvmap = 0; + int spellbookLastTabBattle = 4; + int spellbookLastTabAdvmap = 4; +}; + /// Class that contains potentially serializeable state of a local player class PlayerLocalState { @@ -34,18 +44,10 @@ class PlayerLocalState std::vector wanderingHeroes; //our heroes on the adventure map (not the garrisoned ones) std::vector ownedTowns; //our towns on the adventure map - void saveHeroPaths(std::map & paths); - void loadHeroPaths(std::map & paths); + PlayerSpellbookSetting spellbookSettings; + void syncronizeState(); public: - struct SpellbookLastSetting - { - //on which page we left spellbook - int spellbookLastPageBattle = 0; - int spellbookLastPageAdvmap = 0; - int spellbookLastTabBattle = 4; - int spellbookLastTabAdvmap = 4; - } spellbookSettings; explicit PlayerLocalState(CPlayerInterface & owner); @@ -53,6 +55,9 @@ public: void setHeroAsleep(const CGHeroInstance * hero); void setHeroAwaken(const CGHeroInstance * hero); + const PlayerSpellbookSetting & getSpellbookSettings() const; + void setSpellbookSettings(const PlayerSpellbookSetting & newSettings); + const std::vector & getOwnedTowns(); const CGTownInstance * getOwnedTown(size_t index); void addOwnedTown(const CGTownInstance * hero); @@ -81,6 +86,9 @@ public: const CGTownInstance * getCurrentTown() const; const CArmedInstance * getCurrentArmy() const; + void serialize(JsonNode & dest) const; + void deserialize(const JsonNode & source); + /// Changes currently selected object void setSelection(const CArmedInstance *sel); }; diff --git a/client/adventureMap/AdventureMapShortcuts.cpp b/client/adventureMap/AdventureMapShortcuts.cpp index fe7253cb0..d5c5ce321 100644 --- a/client/adventureMap/AdventureMapShortcuts.cpp +++ b/client/adventureMap/AdventureMapShortcuts.cpp @@ -24,6 +24,7 @@ #include "../windows/CKingdomInterface.h" #include "../windows/CSpellWindow.h" #include "../windows/CMarketWindow.h" +#include "../windows/GUIClasses.h" #include "../windows/settings/SettingsMainWindow.h" #include "AdventureMapInterface.h" #include "AdventureOptions.h" @@ -36,11 +37,14 @@ #include "../../lib/mapObjects/CGTownInstance.h" #include "../../lib/mapping/CMap.h" #include "../../lib/pathfinder/CGPathNode.h" +#include "../../lib/mapObjectConstructors/CObjectClassesHandler.h" AdventureMapShortcuts::AdventureMapShortcuts(AdventureMapInterface & owner) : owner(owner) , state(EAdventureState::NOT_INITIALIZED) , mapLevel(0) + , searchLast("") + , searchPos(0) {} void AdventureMapShortcuts::setState(EAdventureState newState) @@ -109,7 +113,9 @@ std::vector AdventureMapShortcuts::getShortcuts() { EShortcut::ADVENTURE_MOVE_HERO_EE, optionHeroSelected(), [this]() { this->moveHeroDirectional({+1, 0}); } }, { EShortcut::ADVENTURE_MOVE_HERO_NW, optionHeroSelected(), [this]() { this->moveHeroDirectional({-1, -1}); } }, { EShortcut::ADVENTURE_MOVE_HERO_NN, optionHeroSelected(), [this]() { this->moveHeroDirectional({ 0, -1}); } }, - { EShortcut::ADVENTURE_MOVE_HERO_NE, optionHeroSelected(), [this]() { this->moveHeroDirectional({+1, -1}); } } + { EShortcut::ADVENTURE_MOVE_HERO_NE, optionHeroSelected(), [this]() { this->moveHeroDirectional({+1, -1}); } }, + { EShortcut::ADVENTURE_SEARCH, optionSidePanelActive(),[this]() { this->search(false); } }, + { EShortcut::ADVENTURE_SEARCH_CONTINUE, optionSidePanelActive(),[this]() { this->search(true); } } }; return result; } @@ -457,6 +463,62 @@ void AdventureMapShortcuts::zoom( int distance) owner.hotkeyZoom(distance, false); } +void AdventureMapShortcuts::search(bool next) +{ + // get all relevant objects + std::vector visitableObjInstances; + for(auto & obj : LOCPLINT->cb->getAllVisitableObjs()) + if(obj->ID != MapObjectID::MONSTER && obj->ID != MapObjectID::HERO && obj->ID != MapObjectID::TOWN) + visitableObjInstances.push_back(obj->id); + + // count of elements for each group (map is already sorted) + std::map mapObjCount; + for(auto & obj : visitableObjInstances) + mapObjCount[{ LOCPLINT->cb->getObjInstance(obj)->getObjectName() }]++; + + // convert to vector for indexed access + std::vector> textCountList; + for (auto itr = mapObjCount.begin(); itr != mapObjCount.end(); ++itr) + textCountList.push_back(*itr); + + // get pos of last selection + int lastSel = 0; + for(int i = 0; i < textCountList.size(); i++) + if(textCountList[i].first == searchLast) + lastSel = i; + + // create texts + std::vector texts; + for(auto & obj : textCountList) + texts.push_back(obj.first + " (" + std::to_string(obj.second) + ")"); + + // function to center element from list on map + auto selectObjOnMap = [this, textCountList, visitableObjInstances](int index) + { + auto selObj = textCountList[index].first; + + // filter for matching objects + std::vector selVisitableObjInstances; + for(auto & obj : visitableObjInstances) + if(selObj == LOCPLINT->cb->getObjInstance(obj)->getObjectName()) + selVisitableObjInstances.push_back(obj); + + if(searchPos + 1 < selVisitableObjInstances.size() && searchLast == selObj) + searchPos++; + else + searchPos = 0; + + auto objInst = LOCPLINT->cb->getObjInstance(selVisitableObjInstances[searchPos]); + owner.centerOnObject(objInst); + searchLast = objInst->getObjectName(); + }; + + if(next) + selectObjOnMap(lastSel); + else + GH.windows().createAndPushWindow(texts, nullptr, CGI->generaltexth->translate("vcmi.adventureMap.search.hover"), CGI->generaltexth->translate("vcmi.adventureMap.search.help"), [selectObjOnMap](int index){ selectObjOnMap(index); }, lastSel, std::vector>(), true); +} + void AdventureMapShortcuts::nextObject() { const CGHeroInstance *h = LOCPLINT->localState->getCurrentHero(); diff --git a/client/adventureMap/AdventureMapShortcuts.h b/client/adventureMap/AdventureMapShortcuts.h index b32f3ea29..b314e7bbd 100644 --- a/client/adventureMap/AdventureMapShortcuts.h +++ b/client/adventureMap/AdventureMapShortcuts.h @@ -33,6 +33,9 @@ class AdventureMapShortcuts EAdventureState state; int mapLevel; + std::string searchLast; + int searchPos; + void showOverview(); void worldViewBack(); void worldViewScale1x(); @@ -71,6 +74,7 @@ class AdventureMapShortcuts void nextTown(); void nextObject(); void zoom( int distance); + void search(bool next); void moveHeroDirectional(const Point & direction); public: diff --git a/client/adventureMap/CList.cpp b/client/adventureMap/CList.cpp index 5c46a510a..d25a612a3 100644 --- a/client/adventureMap/CList.cpp +++ b/client/adventureMap/CList.cpp @@ -29,7 +29,6 @@ #include "../render/Colors.h" #include "../../lib/texts/CGeneralTextHandler.h" -#include "../../lib/CHeroHandler.h" #include "../../lib/IGameSettings.h" #include "../../lib/mapObjects/CGHeroInstance.h" #include "../../lib/mapObjects/CGTownInstance.h" @@ -432,7 +431,7 @@ std::shared_ptr CTownList::CTownItem::genSelection() void CTownList::CTownItem::update() { - size_t iconIndex = town->town->clientInfo.icons[town->hasFort()][town->built >= LOCPLINT->cb->getSettings().getInteger(EGameSettings::TOWNS_BUILDINGS_PER_TURN_CAP)]; + size_t iconIndex = town->getTown()->clientInfo.icons[town->hasFort()][town->built >= LOCPLINT->cb->getSettings().getInteger(EGameSettings::TOWNS_BUILDINGS_PER_TURN_CAP)]; picture->setFrame(iconIndex + 2); redraw(); @@ -451,7 +450,7 @@ void CTownList::CTownItem::open() void CTownList::CTownItem::showTooltip() { - CRClickPopup::createAndPush(town, GH.getCursorPosition()); + CRClickPopup::createAndPush(town, pos.center()); } void CTownList::CTownItem::gesture(bool on, const Point & initialPosition, const Point & finalPosition) diff --git a/client/adventureMap/CMinimap.cpp b/client/adventureMap/CMinimap.cpp index bc3cd2c10..fad393eae 100644 --- a/client/adventureMap/CMinimap.cpp +++ b/client/adventureMap/CMinimap.cpp @@ -50,10 +50,10 @@ ColorRGBA CMinimapInstance::getTileColor(const int3 & pos) const return graphics->playerColors[player.getNum()]; } - if (tile->blocked && (!tile->visitable)) - return tile->terType->minimapBlocked; + if (tile->blocked() && !tile->visitable()) + return tile->getTerrain()->minimapBlocked; else - return tile->terType->minimapUnblocked; + return tile->getTerrain()->minimapUnblocked; } void CMinimapInstance::refreshTile(const int3 &tile) diff --git a/client/adventureMap/MapAudioPlayer.cpp b/client/adventureMap/MapAudioPlayer.cpp index bac234103..a6e360b3a 100644 --- a/client/adventureMap/MapAudioPlayer.cpp +++ b/client/adventureMap/MapAudioPlayer.cpp @@ -81,9 +81,9 @@ void MapAudioPlayer::addObject(const CGObjectInstance * obj) { for(int fy = 0; fy < obj->getHeight(); ++fy) { - int3 currTile(obj->pos.x - fx, obj->pos.y - fy, obj->pos.z); + int3 currTile(obj->anchorPos().x - fx, obj->anchorPos().y - fy, obj->anchorPos().z); - if(LOCPLINT->cb->isInTheMap(currTile) && obj->coveringAt(currTile.x, currTile.y)) + if(LOCPLINT->cb->isInTheMap(currTile) && obj->coveringAt(currTile)) objects[currTile.z][currTile.x][currTile.y].push_back(obj->id); } } @@ -108,7 +108,7 @@ void MapAudioPlayer::addObject(const CGObjectInstance * obj) for(const auto & tile : tiles) { - int3 currTile = obj->pos + tile; + int3 currTile = obj->anchorPos() + tile; if(LOCPLINT->cb->isInTheMap(currTile)) objects[currTile.z][currTile.x][currTile.y].push_back(obj->id); @@ -182,7 +182,7 @@ void MapAudioPlayer::updateMusic() const auto * tile = LOCPLINT->cb->getTile(currentSelection->visitablePos()); if (tile) - CCS->musich->playMusicFromSet("terrain", tile->terType->getJsonKey(), true, false); + CCS->musich->playMusicFromSet("terrain", tile->getTerrain()->getJsonKey(), true, false); } if(audioPlaying && enemyMakingTurn) diff --git a/client/battle/BattleAnimationClasses.cpp b/client/battle/BattleAnimationClasses.cpp index a2175e1db..7d34e8978 100644 --- a/client/battle/BattleAnimationClasses.cpp +++ b/client/battle/BattleAnimationClasses.cpp @@ -881,9 +881,10 @@ uint32_t CastAnimation::getAttackClimaxFrame() const return maxFrames / 2; } -EffectAnimation::EffectAnimation(BattleInterface & owner, const AnimationPath & animationName, int effects, bool reversed): +EffectAnimation::EffectAnimation(BattleInterface & owner, const AnimationPath & animationName, int effects, float transparencyFactor, bool reversed): BattleAnimation(owner), - animation(GH.renderHandler().loadAnimation(animationName, EImageBlitMode::ALPHA)), + animation(GH.renderHandler().loadAnimation(animationName, EImageBlitMode::SIMPLE)), + transparencyFactor(transparencyFactor), effectFlags(effects), effectFinished(false), reversed(reversed) @@ -892,32 +893,32 @@ EffectAnimation::EffectAnimation(BattleInterface & owner, const AnimationPath & } EffectAnimation::EffectAnimation(BattleInterface & owner, const AnimationPath & animationName, std::vector hex, int effects, bool reversed): - EffectAnimation(owner, animationName, effects, reversed) + EffectAnimation(owner, animationName, effects, 1.0f, reversed) { battlehexes = hex; } -EffectAnimation::EffectAnimation(BattleInterface & owner, const AnimationPath & animationName, BattleHex hex, int effects, bool reversed): - EffectAnimation(owner, animationName, effects, reversed) +EffectAnimation::EffectAnimation(BattleInterface & owner, const AnimationPath & animationName, BattleHex hex, int effects, float transparencyFactor, bool reversed): + EffectAnimation(owner, animationName, effects, transparencyFactor, reversed) { assert(hex.isValid()); battlehexes.push_back(hex); } EffectAnimation::EffectAnimation(BattleInterface & owner, const AnimationPath & animationName, std::vector pos, int effects, bool reversed): - EffectAnimation(owner, animationName, effects, reversed) + EffectAnimation(owner, animationName, effects, 1.0f, reversed) { positions = pos; } EffectAnimation::EffectAnimation(BattleInterface & owner, const AnimationPath & animationName, Point pos, int effects, bool reversed): - EffectAnimation(owner, animationName, effects, reversed) + EffectAnimation(owner, animationName, effects, 1.0f, reversed) { positions.push_back(pos); } EffectAnimation::EffectAnimation(BattleInterface & owner, const AnimationPath & animationName, Point pos, BattleHex hex, int effects, bool reversed): - EffectAnimation(owner, animationName, effects, reversed) + EffectAnimation(owner, animationName, effects, 1.0f, reversed) { assert(hex.isValid()); battlehexes.push_back(hex); @@ -951,6 +952,7 @@ bool EffectAnimation::init() be.effectID = ID; be.animation = animation; be.currentFrame = 0; + be.transparencyFactor = transparencyFactor; be.type = reversed ? BattleEffect::AnimType::REVERSE : BattleEffect::AnimType::DEFAULT; for (size_t i = 0; i < std::max(battlehexes.size(), positions.size()); ++i) diff --git a/client/battle/BattleAnimationClasses.h b/client/battle/BattleAnimationClasses.h index fb2e6f85b..83233819b 100644 --- a/client/battle/BattleAnimationClasses.h +++ b/client/battle/BattleAnimationClasses.h @@ -309,9 +309,10 @@ public: class EffectAnimation : public BattleAnimation { std::string soundName; + int effectFlags; + float transparencyFactor; bool effectFinished; bool reversed; - int effectFlags; std::shared_ptr animation; std::vector positions; @@ -335,14 +336,14 @@ public: }; /// Create animation with screen-wide effect - EffectAnimation(BattleInterface & owner, const AnimationPath & animationName, int effects = 0, bool reversed = false); + EffectAnimation(BattleInterface & owner, const AnimationPath & animationName, int effects = 0, float transparencyFactor = 1.f, bool reversed = false); /// Create animation positioned at point(s). Note that positions must be are absolute, including battleint position offset EffectAnimation(BattleInterface & owner, const AnimationPath & animationName, Point pos , int effects = 0, bool reversed = false); EffectAnimation(BattleInterface & owner, const AnimationPath & animationName, std::vector pos , int effects = 0, bool reversed = false); /// Create animation positioned at certain hex(es) - EffectAnimation(BattleInterface & owner, const AnimationPath & animationName, BattleHex hex , int effects = 0, bool reversed = false); + EffectAnimation(BattleInterface & owner, const AnimationPath & animationName, BattleHex hex , int effects = 0, float transparencyFactor = 1.0f, bool reversed = false); EffectAnimation(BattleInterface & owner, const AnimationPath & animationName, std::vector hex, int effects = 0, bool reversed = false); EffectAnimation(BattleInterface & owner, const AnimationPath & animationName, Point pos, BattleHex hex, int effects = 0, bool reversed = false); diff --git a/client/battle/BattleEffectsController.cpp b/client/battle/BattleEffectsController.cpp index d390093d9..0d3caf2ea 100644 --- a/client/battle/BattleEffectsController.cpp +++ b/client/battle/BattleEffectsController.cpp @@ -44,7 +44,7 @@ void BattleEffectsController::displayEffect(EBattleEffect effect, const BattleHe displayEffect(effect, AudioPath(), destTile); } -void BattleEffectsController::displayEffect(EBattleEffect effect, const AudioPath & soundFile, const BattleHex & destTile) +void BattleEffectsController::displayEffect(EBattleEffect effect, const AudioPath & soundFile, const BattleHex & destTile, float transparencyFactor) { size_t effectID = static_cast(effect); @@ -52,7 +52,7 @@ void BattleEffectsController::displayEffect(EBattleEffect effect, const AudioPat CCS->soundh->playSound( soundFile ); - owner.stacksController->addNewAnim(new EffectAnimation(owner, customAnim, destTile)); + owner.stacksController->addNewAnim(new EffectAnimation(owner, customAnim, destTile, 0, transparencyFactor)); } void BattleEffectsController::battleTriggerEffect(const BattleTriggerEffect & bte) @@ -69,7 +69,7 @@ void BattleEffectsController::battleTriggerEffect(const BattleTriggerEffect & bt switch(static_cast(bte.effect)) { case BonusType::HP_REGENERATION: - displayEffect(EBattleEffect::REGENERATION, AudioPath::builtin("REGENER"), stack->getPosition()); + displayEffect(EBattleEffect::REGENERATION, AudioPath::builtin("REGENER"), stack->getPosition(), 0.5); break; case BonusType::MANA_DRAIN: displayEffect(EBattleEffect::MANA_DRAIN, AudioPath::builtin("MANADRAI"), stack->getPosition()); @@ -78,7 +78,7 @@ void BattleEffectsController::battleTriggerEffect(const BattleTriggerEffect & bt displayEffect(EBattleEffect::POISON, AudioPath::builtin("POISON"), stack->getPosition()); break; case BonusType::FEAR: - displayEffect(EBattleEffect::FEAR, AudioPath::builtin("FEAR"), stack->getPosition()); + displayEffect(EBattleEffect::FEAR, AudioPath::builtin("FEAR"), stack->getPosition(), 0.5); break; case BonusType::MORALE: { @@ -124,6 +124,7 @@ void BattleEffectsController::collectRenderableObjects(BattleRenderer & renderer currentFrame %= elem.animation->size(); auto img = elem.animation->getImage(currentFrame, static_cast(elem.type)); + img->setAlpha(255 * elem.transparencyFactor); canvas.draw(img, elem.pos); }); diff --git a/client/battle/BattleEffectsController.h b/client/battle/BattleEffectsController.h index 5e551901a..255313d21 100644 --- a/client/battle/BattleEffectsController.h +++ b/client/battle/BattleEffectsController.h @@ -39,7 +39,8 @@ struct BattleEffect AnimType type; Point pos; //position on the screen - float currentFrame; + float currentFrame = 0.0; + float transparencyFactor = 1.0; std::shared_ptr animation; int effectID; //uniqueID equal ot ID of appropriate CSpellEffectAnim BattleHex tile; //Indicates if effect which hex the effect is drawn on @@ -65,7 +66,7 @@ public: //displays custom effect on the battlefield void displayEffect(EBattleEffect effect, const BattleHex & destTile); - void displayEffect(EBattleEffect effect, const AudioPath & soundFile, const BattleHex & destTile); + void displayEffect(EBattleEffect effect, const AudioPath & soundFile, const BattleHex & destTile, float transparencyFactor = 1.f); void battleTriggerEffect(const BattleTriggerEffect & bte); diff --git a/client/battle/BattleFieldController.cpp b/client/battle/BattleFieldController.cpp index 95ab1c349..27c62535e 100644 --- a/client/battle/BattleFieldController.cpp +++ b/client/battle/BattleFieldController.cpp @@ -114,7 +114,7 @@ BattleFieldController::BattleFieldController(BattleInterface & owner): //preparing cells and hexes cellBorder = GH.renderHandler().loadImage(ImagePath::builtin("CCELLGRD.BMP"), EImageBlitMode::COLORKEY); - cellShade = GH.renderHandler().loadImage(ImagePath::builtin("CCELLSHD.BMP"), EImageBlitMode::ALPHA); + cellShade = GH.renderHandler().loadImage(ImagePath::builtin("CCELLSHD.BMP"), EImageBlitMode::SIMPLE); cellUnitMovementHighlight = GH.renderHandler().loadImage(ImagePath::builtin("UnitMovementHighlight.PNG"), EImageBlitMode::COLORKEY); cellUnitMaxMovementHighlight = GH.renderHandler().loadImage(ImagePath::builtin("UnitMaxMovementHighlight.PNG"), EImageBlitMode::COLORKEY); @@ -124,8 +124,6 @@ BattleFieldController::BattleFieldController(BattleInterface & owner): rangedFullDamageLimitImages = GH.renderHandler().loadAnimation(AnimationPath::builtin("battle/rangeHighlights/rangeHighlightsGreen.json"), EImageBlitMode::COLORKEY); shootingRangeLimitImages = GH.renderHandler().loadAnimation(AnimationPath::builtin("battle/rangeHighlights/rangeHighlightsRed.json"), EImageBlitMode::COLORKEY); - cellShade->setShadowEnabled(true); - if(!owner.siegeController) { auto bfieldType = owner.getBattle()->battleGetBattlefieldType(); diff --git a/client/battle/BattleInterface.cpp b/client/battle/BattleInterface.cpp index cea4410d5..db02f9a2c 100644 --- a/client/battle/BattleInterface.cpp +++ b/client/battle/BattleInterface.cpp @@ -39,7 +39,6 @@ #include "../../lib/CStack.h" #include "../../lib/CConfigHandler.h" #include "../../lib/texts/CGeneralTextHandler.h" -#include "../../lib/CHeroHandler.h" #include "../../lib/gameState/InfoAboutArmy.h" #include "../../lib/mapObjects/CGTownInstance.h" #include "../../lib/networkPacks/PacksForClientBattle.h" @@ -536,9 +535,9 @@ void BattleInterface::displaySpellAnimationQueue(const CSpell * spell, const CSp flags |= EffectAnimation::SCREEN_FILL; if (!destinationTile.isValid()) - stacksController->addNewAnim(new EffectAnimation(*this, animation.resourceName, flags)); + stacksController->addNewAnim(new EffectAnimation(*this, animation.resourceName, flags, animation.transparency)); else - stacksController->addNewAnim(new EffectAnimation(*this, animation.resourceName, destinationTile, flags)); + stacksController->addNewAnim(new EffectAnimation(*this, animation.resourceName, destinationTile, flags, animation.transparency)); } } } diff --git a/client/battle/BattleInterfaceClasses.cpp b/client/battle/BattleInterfaceClasses.cpp index 919118407..f9dde7e68 100644 --- a/client/battle/BattleInterfaceClasses.cpp +++ b/client/battle/BattleInterfaceClasses.cpp @@ -50,10 +50,11 @@ #include "../../lib/CStack.h" #include "../../lib/CConfigHandler.h" #include "../../lib/CCreatureHandler.h" +#include "../../lib/entities/hero/CHeroClass.h" +#include "../../lib/entities/hero/CHero.h" #include "../../lib/gameState/InfoAboutArmy.h" #include "../../lib/texts/CGeneralTextHandler.h" #include "../../lib/texts/TextOperations.h" -#include "../../lib/CHeroHandler.h" #include "../../lib/StartInfo.h" #include "../../lib/mapObjects/CGTownInstance.h" #include "../../lib/networkPacks/PacksForClientBattle.h" @@ -389,15 +390,15 @@ BattleHero::BattleHero(const BattleInterface & owner, const CGHeroInstance * her { AnimationPath animationPath; - if(!hero->type->battleImage.empty()) - animationPath = hero->type->battleImage; + if(!hero->getHeroType()->battleImage.empty()) + animationPath = hero->getHeroType()->battleImage; else if(hero->gender == EHeroGender::FEMALE) - animationPath = hero->type->heroClass->imageBattleFemale; + animationPath = hero->getHeroClass()->imageBattleFemale; else - animationPath = hero->type->heroClass->imageBattleMale; + animationPath = hero->getHeroClass()->imageBattleMale; - animation = GH.renderHandler().loadAnimation(animationPath, EImageBlitMode::ALPHA); + animation = GH.renderHandler().loadAnimation(animationPath, EImageBlitMode::WITH_SHADOW); pos.w = 64; pos.h = 136; @@ -1027,7 +1028,7 @@ void StackQueue::update() int32_t StackQueue::getSiegeShooterIconID() { - return owner.siegeController->getSiegedTown()->town->faction->getIndex(); + return owner.siegeController->getSiegedTown()->getFactionID().getNum(); } std::optional StackQueue::getHoveredUnitIdIfAny() const @@ -1065,11 +1066,13 @@ StackQueue::StackBox::StackBox(StackQueue * owner): roundRect = std::make_shared(Rect(0, 0, 15, 18), ColorRGBA(0, 0, 0, 255), ColorRGBA(241, 216, 120, 255)); round = std::make_shared(4, 2, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE); - int icon_x = pos.w - 17; - int icon_y = pos.h - 18; + Point iconPos(pos.w - 16, pos.h - 16); - stateIcon = std::make_shared(AnimationPath::builtin("VCMI/BATTLEQUEUE/STATESSMALL"), 0, 0, icon_x, icon_y); - stateIcon->visible = false; + defendIcon = std::make_shared(ImagePath::builtin("battle/QueueDefend"), iconPos); + waitIcon = std::make_shared(ImagePath::builtin("battle/QueueWait"), iconPos); + + defendIcon->setEnabled(false); + waitIcon->setEnabled(false); } roundRect->disable(); } @@ -1105,22 +1108,13 @@ void StackQueue::StackBox::setUnit(const battle::Unit * unit, size_t turn, std:: round->setText(tmp); } - if(stateIcon) + if(!owner->embedded) { - if(unit->defended((int)turn) || (turn > 0 && unit->defended((int)turn - 1))) - { - stateIcon->setFrame(0, 0); - stateIcon->visible = true; - } - else if(unit->waited((int)turn)) - { - stateIcon->setFrame(1, 0); - stateIcon->visible = true; - } - else - { - stateIcon->visible = false; - } + bool defended = unit->defended(turn) || (turn > 0 && unit->defended(turn - 1)); + bool waited = unit->waited(turn) && !defended; + + defendIcon->setEnabled(defended); + waitIcon->setEnabled(waited); } } else @@ -1130,9 +1124,11 @@ void StackQueue::StackBox::setUnit(const battle::Unit * unit, size_t turn, std:: icon->visible = false; icon->setFrame(0); amount->setText(""); - - if(stateIcon) - stateIcon->visible = false; + if(!owner->embedded) + { + defendIcon->setEnabled(false); + waitIcon->setEnabled(false); + } } } diff --git a/client/battle/BattleInterfaceClasses.h b/client/battle/BattleInterfaceClasses.h index 819ed3504..9465502bc 100644 --- a/client/battle/BattleInterfaceClasses.h +++ b/client/battle/BattleInterfaceClasses.h @@ -260,7 +260,8 @@ class StackQueue : public CIntObject std::shared_ptr background; std::shared_ptr icon; std::shared_ptr amount; - std::shared_ptr stateIcon; + std::shared_ptr waitIcon; + std::shared_ptr defendIcon; std::shared_ptr round; std::shared_ptr roundRect; diff --git a/client/battle/BattleObstacleController.cpp b/client/battle/BattleObstacleController.cpp index e291ee62b..c5c8391b3 100644 --- a/client/battle/BattleObstacleController.cpp +++ b/client/battle/BattleObstacleController.cpp @@ -50,11 +50,11 @@ void BattleObstacleController::loadObstacleImage(const CObstacleInstance & oi) if (oi.obstacleType == CObstacleInstance::ABSOLUTE_OBSTACLE) { // obstacle uses single bitmap image for animations - obstacleImages[oi.uniqueID] = GH.renderHandler().loadImage(animationName.toType(), EImageBlitMode::COLORKEY); + obstacleImages[oi.uniqueID] = GH.renderHandler().loadImage(animationName.toType(), EImageBlitMode::SIMPLE); } else { - obstacleAnimations[oi.uniqueID] = GH.renderHandler().loadAnimation(animationName, EImageBlitMode::COLORKEY); + obstacleAnimations[oi.uniqueID] = GH.renderHandler().loadAnimation(animationName, EImageBlitMode::SIMPLE); obstacleImages[oi.uniqueID] = obstacleAnimations[oi.uniqueID]->getImage(0); } } @@ -78,7 +78,7 @@ void BattleObstacleController::obstacleRemoved(const std::vectorgetImage(0, 0); if(!first) continue; @@ -105,7 +105,7 @@ void BattleObstacleController::obstaclePlaced(const std::vectorvisibleForSide(side, owner.getBattle()->battleHasNativeStack(side))) continue; - auto animation = GH.renderHandler().loadAnimation(oi->getAppearAnimation(), EImageBlitMode::ALPHA); + auto animation = GH.renderHandler().loadAnimation(oi->getAppearAnimation(), EImageBlitMode::SIMPLE); auto first = animation->getImage(0, 0); if(!first) continue; diff --git a/client/battle/BattleSiegeController.cpp b/client/battle/BattleSiegeController.cpp index f64bc8ac4..2f3c4df5e 100644 --- a/client/battle/BattleSiegeController.cpp +++ b/client/battle/BattleSiegeController.cpp @@ -58,14 +58,14 @@ ImagePath BattleSiegeController::getWallPieceImageName(EWallVisual::EWallVisual }; }; - const std::string & prefix = town->town->clientInfo.siegePrefix; + const std::string & prefix = town->getTown()->clientInfo.siegePrefix; std::string addit = std::to_string(getImageIndex()); switch(what) { case EWallVisual::BACKGROUND_WALL: { - auto faction = town->town->faction->getIndex(); + auto faction = town->getFactionID(); if (faction == ETownType::RAMPART || faction == ETownType::NECROPOLIS || faction == ETownType::DUNGEON || faction == ETownType::STRONGHOLD) return ImagePath::builtinTODO(prefix + "TPW1.BMP"); @@ -111,7 +111,7 @@ ImagePath BattleSiegeController::getWallPieceImageName(EWallVisual::EWallVisual void BattleSiegeController::showWallPiece(Canvas & canvas, EWallVisual::EWallVisual what) { - auto & ci = town->town->clientInfo; + auto & ci = town->getTown()->clientInfo; auto const & pos = ci.siegePositions[what]; if ( wallPieceImages[what] && pos.isValid()) @@ -120,7 +120,7 @@ void BattleSiegeController::showWallPiece(Canvas & canvas, EWallVisual::EWallVis ImagePath BattleSiegeController::getBattleBackgroundName() const { - const std::string & prefix = town->town->clientInfo.siegePrefix; + const std::string & prefix = town->getTown()->clientInfo.siegePrefix; return ImagePath::builtinTODO(prefix + "BACK.BMP"); } @@ -130,8 +130,8 @@ bool BattleSiegeController::getWallPieceExistence(EWallVisual::EWallVisual what) switch (what) { - case EWallVisual::MOAT: return fortifications.hasMoat && town->town->clientInfo.siegePositions.at(EWallVisual::MOAT).isValid(); - case EWallVisual::MOAT_BANK: return fortifications.hasMoat && town->town->clientInfo.siegePositions.at(EWallVisual::MOAT_BANK).isValid(); + case EWallVisual::MOAT: return fortifications.hasMoat && town->getTown()->clientInfo.siegePositions.at(EWallVisual::MOAT).isValid(); + case EWallVisual::MOAT_BANK: return fortifications.hasMoat && town->getTown()->clientInfo.siegePositions.at(EWallVisual::MOAT_BANK).isValid(); case EWallVisual::KEEP_BATTLEMENT: return fortifications.citadelHealth > 0 && owner.getBattle()->battleGetWallState(EWallPart::KEEP) != EWallState::DESTROYED; case EWallVisual::UPPER_BATTLEMENT: return fortifications.upperTowerHealth > 0 && owner.getBattle()->battleGetWallState(EWallPart::UPPER_TOWER) != EWallState::DESTROYED; case EWallVisual::BOTTOM_BATTLEMENT: return fortifications.lowerTowerHealth > 0 && owner.getBattle()->battleGetWallState(EWallPart::BOTTOM_TOWER) != EWallState::DESTROYED; @@ -218,8 +218,8 @@ Point BattleSiegeController::getTurretCreaturePosition( BattleHex position ) con if (posID != 0) { return { - town->town->clientInfo.siegePositions[posID].x, - town->town->clientInfo.siegePositions[posID].y + town->getTown()->clientInfo.siegePositions[posID].x, + town->getTown()->clientInfo.siegePositions[posID].y }; } diff --git a/client/battle/BattleStacksController.cpp b/client/battle/BattleStacksController.cpp index 398337a38..173925244 100644 --- a/client/battle/BattleStacksController.cpp +++ b/client/battle/BattleStacksController.cpp @@ -636,7 +636,7 @@ void BattleStacksController::stackAttacking( const StackAttackInfo & info ) { owner.addToAnimationStage(EAnimationEvents::AFTER_HIT, [=]() { - owner.effectsController->displayEffect(EBattleEffect::DRAIN_LIFE, AudioPath::builtin("DRAINLIF"), attacker->getPosition()); + owner.effectsController->displayEffect(EBattleEffect::DRAIN_LIFE, AudioPath::builtin("DRAINLIF"), attacker->getPosition(), 0.5); }); } diff --git a/client/battle/BattleWindow.cpp b/client/battle/BattleWindow.cpp index 0e6ba0f8e..adcd10e14 100644 --- a/client/battle/BattleWindow.cpp +++ b/client/battle/BattleWindow.cpp @@ -743,7 +743,7 @@ void BattleWindow::bSpellf() const auto artID = blockingBonus->sid.as(); //If we have artifact, put name of our hero. Otherwise assume it's the enemy. //TODO check who *really* is source of bonus - std::string heroName = myHero->hasArt(artID) ? myHero->getNameTranslated() : owner.enemyHero().name; + std::string heroName = myHero->hasArt(artID, true) ? myHero->getNameTranslated() : owner.enemyHero().name; //%s wields the %s, an ancient artifact which creates a p dead to all magic. LOCPLINT->showInfoDialog(boost::str(boost::format(CGI->generaltexth->allTexts[683]) diff --git a/client/battle/CreatureAnimation.cpp b/client/battle/CreatureAnimation.cpp index b028a597f..31ab870cf 100644 --- a/client/battle/CreatureAnimation.cpp +++ b/client/battle/CreatureAnimation.cpp @@ -17,6 +17,7 @@ #include "../render/CAnimation.h" #include "../render/Canvas.h" #include "../render/ColorFilter.h" +#include "../render/Colors.h" #include "../render/IRenderHandler.h" static const ColorRGBA creatureBlueBorder = { 0, 255, 255, 255 }; @@ -199,8 +200,8 @@ CreatureAnimation::CreatureAnimation(const AnimationPath & name_, TSpeedControll speedController(controller), once(false) { - forward = GH.renderHandler().loadAnimation(name_, EImageBlitMode::ALPHA); - reverse = GH.renderHandler().loadAnimation(name_, EImageBlitMode::ALPHA); + forward = GH.renderHandler().loadAnimation(name_, EImageBlitMode::WITH_SHADOW_AND_OVERLAY); + reverse = GH.renderHandler().loadAnimation(name_, EImageBlitMode::WITH_SHADOW_AND_OVERLAY); // if necessary, add one frame into vcmi-only group DEAD if(forward->size(size_t(ECreatureAnimType::DEAD)) == 0) @@ -339,15 +340,14 @@ void CreatureAnimation::nextFrame(Canvas & canvas, const ColorFilter & shifter, if(image) { - image->setShadowEnabled(true); - image->setOverlayEnabled(isIdle()); if (isIdle()) image->setOverlayColor(genBorderColor(getBorderStrength(elapsedTime), border)); + else + image->setOverlayColor(Colors::TRANSPARENCY); image->adjustPalette(shifter, 0); canvas.draw(image, pos.topLeft(), Rect(0, 0, pos.w, pos.h)); - } } diff --git a/client/eventsSDL/InputSourceGameController.cpp b/client/eventsSDL/InputSourceGameController.cpp index 94e3ac450..5d4ff29ec 100644 --- a/client/eventsSDL/InputSourceGameController.cpp +++ b/client/eventsSDL/InputSourceGameController.cpp @@ -18,6 +18,7 @@ #include "../gui/CursorHandler.h" #include "../gui/EventDispatcher.h" #include "../gui/ShortcutHandler.h" +#include "../render/IScreenHandler.h" #include "../../lib/CConfigHandler.h" @@ -198,9 +199,10 @@ void InputSourceGameController::tryToConvertCursor() assert(CCS->curh); if(CCS->curh->getShowType() == Cursor::ShowType::HARDWARE) { + int scalingFactor = GH.screenHandler().getScalingFactor(); const Point & cursorPosition = GH.getCursorPosition(); CCS->curh->changeCursor(Cursor::ShowType::SOFTWARE); - CCS->curh->cursorMove(cursorPosition.x, cursorPosition.y); + CCS->curh->cursorMove(cursorPosition.x * scalingFactor, cursorPosition.y * scalingFactor); GH.input().setCursorPosition(cursorPosition); } } @@ -225,12 +227,13 @@ void InputSourceGameController::doCursorMove(int deltaX, int deltaY) return; const Point & screenSize = GH.screenDimensions(); const Point & cursorPosition = GH.getCursorPosition(); + int scalingFactor = GH.screenHandler().getScalingFactor(); int newX = std::min(std::max(cursorPosition.x + deltaX, 0), screenSize.x); int newY = std::min(std::max(cursorPosition.y + deltaY, 0), screenSize.y); Point targetPosition{newX, newY}; GH.input().setCursorPosition(targetPosition); if(CCS && CCS->curh) - CCS->curh->cursorMove(GH.getCursorPosition().x, GH.getCursorPosition().y); + CCS->curh->cursorMove(GH.getCursorPosition().x * scalingFactor, GH.getCursorPosition().y * scalingFactor); } int InputSourceGameController::getMoveDis(float planDis) diff --git a/client/eventsSDL/InputSourceKeyboard.cpp b/client/eventsSDL/InputSourceKeyboard.cpp index ee43e5c53..d97c45b10 100644 --- a/client/eventsSDL/InputSourceKeyboard.cpp +++ b/client/eventsSDL/InputSourceKeyboard.cpp @@ -111,7 +111,7 @@ void InputSourceKeyboard::handleEventKeyUp(const SDL_KeyboardEvent & key) if(key.repeat != 0) return; // ignore periodic event resends - std::string keyName = SDL_GetKeyName(key.keysym.sym); + std::string keyName = getKeyNameWithModifiers(SDL_GetKeyName(key.keysym.sym)); logGlobal->trace("keyboard: key '%s' released", keyName); if (SDL_IsTextInputActive() == SDL_TRUE) diff --git a/client/eventsSDL/InputSourceTouch.cpp b/client/eventsSDL/InputSourceTouch.cpp index 0b5d48954..1135a0b52 100644 --- a/client/eventsSDL/InputSourceTouch.cpp +++ b/client/eventsSDL/InputSourceTouch.cpp @@ -83,16 +83,18 @@ void InputSourceTouch::handleEventFingerMotion(const SDL_TouchFingerEvent & tfin break; } case TouchState::TAP_DOWN_SHORT: + case TouchState::TAP_DOWN_LONG_AWAIT: { Point distance = convertTouchToMouse(tfinger) - lastTapPosition; if ( std::abs(distance.x) > params.panningSensitivityThreshold || std::abs(distance.y) > params.panningSensitivityThreshold) { - state = TouchState::TAP_DOWN_PANNING; + state = state == TouchState::TAP_DOWN_SHORT ? TouchState::TAP_DOWN_PANNING : TouchState::TAP_DOWN_PANNING_POPUP; GH.events().dispatchGesturePanningStarted(lastTapPosition); } break; } case TouchState::TAP_DOWN_PANNING: + case TouchState::TAP_DOWN_PANNING_POPUP: { emitPanningEvent(tfinger); break; @@ -103,7 +105,6 @@ void InputSourceTouch::handleEventFingerMotion(const SDL_TouchFingerEvent & tfin break; } case TouchState::TAP_DOWN_LONG: - case TouchState::TAP_DOWN_LONG_AWAIT: { // no-op break; @@ -157,8 +158,11 @@ void InputSourceTouch::handleEventFingerDown(const SDL_TouchFingerEvent & tfinge CSH->getGlobalLobby().activateInterface(); break; } - case TouchState::TAP_DOWN_LONG: case TouchState::TAP_DOWN_LONG_AWAIT: + lastTapPosition = convertTouchToMouse(tfinger); + break; + case TouchState::TAP_DOWN_LONG: + case TouchState::TAP_DOWN_PANNING_POPUP: { // no-op break; @@ -205,9 +209,10 @@ void InputSourceTouch::handleEventFingerUp(const SDL_TouchFingerEvent & tfinger) break; } case TouchState::TAP_DOWN_PANNING: + case TouchState::TAP_DOWN_PANNING_POPUP: { GH.events().dispatchGesturePanningEnded(lastTapPosition, convertTouchToMouse(tfinger)); - state = TouchState::IDLE; + state = state == TouchState::TAP_DOWN_PANNING ? TouchState::IDLE : TouchState::TAP_DOWN_LONG_AWAIT; break; } case TouchState::TAP_DOWN_DOUBLE: diff --git a/client/eventsSDL/InputSourceTouch.h b/client/eventsSDL/InputSourceTouch.h index 19a8cca50..2c1d8490c 100644 --- a/client/eventsSDL/InputSourceTouch.h +++ b/client/eventsSDL/InputSourceTouch.h @@ -45,6 +45,12 @@ enum class TouchState // UP -> transition to IDLE TAP_DOWN_PANNING, + // single finger is moving across screen + // DOWN -> ignored + // MOTION -> emit panning event + // UP -> transition to TAP_DOWN_LONG_AWAIT + TAP_DOWN_PANNING_POPUP, + // two fingers are touching the screen // DOWN -> ??? how to handle 3rd finger? Ignore? // MOTION -> emit pinch event @@ -59,7 +65,7 @@ enum class TouchState // right-click popup is active, waiting for new tap to hide popup // DOWN -> ignored - // MOTION -> ignored + // MOTION -> transition to TAP_DOWN_PANNING_POPUP // UP -> transition to IDLE, generate closePopup() event TAP_DOWN_LONG_AWAIT, }; @@ -79,7 +85,7 @@ struct TouchInputParameters uint32_t doubleTouchToleranceDistance = 50; /// moving finger for distance larger than specified will be qualified as panning gesture instead of long press - uint32_t panningSensitivityThreshold = 10; + uint32_t panningSensitivityThreshold = 15; /// gesture will be qualified as pinch if distance between fingers is at least specified here uint32_t pinchSensitivityThreshold = 10; diff --git a/client/globalLobby/GlobalLobbyRoomWindow.cpp b/client/globalLobby/GlobalLobbyRoomWindow.cpp index f744dca2d..943737b72 100644 --- a/client/globalLobby/GlobalLobbyRoomWindow.cpp +++ b/client/globalLobby/GlobalLobbyRoomWindow.cpp @@ -27,7 +27,7 @@ #include "../widgets/ObjectLists.h" #include "../../lib/modding/CModHandler.h" -#include "../../lib/modding/CModInfo.h" +#include "../../lib/modding/ModDescription.h" #include "../../lib/texts/CGeneralTextHandler.h" #include "../../lib/texts/MetaString.h" @@ -128,14 +128,14 @@ GlobalLobbyRoomWindow::GlobalLobbyRoomWindow(GlobalLobbyWindow * window, const s GlobalLobbyRoomModInfo modInfo; modInfo.status = modEntry.second; if (modEntry.second == ModVerificationStatus::EXCESSIVE) - modInfo.version = CGI->modh->getModInfo(modEntry.first).getVerificationInfo().version.toString(); + modInfo.version = CGI->modh->getModInfo(modEntry.first).getVersion().toString(); else modInfo.version = roomDescription.modList.at(modEntry.first).version.toString(); if (modEntry.second == ModVerificationStatus::NOT_INSTALLED) modInfo.modName = roomDescription.modList.at(modEntry.first).name; else - modInfo.modName = CGI->modh->getModInfo(modEntry.first).getVerificationInfo().name; + modInfo.modName = CGI->modh->getModInfo(modEntry.first).getName(); modVerificationList.push_back(modInfo); } diff --git a/client/gui/CGuiHandler.cpp b/client/gui/CGuiHandler.cpp index ebacdc1b4..6608ca44d 100644 --- a/client/gui/CGuiHandler.cpp +++ b/client/gui/CGuiHandler.cpp @@ -250,8 +250,8 @@ void CGuiHandler::setStatusbar(std::shared_ptr newStatusBar) void CGuiHandler::onScreenResize(bool resolutionChanged) { if(resolutionChanged) - { screenHandler().onScreenResize(); - } + windows().onScreenResize(); + CCS->curh->onScreenResize(); } diff --git a/client/gui/CursorHandler.cpp b/client/gui/CursorHandler.cpp index eaa8cfd98..fbc5922f8 100644 --- a/client/gui/CursorHandler.cpp +++ b/client/gui/CursorHandler.cpp @@ -312,3 +312,8 @@ void CursorHandler::changeCursor(Cursor::ShowType newShowType) break; } } + +void CursorHandler::onScreenResize() +{ + cursor->setImage(getCurrentImage(), getPivotOffset()); +} diff --git a/client/gui/CursorHandler.h b/client/gui/CursorHandler.h index 539c577bd..acaccaaa8 100644 --- a/client/gui/CursorHandler.h +++ b/client/gui/CursorHandler.h @@ -182,6 +182,7 @@ public: void hide(); void show(); + void onScreenResize(); /// change cursor's positions to (x, y) void cursorMove(const int & x, const int & y); diff --git a/client/gui/Shortcut.h b/client/gui/Shortcut.h index 66019bd77..bd8c57a26 100644 --- a/client/gui/Shortcut.h +++ b/client/gui/Shortcut.h @@ -161,6 +161,8 @@ enum class EShortcut ADVENTURE_RESTART_GAME, ADVENTURE_TO_MAIN_MENU, ADVENTURE_QUIT_GAME, + ADVENTURE_SEARCH, + ADVENTURE_SEARCH_CONTINUE, // Move hero one tile in specified direction. Bound to cursors & numpad buttons ADVENTURE_MOVE_HERO_SW, diff --git a/client/gui/ShortcutHandler.cpp b/client/gui/ShortcutHandler.cpp index a4cffb8c4..19c3cc728 100644 --- a/client/gui/ShortcutHandler.cpp +++ b/client/gui/ShortcutHandler.cpp @@ -209,6 +209,8 @@ EShortcut ShortcutHandler::findShortcut(const std::string & identifier ) const {"adventureZoomIn", EShortcut::ADVENTURE_ZOOM_IN }, {"adventureZoomOut", EShortcut::ADVENTURE_ZOOM_OUT }, {"adventureZoomReset", EShortcut::ADVENTURE_ZOOM_RESET }, + {"adventureSearch", EShortcut::ADVENTURE_SEARCH }, + {"adventureSearchContinue", EShortcut::ADVENTURE_SEARCH_CONTINUE }, {"battleToggleHeroesStats", EShortcut::BATTLE_TOGGLE_HEROES_STATS}, {"battleToggleQueue", EShortcut::BATTLE_TOGGLE_QUEUE }, {"battleUseCreatureSpell", EShortcut::BATTLE_USE_CREATURE_SPELL }, diff --git a/client/lobby/CBonusSelection.cpp b/client/lobby/CBonusSelection.cpp index d1db2d671..ced7705fd 100644 --- a/client/lobby/CBonusSelection.cpp +++ b/client/lobby/CBonusSelection.cpp @@ -38,10 +38,10 @@ #include "../gui/CGuiHandler.h" #include "../gui/Shortcut.h" #include "../gui/WindowHandler.h" +#include "../adventureMap/AdventureMapInterface.h" #include "../../lib/CConfigHandler.h" #include "../../lib/CCreatureHandler.h" -#include "../../lib/CHeroHandler.h" #include "../../lib/CSkillHandler.h" #include "../../lib/StartInfo.h" #include "../../lib/entities/building/CBuilding.h" @@ -49,6 +49,7 @@ #include "../../lib/entities/faction/CFaction.h" #include "../../lib/entities/faction/CTown.h" #include "../../lib/entities/faction/CTownHandler.h" +#include "../../lib/entities/hero/CHeroHandler.h" #include "../../lib/filesystem/Filesystem.h" #include "../../lib/texts/CGeneralTextHandler.h" @@ -98,7 +99,7 @@ CBonusSelection::CBonusSelection() int availableSpace = videoButtonActive ? 225 : 285; mapName = std::make_shared(481, 219, FONT_BIG, ETextAlignment::TOPLEFT, Colors::YELLOW, CSH->mi->getNameTranslated(), availableSpace ); labelMapDescription = std::make_shared(481, 253, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::YELLOW, CGI->generaltexth->allTexts[496]); - mapDescription = std::make_shared("", Rect(480, 278, 292, 108), 1); + mapDescription = std::make_shared("", Rect(480, 278, 286, 108), 1); labelChooseBonus = std::make_shared(475, 432, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE, CGI->generaltexth->allTexts[71]); groupBonuses = std::make_shared(std::bind(&IServerAPI::setCampaignBonus, CSH, _1)); @@ -389,10 +390,13 @@ void CBonusSelection::goBack() if(CSH->getState() != EClientState::GAMEPLAY) { GH.windows().popWindows(2); + CMM->playMusic(); } else { close(); + if(adventureInt) + adventureInt->onAudioResumed(); } // TODO: we can actually only pop bonus selection interface for custom campaigns // Though this would require clearing CLobbyScreen::bonusSel pointer when poping this interface @@ -403,7 +407,6 @@ void CBonusSelection::goBack() CSH->state = EClientState::LOBBY; } */ - CMM->playMusic(); } void CBonusSelection::startMap() diff --git a/client/lobby/CLobbyScreen.cpp b/client/lobby/CLobbyScreen.cpp index fbf0dfce7..ead6f3ef3 100644 --- a/client/lobby/CLobbyScreen.cpp +++ b/client/lobby/CLobbyScreen.cpp @@ -188,7 +188,7 @@ void CLobbyScreen::toggleMode(bool host) return; auto buttonColor = host ? Colors::WHITE : Colors::ORANGE; - buttonSelect->setTextOverlay(CGI->generaltexth->allTexts[500], FONT_SMALL, buttonColor); + buttonSelect->setTextOverlay(" " + CGI->generaltexth->allTexts[500], FONT_SMALL, buttonColor); buttonOptions->setTextOverlay(CGI->generaltexth->allTexts[501], FONT_SMALL, buttonColor); if (buttonTurnOptions) @@ -199,7 +199,7 @@ void CLobbyScreen::toggleMode(bool host) if(buttonRMG) { - buttonRMG->setTextOverlay(CGI->generaltexth->allTexts[740], FONT_SMALL, buttonColor); + buttonRMG->setTextOverlay(" " + CGI->generaltexth->allTexts[740], FONT_SMALL, buttonColor); buttonRMG->block(!host); } buttonSelect->block(!host); diff --git a/client/lobby/CSelectionBase.cpp b/client/lobby/CSelectionBase.cpp index 9a767f5f7..7b49da41c 100644 --- a/client/lobby/CSelectionBase.cpp +++ b/client/lobby/CSelectionBase.cpp @@ -43,7 +43,6 @@ #include "../render/IFont.h" #include "../render/IRenderHandler.h" -#include "../../lib/CHeroHandler.h" #include "../../lib/CRandomGenerator.h" #include "../../lib/CThreadHelper.h" #include "../../lib/filesystem/Filesystem.h" @@ -188,8 +187,8 @@ InfoCard::InfoCard() iconsVictoryCondition = std::make_shared(AnimationPath::builtin("SCNRVICT"), 0, 0, 24, 302); iconsLossCondition = std::make_shared(AnimationPath::builtin("SCNRLOSS"), 0, 0, 24, 359); - labelVictoryConditionText = std::make_shared(60, 307, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE); - labelLossConditionText = std::make_shared(60, 366, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE); + labelVictoryConditionText = std::make_shared(60, 307, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE, "", 290); + labelLossConditionText = std::make_shared(60, 366, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE, "", 290); labelDifficulty = std::make_shared(62, 472, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE); labelDifficultyPercent = std::make_shared(311, 472, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE); diff --git a/client/lobby/OptionsTab.cpp b/client/lobby/OptionsTab.cpp index 674bb81ee..4d5aa771c 100644 --- a/client/lobby/OptionsTab.cpp +++ b/client/lobby/OptionsTab.cpp @@ -39,12 +39,13 @@ #include "../../lib/entities/faction/CFaction.h" #include "../../lib/entities/faction/CTown.h" #include "../../lib/entities/faction/CTownHandler.h" +#include "../../lib/entities/hero/CHeroHandler.h" +#include "../../lib/entities/hero/CHeroClass.h" #include "../../lib/filesystem/Filesystem.h" #include "../../lib/networkPacks/PacksForLobby.h" #include "../../lib/texts/CGeneralTextHandler.h" #include "../../lib/CArtHandler.h" #include "../../lib/CConfigHandler.h" -#include "../../lib/CHeroHandler.h" #include "../../lib/mapping/CMapInfo.h" #include "../../lib/mapping/CMapHeader.h" @@ -764,9 +765,9 @@ void OptionsTab::SelectionWindow::sliderMove(int slidPos) redraw(); } -bool OptionsTab::SelectionWindow::receiveEvent(const Point & position, int eventType) const +void OptionsTab::SelectionWindow::notFocusedClick() { - return true; // capture click also outside of window + close(); } void OptionsTab::SelectionWindow::clickReleased(const Point & cursorPosition) @@ -774,12 +775,6 @@ void OptionsTab::SelectionWindow::clickReleased(const Point & cursorPosition) if(slider && slider->pos.isInside(cursorPosition)) return; - if(!pos.isInside(cursorPosition)) - { - close(); - return; - } - int elem = getElement(cursorPosition); setElement(elem, true); @@ -835,9 +830,9 @@ OptionsTab::HandicapWindow::HandicapWindow() if(i == 0) { if(isIncome) - labels.push_back(std::make_shared(xPos, 35, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE, CGI->generaltexth->translate("core.jktext.32"))); + labels.push_back(std::make_shared(xPos, 38, FONT_TINY, ETextAlignment::TOPLEFT, Colors::WHITE, CGI->generaltexth->translate("core.jktext.32"))); else if(isGrowth) - labels.push_back(std::make_shared(xPos, 35, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE, CGI->generaltexth->translate("core.genrltxt.194"))); + labels.push_back(std::make_shared(xPos, 38, FONT_TINY, ETextAlignment::TOPLEFT, Colors::WHITE, CGI->generaltexth->translate("core.genrltxt.194"))); else anim.push_back(std::make_shared(AnimationPath::builtin("SMALRES"), GameResID(resource), 0, 15 + xPos + (j == 0 ? 10 : 0), 35)); } @@ -897,15 +892,9 @@ OptionsTab::HandicapWindow::HandicapWindow() center(); } -bool OptionsTab::HandicapWindow::receiveEvent(const Point & position, int eventType) const +void OptionsTab::HandicapWindow::notFocusedClick() { - return true; // capture click also outside of window -} - -void OptionsTab::HandicapWindow::clickReleased(const Point & cursorPosition) -{ - if(!pos.isInside(cursorPosition)) // make it possible to close window by touching/clicking outside of window - close(); + close(); } OptionsTab::SelectedBox::SelectedBox(Point position, PlayerSettings & playerSettings, SelType type) @@ -1035,14 +1024,13 @@ OptionsTab::PlayerOptionsEntry::PlayerOptionsEntry(const PlayerSettings & S, con labelPlayerNameEdit = std::make_shared(Rect(6, 3, 95, 15), EFonts::FONT_SMALL, ETextAlignment::CENTER, false); labelPlayerNameEdit->setText(name); } - const auto & font = GH.renderHandler().loadFont(FONT_SMALL); - labelWhoCanPlay = std::make_shared(Rect(6, 23, 45, font->getLineHeight()*2), EFonts::FONT_TINY, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->arraytxt[206 + whoCanPlay]); + labelWhoCanPlay = std::make_shared(Rect(6, 21, 45, 26), EFonts::FONT_TINY, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->arraytxt[206 + whoCanPlay]); auto hasHandicap = [this](){ return s->handicap.startBonus.empty() && s->handicap.percentIncome == 100 && s->handicap.percentGrowth == 100; }; std::string labelHandicapText = hasHandicap() ? CGI->generaltexth->arraytxt[210] : MetaString::createFromTextID("vcmi.lobby.handicap").toString(); - labelHandicap = std::make_shared(Rect(57, 24, 47, font->getLineHeight()*2), EFonts::FONT_TINY, ETextAlignment::CENTER, Colors::WHITE, labelHandicapText); - handicap = std::make_shared(Rect(56, 24, 49, font->getLineHeight()*2), [](){ + labelHandicap = std::make_shared(Rect(55, 23, 46, 24), EFonts::FONT_TINY, ETextAlignment::CENTER, Colors::WHITE, labelHandicapText); + handicap = std::make_shared(Rect(53, 23, 50, 24), [](){ if(!CSH->isHost()) return; diff --git a/client/lobby/OptionsTab.h b/client/lobby/OptionsTab.h index 8bef7c9c2..e74666e6d 100644 --- a/client/lobby/OptionsTab.h +++ b/client/lobby/OptionsTab.h @@ -63,8 +63,7 @@ public: std::map>> textinputs; std::vector> buttons; - bool receiveEvent(const Point & position, int eventType) const override; - void clickReleased(const Point & cursorPosition) override; + void notFocusedClick() override; public: HandicapWindow(); }; @@ -167,7 +166,7 @@ private: void sliderMove(int slidPos); - bool receiveEvent(const Point & position, int eventType) const override; + void notFocusedClick() override; void clickReleased(const Point & cursorPosition) override; void showPopupWindow(const Point & cursorPosition) override; diff --git a/client/lobby/RandomMapTab.cpp b/client/lobby/RandomMapTab.cpp index bf25bedbc..9b13ece93 100644 --- a/client/lobby/RandomMapTab.cpp +++ b/client/lobby/RandomMapTab.cpp @@ -263,7 +263,7 @@ void RandomMapTab::setMapGenOptions(std::shared_ptr opts) humanCountAllowed = tmpl->getHumanPlayers().getNumbers(); // Unused now? } - si8 playerLimit = opts->getMaxPlayersCount(); + si8 playerLimit = opts->getPlayerLimit(); si8 humanOrCpuPlayerCount = opts->getHumanOrCpuPlayerCount(); si8 compOnlyPlayersCount = opts->getCompOnlyPlayerCount(); @@ -469,6 +469,8 @@ TeamAlignmentsWidget::TeamAlignmentsWidget(RandomMapTab & randomMapTab): variables["totalPlayers"].Integer() = totalPlayers; pos.w = variables["windowSize"]["x"].Integer() + totalPlayers * variables["cellMargin"]["x"].Integer(); + auto widthExtend = std::max(pos.w, 220) - pos.w; // too small for buttons + pos.w += widthExtend; pos.h = variables["windowSize"]["y"].Integer() + totalPlayers * variables["cellMargin"]["y"].Integer(); variables["backgroundRect"]["x"].Integer() = 0; variables["backgroundRect"]["y"].Integer() = 0; @@ -553,7 +555,7 @@ TeamAlignmentsWidget::TeamAlignmentsWidget(RandomMapTab & randomMapTab): for(int teamId = 0; teamId < totalPlayers; ++teamId) { - variables["point"]["x"].Integer() = variables["cellOffset"]["x"].Integer() + plId * variables["cellMargin"]["x"].Integer(); + variables["point"]["x"].Integer() = variables["cellOffset"]["x"].Integer() + plId * variables["cellMargin"]["x"].Integer() + (widthExtend / 2); variables["point"]["y"].Integer() = variables["cellOffset"]["y"].Integer() + teamId * variables["cellMargin"]["y"].Integer(); auto button = buildWidget(variables["button"]); players.back()->addToggle(teamId, std::dynamic_pointer_cast(button)); diff --git a/client/lobby/SelectionTab.cpp b/client/lobby/SelectionTab.cpp index 75b8181ee..6be7e9d40 100644 --- a/client/lobby/SelectionTab.cpp +++ b/client/lobby/SelectionTab.cpp @@ -42,9 +42,11 @@ #include "../../lib/mapping/CMapInfo.h" #include "../../lib/mapping/CMapHeader.h" #include "../../lib/mapping/MapFormat.h" +#include "../../lib/networkPacks/PacksForLobby.h" #include "../../lib/texts/CGeneralTextHandler.h" #include "../../lib/texts/TextOperations.h" #include "../../lib/TerrainHandler.h" +#include "../../lib/UnlockGuard.h" bool mapSorter::operator()(const std::shared_ptr aaa, const std::shared_ptr bbb) { @@ -152,7 +154,7 @@ static ESortBy getSortBySelectionScreen(ESelectionScreen Type) } SelectionTab::SelectionTab(ESelectionScreen Type) - : CIntObject(LCLICK | SHOW_POPUP | KEYBOARD | DOUBLECLICK), callOnSelect(nullptr), tabType(Type), selectionPos(0), sortModeAscending(true), inputNameRect{32, 539, 350, 20}, curFolder(""), currentMapSizeFilter(0), showRandom(false) + : CIntObject(LCLICK | SHOW_POPUP | KEYBOARD | DOUBLECLICK), callOnSelect(nullptr), tabType(Type), selectionPos(0), sortModeAscending(true), inputNameRect{32, 539, 350, 20}, curFolder(""), currentMapSizeFilter(0), showRandom(false), deleteMode(false) { OBJECT_CONSTRUCTION; @@ -192,20 +194,23 @@ SelectionTab::SelectionTab(ESelectionScreen Type) int positionsToShow = 18; std::string tabTitle; + std::string tabTitleDelete; switch(tabType) { case ESelectionScreen::newGame: - tabTitle = CGI->generaltexth->arraytxt[229]; + tabTitle = "{" + CGI->generaltexth->arraytxt[229] + "}"; + tabTitleDelete = "{red|" + CGI->generaltexth->translate("vcmi.lobby.deleteMapTitle") + "}"; break; case ESelectionScreen::loadGame: - tabTitle = CGI->generaltexth->arraytxt[230]; + tabTitle = "{" + CGI->generaltexth->arraytxt[230] + "}"; + tabTitleDelete = "{red|" + CGI->generaltexth->translate("vcmi.lobby.deleteSaveGameTitle") + "}"; break; case ESelectionScreen::saveGame: positionsToShow = 16; - tabTitle = CGI->generaltexth->arraytxt[231]; + tabTitle = "{" + CGI->generaltexth->arraytxt[231] + "}"; break; case ESelectionScreen::campaignList: - tabTitle = CGI->generaltexth->allTexts[726]; + tabTitle = "{" + CGI->generaltexth->allTexts[726] + "}"; setRedrawParent(true); // we use parent background so we need to make sure it's will be redrawn too pos.w = parent->pos.w; pos.h = parent->pos.h; @@ -225,12 +230,26 @@ SelectionTab::SelectionTab(ESelectionScreen Type) auto sortByDate = std::make_shared(Point(371, 85), AnimationPath::builtin("selectionTabSortDate"), CButton::tooltip("", CGI->generaltexth->translate("vcmi.lobby.sortDate")), std::bind(&SelectionTab::sortBy, this, ESortBy::_changeDate), EShortcut::MAPS_SORT_CHANGEDATE); sortByDate->setOverlay(std::make_shared(ImagePath::builtin("lobby/selectionTabSortDate"))); buttonsSortBy.push_back(sortByDate); + + if(tabType == ESelectionScreen::loadGame || tabType == ESelectionScreen::newGame) + { + buttonDeleteMode = std::make_shared(Point(367, 18), AnimationPath::builtin("lobby/deleteButton"), CButton::tooltip("", CGI->generaltexth->translate("vcmi.lobby.deleteMode")), [this, tabTitle, tabTitleDelete](){ + deleteMode = !deleteMode; + if(deleteMode) + labelTabTitle->setText(tabTitleDelete); + else + labelTabTitle->setText(tabTitle); + }); + + if(tabType == ESelectionScreen::newGame) + buttonDeleteMode->setEnabled(false); + } } for(int i = 0; i < positionsToShow; i++) listItems.push_back(std::make_shared(Point(30, 129 + i * 25))); - labelTabTitle = std::make_shared(205, 28, FONT_MEDIUM, ETextAlignment::CENTER, Colors::YELLOW, tabTitle); + labelTabTitle = std::make_shared(205, 28, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, tabTitle); slider = std::make_shared(Point(372, 86 + (enableUiEnhancements ? 30 : 0)), (tabType != ESelectionScreen::saveGame ? 480 : 430) - (enableUiEnhancements ? 30 : 0), std::bind(&SelectionTab::sliderMove, this, _1), positionsToShow, (int)curItems.size(), 0, Orientation::VERTICAL, CSlider::BLUE); slider->setPanningStep(24); @@ -242,10 +261,10 @@ SelectionTab::SelectionTab(ESelectionScreen Type) void SelectionTab::toggleMode() { + allItems.clear(); + curItems.clear(); if(CSH->isGuest()) { - allItems.clear(); - curItems.clear(); if(slider) slider->block(true); } @@ -263,9 +282,12 @@ void SelectionTab::toggleMode() } case ESelectionScreen::loadGame: - inputName->disable(); - parseSaves(getFiles("Saves/", EResType::SAVEGAME)); - break; + { + inputName->disable(); + auto unsupported = parseSaves(getFiles("Saves/", EResType::SAVEGAME)); + handleUnsupportedSavegames(unsupported); + break; + } case ESelectionScreen::saveGame: parseSaves(getFiles("Saves/", EResType::SAVEGAME)); @@ -307,9 +329,37 @@ void SelectionTab::clickReleased(const Point & cursorPosition) { int line = getLine(); - if(line != -1) + if(line != -1 && curItems.size() > line) { - select(line); + if(!deleteMode) + select(line); + else + { + int py = line + slider->getValue(); + vstd::amax(py, 0); + vstd::amin(py, curItems.size() - 1); + + if(curItems[py]->isFolder && boost::algorithm::starts_with(curItems[py]->folderName, "..")) + { + select(line); + return; + } + + if(!curItems[py]->isFolder) + CInfoWindow::showYesNoDialog(CGI->generaltexth->translate("vcmi.lobby.deleteFile") + "\n\n" + curItems[py]->fullFileURI, std::vector>(), [this, py](){ + LobbyDelete ld; + ld.type = tabType == ESelectionScreen::newGame ? LobbyDelete::EType::RANDOMMAP : LobbyDelete::EType::SAVEGAME; + ld.name = curItems[py]->fileURI; + CSH->sendLobbyPack(ld); + }, nullptr); + else + CInfoWindow::showYesNoDialog(CGI->generaltexth->translate("vcmi.lobby.deleteFolder") + "\n\n" + curFolder + curItems[py]->folderName, std::vector>(), [this, py](){ + LobbyDelete ld; + ld.type = LobbyDelete::EType::SAVEGAME_FOLDER; + ld.name = curFolder + curItems[py]->folderName; + CSH->sendLobbyPack(ld); + }, nullptr); + } } #ifdef VCMI_MOBILE // focus input field if clicked inside it @@ -475,16 +525,19 @@ void SelectionTab::filter(int size, bool selectFirst) curItems.clear(); + if(buttonDeleteMode) + buttonDeleteMode->setEnabled(tabType != ESelectionScreen::newGame || showRandom); + for(auto elem : allItems) { if((elem->mapHeader && (!size || elem->mapHeader->width == size)) || tabType == ESelectionScreen::campaignList) { if(showRandom) - curFolder = "RANDOMMAPS/"; + curFolder = "RandomMaps/"; auto [folderName, baseFolder, parentExists, fileInFolder] = checkSubfolder(elem->originalFileURI); - if((showRandom && baseFolder != "RANDOMMAPS") || (!showRandom && baseFolder == "RANDOMMAPS")) + if((showRandom && baseFolder != "RandomMaps") || (!showRandom && baseFolder == "RandomMaps")) continue; if(parentExists && !showRandom) @@ -501,6 +554,7 @@ void SelectionTab::filter(int size, bool selectFirst) auto folder = std::make_shared(); folder->isFolder = true; folder->folderName = folderName; + folder->isAutoSaveFolder = boost::starts_with(baseFolder, "Autosave/") && folderName != "Autosave"; auto itemIt = boost::range::find_if(curItems, [folder](std::shared_ptr e) { return e->folderName == folder->folderName; }); if (itemIt == curItems.end() && folderName != "") { curItems.push_back(folder); @@ -561,7 +615,11 @@ void SelectionTab::sort() int firstMapIndex = boost::range::find_if(curItems, [](std::shared_ptr e) { return !e->isFolder; }) - curItems.begin(); if(!sortModeAscending) + { + if(firstMapIndex) + std::reverse(std::next(curItems.begin(), boost::starts_with(curItems[0]->folderName, "..") ? 1 : 0), std::next(curItems.begin(), firstMapIndex - 1)); std::reverse(std::next(curItems.begin(), firstMapIndex), curItems.end()); + } updateListItems(); redraw(); @@ -693,7 +751,7 @@ void SelectionTab::selectFileName(std::string fname) for(int i = (int)allItems.size() - 1; i >= 0; i--) { - if(allItems[i]->fileURI == fname) + if(boost::to_upper_copy(allItems[i]->fileURI) == fname) { auto [folderName, baseFolder, parentExists, fileInFolder] = checkSubfolder(allItems[i]->originalFileURI); curFolder = baseFolder != "" ? baseFolder + "/" : ""; @@ -704,7 +762,7 @@ void SelectionTab::selectFileName(std::string fname) for(int i = (int)curItems.size() - 1; i >= 0; i--) { - if(curItems[i]->fileURI == fname) + if(boost::to_upper_copy(curItems[i]->fileURI) == fname) { slider->scrollTo(i); selectAbs(i); @@ -715,7 +773,7 @@ void SelectionTab::selectFileName(std::string fname) selectAbs(-1); if(tabType == ESelectionScreen::saveGame && inputName->getText().empty()) - inputName->setText("NEWGAME"); + inputName->setText(CGI->generaltexth->translate("core.genrltxt.11")); } void SelectionTab::selectNewestFile() @@ -808,7 +866,8 @@ void SelectionTab::parseMaps(const std::unordered_set & files) try { auto mapInfo = std::make_shared(); - mapInfo->mapInit(file.getName()); + mapInfo->mapInit(file.getOriginalName()); + mapInfo->name = mapInfo->getNameForList(); if (isMapSupported(*mapInfo)) allItems.push_back(mapInfo); @@ -820,14 +879,17 @@ void SelectionTab::parseMaps(const std::unordered_set & files) } } -void SelectionTab::parseSaves(const std::unordered_set & files) +std::vector SelectionTab::parseSaves(const std::unordered_set & files) { + std::vector unsupported; + for(auto & file : files) { try { auto mapInfo = std::make_shared(); mapInfo->saveInit(file); + mapInfo->name = mapInfo->getNameForList(); // Filter out other game modes bool isCampaign = mapInfo->scenarioOptionsOfSave->mode == EStartMode::CAMPAIGN; @@ -859,24 +921,72 @@ void SelectionTab::parseSaves(const std::unordered_set & files) allItems.push_back(mapInfo); } - catch(const std::exception & e) + catch(const IdentifierResolutionException & e) { logGlobal->error("Error: Failed to process %s: %s", file.getName(), e.what()); } + catch(const std::exception & e) + { + unsupported.push_back(file); // IdentifierResolutionException is not relevant -> not ask to delete, when mods are disabled + logGlobal->error("Error: Failed to process %s: %s", file.getName(), e.what()); + } + } + + return unsupported; +} + +void SelectionTab::handleUnsupportedSavegames(const std::vector & files) +{ + if(CSH->isHost() && files.size()) + { + MetaString text = MetaString::createFromTextID("vcmi.lobby.deleteUnsupportedSave"); + text.replaceNumber(files.size()); + CInfoWindow::showYesNoDialog(text.toString(), std::vector>(), [files](){ + for(auto & file : files) + { + LobbyDelete ld; + ld.type = LobbyDelete::EType::SAVEGAME; + ld.name = file.getName(); + CSH->sendLobbyPack(ld); + } + }, nullptr); } } void SelectionTab::parseCampaigns(const std::unordered_set & files) { + auto campaignSets = JsonNode(JsonPath::builtin("config/campaignSets.json")); + auto mainmenu = JsonNode(JsonPath::builtin("config/mainmenu.json")); + allItems.reserve(files.size()); for(auto & file : files) { auto info = std::make_shared(); - //allItems[i].date = std::asctime(std::localtime(&files[i].date)); - info->fileURI = file.getName(); + info->fileURI = file.getOriginalName(); info->campaignInit(); + info->name = info->getNameForList(); + if(info->campaign) - allItems.push_back(info); + { + // skip campaigns organized in sets + std::string foundInSet = ""; + for (auto const & set : campaignSets.Struct()) + for (auto const & item : set.second["items"].Vector()) + if(file.getName() == ResourcePath(item["file"].String()).getName()) + foundInSet = set.first; + + // set has to be used in main menu + bool setInMainmenu = false; + if(!foundInSet.empty()) + for (auto const & item : mainmenu["window"]["items"].Vector()) + if(item["name"].String() == "campaign") + for (auto const & button : item["buttons"].Vector()) + if(boost::algorithm::ends_with(boost::algorithm::to_lower_copy(button["command"].String()), boost::algorithm::to_lower_copy(foundInSet))) + setInMainmenu = true; + + if(!setInMainmenu) + allItems.push_back(info); + } } } @@ -901,7 +1011,7 @@ SelectionTab::ListItem::ListItem(Point position) { OBJECT_CONSTRUCTION; pictureEmptyLine = std::make_shared(ImagePath::builtin("camcust"), Rect(25, 121, 349, 26), -8, -14); - labelName = std::make_shared(184, 0, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, "", 185); + labelName = std::make_shared(LABEL_POS_X, 0, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, "", 185); labelName->setAutoRedraw(false); labelAmountOfPlayers = std::make_shared(8, 0, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE); labelAmountOfPlayers->setAutoRedraw(false); @@ -945,6 +1055,16 @@ void SelectionTab::ListItem::updateItem(std::shared_ptr info, bool labelNumberOfCampaignMaps->disable(); labelName->enable(); labelName->setMaxWidth(316); + if(info->isAutoSaveFolder) // align autosave folder left (starting timestamps in list should be in one line) + { + labelName->alignment = ETextAlignment::CENTERLEFT; + labelName->moveTo(Point(pos.x + 80, labelName->pos.y)); + } + else + { + labelName->alignment = ETextAlignment::CENTER; + labelName->moveTo(Point(pos.x + LABEL_POS_X, labelName->pos.y)); + } labelName->setText(info->folderName); labelName->setColor(color); return; @@ -966,6 +1086,8 @@ void SelectionTab::ListItem::updateItem(std::shared_ptr info, bool labelNumberOfCampaignMaps->setText(ostr.str()); labelNumberOfCampaignMaps->setColor(color); labelName->setMaxWidth(316); + labelName->alignment = ETextAlignment::CENTER; + labelName->moveTo(Point(pos.x + LABEL_POS_X, labelName->pos.y)); } else { @@ -987,7 +1109,9 @@ void SelectionTab::ListItem::updateItem(std::shared_ptr info, bool iconLossCondition->enable(); iconLossCondition->setFrame(info->mapHeader->defeatIconIndex, 0); labelName->setMaxWidth(185); + labelName->alignment = ETextAlignment::CENTER; + labelName->moveTo(Point(pos.x + LABEL_POS_X, labelName->pos.y)); } - labelName->setText(info->getNameForList()); + labelName->setText(info->name); labelName->setColor(color); } diff --git a/client/lobby/SelectionTab.h b/client/lobby/SelectionTab.h index 67e39047c..2cef080ed 100644 --- a/client/lobby/SelectionTab.h +++ b/client/lobby/SelectionTab.h @@ -33,7 +33,9 @@ public: ElementInfo() : CMapInfo() { } ~ElementInfo() { } std::string folderName = ""; + std::string name = ""; bool isFolder = false; + bool isAutoSaveFolder = false; }; /// Class which handles map sorting by different criteria @@ -59,6 +61,8 @@ class SelectionTab : public CIntObject std::shared_ptr pictureEmptyLine; std::shared_ptr labelName; + const int LABEL_POS_X = 184; + ListItem(Point position); void updateItem(std::shared_ptr info = {}, bool selected = false); }; @@ -68,6 +72,8 @@ class SelectionTab : public CIntObject // FIXME: CSelectionBase use them too! std::shared_ptr iconsVictoryCondition; std::shared_ptr iconsLossCondition; + + std::vector> unSupportedSaves; public: std::vector> allItems; std::vector> curItems; @@ -116,11 +122,16 @@ private: ESelectionScreen tabType; Rect inputNameRect; + std::shared_ptr buttonDeleteMode; + bool deleteMode; + auto checkSubfolder(std::string path); bool isMapSupported(const CMapInfo & info); void parseMaps(const std::unordered_set & files); - void parseSaves(const std::unordered_set & files); + std::vector parseSaves(const std::unordered_set & files); void parseCampaigns(const std::unordered_set & files); std::unordered_set getFiles(std::string dirURI, EResType resType); + + void handleUnsupportedSavegames(const std::vector & files); }; diff --git a/client/mainmenu/CCampaignScreen.cpp b/client/mainmenu/CCampaignScreen.cpp index 4644ba18d..68d4cbdc4 100644 --- a/client/mainmenu/CCampaignScreen.cpp +++ b/client/mainmenu/CCampaignScreen.cpp @@ -37,7 +37,6 @@ #include "../../lib/spells/CSpellHandler.h" #include "../../lib/CConfigHandler.h" #include "../../lib/CSkillHandler.h" -#include "../../lib/CHeroHandler.h" #include "../../lib/CCreatureHandler.h" #include "../../lib/campaign/CampaignHandler.h" @@ -68,7 +67,8 @@ CCampaignScreen::CCampaignScreen(const JsonNode & config, std::string name) } for(const JsonNode & node : config[name]["items"].Vector()) - campButtons.push_back(std::make_shared(node, config, campaignSet)); + if(CResourceHandler::get()->existsResource(ResourcePath(node["file"].String(), EResType::CAMPAIGN))) + campButtons.push_back(std::make_shared(node, config, campaignSet)); } void CCampaignScreen::activate() diff --git a/client/mainmenu/CMainMenu.cpp b/client/mainmenu/CMainMenu.cpp index d19275185..f3702101c 100644 --- a/client/mainmenu/CMainMenu.cpp +++ b/client/mainmenu/CMainMenu.cpp @@ -38,6 +38,7 @@ #include "../widgets/VideoWidget.h" #include "../windows/InfoWindows.h" #include "../CServerHandler.h" +#include "../render/AssetGenerator.h" #include "../CGameInfo.h" #include "../CPlayerInterface.h" @@ -361,17 +362,6 @@ void CMainMenu::update() menu->switchToTab(menu->getActiveTab()); } - static bool warnedAboutModDependencies = false; - - if (!warnedAboutModDependencies) - { - warnedAboutModDependencies = true; - auto errorMessages = CGI->modh->getModLoadErrors(); - - if (!errorMessages.empty()) - CInfoWindow::showInfoDialog(errorMessages, std::vector>(), PlayerColor(1)); - } - // Handles mouse and key input GH.handleEvents(); GH.windows().simpleRedraw(); @@ -403,6 +393,9 @@ void CMainMenu::openCampaignScreen(std::string name) { auto const & config = CMainMenuConfig::get().getCampaigns(); + AssetGenerator::createCampaignBackground(); + AssetGenerator::createChroniclesCampaignImages(); + if(!vstd::contains(config.Struct(), name)) { logGlobal->error("Unknown campaign set: %s", name); @@ -413,7 +406,9 @@ void CMainMenu::openCampaignScreen(std::string name) for (auto const & entry : config[name]["items"].Vector()) { ResourcePath resourceID(entry["file"].String(), EResType::CAMPAIGN); - if (!CResourceHandler::get()->existsResource(resourceID)) + if(entry["optional"].Bool()) + continue; + if(!CResourceHandler::get()->existsResource(resourceID)) campaignsFound = false; } @@ -540,7 +535,7 @@ CMultiPlayers::CMultiPlayers(const std::vector & playerNames, ESele std::string text = CGI->generaltexth->allTexts[446]; boost::replace_all(text, "\t", "\n"); - textTitle = std::make_shared(text, Rect(25, 20, 315, 50), 0, FONT_BIG, ETextAlignment::CENTER, Colors::WHITE); //HOTSEAT Please enter names + textTitle = std::make_shared(text, Rect(25, 10, 315, 60), 0, FONT_BIG, ETextAlignment::CENTER, Colors::WHITE); //HOTSEAT Please enter names for(int i = 0; i < inputNames.size(); i++) { diff --git a/client/mainmenu/CPrologEpilogVideo.cpp b/client/mainmenu/CPrologEpilogVideo.cpp index 3b68e5536..fbd7a9c22 100644 --- a/client/mainmenu/CPrologEpilogVideo.cpp +++ b/client/mainmenu/CPrologEpilogVideo.cpp @@ -48,6 +48,7 @@ CPrologEpilogVideo::CPrologEpilogVideo(CampaignScenarioPrologEpilog _spe, std::f if (videoPlayer->pos.h == 400) videoPlayer->moveBy(Point(0, 100)); + CCS->musich->setVolume(CCS->musich->getVolume() / 2); // background volume is too loud by default CCS->musich->playMusic(spe.prologMusic, true, true); voiceDurationMilliseconds = CCS->soundh->getSoundDurationMilliseconds(spe.prologVoice); voiceSoundHandle = CCS->soundh->playSound(spe.prologVoice); @@ -59,7 +60,10 @@ CPrologEpilogVideo::CPrologEpilogVideo(CampaignScenarioPrologEpilog _spe, std::f CCS->soundh->setCallback(voiceSoundHandle, onVoiceStop); text = std::make_shared(Rect(100, 500, 600, 100), EFonts::FONT_BIG, ETextAlignment::CENTER, Colors::METALLIC_GOLD, spe.prologText.toString()); - text->scrollTextTo(-50); // beginning of text in the vertical middle of black area + if(text->getLines().size() == 3) + text->scrollTextTo(-25); // beginning of text in the vertical middle of black area + else if(text->getLines().size() > 3) + text->scrollTextTo(-50); // beginning of text in the vertical middle of black area } void CPrologEpilogVideo::tick(uint32_t msPassed) @@ -88,6 +92,7 @@ void CPrologEpilogVideo::show(Canvas & to) void CPrologEpilogVideo::clickPressed(const Point & cursorPosition) { + CCS->musich->setVolume(CCS->musich->getVolume() * 2); // restore background volume close(); CCS->soundh->resetCallback(voiceSoundHandle); // reset callback to avoid memory corruption since 'this' will be destroyed CCS->soundh->stopSound(voiceSoundHandle); diff --git a/client/mapView/MapRenderer.cpp b/client/mapView/MapRenderer.cpp index 6688812a6..9b56ccd9d 100644 --- a/client/mapView/MapRenderer.cpp +++ b/client/mapView/MapRenderer.cpp @@ -143,7 +143,7 @@ void MapRendererTerrain::renderTile(IMapRendererContext & context, Canvas & targ { const TerrainTile & mapTile = context.getMapTile(coordinates); - int32_t terrainIndex = mapTile.terType->getIndex(); + int32_t terrainIndex = mapTile.getTerrainID(); int32_t imageIndex = mapTile.terView; int32_t rotationIndex = mapTile.extTileFlags % 4; @@ -152,11 +152,11 @@ void MapRendererTerrain::renderTile(IMapRendererContext & context, Canvas & targ assert(image); if (!image) { - logGlobal->error("Failed to find image %d for terrain %s on tile %s", imageIndex, mapTile.terType->getNameTranslated(), coordinates.toString()); + logGlobal->error("Failed to find image %d for terrain %s on tile %s", imageIndex, mapTile.getTerrain()->getNameTranslated(), coordinates.toString()); return; } - for( auto const & element : mapTile.terType->paletteAnimation) + for( auto const & element : mapTile.getTerrain()->paletteAnimation) image->shiftPalette(element.start, element.length, context.terrainImageIndex(element.length)); target.draw(image, Point(0, 0)); @@ -166,7 +166,7 @@ uint8_t MapRendererTerrain::checksum(IMapRendererContext & context, const int3 & { const TerrainTile & mapTile = context.getMapTile(coordinates); - if(!mapTile.terType->paletteAnimation.empty()) + if(!mapTile.getTerrain()->paletteAnimation.empty()) return context.terrainImageIndex(250); return 0xff - 1; } @@ -184,16 +184,16 @@ void MapRendererRiver::renderTile(IMapRendererContext & context, Canvas & target { const TerrainTile & mapTile = context.getMapTile(coordinates); - if(mapTile.riverType->getId() == River::NO_RIVER) + if(!mapTile.hasRiver()) return; - int32_t terrainIndex = mapTile.riverType->getIndex(); + int32_t terrainIndex = mapTile.getRiverID(); int32_t imageIndex = mapTile.riverDir; int32_t rotationIndex = (mapTile.extTileFlags >> 2) % 4; const auto & image = storage.find(terrainIndex, rotationIndex, imageIndex); - for( auto const & element : mapTile.riverType->paletteAnimation) + for( auto const & element : mapTile.getRiver()->paletteAnimation) image->shiftPalette(element.start, element.length, context.terrainImageIndex(element.length)); target.draw(image, Point(0, 0)); @@ -203,7 +203,7 @@ uint8_t MapRendererRiver::checksum(IMapRendererContext & context, const int3 & c { const TerrainTile & mapTile = context.getMapTile(coordinates); - if(!mapTile.riverType->paletteAnimation.empty()) + if(!mapTile.getRiver()->paletteAnimation.empty()) return context.terrainImageIndex(250); return 0xff-1; } @@ -224,9 +224,9 @@ void MapRendererRoad::renderTile(IMapRendererContext & context, Canvas & target, if(context.isInMap(coordinatesAbove)) { const TerrainTile & mapTileAbove = context.getMapTile(coordinatesAbove); - if(mapTileAbove.roadType->getId() != Road::NO_ROAD) + if(mapTileAbove.hasRoad()) { - int32_t terrainIndex = mapTileAbove.roadType->getIndex(); + int32_t terrainIndex = mapTileAbove.getRoadID(); int32_t imageIndex = mapTileAbove.roadDir; int32_t rotationIndex = (mapTileAbove.extTileFlags >> 4) % 4; @@ -236,9 +236,9 @@ void MapRendererRoad::renderTile(IMapRendererContext & context, Canvas & target, } const TerrainTile & mapTile = context.getMapTile(coordinates); - if(mapTile.roadType->getId() != Road::NO_ROAD) + if(mapTile.hasRoad()) { - int32_t terrainIndex = mapTile.roadType->getIndex(); + int32_t terrainIndex = mapTile.getRoadID(); int32_t imageIndex = mapTile.roadDir; int32_t rotationIndex = (mapTile.extTileFlags >> 4) % 4; @@ -316,7 +316,7 @@ uint8_t MapRendererBorder::checksum(IMapRendererContext & context, const int3 & MapRendererFow::MapRendererFow() { fogOfWarFullHide = GH.renderHandler().loadAnimation(AnimationPath::builtin("TSHRC"), EImageBlitMode::OPAQUE); - fogOfWarPartialHide = GH.renderHandler().loadAnimation(AnimationPath::builtin("TSHRE"), EImageBlitMode::ALPHA); + fogOfWarPartialHide = GH.renderHandler().loadAnimation(AnimationPath::builtin("TSHRE"), EImageBlitMode::SIMPLE); static const std::vector rotations = {22, 15, 2, 13, 12, 16, 28, 17, 20, 19, 7, 24, 26, 25, 30, 32, 27}; @@ -383,24 +383,25 @@ std::shared_ptr MapRendererObjects::getBaseAnimation(const CGObjectI } bool generateMovementGroups = (info->id == Obj::BOAT) || (info->id == Obj::HERO); + bool enableOverlay = obj->ID != Obj::BOAT && obj->ID != Obj::HERO && obj->getOwner() != PlayerColor::UNFLAGGABLE; // Boat appearance files only contain single, unanimated image // proper boat animations are actually in different file if (info->id == Obj::BOAT) if(auto boat = dynamic_cast(obj); boat && !boat->actualAnimation.empty()) - return getAnimation(boat->actualAnimation, generateMovementGroups); + return getAnimation(boat->actualAnimation, generateMovementGroups, enableOverlay); - return getAnimation(info->animationFile, generateMovementGroups); + return getAnimation(info->animationFile, generateMovementGroups, enableOverlay); } -std::shared_ptr MapRendererObjects::getAnimation(const AnimationPath & filename, bool generateMovementGroups) +std::shared_ptr MapRendererObjects::getAnimation(const AnimationPath & filename, bool generateMovementGroups, bool enableOverlay) { auto it = animations.find(filename); if(it != animations.end()) return it->second; - auto ret = GH.renderHandler().loadAnimation(filename, EImageBlitMode::ALPHA); + auto ret = GH.renderHandler().loadAnimation(filename, enableOverlay ? EImageBlitMode::WITH_SHADOW_AND_OVERLAY : EImageBlitMode::WITH_SHADOW); animations[filename] = ret; if(generateMovementGroups) @@ -427,14 +428,14 @@ std::shared_ptr MapRendererObjects::getFlagAnimation(const CGObjectI { assert(dynamic_cast(obj) != nullptr); assert(obj->tempOwner.isValidPlayer()); - return getAnimation(AnimationPath::builtin(heroFlags[obj->tempOwner.getNum()]), true); + return getAnimation(AnimationPath::builtin(heroFlags[obj->tempOwner.getNum()]), true, false); } if(obj->ID == Obj::BOAT) { const auto * boat = dynamic_cast(obj); if(boat && boat->hero && !boat->flagAnimations[boat->hero->tempOwner.getNum()].empty()) - return getAnimation(boat->flagAnimations[boat->hero->tempOwner.getNum()], true); + return getAnimation(boat->flagAnimations[boat->hero->tempOwner.getNum()], true, false); } return nullptr; @@ -447,7 +448,7 @@ std::shared_ptr MapRendererObjects::getOverlayAnimation(const CGObje // Boats have additional animation with waves around boat const auto * boat = dynamic_cast(obj); if(boat && boat->hero && !boat->overlayAnimation.empty()) - return getAnimation(boat->overlayAnimation, true); + return getAnimation(boat->overlayAnimation, true, false); } return nullptr; } @@ -478,22 +479,14 @@ void MapRendererObjects::renderImage(IMapRendererContext & context, Canvas & tar return; image->setAlpha(transparency); - image->setShadowEnabled(true); - if (object->ID != Obj::HERO) + if (object->ID != Obj::HERO) // heroes use separate image with flag instead of player-colored palette { - image->setOverlayEnabled(object->getOwner().isValidPlayer() || object->getOwner() == PlayerColor::NEUTRAL); - if (object->getOwner().isValidPlayer()) image->setOverlayColor(graphics->playerColors[object->getOwner().getNum()]); if (object->getOwner() == PlayerColor::NEUTRAL) image->setOverlayColor(graphics->neutralColor); } - else - { - // heroes use separate image with flag instead of player-colored palette - image->setOverlayEnabled(false); - } Point offsetPixels = context.objectImageOffset(object->id, coordinates); @@ -567,10 +560,10 @@ uint8_t MapRendererObjects::checksum(IMapRendererContext & context, const int3 & } MapRendererOverlay::MapRendererOverlay() - : imageGrid(GH.renderHandler().loadImage(ImagePath::builtin("debug/grid"), EImageBlitMode::ALPHA)) - , imageBlocked(GH.renderHandler().loadImage(ImagePath::builtin("debug/blocked"), EImageBlitMode::ALPHA)) - , imageVisitable(GH.renderHandler().loadImage(ImagePath::builtin("debug/visitable"), EImageBlitMode::ALPHA)) - , imageSpellRange(GH.renderHandler().loadImage(ImagePath::builtin("debug/spellRange"), EImageBlitMode::ALPHA)) + : imageGrid(GH.renderHandler().loadImage(ImagePath::builtin("debug/grid"), EImageBlitMode::COLORKEY)) + , imageBlocked(GH.renderHandler().loadImage(ImagePath::builtin("debug/blocked"), EImageBlitMode::COLORKEY)) + , imageVisitable(GH.renderHandler().loadImage(ImagePath::builtin("debug/visitable"), EImageBlitMode::COLORKEY)) + , imageSpellRange(GH.renderHandler().loadImage(ImagePath::builtin("debug/spellRange"), EImageBlitMode::COLORKEY)) { } @@ -591,8 +584,8 @@ void MapRendererOverlay::renderTile(IMapRendererContext & context, Canvas & targ if(context.objectTransparency(objectID, coordinates) > 0 && !context.isActiveHero(object)) { - visitable |= object->visitableAt(coordinates.x, coordinates.y); - blocking |= object->blockingAt(coordinates.x, coordinates.y); + visitable |= object->visitableAt(coordinates); + blocking |= object->blockingAt(coordinates); } } @@ -626,7 +619,7 @@ uint8_t MapRendererOverlay::checksum(IMapRendererContext & context, const int3 & } MapRendererPath::MapRendererPath() - : pathNodes(GH.renderHandler().loadAnimation(AnimationPath::builtin("ADAG"), EImageBlitMode::ALPHA)) + : pathNodes(GH.renderHandler().loadAnimation(AnimationPath::builtin("ADAG"), EImageBlitMode::SIMPLE)) { } diff --git a/client/mapView/MapRenderer.h b/client/mapView/MapRenderer.h index e5bc2066c..e248f8cec 100644 --- a/client/mapView/MapRenderer.h +++ b/client/mapView/MapRenderer.h @@ -77,7 +77,7 @@ class MapRendererObjects std::shared_ptr getFlagAnimation(const CGObjectInstance * obj); std::shared_ptr getOverlayAnimation(const CGObjectInstance * obj); - std::shared_ptr getAnimation(const AnimationPath & filename, bool generateMovementGroups); + std::shared_ptr getAnimation(const AnimationPath & filename, bool generateMovementGroups, bool enableOverlay); std::shared_ptr getImage(IMapRendererContext & context, const CGObjectInstance * obj, const std::shared_ptr & animation) const; diff --git a/client/mapView/MapRendererContext.cpp b/client/mapView/MapRendererContext.cpp index 239541ae5..c12c1ccc4 100644 --- a/client/mapView/MapRendererContext.cpp +++ b/client/mapView/MapRendererContext.cpp @@ -120,7 +120,7 @@ size_t MapRendererBaseContext::objectGroupIndex(ObjectInstanceID objectID) const Point MapRendererBaseContext::objectImageOffset(ObjectInstanceID objectID, const int3 & coordinates) const { const CGObjectInstance * object = getObject(objectID); - int3 offsetTiles(object->getPosition() - coordinates); + int3 offsetTiles(object->anchorPos() - coordinates); return Point(offsetTiles) * Point(32, 32); } @@ -275,7 +275,7 @@ std::string MapRendererAdventureContext::overlayText(const int3 & coordinates) c const auto & tile = getMapTile(coordinates); - if (!tile.visitable) + if (!tile.visitable()) return {}; return tile.visitableObjects.back()->getObjectName(); @@ -288,7 +288,7 @@ ColorRGBA MapRendererAdventureContext::overlayTextColor(const int3 & coordinates const auto & tile = getMapTile(coordinates); - if (!tile.visitable) + if (!tile.visitable()) return {}; const auto * object = tile.visitableObjects.back(); @@ -498,7 +498,7 @@ size_t MapRendererWorldViewContext::overlayImageIndex(const int3 & coordinates) { const auto * object = getObject(objectID); - if(!object->visitableAt(coordinates.x, coordinates.y)) + if(!object->visitableAt(coordinates)) continue; ObjectPosInfo info(object); @@ -548,7 +548,10 @@ size_t MapRendererSpellViewContext::overlayImageIndex(const int3 & coordinates) return iconIndex; } - return MapRendererWorldViewContext::overlayImageIndex(coordinates); + if (MapRendererBaseContext::isVisible(coordinates)) + return MapRendererWorldViewContext::overlayImageIndex(coordinates); + else + return std::numeric_limits::max(); } MapRendererPuzzleMapContext::MapRendererPuzzleMapContext(const MapRendererContextState & viewState) diff --git a/client/mapView/MapRendererContextState.cpp b/client/mapView/MapRendererContextState.cpp index aa1a6ab0a..4f7b11726 100644 --- a/client/mapView/MapRendererContextState.cpp +++ b/client/mapView/MapRendererContextState.cpp @@ -49,14 +49,13 @@ void MapRendererContextState::addObject(const CGObjectInstance * obj) { for(int fy = 0; fy < obj->getHeight(); ++fy) { - int3 currTile(obj->pos.x - fx, obj->pos.y - fy, obj->pos.z); + int3 currTile(obj->anchorPos().x - fx, obj->anchorPos().y - fy, obj->anchorPos().z); - if(LOCPLINT->cb->isInTheMap(currTile) && obj->coveringAt(currTile.x, currTile.y)) + if(LOCPLINT->cb->isInTheMap(currTile) && obj->coveringAt(currTile)) { auto & container = objects[currTile.z][currTile.x][currTile.y]; - - container.push_back(obj->id); - boost::range::sort(container, compareObjectBlitOrder); + auto position = std::upper_bound(container.begin(), container.end(), obj->id, compareObjectBlitOrder); + container.insert(position, obj->id); } } } @@ -73,7 +72,7 @@ void MapRendererContextState::addMovingObject(const CGObjectInstance * object, c { for(int y = yFrom; y <= yDest; ++y) { - int3 currTile(x, y, object->pos.z); + int3 currTile(x, y, object->anchorPos().z); if(LOCPLINT->cb->isInTheMap(currTile)) { diff --git a/client/mapView/MapViewController.cpp b/client/mapView/MapViewController.cpp index ec33d6f5b..fa653725b 100644 --- a/client/mapView/MapViewController.cpp +++ b/client/mapView/MapViewController.cpp @@ -317,7 +317,7 @@ bool MapViewController::isEventVisible(const CGObjectInstance * obj, const Playe if(obj->isVisitable()) return context->isVisible(obj->visitablePos()); else - return context->isVisible(obj->pos); + return context->isVisible(obj->anchorPos()); } bool MapViewController::isEventVisible(const CGHeroInstance * obj, const int3 & from, const int3 & dest) diff --git a/client/mapView/mapHandler.cpp b/client/mapView/mapHandler.cpp index bd19e421a..eb30680fd 100644 --- a/client/mapView/mapHandler.cpp +++ b/client/mapView/mapHandler.cpp @@ -55,11 +55,11 @@ std::string CMapHandler::getTerrainDescr(const int3 & pos, bool rightClick) cons if(t.hasFavorableWinds()) return CGI->objtypeh->getObjectName(Obj::FAVORABLE_WINDS, 0); - std::string result = t.terType->getNameTranslated(); + std::string result = t.getTerrain()->getNameTranslated(); for(const auto & object : map->objects) { - if(object && object->coveringAt(pos.x, pos.y) && object->pos.z == pos.z && object->isTile2Terrain()) + if(object && object->coveringAt(pos) && object->isTile2Terrain()) { result = object->getObjectName(); break; @@ -103,15 +103,15 @@ bool CMapHandler::compareObjectBlitOrder(const CGObjectInstance * a, const CGObj for(const auto & aOffset : a->getBlockedOffsets()) { - int3 testTarget = a->pos + aOffset + int3(0, 1, 0); - if(b->blockingAt(testTarget.x, testTarget.y)) + int3 testTarget = a->anchorPos() + aOffset + int3(0, 1, 0); + if(b->blockingAt(testTarget)) bBlocksA += 1; } for(const auto & bOffset : b->getBlockedOffsets()) { - int3 testTarget = b->pos + bOffset + int3(0, 1, 0); - if(a->blockingAt(testTarget.x, testTarget.y)) + int3 testTarget = b->anchorPos() + bOffset + int3(0, 1, 0); + if(a->blockingAt(testTarget)) aBlocksB += 1; } @@ -126,8 +126,8 @@ bool CMapHandler::compareObjectBlitOrder(const CGObjectInstance * a, const CGObj return aBlocksB < bBlocksA; // object that don't have clear priority via tile blocking will appear based on their row - if(a->pos.y != b->pos.y) - return a->pos.y < b->pos.y; + if(a->anchorPos().y != b->anchorPos().y) + return a->anchorPos().y < b->anchorPos().y; // heroes should appear on top of objects on the same tile if(b->ID==Obj::HERO && a->ID!=Obj::HERO) diff --git a/client/media/CEmptyVideoPlayer.h b/client/media/CEmptyVideoPlayer.h index 619591c9b..6497f20a2 100644 --- a/client/media/CEmptyVideoPlayer.h +++ b/client/media/CEmptyVideoPlayer.h @@ -14,10 +14,6 @@ class CEmptyVideoPlayer final : public IVideoPlayer { public: - void playSpellbookAnimation(const VideoPath & name, const Point & position) override - { - } - /// Load video from specified path std::unique_ptr open(const VideoPath & name, float scaleFactor) override { diff --git a/client/media/CMusicHandler.cpp b/client/media/CMusicHandler.cpp index 2fbf48118..0a04b0633 100644 --- a/client/media/CMusicHandler.cpp +++ b/client/media/CMusicHandler.cpp @@ -265,7 +265,12 @@ void MusicEntry::load(const AudioPath & musicURI) try { - auto * musicFile = MakeSDLRWops(CResourceHandler::get()->load(currentName)); + std::unique_ptr stream = CResourceHandler::get()->load(currentName); + + if(musicURI.getName() == "BLADEFWCAMPAIGN") // handle defect MP3 file - ffprobe says: Skipping 52 bytes of junk at 0. + stream->seek(52); + + auto * musicFile = MakeSDLRWops(std::move(stream)); music = Mix_LoadMUS_RW(musicFile, SDL_TRUE); } catch(std::exception & e) diff --git a/client/media/CSoundHandler.cpp b/client/media/CSoundHandler.cpp index 79562f15c..bd099e728 100644 --- a/client/media/CSoundHandler.cpp +++ b/client/media/CSoundHandler.cpp @@ -147,18 +147,24 @@ uint32_t CSoundHandler::getSoundDurationMilliseconds(const AudioPath & sound) auto data = CResourceHandler::get()->load(resourcePath)->readAll(); - SDL_AudioSpec spec; - uint32_t audioLen; - uint8_t * audioBuf; uint32_t milliseconds = 0; - if(SDL_LoadWAV_RW(SDL_RWFromMem(data.first.get(), data.second), 1, &spec, &audioBuf, &audioLen) != nullptr) + Mix_Chunk * chunk = Mix_LoadWAV_RW(SDL_RWFromMem(data.first.get(), data.second), 1); + + int freq = 0; + Uint16 fmt = 0; + int channels = 0; + if(!Mix_QuerySpec(&freq, &fmt, &channels)) + return 0; + + if(chunk != nullptr) { - SDL_FreeWAV(audioBuf); - uint32_t sampleSize = SDL_AUDIO_BITSIZE(spec.format) / 8; - uint32_t sampleCount = audioLen / sampleSize; - uint32_t sampleLen = sampleCount / spec.channels; - milliseconds = 1000 * sampleLen / spec.freq; + Uint32 sampleSizeBytes = (fmt & 0xFF) / 8; + Uint32 samples = (chunk->alen / sampleSizeBytes); + Uint32 frames = (samples / channels); + milliseconds = ((frames * 1000) / freq); + + Mix_FreeChunk(chunk); } return milliseconds; @@ -234,6 +240,18 @@ void CSoundHandler::stopSound(int handler) Mix_HaltChannel(handler); } +void CSoundHandler::pauseSound(int handler) +{ + if(isInitialized() && handler != -1) + Mix_Pause(handler); +} + +void CSoundHandler::resumeSound(int handler) +{ + if(isInitialized() && handler != -1) + Mix_Resume(handler); +} + ui32 CSoundHandler::getVolume() const { return volume; diff --git a/client/media/CSoundHandler.h b/client/media/CSoundHandler.h index 5a10a5493..3450cbffb 100644 --- a/client/media/CSoundHandler.h +++ b/client/media/CSoundHandler.h @@ -67,6 +67,8 @@ public: int playSound(std::pair, si64> & data, int repeats = 0, bool cache = false) final; int playSoundFromSet(std::vector & sound_vec) final; void stopSound(int handler) final; + void pauseSound(int handler) final; + void resumeSound(int handler) final; void setCallback(int channel, std::function function) final; void resetCallback(int channel) final; diff --git a/client/media/CVideoHandler.cpp b/client/media/CVideoHandler.cpp index 51e17ff10..7853c9f1b 100644 --- a/client/media/CVideoHandler.cpp +++ b/client/media/CVideoHandler.cpp @@ -33,7 +33,9 @@ extern "C" { #include #include #include +#include #include +#include } // Define a set of functions to read data @@ -314,6 +316,12 @@ bool CVideoInstance::loadNextFrame() return true; } + +double CVideoInstance::timeStamp() +{ + return getCurrentFrameEndTime(); +} + bool CVideoInstance::videoEnded() { return getCurrentFrame() == nullptr; @@ -383,12 +391,38 @@ void CVideoInstance::tick(uint32_t msPassed) if(videoEnded()) throw std::runtime_error("Video already ended!"); - frameTime += msPassed / 1000.0; + if(startTime == std::chrono::steady_clock::time_point()) + startTime = std::chrono::steady_clock::now(); - if(frameTime >= getCurrentFrameEndTime()) + auto nowTime = std::chrono::steady_clock::now(); + double difference = std::chrono::duration_cast(nowTime - startTime).count() / 1000.0; + + int frameskipCounter = 0; + while(!videoEnded() && difference >= getCurrentFrameEndTime() + getCurrentFrameDuration() && frameskipCounter < MAX_FRAMESKIP) // Frameskip + { + decodeNextFrame(); + frameskipCounter++; + } + if(!videoEnded() && difference >= getCurrentFrameEndTime()) loadNextFrame(); } + +void CVideoInstance::activate() +{ + if(deactivationStartTime != std::chrono::steady_clock::time_point()) + { + auto pauseDuration = std::chrono::steady_clock::now() - deactivationStartTime; + startTime += pauseDuration; + deactivationStartTime = std::chrono::steady_clock::time_point(); + } +} + +void CVideoInstance::deactivate() +{ + deactivationStartTime = std::chrono::steady_clock::now(); +} + struct FFMpegFormatDescription { uint8_t sampleSizeBytes; @@ -501,32 +535,71 @@ std::pair, si64> CAudioInstance::extractAudio(const Vide int numChannels = codecpar->ch_layout.nb_channels; #endif - samples.reserve(44100 * 5); // arbitrary 5-second buffer + samples.reserve(44100 * 5); // arbitrary 5-second buffer to reduce reallocations - for (;;) + if (formatProperties.isPlanar && numChannels > 1) { - decodeNextFrame(); - const AVFrame * frame = getCurrentFrame(); + // Format is 'planar', which is not supported by wav / SDL + // Use swresample part of ffmpeg to deplanarize audio into format supported by wav / SDL - if (!frame) - break; + auto sourceFormat = static_cast(codecpar->format); + auto targetFormat = av_get_alt_sample_fmt(sourceFormat, false); - int samplesToRead = frame->nb_samples * numChannels; - int bytesToRead = samplesToRead * formatProperties.sampleSizeBytes; + SwrContext * swr_ctx = swr_alloc(); - if (formatProperties.isPlanar && numChannels > 1) +#if (LIBAVUTIL_VERSION_MAJOR < 58) + av_opt_set_channel_layout(swr_ctx, "in_chlayout", codecpar->channel_layout, 0); + av_opt_set_channel_layout(swr_ctx, "out_chlayout", codecpar->channel_layout, 0); +#else + av_opt_set_chlayout(swr_ctx, "in_chlayout", &codecpar->ch_layout, 0); + av_opt_set_chlayout(swr_ctx, "out_chlayout", &codecpar->ch_layout, 0); +#endif + av_opt_set_int(swr_ctx, "in_sample_rate", codecpar->sample_rate, 0); + av_opt_set_sample_fmt(swr_ctx, "in_sample_fmt", sourceFormat, 0); + av_opt_set_int(swr_ctx, "out_sample_rate", codecpar->sample_rate, 0); + av_opt_set_sample_fmt(swr_ctx, "out_sample_fmt", targetFormat, 0); + + int initResult = swr_init(swr_ctx); + if (initResult < 0) + throwFFmpegError(initResult); + + std::vector frameSamplesBuffer; + for (;;) { - // Workaround for lack of resampler - // Currently, ffmpeg on conan systems is built without sws resampler - // Because of that, and because wav format does not supports 'planar' formats from ffmpeg - // we need to de-planarize it and convert to "normal" (non-planar / interleaved) stream - samples.reserve(samples.size() + bytesToRead); - for (int sm = 0; sm < frame->nb_samples; ++sm) - for (int ch = 0; ch < numChannels; ++ch) - samples.insert(samples.end(), frame->data[ch] + sm * formatProperties.sampleSizeBytes, frame->data[ch] + (sm+1) * formatProperties.sampleSizeBytes ); + decodeNextFrame(); + const AVFrame * frame = getCurrentFrame(); + + if (!frame) + break; + + size_t samplesToRead = frame->nb_samples * numChannels; + size_t bytesToRead = samplesToRead * formatProperties.sampleSizeBytes; + frameSamplesBuffer.resize(std::max(frameSamplesBuffer.size(), bytesToRead)); + uint8_t * frameSamplesPtr = frameSamplesBuffer.data(); + + int result = swr_convert(swr_ctx, &frameSamplesPtr, frame->nb_samples, const_cast(frame->data), frame->nb_samples); + + if (result < 0) + throwFFmpegError(result); + + size_t samplesToCopy = result * numChannels; + size_t bytesToCopy = samplesToCopy * formatProperties.sampleSizeBytes; + samples.insert(samples.end(), frameSamplesBuffer.begin(), frameSamplesBuffer.begin() + bytesToCopy); } - else + swr_free(&swr_ctx); + } + else + { + for (;;) { + decodeNextFrame(); + const AVFrame * frame = getCurrentFrame(); + + if (!frame) + break; + + size_t samplesToRead = frame->nb_samples * numChannels; + size_t bytesToRead = samplesToRead * formatProperties.sampleSizeBytes; samples.insert(samples.end(), frame->data[0], frame->data[0] + bytesToRead); } } @@ -564,69 +637,6 @@ std::pair, si64> CAudioInstance::extractAudio(const Vide return dat; } -bool CVideoPlayer::openAndPlayVideoImpl(const VideoPath & name, const Point & position, bool useOverlay, bool stopOnKey) -{ - CVideoInstance instance; - CAudioInstance audio; - - auto extractedAudio = audio.extractAudio(name); - int audioHandle = CCS->soundh->playSound(extractedAudio); - - if (!instance.openInput(name)) - return true; - - instance.openVideo(); - instance.prepareOutput(1, true); - - auto lastTimePoint = boost::chrono::steady_clock::now(); - - while(instance.loadNextFrame()) - { - if(stopOnKey) - { - GH.input().fetchEvents(); - if(GH.input().ignoreEventsUntilInput()) - { - CCS->soundh->stopSound(audioHandle); - return false; - } - } - - SDL_Rect rect; - rect.x = position.x; - rect.y = position.y; - rect.w = instance.dimensions.x; - rect.h = instance.dimensions.y; - - SDL_RenderFillRect(mainRenderer, &rect); - - if(instance.textureYUV) - SDL_RenderCopy(mainRenderer, instance.textureYUV, nullptr, &rect); - else - SDL_RenderCopy(mainRenderer, instance.textureRGB, nullptr, &rect); - - SDL_RenderPresent(mainRenderer); - - // Framerate delay - double targetFrameTimeSeconds = instance.getCurrentFrameDuration(); - auto targetFrameTime = boost::chrono::milliseconds(static_cast(1000 * targetFrameTimeSeconds)); - - auto timePointAfterPresent = boost::chrono::steady_clock::now(); - auto timeSpentBusy = boost::chrono::duration_cast(timePointAfterPresent - lastTimePoint); - - if(targetFrameTime > timeSpentBusy) - boost::this_thread::sleep_for(targetFrameTime - timeSpentBusy); - - lastTimePoint = boost::chrono::steady_clock::now(); - } - return true; -} - -void CVideoPlayer::playSpellbookAnimation(const VideoPath & name, const Point & position) -{ - openAndPlayVideoImpl(name, position * GH.screenHandler().getScalingFactor(), false, false); -} - std::unique_ptr CVideoPlayer::open(const VideoPath & name, float scaleFactor) { auto result = std::make_unique(); @@ -643,6 +653,15 @@ std::unique_ptr CVideoPlayer::open(const VideoPath & name, float std::pair, si64> CVideoPlayer::getAudio(const VideoPath & videoToOpen) { + AudioPath audioPath = videoToOpen.toType(); + AudioPath audioPathVideoDir = audioPath.addPrefix("VIDEO/"); + + if(CResourceHandler::get()->existsResource(audioPath)) + return CResourceHandler::get()->load(audioPath)->readAll(); + + if(CResourceHandler::get()->existsResource(audioPathVideoDir)) + return CResourceHandler::get()->load(audioPathVideoDir)->readAll(); + CAudioInstance audio; return audio.extractAudio(videoToOpen); } diff --git a/client/media/CVideoHandler.h b/client/media/CVideoHandler.h index d40582b62..591f83b74 100644 --- a/client/media/CVideoHandler.h +++ b/client/media/CVideoHandler.h @@ -77,10 +77,13 @@ class CVideoInstance final : public IVideoInstance, public FFMpegStream SDL_Surface * surface = nullptr; Point dimensions; - /// video playback current progress, in seconds - double frameTime = 0.0; + /// video playback start time point + std::chrono::steady_clock::time_point startTime; + std::chrono::steady_clock::time_point deactivationStartTime; void prepareOutput(float scaleFactor, bool useTextureOutput); + + const int MAX_FRAMESKIP = 5; public: ~CVideoInstance(); @@ -88,20 +91,21 @@ public: void openVideo(); bool loadNextFrame(); + double timeStamp() final; bool videoEnded() final; Point size() final; void show(const Point & position, Canvas & canvas) final; void tick(uint32_t msPassed) final; + void activate() final; + void deactivate() final; }; class CVideoPlayer final : public IVideoPlayer { - bool openAndPlayVideoImpl(const VideoPath & name, const Point & position, bool useOverlay, bool stopOnKey); void openVideoFile(CVideoInstance & state, const VideoPath & fname); public: - void playSpellbookAnimation(const VideoPath & name, const Point & position) final; std::unique_ptr open(const VideoPath & name, float scaleFactor) final; std::pair, si64> getAudio(const VideoPath & videoToOpen) final; }; diff --git a/client/media/ISoundPlayer.h b/client/media/ISoundPlayer.h index 9b3d9d5e9..ffcb90dce 100644 --- a/client/media/ISoundPlayer.h +++ b/client/media/ISoundPlayer.h @@ -22,6 +22,8 @@ public: virtual int playSound(std::pair, si64> & data, int repeats = 0, bool cache = false) = 0; virtual int playSoundFromSet(std::vector & sound_vec) = 0; virtual void stopSound(int handler) = 0; + virtual void pauseSound(int handler) = 0; + virtual void resumeSound(int handler) = 0; virtual ui32 getVolume() const = 0; virtual void setVolume(ui32 percent) = 0; diff --git a/client/media/IVideoPlayer.h b/client/media/IVideoPlayer.h index 2c979a088..35f385bc1 100644 --- a/client/media/IVideoPlayer.h +++ b/client/media/IVideoPlayer.h @@ -20,6 +20,9 @@ VCMI_LIB_NAMESPACE_END class IVideoInstance { public: + /// Returns current video timestamp + virtual double timeStamp() = 0; + /// Returns true if video playback is over virtual bool videoEnded() = 0; @@ -32,15 +35,16 @@ public: /// Advances video playback by specified duration virtual void tick(uint32_t msPassed) = 0; + /// activate or deactivate video + virtual void activate() = 0; + virtual void deactivate() = 0; + virtual ~IVideoInstance() = default; }; class IVideoPlayer : boost::noncopyable { public: - /// Plays video on top of the screen, returns only after playback is over - virtual void playSpellbookAnimation(const VideoPath & name, const Point & position) = 0; - /// Load video from specified path. Returns nullptr on failure virtual std::unique_ptr open(const VideoPath & name, float scaleFactor) = 0; diff --git a/client/render/AssetGenerator.cpp b/client/render/AssetGenerator.cpp index ca03969f5..5aeb5f734 100644 --- a/client/render/AssetGenerator.cpp +++ b/client/render/AssetGenerator.cpp @@ -18,6 +18,10 @@ #include "../render/IRenderHandler.h" #include "../lib/filesystem/Filesystem.h" +#include "../lib/GameSettings.h" +#include "../lib/IGameSettings.h" +#include "../lib/json/JsonNode.h" +#include "../lib/VCMI_Lib.h" void AssetGenerator::generateAll() { @@ -26,6 +30,8 @@ void AssetGenerator::generateAll() for (int i = 0; i < PlayerColor::PLAYER_LIMIT_I; ++i) createPlayerColoredBackground(PlayerColor(i)); createCombatUnitNumberWindow(); + createCampaignBackground(); + createChroniclesCampaignImages(); } void AssetGenerator::createAdventureOptionsCleanBackground() @@ -138,22 +144,23 @@ void AssetGenerator::createPlayerColoredBackground(const PlayerColor & player) std::shared_ptr texture = GH.renderHandler().loadImage(locator, EImageBlitMode::OPAQUE); - // Color transform to make color of brown DIBOX.PCX texture match color of specified player + // transform to make color of brown DIBOX.PCX texture match color of specified player + auto filterSettings = VLC->settingsHandler->getFullConfig()["interface"]["playerColoredBackground"]; static const std::array filters = { - ColorFilter::genRangeShifter( 0.25, 0, 0, 1.25, 0.00, 0.00 ), // red - ColorFilter::genRangeShifter( 0, 0, 0, 0.45, 1.20, 4.50 ), // blue - ColorFilter::genRangeShifter( 0.40, 0.27, 0.23, 1.10, 1.20, 1.15 ), // tan - ColorFilter::genRangeShifter( -0.27, 0.10, -0.27, 0.70, 1.70, 0.70 ), // green - ColorFilter::genRangeShifter( 0.47, 0.17, -0.27, 1.60, 1.20, 0.70 ), // orange - ColorFilter::genRangeShifter( 0.12, -0.1, 0.25, 1.15, 1.20, 2.20 ), // purple - ColorFilter::genRangeShifter( -0.13, 0.23, 0.23, 0.90, 1.20, 2.20 ), // teal - ColorFilter::genRangeShifter( 0.44, 0.15, 0.25, 1.00, 1.00, 1.75 ) // pink + ColorFilter::genRangeShifter( filterSettings["red" ].convertTo>() ), + ColorFilter::genRangeShifter( filterSettings["blue" ].convertTo>() ), + ColorFilter::genRangeShifter( filterSettings["tan" ].convertTo>() ), + ColorFilter::genRangeShifter( filterSettings["green" ].convertTo>() ), + ColorFilter::genRangeShifter( filterSettings["orange"].convertTo>() ), + ColorFilter::genRangeShifter( filterSettings["purple"].convertTo>() ), + ColorFilter::genRangeShifter( filterSettings["teal" ].convertTo>() ), + ColorFilter::genRangeShifter( filterSettings["pink" ].convertTo>() ) }; assert(player.isValidPlayer()); if (!player.isValidPlayer()) { - logGlobal->error("Unable to colorize to invalid player color %d!", static_cast(player.getNum())); + logGlobal->error("Unable to colorize to invalid player color %d!", player.getNum()); return; } @@ -201,3 +208,121 @@ void AssetGenerator::createCombatUnitNumberWindow() texture->adjustPalette(shifterNeutral, ignoredMask); texture->exportBitmap(*CResourceHandler::get("local")->getResourceName(savePathNeutral)); } + +void AssetGenerator::createCampaignBackground() +{ + std::string filename = "data/CampaignBackground8.png"; + + if(CResourceHandler::get()->existsResource(ResourcePath(filename))) // overridden by mod, no generation + return; + + if(!CResourceHandler::get("local")->createResource(filename)) + return; + ResourcePath savePath(filename, EResType::IMAGE); + + auto locator = ImageLocator(ImagePath::builtin("CAMPBACK")); + locator.scalingFactor = 1; + + std::shared_ptr img = GH.renderHandler().loadImage(locator, EImageBlitMode::OPAQUE); + Canvas canvas = Canvas(Point(800, 600), CanvasScalingPolicy::IGNORE); + + canvas.draw(img, Point(0, 0), Rect(0, 0, 800, 600)); + + // left image + canvas.draw(img, Point(220, 73), Rect(290, 73, 141, 115)); + canvas.draw(img, Point(37, 70), Rect(87, 70, 207, 120)); + + // right image + canvas.draw(img, Point(513, 67), Rect(463, 67, 71, 126)); + canvas.draw(img, Point(586, 71), Rect(536, 71, 207, 117)); + + // middle image + canvas.draw(img, Point(306, 68), Rect(86, 68, 209, 122)); + + // disabled fields + canvas.draw(img, Point(40, 72), Rect(313, 74, 197, 114)); + canvas.draw(img, Point(310, 72), Rect(313, 74, 197, 114)); + canvas.draw(img, Point(590, 72), Rect(313, 74, 197, 114)); + canvas.draw(img, Point(43, 245), Rect(313, 74, 197, 114)); + canvas.draw(img, Point(313, 244), Rect(313, 74, 197, 114)); + canvas.draw(img, Point(586, 246), Rect(313, 74, 197, 114)); + canvas.draw(img, Point(34, 417), Rect(313, 74, 197, 114)); + canvas.draw(img, Point(404, 414), Rect(313, 74, 197, 114)); + + // skull + auto locatorSkull = ImageLocator(ImagePath::builtin("CAMPNOSC")); + locatorSkull.scalingFactor = 1; + std::shared_ptr imgSkull = GH.renderHandler().loadImage(locatorSkull, EImageBlitMode::OPAQUE); + canvas.draw(imgSkull, Point(562, 509), Rect(178, 108, 43, 19)); + + std::shared_ptr image = GH.renderHandler().createImage(canvas.getInternalSurface()); + + image->exportBitmap(*CResourceHandler::get("local")->getResourceName(savePath)); +} + +void AssetGenerator::createChroniclesCampaignImages() +{ + for(int i = 1; i < 9; i++) + { + std::string filename = "data/CampaignHc" + std::to_string(i) + "Image.png"; + + if(CResourceHandler::get()->existsResource(ResourcePath(filename))) // overridden by mod, no generation + continue; + + auto imgPathBg = ImagePath::builtin("data/chronicles_" + std::to_string(i) + "/GamSelBk"); + if(!CResourceHandler::get()->existsResource(imgPathBg)) // Chronicle episode not installed + continue; + + if(!CResourceHandler::get("local")->createResource(filename)) + continue; + ResourcePath savePath(filename, EResType::IMAGE); + + auto locator = ImageLocator(imgPathBg); + locator.scalingFactor = 1; + + std::shared_ptr img = GH.renderHandler().loadImage(locator, EImageBlitMode::OPAQUE); + Canvas canvas = Canvas(Point(200, 116), CanvasScalingPolicy::IGNORE); + + switch (i) + { + case 1: + canvas.draw(img, Point(0, 0), Rect(149, 144, 200, 116)); + break; + case 2: + canvas.draw(img, Point(0, 0), Rect(156, 150, 200, 116)); + break; + case 3: + canvas.draw(img, Point(0, 0), Rect(171, 153, 200, 116)); + break; + case 4: + canvas.draw(img, Point(0, 0), Rect(35, 358, 200, 116)); + break; + case 5: + canvas.draw(img, Point(0, 0), Rect(216, 248, 200, 116)); + break; + case 6: + canvas.draw(img, Point(0, 0), Rect(58, 234, 200, 116)); + break; + case 7: + canvas.draw(img, Point(0, 0), Rect(184, 219, 200, 116)); + break; + case 8: + canvas.draw(img, Point(0, 0), Rect(268, 210, 200, 116)); + + //skull + auto locatorSkull = ImageLocator(ImagePath::builtin("CampSP1")); + locatorSkull.scalingFactor = 1; + std::shared_ptr imgSkull = GH.renderHandler().loadImage(locatorSkull, EImageBlitMode::OPAQUE); + canvas.draw(imgSkull, Point(162, 94), Rect(162, 94, 41, 22)); + canvas.draw(img, Point(162, 94), Rect(424, 304, 14, 4)); + canvas.draw(img, Point(162, 98), Rect(424, 308, 10, 4)); + canvas.draw(img, Point(158, 102), Rect(424, 312, 10, 4)); + canvas.draw(img, Point(154, 106), Rect(424, 316, 10, 4)); + break; + } + + std::shared_ptr image = GH.renderHandler().createImage(canvas.getInternalSurface()); + + image->exportBitmap(*CResourceHandler::get("local")->getResourceName(savePath)); + } +} diff --git a/client/render/AssetGenerator.h b/client/render/AssetGenerator.h index 2b1c0b3a2..2eb73a886 100644 --- a/client/render/AssetGenerator.h +++ b/client/render/AssetGenerator.h @@ -21,4 +21,6 @@ public: static void createBigSpellBook(); static void createPlayerColoredBackground(const PlayerColor & player); static void createCombatUnitNumberWindow(); + static void createCampaignBackground(); + static void createChroniclesCampaignImages(); }; diff --git a/client/render/ColorFilter.cpp b/client/render/ColorFilter.cpp index 8b2288b4e..9e530009f 100644 --- a/client/render/ColorFilter.cpp +++ b/client/render/ColorFilter.cpp @@ -70,6 +70,13 @@ ColorFilter ColorFilter::genRangeShifter( float minR, float minG, float minB, fl 1.f); } +ColorFilter ColorFilter::genRangeShifter( std::vector parameters ) +{ + assert(std::size(parameters) == 6); + + return genRangeShifter(parameters[0], parameters[1], parameters[2], parameters[3], parameters[4], parameters[5]); +} + ColorFilter ColorFilter::genMuxerShifter( ChannelMuxer r, ChannelMuxer g, ChannelMuxer b, float a ) { return ColorFilter(r, g, b, a); diff --git a/client/render/ColorFilter.h b/client/render/ColorFilter.h index 5df63ec67..2a0268fe0 100644 --- a/client/render/ColorFilter.h +++ b/client/render/ColorFilter.h @@ -44,6 +44,7 @@ public: /// Generates object that transforms each channel independently static ColorFilter genRangeShifter( float minR, float minG, float minB, float maxR, float maxG, float maxB ); + static ColorFilter genRangeShifter( std::vector parameters ); /// Generates object that performs arbitrary mixing between any channels static ColorFilter genMuxerShifter( ChannelMuxer r, ChannelMuxer g, ChannelMuxer b, float a ); diff --git a/client/render/Graphics.cpp b/client/render/Graphics.cpp index 0c88f508a..9792e83e7 100644 --- a/client/render/Graphics.cpp +++ b/client/render/Graphics.cpp @@ -28,7 +28,6 @@ #include "../lib/modding/CModHandler.h" #include "../lib/modding/ModScope.h" #include "../lib/VCMI_Lib.h" -#include "../lib/CHeroHandler.h" #include diff --git a/client/render/IImage.h b/client/render/IImage.h index 5e60b0c6a..a6e4e8b7b 100644 --- a/client/render/IImage.h +++ b/client/render/IImage.h @@ -37,9 +37,29 @@ enum class EImageBlitMode : uint8_t /// RGBA: full alpha transparency range, e.g. shadows COLORKEY, - /// Should be avoided if possible, use only for images that use def's with semi-transparency - /// Indexed or RGBA: Image might have full alpha transparency range, e.g. shadows - ALPHA + /// Full transparency including shadow, but treated as a single image + /// Indexed: Image can have alpha transparency, e.g. shadow + /// RGBA: full alpha transparency range, e.g. shadows + /// Upscaled form: single image, no option to display shadow separately + SIMPLE, + + /// RGBA, may consist from 2 separate parts: base and shadow, overlay not preset or treated as part of body + WITH_SHADOW, + + /// RGBA, may consist from 3 separate parts: base, shadow, and overlay + WITH_SHADOW_AND_OVERLAY, + + /// RGBA, contains only body, with shadow and overlay disabled + ONLY_BODY, + + /// RGBA, contains only body, with shadow disabled and overlay treated as part of body + ONLY_BODY_IGNORE_OVERLAY, + + /// RGBA, contains only shadow + ONLY_SHADOW, + + /// RGBA, contains only overlay + ONLY_OVERLAY, }; /// Base class for images for use in client code. @@ -75,10 +95,7 @@ public: //only indexed bitmaps with 7 special colors virtual void setOverlayColor(const ColorRGBA & color) = 0; - virtual void setShadowEnabled(bool on) = 0; - virtual void setBodyEnabled(bool on) = 0; - virtual void setOverlayEnabled(bool on) = 0; - virtual std::shared_ptr getSharedImage() const = 0; + virtual std::shared_ptr getSharedImage() const = 0; virtual ~IImage() = default; }; @@ -94,12 +111,12 @@ public: virtual bool isTransparent(const Point & coords) const = 0; virtual void draw(SDL_Surface * where, SDL_Palette * palette, const Point & dest, const Rect * src, const ColorRGBA & colorMultiplier, uint8_t alpha, EImageBlitMode mode) const = 0; - virtual std::shared_ptr createImageReference(EImageBlitMode mode) = 0; + virtual std::shared_ptr createImageReference(EImageBlitMode mode) const = 0; - virtual std::shared_ptr horizontalFlip() const = 0; - virtual std::shared_ptr verticalFlip() const = 0; - virtual std::shared_ptr scaleInteger(int factor, SDL_Palette * palette) const = 0; - virtual std::shared_ptr scaleTo(const Point & size, SDL_Palette * palette) const = 0; + virtual std::shared_ptr horizontalFlip() const = 0; + virtual std::shared_ptr verticalFlip() const = 0; + virtual std::shared_ptr scaleInteger(int factor, SDL_Palette * palette) const = 0; + virtual std::shared_ptr scaleTo(const Point & size, SDL_Palette * palette) const = 0; virtual ~ISharedImage() = default; diff --git a/client/render/IScreenHandler.h b/client/render/IScreenHandler.h index 32abf7711..f3b6d5eee 100644 --- a/client/render/IScreenHandler.h +++ b/client/render/IScreenHandler.h @@ -44,6 +44,8 @@ public: /// Dimensions of logical output. Can be different if scaling is used virtual Point getLogicalResolution() const = 0; + virtual int getInterfaceScalingPercentage() const = 0; + virtual int getScalingFactor() const = 0; /// Window has focus diff --git a/client/render/ImageLocator.cpp b/client/render/ImageLocator.cpp index a9f732923..515e767a0 100644 --- a/client/render/ImageLocator.cpp +++ b/client/render/ImageLocator.cpp @@ -15,15 +15,17 @@ #include "../../lib/json/JsonNode.h" - ImageLocator::ImageLocator(const JsonNode & config) - : image(ImagePath::fromJson(config["file"])) - , defFile(AnimationPath::fromJson(config["defFile"])) - , defFrame(config["defFrame"].Integer()) + : defFrame(config["defFrame"].Integer()) , defGroup(config["defGroup"].Integer()) , verticalFlip(config["verticalFlip"].Bool()) , horizontalFlip(config["horizontalFlip"].Bool()) { + if(!config["file"].isNull()) + image = ImagePath::fromJson(config["file"]); + + if(!config["defFile"].isNull()) + defFile = AnimationPath::fromJson(config["defFile"]); } ImageLocator::ImageLocator(const ImagePath & path) @@ -71,6 +73,7 @@ ImageLocator ImageLocator::copyFile() const { ImageLocator result; result.scalingFactor = 1; + result.preScaledFactor = preScaledFactor; result.image = image; result.defFile = defFile; result.defFrame = defFrame; @@ -123,8 +126,12 @@ std::string ImageLocator::toString() const if (playerColored.isValidPlayer()) result += "-player" + playerColored.toString(); - if (layer != EImageLayer::ALL) - result += "-layer" + std::to_string(static_cast(layer)); + if (layer == EImageBlitMode::ONLY_OVERLAY) + result += "-overlay"; + + if (layer == EImageBlitMode::ONLY_SHADOW) + result += "-shadow"; + return result; } diff --git a/client/render/ImageLocator.h b/client/render/ImageLocator.h index e53679be0..1868caaf2 100644 --- a/client/render/ImageLocator.h +++ b/client/render/ImageLocator.h @@ -9,18 +9,11 @@ */ #pragma once +#include "IImage.h" + #include "../../lib/filesystem/ResourcePath.h" #include "../../lib/constants/EntityIdentifiers.h" -enum class EImageLayer -{ - ALL, - - BODY, - SHADOW, - OVERLAY, -}; - struct ImageLocator { std::optional image; @@ -28,12 +21,13 @@ struct ImageLocator int defFrame = -1; int defGroup = -1; - PlayerColor playerColored = PlayerColor::CANNOT_DETERMINE; + PlayerColor playerColored = PlayerColor::CANNOT_DETERMINE; // FIXME: treat as identical to blue to avoid double-loading? bool verticalFlip = false; bool horizontalFlip = false; int8_t scalingFactor = 0; // 0 = auto / use default scaling - EImageLayer layer = EImageLayer::ALL; + int8_t preScaledFactor = 1; + EImageBlitMode layer = EImageBlitMode::OPAQUE; ImageLocator() = default; ImageLocator(const AnimationPath & path, int frame, int group); diff --git a/client/renderSDL/CBitmapFont.cpp b/client/renderSDL/CBitmapFont.cpp index 53a09460b..a932e6368 100644 --- a/client/renderSDL/CBitmapFont.cpp +++ b/client/renderSDL/CBitmapFont.cpp @@ -99,9 +99,7 @@ static AtlasLayout doAtlasPacking(const std::map & images) void CBitmapFont::loadFont(const ResourcePath & resource, std::unordered_map & loadedChars) { auto data = CResourceHandler::get()->load(resource)->readAll(); - std::string modName = VLC->modh->findResourceOrigin(resource); - std::string modLanguage = VLC->modh->getModLanguage(modName); - std::string modEncoding = Languages::getLanguageOptions(modLanguage).encoding; + std::string modEncoding = VLC->modh->findResourceEncoding(resource); height = data.first[5]; @@ -212,6 +210,12 @@ CBitmapFont::CBitmapFont(const std::string & filename): SDL_FreeSurface(atlasImage); atlasImage = scaledSurface; } + + logGlobal->debug("Loaded BMP font: '%s', height %d, ascent %d", + filename, + getLineHeightScaled(), + getFontAscentScaled() + ); } CBitmapFont::~CBitmapFont() diff --git a/client/renderSDL/CTrueTypeFont.cpp b/client/renderSDL/CTrueTypeFont.cpp index 2119ba14a..94faf6502 100644 --- a/client/renderSDL/CTrueTypeFont.cpp +++ b/client/renderSDL/CTrueTypeFont.cpp @@ -64,15 +64,23 @@ int CTrueTypeFont::getFontStyle(const JsonNode &config) const CTrueTypeFont::CTrueTypeFont(const JsonNode & fontConfig): data(loadData(fontConfig)), font(loadFont(fontConfig), TTF_CloseFont), - dropShadow(!fontConfig["noShadow"].Bool()), + blended(true), outline(fontConfig["outline"].Bool()), - blended(true) + dropShadow(!fontConfig["noShadow"].Bool()) { assert(font); TTF_SetFontStyle(font.get(), getFontStyle(fontConfig)); TTF_SetFontHinting(font.get(),TTF_HINTING_MONO); + logGlobal->debug("Loaded TTF font: '%s', point size %d, height %d, ascent %d, descent %d, line skip %d", + fontConfig["file"].String(), + getPointSize(fontConfig["size"]), + TTF_FontHeight(font.get()), + TTF_FontAscent(font.get()), + TTF_FontDescent(font.get()), + TTF_FontLineSkip(font.get()) + ); } CTrueTypeFont::~CTrueTypeFont() = default; @@ -87,14 +95,14 @@ size_t CTrueTypeFont::getLineHeightScaled() const return TTF_FontHeight(font.get()); } -size_t CTrueTypeFont::getGlyphWidthScaled(const char *data) const +size_t CTrueTypeFont::getGlyphWidthScaled(const char *text) const { - return getStringWidthScaled(std::string(data, TextOperations::getUnicodeCharacterSize(*data))); + return getStringWidthScaled(std::string(text, TextOperations::getUnicodeCharacterSize(*text))); } -bool CTrueTypeFont::canRepresentCharacter(const char * data) const +bool CTrueTypeFont::canRepresentCharacter(const char * text) const { - uint32_t codepoint = TextOperations::getUnicodeCodepoint(data, TextOperations::getUnicodeCharacterSize(*data)); + uint32_t codepoint = TextOperations::getUnicodeCodepoint(text, TextOperations::getUnicodeCharacterSize(*text)); #if SDL_TTF_VERSION_ATLEAST(2, 0, 18) return TTF_GlyphIsProvided32(font.get(), codepoint); #elif SDL_TTF_VERSION_ATLEAST(2, 0, 12) @@ -106,10 +114,16 @@ bool CTrueTypeFont::canRepresentCharacter(const char * data) const #endif } -size_t CTrueTypeFont::getStringWidthScaled(const std::string & data) const +size_t CTrueTypeFont::getStringWidthScaled(const std::string & text) const { int width; - TTF_SizeUTF8(font.get(), data.c_str(), &width, nullptr); + TTF_SizeUTF8(font.get(), text.c_str(), &width, nullptr); + + if (outline) + width += getScalingFactor(); + if (dropShadow || outline) + width += getScalingFactor(); + return width; } diff --git a/client/renderSDL/CursorHardware.cpp b/client/renderSDL/CursorHardware.cpp index cbcfaeac2..7c9daea4d 100644 --- a/client/renderSDL/CursorHardware.cpp +++ b/client/renderSDL/CursorHardware.cpp @@ -11,11 +11,14 @@ #include "StdInc.h" #include "CursorHardware.h" +#include "SDL_Extensions.h" + #include "../gui/CGuiHandler.h" #include "../render/IScreenHandler.h" #include "../render/Colors.h" #include "../render/IImage.h" -#include "SDL_Extensions.h" + +#include "../../lib/CConfigHandler.h" #include #include @@ -45,19 +48,28 @@ void CursorHardware::setVisible(bool on) void CursorHardware::setImage(std::shared_ptr image, const Point & pivotOffset) { - auto cursorSurface = CSDL_Ext::newSurface(image->dimensions() * GH.screenHandler().getScalingFactor()); + int videoScalingSettings = GH.screenHandler().getInterfaceScalingPercentage(); + float cursorScalingSettings = settings["video"]["cursorScalingFactor"].Float(); + int cursorScalingPercent = videoScalingSettings * cursorScalingSettings; + Point cursorDimensions = image->dimensions() * GH.screenHandler().getScalingFactor(); + Point cursorDimensionsScaled = image->dimensions() * cursorScalingPercent / 100; + Point pivotOffsetScaled = pivotOffset * cursorScalingPercent / 100 / GH.screenHandler().getScalingFactor(); + + auto cursorSurface = CSDL_Ext::newSurface(cursorDimensions); CSDL_Ext::fillSurface(cursorSurface, CSDL_Ext::toSDL(Colors::TRANSPARENCY)); image->draw(cursorSurface, Point(0,0)); + auto cursorSurfaceScaled = CSDL_Ext::scaleSurface(cursorSurface, cursorDimensionsScaled.x, cursorDimensionsScaled.y ); auto oldCursor = cursor; - cursor = SDL_CreateColorCursor(cursorSurface, pivotOffset.x, pivotOffset.y); + cursor = SDL_CreateColorCursor(cursorSurfaceScaled, pivotOffsetScaled.x, pivotOffsetScaled.y); if (!cursor) logGlobal->error("Failed to set cursor! SDL says %s", SDL_GetError()); SDL_FreeSurface(cursorSurface); + SDL_FreeSurface(cursorSurfaceScaled); GH.dispatchMainThread([this, oldCursor](){ SDL_SetCursor(cursor); diff --git a/client/renderSDL/FontChain.cpp b/client/renderSDL/FontChain.cpp index 44f71097f..331d1c71a 100644 --- a/client/renderSDL/FontChain.cpp +++ b/client/renderSDL/FontChain.cpp @@ -13,8 +13,13 @@ #include "CTrueTypeFont.h" #include "CBitmapFont.h" +#include "../CGameInfo.h" + #include "../../lib/CConfigHandler.h" +#include "../../lib/modding/CModHandler.h" #include "../../lib/texts/TextOperations.h" +#include "../../lib/texts/CGeneralTextHandler.h" +#include "../../lib/texts/Languages.h" void FontChain::renderText(SDL_Surface * surface, const std::string & data, const ColorRGBA & color, const Point & pos) const { @@ -39,7 +44,7 @@ size_t FontChain::getFontAscentScaled() const return maxHeight; } -bool FontChain::bitmapFontsPrioritized() const +bool FontChain::bitmapFontsPrioritized(const std::string & bitmapFontName) const { const std::string & fontType = settings["video"]["fontsType"].String(); if (fontType == "original") @@ -55,6 +60,17 @@ bool FontChain::bitmapFontsPrioritized() const if (!vstd::isAlmostEqual(1.0, settings["video"]["fontScalingFactor"].Float())) return false; // If player requested non-100% scaling - use scalable fonts + std::string gameLanguage = CGI->generaltexth->getPreferredLanguage(); + std::string gameEncoding = Languages::getLanguageOptions(gameLanguage).encoding; + std::string fontEncoding = CGI->modh->findResourceEncoding(ResourcePath("data/" + bitmapFontName, EResType::BMP_FONT)); + + // player uses language with different encoding than his bitmap fonts + // for example, Polish language with English fonts or Chinese language which can't use H3 fonts at all + // this may result in unintended mixing of ttf and bitmap fonts, which may have a bit different look + // so in this case prefer ttf fonts that are likely to cover target language better than H3 fonts + if (fontEncoding != gameEncoding) + return false; + return true; // else - use original bitmap fonts } @@ -65,7 +81,7 @@ void FontChain::addTrueTypeFont(const JsonNode & trueTypeConfig) void FontChain::addBitmapFont(const std::string & bitmapFilename) { - if (bitmapFontsPrioritized()) + if (bitmapFontsPrioritized(bitmapFilename)) chain.insert(chain.begin(), std::make_unique(bitmapFilename)); else chain.push_back(std::make_unique(bitmapFilename)); diff --git a/client/renderSDL/FontChain.h b/client/renderSDL/FontChain.h index b07902cd4..b66d5cce2 100644 --- a/client/renderSDL/FontChain.h +++ b/client/renderSDL/FontChain.h @@ -29,7 +29,7 @@ class FontChain final : public IFont void renderText(SDL_Surface * surface, const std::string & data, const ColorRGBA & color, const Point & pos) const override; size_t getFontAscentScaled() const override; - bool bitmapFontsPrioritized() const; + bool bitmapFontsPrioritized(const std::string & bitmapFontName) const; public: FontChain() = default; diff --git a/client/renderSDL/ImageScaled.cpp b/client/renderSDL/ImageScaled.cpp index 8ad15e00a..219efb999 100644 --- a/client/renderSDL/ImageScaled.cpp +++ b/client/renderSDL/ImageScaled.cpp @@ -21,19 +21,17 @@ #include -ImageScaled::ImageScaled(const ImageLocator & inputLocator, const std::shared_ptr & source, EImageBlitMode mode) +ImageScaled::ImageScaled(const ImageLocator & inputLocator, const std::shared_ptr & source, EImageBlitMode mode) : source(source) , locator(inputLocator) , colorMultiplier(Colors::WHITE_TRUE) , alphaValue(SDL_ALPHA_OPAQUE) , blitMode(mode) { - setBodyEnabled(true); - if (mode == EImageBlitMode::ALPHA) - setShadowEnabled(true); + prepareImages(); } -std::shared_ptr ImageScaled::getSharedImage() const +std::shared_ptr ImageScaled::getSharedImage() const { return body; } @@ -92,8 +90,7 @@ void ImageScaled::setOverlayColor(const ColorRGBA & color) void ImageScaled::playerColored(PlayerColor player) { playerColor = player; - if (body) - setBodyEnabled(true); // regenerate + prepareImages(); } void ImageScaled::shiftPalette(uint32_t firstColorID, uint32_t colorsToMove, uint32_t distanceToMove) @@ -106,41 +103,63 @@ void ImageScaled::adjustPalette(const ColorFilter &shifter, uint32_t colorsToSki // TODO: implement } -void ImageScaled::setShadowEnabled(bool on) +void ImageScaled::prepareImages() { - assert(blitMode == EImageBlitMode::ALPHA); - if (on) + switch(blitMode) { - locator.layer = EImageLayer::SHADOW; - locator.playerColored = PlayerColor::CANNOT_DETERMINE; - shadow = GH.renderHandler().loadImage(locator, blitMode)->getSharedImage(); - } - else - shadow = nullptr; -} + case EImageBlitMode::OPAQUE: + case EImageBlitMode::COLORKEY: + case EImageBlitMode::SIMPLE: + locator.layer = blitMode; + locator.playerColored = playerColor; + body = GH.renderHandler().loadImage(locator, blitMode)->getSharedImage(); + break; -void ImageScaled::setBodyEnabled(bool on) -{ - if (on) + case EImageBlitMode::WITH_SHADOW_AND_OVERLAY: + case EImageBlitMode::ONLY_BODY: + locator.layer = EImageBlitMode::ONLY_BODY; + locator.playerColored = playerColor; + body = GH.renderHandler().loadImage(locator, blitMode)->getSharedImage(); + break; + + case EImageBlitMode::WITH_SHADOW: + case EImageBlitMode::ONLY_BODY_IGNORE_OVERLAY: + locator.layer = EImageBlitMode::ONLY_BODY_IGNORE_OVERLAY; + locator.playerColored = playerColor; + body = GH.renderHandler().loadImage(locator, blitMode)->getSharedImage(); + break; + + case EImageBlitMode::ONLY_SHADOW: + case EImageBlitMode::ONLY_OVERLAY: + body = nullptr; + break; + } + + switch(blitMode) { - locator.layer = blitMode == EImageBlitMode::ALPHA ? EImageLayer::BODY : EImageLayer::ALL; - locator.playerColored = playerColor; - body = GH.renderHandler().loadImage(locator, blitMode)->getSharedImage(); + case EImageBlitMode::SIMPLE: + case EImageBlitMode::WITH_SHADOW: + case EImageBlitMode::ONLY_SHADOW: + case EImageBlitMode::WITH_SHADOW_AND_OVERLAY: + locator.layer = EImageBlitMode::ONLY_SHADOW; + locator.playerColored = PlayerColor::CANNOT_DETERMINE; + shadow = GH.renderHandler().loadImage(locator, blitMode)->getSharedImage(); + break; + default: + shadow = nullptr; + break; } - else - body = nullptr; -} - -void ImageScaled::setOverlayEnabled(bool on) -{ - assert(blitMode == EImageBlitMode::ALPHA); - if (on) + switch(blitMode) { - locator.layer = EImageLayer::OVERLAY; - locator.playerColored = PlayerColor::CANNOT_DETERMINE; - overlay = GH.renderHandler().loadImage(locator, blitMode)->getSharedImage(); + case EImageBlitMode::ONLY_OVERLAY: + case EImageBlitMode::WITH_SHADOW_AND_OVERLAY: + locator.layer = EImageBlitMode::ONLY_OVERLAY; + locator.playerColored = PlayerColor::CANNOT_DETERMINE; + overlay = GH.renderHandler().loadImage(locator, blitMode)->getSharedImage(); + break; + default: + overlay = nullptr; + break; } - else - overlay = nullptr; } diff --git a/client/renderSDL/ImageScaled.h b/client/renderSDL/ImageScaled.h index b0dedc227..40f4b2c7e 100644 --- a/client/renderSDL/ImageScaled.h +++ b/client/renderSDL/ImageScaled.h @@ -25,16 +25,16 @@ class ImageScaled final : public IImage private: /// Original unscaled image - std::shared_ptr source; + std::shared_ptr source; /// Upscaled shadow of our image, may be null - std::shared_ptr shadow; + std::shared_ptr shadow; /// Upscaled main part of our image, may be null - std::shared_ptr body; + std::shared_ptr body; /// Upscaled overlay (player color, selection highlight) of our image, may be null - std::shared_ptr overlay; + std::shared_ptr overlay; ImageLocator locator; @@ -44,8 +44,9 @@ private: uint8_t alphaValue; EImageBlitMode blitMode; + void prepareImages(); public: - ImageScaled(const ImageLocator & locator, const std::shared_ptr & source, EImageBlitMode mode); + ImageScaled(const ImageLocator & locator, const std::shared_ptr & source, EImageBlitMode mode); void scaleInteger(int factor) override; void scaleTo(const Point & size) override; @@ -60,8 +61,5 @@ public: void shiftPalette(uint32_t firstColorID, uint32_t colorsToMove, uint32_t distanceToMove) override; void adjustPalette(const ColorFilter & shifter, uint32_t colorsToSkipMask) override; - void setShadowEnabled(bool on) override; - void setBodyEnabled(bool on) override; - void setOverlayEnabled(bool on) override; - std::shared_ptr getSharedImage() const override; + std::shared_ptr getSharedImage() const override; }; diff --git a/client/renderSDL/RenderHandler.cpp b/client/renderSDL/RenderHandler.cpp index be0d5db3b..0447c9786 100644 --- a/client/renderSDL/RenderHandler.cpp +++ b/client/renderSDL/RenderHandler.cpp @@ -55,6 +55,59 @@ std::shared_ptr RenderHandler::getAnimationFile(const AnimationPath & return result; } +std::optional RenderHandler::getPathForScaleFactor(const ResourcePath & path, const std::string & factor) +{ + if(path.getType() == EResType::IMAGE) + { + auto p = ImagePath::builtin(path.getName()); + if(CResourceHandler::get()->existsResource(p.addPrefix("SPRITES" + factor + "X/"))) + return std::optional(p.addPrefix("SPRITES" + factor + "X/")); + if(CResourceHandler::get()->existsResource(p.addPrefix("DATA" + factor + "X/"))) + return std::optional(p.addPrefix("DATA" + factor + "X/")); + } + else + { + auto p = AnimationPath::builtin(path.getName()); + auto pJson = p.toType(); + if(CResourceHandler::get()->existsResource(p.addPrefix("SPRITES" + factor + "X/"))) + return std::optional(p.addPrefix("SPRITES" + factor + "X/")); + if(CResourceHandler::get()->existsResource(pJson)) + return std::optional(p); + if(CResourceHandler::get()->existsResource(pJson.addPrefix("SPRITES" + factor + "X/"))) + return std::optional(p.addPrefix("SPRITES" + factor + "X/")); + } + + return std::nullopt; +} + +std::pair RenderHandler::getScalePath(const ResourcePath & p) +{ + auto path = p; + int scaleFactor = 1; + if(getScalingFactor() > 1) + { + std::vector factorsToCheck = {getScalingFactor(), 4, 3, 2}; + for(auto factorToCheck : factorsToCheck) + { + std::string name = boost::algorithm::to_upper_copy(p.getName()); + boost::replace_all(name, "SPRITES/", std::string("SPRITES") + std::to_string(factorToCheck) + std::string("X/")); + boost::replace_all(name, "DATA/", std::string("DATA") + std::to_string(factorToCheck) + std::string("X/")); + ResourcePath scaledPath = ImagePath::builtin(name); + if(p.getType() != EResType::IMAGE) + scaledPath = AnimationPath::builtin(name); + auto tmpPath = getPathForScaleFactor(scaledPath, std::to_string(factorToCheck)); + if(tmpPath) + { + path = *tmpPath; + scaleFactor = factorToCheck; + break; + } + } + } + + return std::pair(path, scaleFactor); +}; + void RenderHandler::initFromJson(AnimationLayoutMap & source, const JsonNode & config) { std::string basepath; @@ -89,14 +142,22 @@ void RenderHandler::initFromJson(AnimationLayoutMap & source, const JsonNode & c JsonNode toAdd = node; JsonUtils::inherit(toAdd, base); - toAdd["file"].String() = basepath + node["file"].String(); + + if (toAdd.Struct().count("file")) + toAdd["file"].String() = basepath + node["file"].String(); + + if (toAdd.Struct().count("defFile")) + toAdd["defFile"].String() = basepath + node["defFile"].String(); + source[group][frame] = ImageLocator(toAdd); } } RenderHandler::AnimationLayoutMap & RenderHandler::getAnimationLayout(const AnimationPath & path) { - AnimationPath actualPath = boost::starts_with(path.getName(), "SPRITES") ? path : path.addPrefix("SPRITES/"); + auto tmp = getScalePath(path); + auto animPath = AnimationPath::builtin(tmp.first.getName()); + AnimationPath actualPath = boost::starts_with(animPath.getName(), "SPRITES") ? animPath : animPath.addPrefix("SPRITES/"); auto it = animationLayouts.find(actualPath); @@ -123,11 +184,15 @@ RenderHandler::AnimationLayoutMap & RenderHandler::getAnimationLayout(const Anim std::unique_ptr textData(new ui8[stream->getSize()]); stream->read(textData.get(), stream->getSize()); - const JsonNode config(reinterpret_cast(textData.get()), stream->getSize(), path.getOriginalName()); + const JsonNode config(reinterpret_cast(textData.get()), stream->getSize(), animPath.getOriginalName()); initFromJson(result, config); } + for(auto & g : result) + for(auto & i : g.second) + i.preScaledFactor = tmp.second; + animationLayouts[actualPath] = result; return animationLayouts[actualPath]; } @@ -153,7 +218,7 @@ ImageLocator RenderHandler::getLocatorForAnimationFrame(const AnimationPath & pa return ImageLocator(path, frame, group); } -std::shared_ptr RenderHandler::loadImageImpl(const ImageLocator & locator) +std::shared_ptr RenderHandler::loadImageImpl(const ImageLocator & locator) { auto it = imageFiles.find(locator); if (it != imageFiles.end()) @@ -172,24 +237,34 @@ std::shared_ptr RenderHandler::loadImageImpl(const ImageLocator & return scaledImage; } -std::shared_ptr RenderHandler::loadImageFromFileUncached(const ImageLocator & locator) +std::shared_ptr RenderHandler::loadImageFromFileUncached(const ImageLocator & locator) { if (locator.image) { // TODO: create EmptySharedImage class that will be instantiated if image does not exists or fails to load - return std::make_shared(*locator.image); + return std::make_shared(*locator.image, locator.preScaledFactor); } if (locator.defFile) { auto defFile = getAnimationFile(*locator.defFile); - return std::make_shared(defFile.get(), locator.defFrame, locator.defGroup); + int preScaledFactor = locator.preScaledFactor; + if(!defFile) // no prescale for this frame + { + auto tmpPath = (*locator.defFile).getName(); + boost::algorithm::replace_all(tmpPath, "SPRITES2X/", "SPRITES/"); + boost::algorithm::replace_all(tmpPath, "SPRITES3X/", "SPRITES/"); + boost::algorithm::replace_all(tmpPath, "SPRITES4X/", "SPRITES/"); + preScaledFactor = 1; + defFile = getAnimationFile(AnimationPath::builtin(tmpPath)); + } + return std::make_shared(defFile.get(), locator.defFrame, locator.defGroup, preScaledFactor); } throw std::runtime_error("Invalid image locator received!"); } -void RenderHandler::storeCachedImage(const ImageLocator & locator, std::shared_ptr image) +void RenderHandler::storeCachedImage(const ImageLocator & locator, std::shared_ptr image) { imageFiles[locator] = image; @@ -202,7 +277,7 @@ void RenderHandler::storeCachedImage(const ImageLocator & locator, std::shared_p #endif } -std::shared_ptr RenderHandler::loadImageFromFile(const ImageLocator & locator) +std::shared_ptr RenderHandler::loadImageFromFile(const ImageLocator & locator) { if (imageFiles.count(locator)) return imageFiles.at(locator); @@ -212,7 +287,7 @@ std::shared_ptr RenderHandler::loadImageFromFile(const ImageLocato return result; } -std::shared_ptr RenderHandler::transformImage(const ImageLocator & locator, std::shared_ptr image) +std::shared_ptr RenderHandler::transformImage(const ImageLocator & locator, std::shared_ptr image) { if (imageFiles.count(locator)) return imageFiles.at(locator); @@ -229,27 +304,19 @@ std::shared_ptr RenderHandler::transformImage(const ImageLocator & return result; } -std::shared_ptr RenderHandler::scaleImage(const ImageLocator & locator, std::shared_ptr image) +std::shared_ptr RenderHandler::scaleImage(const ImageLocator & locator, std::shared_ptr image) { if (imageFiles.count(locator)) return imageFiles.at(locator); - auto handle = image->createImageReference(locator.layer == EImageLayer::ALL ? EImageBlitMode::OPAQUE : EImageBlitMode::ALPHA); + auto handle = image->createImageReference(locator.layer); assert(locator.scalingFactor != 1); // should be filtered-out before - - handle->setBodyEnabled(locator.layer == EImageLayer::ALL || locator.layer == EImageLayer::BODY); - if (locator.layer != EImageLayer::ALL) - { - handle->setOverlayEnabled(locator.layer == EImageLayer::OVERLAY); - handle->setShadowEnabled( locator.layer == EImageLayer::SHADOW); - } - if (locator.layer == EImageLayer::ALL && locator.playerColored != PlayerColor::CANNOT_DETERMINE) + if (locator.playerColored != PlayerColor::CANNOT_DETERMINE) handle->playerColored(locator.playerColored); handle->scaleInteger(locator.scalingFactor); - // TODO: try to optimize image size (possibly even before scaling?) - trim image borders if they are completely transparent auto result = handle->getSharedImage(); storeCachedImage(locator, result); return result; @@ -257,10 +324,39 @@ std::shared_ptr RenderHandler::scaleImage(const ImageLocator & loc std::shared_ptr RenderHandler::loadImage(const ImageLocator & locator, EImageBlitMode mode) { - if (locator.scalingFactor == 0 && getScalingFactor() != 1 ) + ImageLocator adjustedLocator = locator; + + if(adjustedLocator.image) { - auto unscaledLocator = locator; - auto scaledLocator = locator; + std::string imgPath = (*adjustedLocator.image).getName(); + if(adjustedLocator.layer == EImageBlitMode::ONLY_OVERLAY) + imgPath += "-OVERLAY"; + if(adjustedLocator.layer == EImageBlitMode::ONLY_SHADOW) + imgPath += "-SHADOW"; + + if(CResourceHandler::get()->existsResource(ImagePath::builtin(imgPath)) || + CResourceHandler::get()->existsResource(ImagePath::builtin(imgPath).addPrefix("DATA/")) || + CResourceHandler::get()->existsResource(ImagePath::builtin(imgPath).addPrefix("SPRITES/"))) + adjustedLocator.image = ImagePath::builtin(imgPath); + } + + if(adjustedLocator.defFile && adjustedLocator.scalingFactor == 0) + { + auto tmp = getScalePath(*adjustedLocator.defFile); + adjustedLocator.defFile = AnimationPath::builtin(tmp.first.getName()); + adjustedLocator.preScaledFactor = tmp.second; + } + if(adjustedLocator.image && adjustedLocator.scalingFactor == 0) + { + auto tmp = getScalePath(*adjustedLocator.image); + adjustedLocator.image = ImagePath::builtin(tmp.first.getName()); + adjustedLocator.preScaledFactor = tmp.second; + } + + if (adjustedLocator.scalingFactor == 0 && getScalingFactor() != 1 ) + { + auto unscaledLocator = adjustedLocator; + auto scaledLocator = adjustedLocator; unscaledLocator.scalingFactor = 1; scaledLocator.scalingFactor = getScalingFactor(); @@ -269,22 +365,22 @@ std::shared_ptr RenderHandler::loadImage(const ImageLocator & locator, E return std::make_shared(scaledLocator, unscaledImage, mode); } - if (locator.scalingFactor == 0) + if (adjustedLocator.scalingFactor == 0) { - auto scaledLocator = locator; + auto scaledLocator = adjustedLocator; scaledLocator.scalingFactor = getScalingFactor(); return loadImageImpl(scaledLocator)->createImageReference(mode); } else - { - return loadImageImpl(locator)->createImageReference(mode); - } + return loadImageImpl(adjustedLocator)->createImageReference(mode); } std::shared_ptr RenderHandler::loadImage(const AnimationPath & path, int frame, int group, EImageBlitMode mode) { - ImageLocator locator = getLocatorForAnimationFrame(path, frame, group); + auto tmp = getScalePath(path); + ImageLocator locator = getLocatorForAnimationFrame(AnimationPath::builtin(tmp.first.getName()), frame, group); + locator.preScaledFactor = tmp.second; return loadImage(locator, mode); } @@ -296,7 +392,7 @@ std::shared_ptr RenderHandler::loadImage(const ImagePath & path, EImageB std::shared_ptr RenderHandler::createImage(SDL_Surface * source) { - return std::make_shared(source)->createImageReference(EImageBlitMode::ALPHA); + return std::make_shared(source)->createImageReference(EImageBlitMode::SIMPLE); } std::shared_ptr RenderHandler::loadAnimation(const AnimationPath & path, EImageBlitMode mode) @@ -342,6 +438,8 @@ std::shared_ptr RenderHandler::loadFont(EFonts font) return fonts.at(font); const int8_t index = static_cast(font); + logGlobal->debug("Loading font %d", static_cast(index)); + auto configList = CResourceHandler::get()->getResourcesWithName(JsonPath::builtin("config/fonts.json")); std::shared_ptr loadedFont = std::make_shared(); std::string bitmapPath; diff --git a/client/renderSDL/RenderHandler.h b/client/renderSDL/RenderHandler.h index 20b13306a..43df617a1 100644 --- a/client/renderSDL/RenderHandler.h +++ b/client/renderSDL/RenderHandler.h @@ -25,24 +25,26 @@ class RenderHandler : public IRenderHandler std::map> animationFiles; std::map animationLayouts; - std::map> imageFiles; + std::map> imageFiles; std::map> fonts; std::shared_ptr getAnimationFile(const AnimationPath & path); + std::optional getPathForScaleFactor(const ResourcePath & path, const std::string & factor); + std::pair getScalePath(const ResourcePath & p); AnimationLayoutMap & getAnimationLayout(const AnimationPath & path); void initFromJson(AnimationLayoutMap & layout, const JsonNode & config); void addImageListEntry(size_t index, size_t group, const std::string & listName, const std::string & imageName); void addImageListEntries(const EntityService * service); - void storeCachedImage(const ImageLocator & locator, std::shared_ptr image); + void storeCachedImage(const ImageLocator & locator, std::shared_ptr image); - std::shared_ptr loadImageImpl(const ImageLocator & config); + std::shared_ptr loadImageImpl(const ImageLocator & config); - std::shared_ptr loadImageFromFileUncached(const ImageLocator & locator); - std::shared_ptr loadImageFromFile(const ImageLocator & locator); + std::shared_ptr loadImageFromFileUncached(const ImageLocator & locator); + std::shared_ptr loadImageFromFile(const ImageLocator & locator); - std::shared_ptr transformImage(const ImageLocator & locator, std::shared_ptr image); - std::shared_ptr scaleImage(const ImageLocator & locator, std::shared_ptr image); + std::shared_ptr transformImage(const ImageLocator & locator, std::shared_ptr image); + std::shared_ptr scaleImage(const ImageLocator & locator, std::shared_ptr image); ImageLocator getLocatorForAnimationFrame(const AnimationPath & path, int frame, int group); diff --git a/client/renderSDL/SDLImage.cpp b/client/renderSDL/SDLImage.cpp index 83067b8a2..84d37e7ee 100644 --- a/client/renderSDL/SDLImage.cpp +++ b/client/renderSDL/SDLImage.cpp @@ -89,11 +89,12 @@ int IImage::height() const return dimensions().y; } -SDLImageShared::SDLImageShared(const CDefFile * data, size_t frame, size_t group) +SDLImageShared::SDLImageShared(const CDefFile * data, size_t frame, size_t group, int preScaleFactor) : surf(nullptr), margins(0, 0), fullSize(0, 0), - originalPalette(nullptr) + originalPalette(nullptr), + preScaleFactor(preScaleFactor) { SDLImageLoader loader(this); data->loadFrame(frame, group, loader); @@ -101,11 +102,12 @@ SDLImageShared::SDLImageShared(const CDefFile * data, size_t frame, size_t group savePalette(); } -SDLImageShared::SDLImageShared(SDL_Surface * from) +SDLImageShared::SDLImageShared(SDL_Surface * from, int preScaleFactor) : surf(nullptr), margins(0, 0), fullSize(0, 0), - originalPalette(nullptr) + originalPalette(nullptr), + preScaleFactor(preScaleFactor) { surf = from; if (surf == nullptr) @@ -118,11 +120,12 @@ SDLImageShared::SDLImageShared(SDL_Surface * from) fullSize.y = surf->h; } -SDLImageShared::SDLImageShared(const ImagePath & filename) +SDLImageShared::SDLImageShared(const ImagePath & filename, int preScaleFactor) : surf(nullptr), margins(0, 0), fullSize(0, 0), - originalPalette(nullptr) + originalPalette(nullptr), + preScaleFactor(preScaleFactor) { surf = BitmapHandler::loadBitmap(filename); @@ -136,6 +139,8 @@ SDLImageShared::SDLImageShared(const ImagePath & filename) savePalette(); fullSize.x = surf->w; fullSize.y = surf->h; + + optimizeSurface(); } } @@ -177,7 +182,7 @@ void SDLImageShared::draw(SDL_Surface * where, SDL_Palette * palette, const Poin if (palette && surf->format->palette) SDL_SetSurfacePalette(surf, palette); - if(surf->format->palette && mode == EImageBlitMode::ALPHA) + if(surf->format->palette && mode != EImageBlitMode::OPAQUE && mode != EImageBlitMode::COLORKEY) { CSDL_Ext::blit8bppAlphaTo24bpp(surf, sourceRect, where, destShift, alpha); } @@ -258,6 +263,13 @@ void SDLImageShared::optimizeSurface() SDL_SetSurfaceBlendMode(surf, SDL_BLENDMODE_NONE); SDL_BlitSurface(surf, &rectSDL, newSurface, nullptr); + if (SDL_HasColorKey(surf)) + { + uint32_t colorKey; + SDL_GetColorKey(surf, &colorKey); + SDL_SetColorKey(newSurface, SDL_TRUE, colorKey); + } + SDL_FreeSurface(surf); surf = newSurface; @@ -266,17 +278,26 @@ void SDLImageShared::optimizeSurface() } } -std::shared_ptr SDLImageShared::scaleInteger(int factor, SDL_Palette * palette) const +std::shared_ptr SDLImageShared::scaleInteger(int factor, SDL_Palette * palette) const { if (factor <= 0) throw std::runtime_error("Unable to scale by integer value of " + std::to_string(factor)); - if (palette && surf && surf->format->palette) + if (!surf) + return shared_from_this(); + + if (palette && surf->format->palette) SDL_SetSurfacePalette(surf, palette); - SDL_Surface * scaled = CSDL_Ext::scaleSurfaceIntegerFactor(surf, factor, EScalingAlgorithm::XBRZ); + SDL_Surface * scaled = nullptr; + if(preScaleFactor == factor) + return shared_from_this(); + else if(preScaleFactor == 1) + scaled = CSDL_Ext::scaleSurfaceIntegerFactor(surf, factor, EScalingAlgorithm::XBRZ); + else + scaled = CSDL_Ext::scaleSurface(surf, (surf->w / preScaleFactor) * factor, (surf->h / preScaleFactor) * factor); - auto ret = std::make_shared(scaled); + auto ret = std::make_shared(scaled, preScaleFactor); ret->fullSize.x = fullSize.x * factor; ret->fullSize.y = fullSize.y * factor; @@ -288,16 +309,16 @@ std::shared_ptr SDLImageShared::scaleInteger(int factor, SDL_Palet // erase our own reference SDL_FreeSurface(scaled); - if (surf && surf->format->palette) + if (surf->format->palette) SDL_SetSurfacePalette(surf, originalPalette); return ret; } -std::shared_ptr SDLImageShared::scaleTo(const Point & size, SDL_Palette * palette) const +std::shared_ptr SDLImageShared::scaleTo(const Point & size, SDL_Palette * palette) const { - float scaleX = float(size.x) / dimensions().x; - float scaleY = float(size.y) / dimensions().y; + float scaleX = static_cast(size.x) / fullSize.x; + float scaleY = static_cast(size.y) / fullSize.y; if (palette && surf->format->palette) SDL_SetSurfacePalette(surf, palette); @@ -311,7 +332,7 @@ std::shared_ptr SDLImageShared::scaleTo(const Point & size, SDL_Pa else CSDL_Ext::setDefaultColorKey(scaled);//just in case - auto ret = std::make_shared(scaled); + auto ret = std::make_shared(scaled, preScaleFactor); ret->fullSize.x = (int) round((float)fullSize.x * scaleX); ret->fullSize.y = (int) round((float)fullSize.y * scaleY); @@ -348,17 +369,17 @@ void SDLImageIndexed::playerColored(PlayerColor player) bool SDLImageShared::isTransparent(const Point & coords) const { if (surf) - return CSDL_Ext::isTransparent(surf, coords.x, coords.y); + return CSDL_Ext::isTransparent(surf, coords.x - margins.x, coords.y - margins.y); else return true; } Point SDLImageShared::dimensions() const { - return fullSize; + return fullSize / preScaleFactor; } -std::shared_ptr SDLImageShared::createImageReference(EImageBlitMode mode) +std::shared_ptr SDLImageShared::createImageReference(EImageBlitMode mode) const { if (surf && surf->format->palette) return std::make_shared(shared_from_this(), originalPalette, mode); @@ -366,10 +387,10 @@ std::shared_ptr SDLImageShared::createImageReference(EImageBlitMode mode return std::make_shared(shared_from_this(), mode); } -std::shared_ptr SDLImageShared::horizontalFlip() const +std::shared_ptr SDLImageShared::horizontalFlip() const { SDL_Surface * flipped = CSDL_Ext::horizontalFlip(surf); - auto ret = std::make_shared(flipped); + auto ret = std::make_shared(flipped, preScaleFactor); ret->fullSize = fullSize; ret->margins.x = margins.x; ret->margins.y = fullSize.y - surf->h - margins.y; @@ -378,10 +399,10 @@ std::shared_ptr SDLImageShared::horizontalFlip() const return ret; } -std::shared_ptr SDLImageShared::verticalFlip() const +std::shared_ptr SDLImageShared::verticalFlip() const { SDL_Surface * flipped = CSDL_Ext::verticalFlip(surf); - auto ret = std::make_shared(flipped); + auto ret = std::make_shared(flipped, preScaleFactor); ret->fullSize = fullSize; ret->margins.x = fullSize.x - surf->w - margins.x; ret->margins.y = margins.y; @@ -416,7 +437,7 @@ void SDLImageIndexed::shiftPalette(uint32_t firstColorID, uint32_t colorsToMove, void SDLImageIndexed::adjustPalette(const ColorFilter & shifter, uint32_t colorsToSkipMask) { // If shadow is enabled, following colors must be skipped unconditionally - if (shadowEnabled) + if (blitMode == EImageBlitMode::WITH_SHADOW || blitMode == EImageBlitMode::WITH_SHADOW_AND_OVERLAY) colorsToSkipMask |= (1 << 0) + (1 << 1) + (1 << 4); // Note: here we skip first colors in the palette that are predefined in H3 images @@ -432,19 +453,14 @@ void SDLImageIndexed::adjustPalette(const ColorFilter & shifter, uint32_t colors } } -SDLImageIndexed::SDLImageIndexed(const std::shared_ptr & image, SDL_Palette * originalPalette, EImageBlitMode mode) +SDLImageIndexed::SDLImageIndexed(const std::shared_ptr & image, SDL_Palette * originalPalette, EImageBlitMode mode) :SDLImageBase::SDLImageBase(image, mode) ,originalPalette(originalPalette) { - currentPalette = SDL_AllocPalette(originalPalette->ncolors); SDL_SetPaletteColors(currentPalette, originalPalette->colors, 0, originalPalette->ncolors); - if (mode == EImageBlitMode::ALPHA) - { - setOverlayColor(Colors::TRANSPARENCY); - setShadowTransparency(1.0); - } + preparePalette(); } SDLImageIndexed::~SDLImageIndexed() @@ -491,36 +507,42 @@ void SDLImageIndexed::setOverlayColor(const ColorRGBA & color) } } -void SDLImageIndexed::setShadowEnabled(bool on) +void SDLImageIndexed::preparePalette() { - if (on) - setShadowTransparency(1.0); + switch(blitMode) + { + case EImageBlitMode::ONLY_SHADOW: + case EImageBlitMode::ONLY_OVERLAY: + adjustPalette(ColorFilter::genAlphaShifter(0), 0); + break; + } - if (!on && blitMode == EImageBlitMode::ALPHA) - setShadowTransparency(0.0); + switch(blitMode) + { + case EImageBlitMode::SIMPLE: + case EImageBlitMode::WITH_SHADOW: + case EImageBlitMode::ONLY_SHADOW: + case EImageBlitMode::WITH_SHADOW_AND_OVERLAY: + setShadowTransparency(1.0); + break; + case EImageBlitMode::ONLY_BODY: + case EImageBlitMode::ONLY_BODY_IGNORE_OVERLAY: + case EImageBlitMode::ONLY_OVERLAY: + setShadowTransparency(0.0); + break; + } - shadowEnabled = on; -} - -void SDLImageIndexed::setBodyEnabled(bool on) -{ - if (on) - adjustPalette(ColorFilter::genEmptyShifter(), 0); - else - adjustPalette(ColorFilter::genAlphaShifter(0), 0); - - bodyEnabled = on; -} - -void SDLImageIndexed::setOverlayEnabled(bool on) -{ - if (on) - setOverlayColor(Colors::WHITE_TRUE); - - if (!on && blitMode == EImageBlitMode::ALPHA) - setOverlayColor(Colors::TRANSPARENCY); - - overlayEnabled = on; + switch(blitMode) + { + case EImageBlitMode::ONLY_OVERLAY: + case EImageBlitMode::WITH_SHADOW_AND_OVERLAY: + setOverlayColor(Colors::WHITE_TRUE); + break; + case EImageBlitMode::ONLY_SHADOW: + case EImageBlitMode::ONLY_BODY: + setOverlayColor(Colors::TRANSPARENCY); + break; + } } SDLImageShared::~SDLImageShared() @@ -529,13 +551,13 @@ SDLImageShared::~SDLImageShared() SDL_FreePalette(originalPalette); } -SDLImageBase::SDLImageBase(const std::shared_ptr & image, EImageBlitMode mode) +SDLImageBase::SDLImageBase(const std::shared_ptr & image, EImageBlitMode mode) :image(image) , alphaValue(SDL_ALPHA_OPAQUE) , blitMode(mode) {} -std::shared_ptr SDLImageBase::getSharedImage() const +std::shared_ptr SDLImageBase::getSharedImage() const { return image; } @@ -600,21 +622,6 @@ void SDLImageBase::setBlitMode(EImageBlitMode mode) blitMode = mode; } -void SDLImageRGB::setShadowEnabled(bool on) -{ - // Not supported. Theoretically we can try to extract all pixels of specific colors, but better to use 8-bit images or composite images -} - -void SDLImageRGB::setBodyEnabled(bool on) -{ - // Not supported. Theoretically we can try to extract all pixels of specific colors, but better to use 8-bit images or composite images -} - -void SDLImageRGB::setOverlayEnabled(bool on) -{ - // Not supported. Theoretically we can try to extract all pixels of specific colors, but better to use 8-bit images or composite images -} - void SDLImageRGB::setOverlayColor(const ColorRGBA & color) {} diff --git a/client/renderSDL/SDLImage.h b/client/renderSDL/SDLImage.h index e9465eda4..833ccec42 100644 --- a/client/renderSDL/SDLImage.h +++ b/client/renderSDL/SDLImage.h @@ -35,6 +35,9 @@ class SDLImageShared final : public ISharedImage, public std::enable_shared_from //total size including borders Point fullSize; + //pre scaled image + int preScaleFactor; + // Keep the original palette, in order to do color switching operation void savePalette(); @@ -42,11 +45,11 @@ class SDLImageShared final : public ISharedImage, public std::enable_shared_from public: //Load image from def file - SDLImageShared(const CDefFile *data, size_t frame, size_t group=0); + SDLImageShared(const CDefFile *data, size_t frame, size_t group=0, int preScaleFactor=1); //Load from bitmap file - SDLImageShared(const ImagePath & filename); + SDLImageShared(const ImagePath & filename, int preScaleFactor=1); //Create using existing surface, extraRef will increase refcount on SDL_Surface - SDLImageShared(SDL_Surface * from); + SDLImageShared(SDL_Surface * from, int preScaleFactor=1); ~SDLImageShared(); void draw(SDL_Surface * where, SDL_Palette * palette, const Point & dest, const Rect * src, const ColorRGBA & colorMultiplier, uint8_t alpha, EImageBlitMode mode) const override; @@ -54,11 +57,11 @@ public: void exportBitmap(const boost::filesystem::path & path, SDL_Palette * palette) const override; Point dimensions() const override; bool isTransparent(const Point & coords) const override; - std::shared_ptr createImageReference(EImageBlitMode mode) override; - std::shared_ptr horizontalFlip() const override; - std::shared_ptr verticalFlip() const override; - std::shared_ptr scaleInteger(int factor, SDL_Palette * palette) const override; - std::shared_ptr scaleTo(const Point & size, SDL_Palette * palette) const override; + std::shared_ptr createImageReference(EImageBlitMode mode) const override; + std::shared_ptr horizontalFlip() const override; + std::shared_ptr verticalFlip() const override; + std::shared_ptr scaleInteger(int factor, SDL_Palette * palette) const override; + std::shared_ptr scaleTo(const Point & size, SDL_Palette * palette) const override; friend class SDLImageLoader; }; @@ -66,19 +69,19 @@ public: class SDLImageBase : public IImage, boost::noncopyable { protected: - std::shared_ptr image; + std::shared_ptr image; uint8_t alphaValue; EImageBlitMode blitMode; public: - SDLImageBase(const std::shared_ptr & image, EImageBlitMode mode); + SDLImageBase(const std::shared_ptr & image, EImageBlitMode mode); bool isTransparent(const Point & coords) const override; Point dimensions() const override; void setAlpha(uint8_t value) override; void setBlitMode(EImageBlitMode mode) override; - std::shared_ptr getSharedImage() const override; + std::shared_ptr getSharedImage() const override; }; class SDLImageIndexed final : public SDLImageBase @@ -86,13 +89,10 @@ class SDLImageIndexed final : public SDLImageBase SDL_Palette * currentPalette = nullptr; SDL_Palette * originalPalette = nullptr; - bool bodyEnabled = true; - bool shadowEnabled = false; - bool overlayEnabled = false; - void setShadowTransparency(float factor); + void preparePalette(); public: - SDLImageIndexed(const std::shared_ptr & image, SDL_Palette * palette, EImageBlitMode mode); + SDLImageIndexed(const std::shared_ptr & image, SDL_Palette * palette, EImageBlitMode mode); ~SDLImageIndexed(); void draw(SDL_Surface * where, const Point & pos, const Rect * src) const override; @@ -103,10 +103,6 @@ public: void scaleInteger(int factor) override; void scaleTo(const Point & size) override; void exportBitmap(const boost::filesystem::path & path) const override; - - void setShadowEnabled(bool on) override; - void setBodyEnabled(bool on) override; - void setOverlayEnabled(bool on) override; }; class SDLImageRGB final : public SDLImageBase @@ -122,8 +118,4 @@ public: void scaleInteger(int factor) override; void scaleTo(const Point & size) override; void exportBitmap(const boost::filesystem::path & path) const override; - - void setShadowEnabled(bool on) override; - void setBodyEnabled(bool on) override; - void setOverlayEnabled(bool on) override; }; diff --git a/client/renderSDL/SDL_Extensions.cpp b/client/renderSDL/SDL_Extensions.cpp index b7d398242..237f6800d 100644 --- a/client/renderSDL/SDL_Extensions.cpp +++ b/client/renderSDL/SDL_Extensions.cpp @@ -90,7 +90,7 @@ SDL_Surface * CSDL_Ext::newSurface(const Point & dimensions, SDL_Surface * mod) if (mod->format->palette) { assert(ret->format->palette); - assert(ret->format->palette->ncolors == mod->format->palette->ncolors); + assert(ret->format->palette->ncolors >= mod->format->palette->ncolors); memcpy(ret->format->palette->colors, mod->format->palette->colors, mod->format->palette->ncolors * sizeof(SDL_Color)); } return ret; diff --git a/client/renderSDL/ScreenHandler.cpp b/client/renderSDL/ScreenHandler.cpp index c8d99e6fa..7614335e8 100644 --- a/client/renderSDL/ScreenHandler.cpp +++ b/client/renderSDL/ScreenHandler.cpp @@ -84,19 +84,39 @@ Rect ScreenHandler::convertLogicalPointsToWindow(const Rect & input) const return result; } +int ScreenHandler::getInterfaceScalingPercentage() const +{ + auto [minimalScaling, maximalScaling] = getSupportedScalingRange(); + + int userScaling = settings["video"]["resolution"]["scaling"].Integer(); + + if (userScaling == 0) // autodetection + { +#ifdef VCMI_MOBILE + // for mobiles - stay at maximum scaling unless we have large screen + // might be better to check screen DPI / physical dimensions, but way more complex, and may result in different edge cases, e.g. chromebooks / tv's + int preferredMinimalScaling = 200; +#else + // for PC - avoid downscaling if possible + int preferredMinimalScaling = 100; +#endif + // prefer a little below maximum - to give space for extended UI + int preferredMaximalScaling = maximalScaling * 10 / 12; + userScaling = std::max(std::min(maximalScaling, preferredMinimalScaling), preferredMaximalScaling); + } + + int scaling = std::clamp(userScaling, minimalScaling, maximalScaling); + return scaling; +} + Point ScreenHandler::getPreferredLogicalResolution() const { Point renderResolution = getRenderResolution(); double reservedAreaWidth = settings["video"]["reservedWidth"].Float(); + + int scaling = getInterfaceScalingPercentage(); Point availableResolution = Point(renderResolution.x * (1 - reservedAreaWidth), renderResolution.y); - - auto [minimalScaling, maximalScaling] = getSupportedScalingRange(); - - int userScaling = settings["video"]["resolution"]["scaling"].Integer(); - int scaling = std::clamp(userScaling, minimalScaling, maximalScaling); - Point logicalResolution = availableResolution * 100.0 / scaling; - return logicalResolution; } @@ -335,25 +355,22 @@ EUpscalingFilter ScreenHandler::loadUpscalingFilter() const if (filter != EUpscalingFilter::AUTO) return filter; - // for now - always fallback to no filter - return EUpscalingFilter::NONE; - // else - autoselect -// Point outputResolution = getRenderResolution(); -// Point logicalResolution = getPreferredLogicalResolution(); -// -// float scaleX = static_cast(outputResolution.x) / logicalResolution.x; -// float scaleY = static_cast(outputResolution.x) / logicalResolution.x; -// float scaling = std::min(scaleX, scaleY); -// -// if (scaling <= 1.0f) -// return EUpscalingFilter::NONE; -// if (scaling <= 2.0f) -// return EUpscalingFilter::XBRZ_2; -// if (scaling <= 3.0f) -// return EUpscalingFilter::XBRZ_3; -// -// return EUpscalingFilter::XBRZ_4; + Point outputResolution = getRenderResolution(); + Point logicalResolution = getPreferredLogicalResolution(); + + float scaleX = static_cast(outputResolution.x) / logicalResolution.x; + float scaleY = static_cast(outputResolution.x) / logicalResolution.x; + float scaling = std::min(scaleX, scaleY); + + if (scaling <= 1.001f) + return EUpscalingFilter::NONE; // running at original resolution or even lower than that - no need for xbrz + if (scaling <= 2.001f) + return EUpscalingFilter::XBRZ_2; // resolutions below 1200p (including 1080p / FullHD) + if (scaling <= 3.001f) + return EUpscalingFilter::XBRZ_3; // resolutions below 2400p (including 1440p and 2160p / 4K) + + return EUpscalingFilter::XBRZ_4; // Only for massive displays, e.g. 8K } void ScreenHandler::selectUpscalingFilter() diff --git a/client/renderSDL/ScreenHandler.h b/client/renderSDL/ScreenHandler.h index e15958b55..6a9026d7b 100644 --- a/client/renderSDL/ScreenHandler.h +++ b/client/renderSDL/ScreenHandler.h @@ -112,6 +112,8 @@ public: int getScalingFactor() const final; + int getInterfaceScalingPercentage() const final; + std::vector getSupportedResolutions() const final; std::vector getSupportedResolutions(int displayIndex) const; std::tuple getSupportedScalingRange() const final; diff --git a/client/widgets/Buttons.cpp b/client/widgets/Buttons.cpp index 2284e3764..9725b628b 100644 --- a/client/widgets/Buttons.cpp +++ b/client/widgets/Buttons.cpp @@ -49,7 +49,8 @@ void ButtonBase::update() // hero movement speed buttons: only three frames: normal, pressed and blocked/highlighted if (state == EButtonState::HIGHLIGHTED && image->size() < 4) image->setFrame(image->size()-1); - image->setFrame(stateToIndex[vstd::to_underlying(state)]); + else + image->setFrame(stateToIndex[vstd::to_underlying(state)]); } if (isActive()) diff --git a/client/widgets/CArtPlace.cpp b/client/widgets/CArtPlace.cpp deleted file mode 100644 index dbae173a2..000000000 --- a/client/widgets/CArtPlace.cpp +++ /dev/null @@ -1,248 +0,0 @@ -/* - * CArtPlace.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 "CArtPlace.h" - -#include "../gui/CGuiHandler.h" -#include "../gui/Shortcut.h" - -#include "CComponent.h" - -#include "../windows/GUIClasses.h" -#include "../render/Canvas.h" -#include "../render/Colors.h" -#include "../render/IRenderHandler.h" -#include "../CPlayerInterface.h" -#include "../CGameInfo.h" - -#include "../../CCallback.h" -#include "../../lib/texts/CGeneralTextHandler.h" -#include "../../lib/ArtifactUtils.h" -#include "../../lib/mapObjects/CGHeroInstance.h" -#include "../../lib/networkPacks/ArtifactLocation.h" -#include "../../lib/CConfigHandler.h" - -void CArtPlace::setInternals(const CArtifactInstance * artInst) -{ - ourArt = artInst; - if(!artInst) - { - image->disable(); - text.clear(); - hoverText = CGI->generaltexth->allTexts[507]; - return; - } - - imageIndex = artInst->artType->getIconIndex(); - if(artInst->getTypeId() == ArtifactID::SPELL_SCROLL) - { - auto spellID = artInst->getScrollSpellID(); - assert(spellID.num >= 0); - - if(settings["general"]["enableUiEnhancements"].Bool()) - { - imageIndex = spellID.num; - if(component.type != ComponentType::SPELL_SCROLL) - { - image->setScale(Point(pos.w, 34)); - image->setAnimationPath(AnimationPath::builtin("spellscr"), imageIndex); - image->moveTo(Point(pos.x, pos.y + 4)); - } - } - // Add spell component info (used to provide a pic in r-click popup) - component.type = ComponentType::SPELL_SCROLL; - component.subType = spellID; - } - else - { - if(settings["general"]["enableUiEnhancements"].Bool() && component.type != ComponentType::ARTIFACT) - { - image->setScale(Point()); - image->setAnimationPath(AnimationPath::builtin("artifact"), imageIndex); - image->moveTo(Point(pos.x, pos.y)); - } - component.type = ComponentType::ARTIFACT; - component.subType = artInst->getTypeId(); - } - image->enable(); - text = artInst->getDescription(); -} - -CArtPlace::CArtPlace(Point position, const CArtifactInstance * art) - : SelectableSlot(Rect(position, Point(44, 44)), Point(1, 1)) - , ourArt(art) - , locked(false) -{ - OBJECT_CONSTRUCTION; - - imageIndex = 0; - if(locked) - imageIndex = ArtifactID::ART_LOCK; - else if(ourArt) - imageIndex = ourArt->artType->getIconIndex(); - - image = std::make_shared(AnimationPath::builtin("artifact"), imageIndex); - image->disable(); - moveSelectionForeground(); -} - -const CArtifactInstance * CArtPlace::getArt() const -{ - return ourArt; -} - -CCommanderArtPlace::CCommanderArtPlace(Point position, const CGHeroInstance * commanderOwner, ArtifactPosition artSlot, const CArtifactInstance * art) - : CArtPlace(position, art), - commanderOwner(commanderOwner), - commanderSlotID(artSlot.num) -{ - setArtifact(art); -} - -void CCommanderArtPlace::returnArtToHeroCallback() -{ - ArtifactPosition artifactPos = commanderSlotID; - ArtifactPosition freeSlot = ArtifactUtils::getArtBackpackPosition(commanderOwner, getArt()->getTypeId()); - if(freeSlot == ArtifactPosition::PRE_FIRST) - { - LOCPLINT->showInfoDialog(CGI->generaltexth->translate("core.genrltxt.152")); - } - else - { - ArtifactLocation src(commanderOwner->id, artifactPos); - src.creature = SlotID::COMMANDER_SLOT_PLACEHOLDER; - ArtifactLocation dst(commanderOwner->id, freeSlot); - - if(getArt()->canBePutAt(commanderOwner, freeSlot, true)) - { - LOCPLINT->cb->swapArtifacts(src, dst); - setArtifact(nullptr); - parent->redraw(); - } - } -} - -void CCommanderArtPlace::clickPressed(const Point & cursorPosition) -{ - if(getArt() && text.size()) - LOCPLINT->showYesNoDialog(CGI->generaltexth->translate("vcmi.commanderWindow.artifactMessage"), [this]() { returnArtToHeroCallback(); }, []() {}); -} - -void CCommanderArtPlace::showPopupWindow(const Point & cursorPosition) -{ - if(getArt() && text.size()) - CArtPlace::showPopupWindow(cursorPosition); -} - -void CArtPlace::lockSlot(bool on) -{ - if(locked == on) - return; - - locked = on; - - if(on) - image->setFrame(ArtifactID::ART_LOCK); - else if(ourArt) - image->setFrame(imageIndex); - else - image->setFrame(0); -} - -bool CArtPlace::isLocked() const -{ - return locked; -} - -void CArtPlace::clickPressed(const Point & cursorPosition) -{ - if(clickPressedCallback) - clickPressedCallback(*this, cursorPosition); -} - -void CArtPlace::showPopupWindow(const Point & cursorPosition) -{ - if(showPopupCallback) - showPopupCallback(*this, cursorPosition); -} - -void CArtPlace::gesture(bool on, const Point & initialPosition, const Point & finalPosition) -{ - if(!on) - return; - - if(gestureCallback) - gestureCallback(*this, initialPosition); -} - -void CArtPlace::setArtifact(const CArtifactInstance * art) -{ - setInternals(art); - if(art) - { - image->setFrame(locked ? static_cast(ArtifactID::ART_LOCK) : imageIndex); - - if(locked) // Locks should appear as empty. - hoverText = CGI->generaltexth->allTexts[507]; - else - hoverText = boost::str(boost::format(CGI->generaltexth->heroscrn[1]) % ourArt->artType->getNameTranslated()); - } - else - { - lockSlot(false); - } -} - -void CArtPlace::setClickPressedCallback(const ClickFunctor & callback) -{ - clickPressedCallback = callback; -} - -void CArtPlace::setShowPopupCallback(const ClickFunctor & callback) -{ - showPopupCallback = callback; -} - -void CArtPlace::setGestureCallback(const ClickFunctor & callback) -{ - gestureCallback = callback; -} - -void CArtPlace::addCombinedArtInfo(const std::map> & arts) -{ - for(const auto & availableArts : arts) - { - const auto combinedArt = availableArts.first.toArtifact(); - MetaString info; - info.appendEOL(); - info.appendEOL(); - info.appendRawString("{"); - info.appendName(combinedArt->getId()); - info.appendRawString("}"); - info.appendRawString(" (%d/%d)"); - info.replaceNumber(availableArts.second.size()); - info.replaceNumber(combinedArt->getConstituents().size()); - for(const auto part : combinedArt->getConstituents()) - { - info.appendEOL(); - if(vstd::contains(availableArts.second, part->getId())) - { - info.appendName(part->getId()); - } - else - { - info.appendRawString("{#A9A9A9|"); - info.appendName(part->getId()); - info.appendRawString("}"); - } - } - text += info.toString(); - } -} diff --git a/client/widgets/CArtifactsOfHeroAltar.cpp b/client/widgets/CArtifactsOfHeroAltar.cpp index bcef56cd1..795243eca 100644 --- a/client/widgets/CArtifactsOfHeroAltar.cpp +++ b/client/widgets/CArtifactsOfHeroAltar.cpp @@ -22,6 +22,8 @@ CArtifactsOfHeroAltar::CArtifactsOfHeroAltar(const Point & position) { init(position, std::bind(&CArtifactsOfHeroBase::scrollBackpack, this, _1)); + setClickPressedArtPlacesCallback(std::bind(&CArtifactsOfHeroBase::clickPressedArtPlace, this, _1, _2)); + setShowPopupArtPlacesCallback(std::bind(&CArtifactsOfHeroBase::showPopupArtPlace, this, _1, _2)); enableGesture(); // The backpack is in the altar window above and to the right for(auto & slot : backpack) diff --git a/client/widgets/CArtifactsOfHeroBackpack.cpp b/client/widgets/CArtifactsOfHeroBackpack.cpp index 6e77636a3..4cfde13ec 100644 --- a/client/widgets/CArtifactsOfHeroBackpack.cpp +++ b/client/widgets/CArtifactsOfHeroBackpack.cpp @@ -40,6 +40,8 @@ CArtifactsOfHeroBackpack::CArtifactsOfHeroBackpack() visibleCapacityMax = visibleCapacityMax > backpackCap ? backpackCap : visibleCapacityMax; initAOHbackpack(visibleCapacityMax, backpackCap < 0 || visibleCapacityMax < backpackCap); + setClickPressedArtPlacesCallback(std::bind(&CArtifactsOfHeroBase::clickPressedArtPlace, this, _1, _2)); + setShowPopupArtPlacesCallback(std::bind(&CArtifactsOfHeroBase::showPopupArtPlace, this, _1, _2)); } void CArtifactsOfHeroBackpack::onSliderMoved(int newVal) @@ -83,9 +85,7 @@ void CArtifactsOfHeroBackpack::initAOHbackpack(size_t slots, bool slider) slotSizeWithMargin * (artPlaceIdx / slotsColumnsMax)); backpackSlotsBackgrounds.emplace_back(std::make_shared(ImagePath::builtin("heroWindow/artifactSlotEmpty"), pos)); artPlace = std::make_shared(pos); - artPlace->setArtifact(nullptr); - artPlace->setClickPressedCallback(std::bind(&CArtifactsOfHeroBase::clickPrassedArtPlace, this, _1, _2)); - artPlace->setShowPopupCallback(std::bind(&CArtifactsOfHeroBase::showPopupArtPlace, this, _1, _2)); + artPlace->setArtifact(ArtifactID(ArtifactID::NONE)); artPlaceIdx++; } @@ -126,12 +126,11 @@ size_t CArtifactsOfHeroBackpack::calcRows(size_t slots) CArtifactsOfHeroQuickBackpack::CArtifactsOfHeroQuickBackpack(const ArtifactPosition filterBySlot) : CArtifactsOfHeroBackpack(0, 0) { - assert(ArtifactUtils::checkIfSlotValid(*getHero(), filterBySlot)); - if(!ArtifactUtils::isSlotEquipment(filterBySlot)) return; this->filterBySlot = filterBySlot; + setShowPopupArtPlacesCallback(std::bind(&CArtifactsOfHeroBase::showPopupArtPlace, this, _1, _2)); } void CArtifactsOfHeroQuickBackpack::setHero(const CGHeroInstance * hero) @@ -153,7 +152,7 @@ void CArtifactsOfHeroQuickBackpack::setHero(const CGHeroInstance * hero) std::map filteredArts; for(auto & slotInfo : curHero->artifactsInBackpack) if(slotInfo.artifact->getTypeId() != artInSlotId && !slotInfo.artifact->isScroll() && - slotInfo.artifact->artType->canBePutAt(curHero, filterBySlot, true)) + slotInfo.artifact->getType()->canBePutAt(curHero, filterBySlot, true)) { filteredArts.insert(std::pair(slotInfo.artifact->getTypeId(), slotInfo.artifact)); } @@ -174,6 +173,7 @@ void CArtifactsOfHeroQuickBackpack::setHero(const CGHeroInstance * hero) slotsColumnsMax = ceilf(sqrtf(requiredSlots)); slotsRowsMax = calcRows(requiredSlots); initAOHbackpack(requiredSlots, false); + setClickPressedArtPlacesCallback(std::bind(&CArtifactsOfHeroBase::clickPressedArtPlace, this, _1, _2)); auto artPlace = backpack.begin(); for(auto & art : filteredArts) setSlotData(*artPlace++, curHero->getArtPos(art.second)); diff --git a/client/widgets/CArtifactsOfHeroBase.cpp b/client/widgets/CArtifactsOfHeroBase.cpp index c44e3d01f..36c0690f4 100644 --- a/client/widgets/CArtifactsOfHeroBase.cpp +++ b/client/widgets/CArtifactsOfHeroBase.cpp @@ -63,18 +63,14 @@ void CArtifactsOfHeroBase::init( auto artPlace = std::make_shared(Point(403 + 46 * s, 365)); backpack.push_back(artPlace); } - for(auto artPlace : artWorn) + for(auto & artPlace : artWorn) { artPlace.second->slot = artPlace.first; - artPlace.second->setArtifact(nullptr); - artPlace.second->setClickPressedCallback(std::bind(&CArtifactsOfHeroBase::clickPrassedArtPlace, this, _1, _2)); - artPlace.second->setShowPopupCallback(std::bind(&CArtifactsOfHeroBase::showPopupArtPlace, this, _1, _2)); + artPlace.second->setArtifact(ArtifactID(ArtifactID::NONE)); } - for(auto artPlace : backpack) + for(const auto & artPlace : backpack) { - artPlace->setArtifact(nullptr); - artPlace->setClickPressedCallback(std::bind(&CArtifactsOfHeroBase::clickPrassedArtPlace, this, _1, _2)); - artPlace->setShowPopupCallback(std::bind(&CArtifactsOfHeroBase::showPopupArtPlace, this, _1, _2)); + artPlace->setArtifact(ArtifactID(ArtifactID::NONE)); } leftBackpackRoll = std::make_shared(Point(379, 364), AnimationPath::builtin("hsbtns3.def"), CButton::tooltip(), [scrollCallback](){scrollCallback(true);}, EShortcut::MOVE_LEFT); @@ -89,36 +85,63 @@ void CArtifactsOfHeroBase::init( setRedrawParent(true); } -void CArtifactsOfHeroBase::clickPrassedArtPlace(CArtPlace & artPlace, const Point & cursorPosition) +void CArtifactsOfHeroBase::setClickPressedArtPlacesCallback(const CArtPlace::ClickFunctor & callback) const { - if(artPlace.isLocked()) - return; - - if(clickPressedCallback) - clickPressedCallback(artPlace, cursorPosition); + for(const auto & [slot, artPlace] : artWorn) + artPlace->setClickPressedCallback(callback); + for(const auto & artPlace : backpack) + artPlace->setClickPressedCallback(callback); } -void CArtifactsOfHeroBase::showPopupArtPlace(CArtPlace & artPlace, const Point & cursorPosition) +void CArtifactsOfHeroBase::setShowPopupArtPlacesCallback(const CArtPlace::ClickFunctor & callback) const { - if(artPlace.isLocked()) - return; - - if(showPopupCallback) - showPopupCallback(artPlace, cursorPosition); + for(const auto & [slot, artPlace] : artWorn) + artPlace->setShowPopupCallback(callback); + for(const auto & artPlace : backpack) + artPlace->setShowPopupCallback(callback); } -void CArtifactsOfHeroBase::gestureArtPlace(CArtPlace & artPlace, const Point & cursorPosition) +void CArtifactsOfHeroBase::clickPressedArtPlace(CComponentHolder & artPlace, const Point & cursorPosition) { - if(artPlace.isLocked()) - return; + if(auto ownedPlace = getArtPlace(cursorPosition)) + { + if(ownedPlace->isLocked()) + return; - if(gestureCallback) - gestureCallback(artPlace, cursorPosition); + if(clickPressedCallback) + clickPressedCallback(*ownedPlace, cursorPosition); + } +} + +void CArtifactsOfHeroBase::showPopupArtPlace(CComponentHolder & artPlace, const Point & cursorPosition) +{ + if(auto ownedPlace = getArtPlace(cursorPosition)) + { + if(ownedPlace->isLocked()) + return; + + if(showPopupCallback) + showPopupCallback(*ownedPlace, cursorPosition); + } +} + +void CArtifactsOfHeroBase::gestureArtPlace(CComponentHolder & artPlace, const Point & cursorPosition) +{ + if(auto ownedPlace = getArtPlace(cursorPosition)) + { + if(ownedPlace->isLocked()) + return; + + if(gestureCallback) + gestureCallback(*ownedPlace, cursorPosition); + } } void CArtifactsOfHeroBase::setHero(const CGHeroInstance * hero) { curHero = hero; + if (!hero) + return; for(auto slot : artWorn) { @@ -137,9 +160,9 @@ void CArtifactsOfHeroBase::scrollBackpack(bool left) LOCPLINT->cb->scrollBackpackArtifacts(curHero->id, left); } -void CArtifactsOfHeroBase::markPossibleSlots(const CArtifactInstance * art, bool assumeDestRemoved) +void CArtifactsOfHeroBase::markPossibleSlots(const CArtifact * art, bool assumeDestRemoved) { - for(auto artPlace : artWorn) + for(const auto & artPlace : artWorn) artPlace.second->selectSlot(art->canBePutAt(curHero, artPlace.second->slot, assumeDestRemoved)); } @@ -154,26 +177,12 @@ void CArtifactsOfHeroBase::unmarkSlots() CArtifactsOfHeroBase::ArtPlacePtr CArtifactsOfHeroBase::getArtPlace(const ArtifactPosition & slot) { - if(ArtifactUtils::isSlotEquipment(slot)) - { - if(artWorn.find(slot) == artWorn.end()) - { - logGlobal->error("CArtifactsOfHero::getArtPlace: invalid slot %d", slot); - return nullptr; - } + if(ArtifactUtils::isSlotEquipment(slot) && artWorn.find(slot) != artWorn.end()) return artWorn[slot]; - } - if(ArtifactUtils::isSlotBackpack(slot)) - { - for(ArtPlacePtr artPlace : backpack) - if(artPlace->slot == slot) - return artPlace; - return nullptr; - } - else - { - return nullptr; - } + if(ArtifactUtils::isSlotBackpack(slot) && slot - ArtifactPosition::BACKPACK_START < backpack.size()) + return(backpack[slot - ArtifactPosition::BACKPACK_START]); + logGlobal->error("CArtifactsOfHero::getArtPlace: invalid slot %d", slot); + return nullptr; } CArtifactsOfHeroBase::ArtPlacePtr CArtifactsOfHeroBase::getArtPlace(const Point & cursorPosition) @@ -260,26 +269,32 @@ void CArtifactsOfHeroBase::setSlotData(ArtPlacePtr artPlace, const ArtifactPosit if(auto slotInfo = curHero->getSlot(slot)) { artPlace->lockSlot(slotInfo->locked); - artPlace->setArtifact(slotInfo->artifact); + artPlace->setArtifact(slotInfo->artifact->getTypeId(), slotInfo->artifact->getScrollSpellID()); if(slotInfo->locked || slotInfo->artifact->isCombined()) return; // If the artifact is part of at least one combined artifact, add additional information std::map> arts; - for(const auto combinedArt : slotInfo->artifact->artType->getPartOf()) + for(const auto combinedArt : slotInfo->artifact->getType()->getPartOf()) { - arts.try_emplace(combinedArt->getId(), std::vector{}); + assert(combinedArt->isCombined()); + arts.try_emplace(combinedArt->getId()); + CArtifactFittingSet fittingSet(*curHero); for(const auto part : combinedArt->getConstituents()) { - if(curHero->hasArt(part->getId(), false, false, false)) + const auto partSlot = fittingSet.getArtPos(part->getId(), false, false); + if(partSlot != ArtifactPosition::PRE_FIRST) + { arts.at(combinedArt->getId()).emplace_back(part->getId()); + fittingSet.lockSlot(partSlot); + } } } artPlace->addCombinedArtInfo(arts); } else { - artPlace->setArtifact(nullptr); + artPlace->setArtifact(ArtifactID(ArtifactID::NONE)); } } diff --git a/client/widgets/CArtifactsOfHeroBase.h b/client/widgets/CArtifactsOfHeroBase.h index c4cb83e33..2ded1efa7 100644 --- a/client/widgets/CArtifactsOfHeroBase.h +++ b/client/widgets/CArtifactsOfHeroBase.h @@ -9,7 +9,7 @@ */ #pragma once -#include "CArtPlace.h" +#include "CComponentHolder.h" #include "Scrollable.h" #include "../gui/Shortcut.h" @@ -33,13 +33,13 @@ public: CArtifactsOfHeroBase(); virtual void putBackPickedArtifact(); - virtual void clickPrassedArtPlace(CArtPlace & artPlace, const Point & cursorPosition); - virtual void showPopupArtPlace(CArtPlace & artPlace, const Point & cursorPosition); - virtual void gestureArtPlace(CArtPlace & artPlace, const Point & cursorPosition); + virtual void clickPressedArtPlace(CComponentHolder & artPlace, const Point & cursorPosition); + virtual void showPopupArtPlace(CComponentHolder & artPlace, const Point & cursorPosition); + virtual void gestureArtPlace(CComponentHolder & artPlace, const Point & cursorPosition); virtual void setHero(const CGHeroInstance * hero); virtual const CGHeroInstance * getHero() const; virtual void scrollBackpack(bool left); - virtual void markPossibleSlots(const CArtifactInstance * art, bool assumeDestRemoved = true); + virtual void markPossibleSlots(const CArtifact * art, bool assumeDestRemoved = true); virtual void unmarkSlots(); virtual ArtPlacePtr getArtPlace(const ArtifactPosition & slot); virtual ArtPlacePtr getArtPlace(const Point & cursorPosition); @@ -50,6 +50,8 @@ public: void enableGesture(); const CArtifactInstance * getArt(const ArtifactPosition & slot) const; void enableKeyboardShortcuts(); + void setClickPressedArtPlacesCallback(const CArtPlace::ClickFunctor & callback) const; + void setShowPopupArtPlacesCallback(const CArtPlace::ClickFunctor & callback) const; const CGHeroInstance * curHero; ArtPlaceMap artWorn; diff --git a/client/widgets/CArtifactsOfHeroKingdom.cpp b/client/widgets/CArtifactsOfHeroKingdom.cpp index e65bd8bfb..9e4643ce4 100644 --- a/client/widgets/CArtifactsOfHeroKingdom.cpp +++ b/client/widgets/CArtifactsOfHeroKingdom.cpp @@ -29,15 +29,15 @@ CArtifactsOfHeroKingdom::CArtifactsOfHeroKingdom(ArtPlaceMap ArtWorn, std::vecto for(auto artPlace : artWorn) { artPlace.second->slot = artPlace.first; - artPlace.second->setArtifact(nullptr); - artPlace.second->setClickPressedCallback(std::bind(&CArtifactsOfHeroBase::clickPrassedArtPlace, this, _1, _2)); + artPlace.second->setArtifact(ArtifactID(ArtifactID::NONE)); + artPlace.second->setClickPressedCallback(std::bind(&CArtifactsOfHeroBase::clickPressedArtPlace, this, _1, _2)); artPlace.second->setShowPopupCallback(std::bind(&CArtifactsOfHeroBase::showPopupArtPlace, this, _1, _2)); } enableGesture(); for(auto artPlace : backpack) { - artPlace->setArtifact(nullptr); - artPlace->setClickPressedCallback(std::bind(&CArtifactsOfHeroBase::clickPrassedArtPlace, this, _1, _2)); + artPlace->setArtifact(ArtifactID(ArtifactID::NONE)); + artPlace->setClickPressedCallback(std::bind(&CArtifactsOfHeroBase::clickPressedArtPlace, this, _1, _2)); artPlace->setShowPopupCallback(std::bind(&CArtifactsOfHeroBase::showPopupArtPlace, this, _1, _2)); } leftBackpackRoll->addCallback(std::bind(&CArtifactsOfHeroBase::scrollBackpack, this, -1)); diff --git a/client/widgets/CArtifactsOfHeroMain.cpp b/client/widgets/CArtifactsOfHeroMain.cpp index e10e0e37a..34458351e 100644 --- a/client/widgets/CArtifactsOfHeroMain.cpp +++ b/client/widgets/CArtifactsOfHeroMain.cpp @@ -21,12 +21,15 @@ CArtifactsOfHeroMain::CArtifactsOfHeroMain(const Point & position) { init(position, std::bind(&CArtifactsOfHeroBase::scrollBackpack, this, _1)); + setClickPressedArtPlacesCallback(std::bind(&CArtifactsOfHeroBase::clickPressedArtPlace, this, _1, _2)); + setShowPopupArtPlacesCallback(std::bind(&CArtifactsOfHeroBase::showPopupArtPlace, this, _1, _2)); enableGesture(); } CArtifactsOfHeroMain::~CArtifactsOfHeroMain() { - CArtifactsOfHeroBase::putBackPickedArtifact(); + if(curHero) + CArtifactsOfHeroBase::putBackPickedArtifact(); } void CArtifactsOfHeroMain::keyPressed(EShortcut key) diff --git a/client/widgets/CArtifactsOfHeroMarket.cpp b/client/widgets/CArtifactsOfHeroMarket.cpp index 7158d0371..fac271b46 100644 --- a/client/widgets/CArtifactsOfHeroMarket.cpp +++ b/client/widgets/CArtifactsOfHeroMarket.cpp @@ -15,30 +15,33 @@ CArtifactsOfHeroMarket::CArtifactsOfHeroMarket(const Point & position, const int selectionWidth) { init(position, std::bind(&CArtifactsOfHeroBase::scrollBackpack, this, _1)); - + setClickPressedArtPlacesCallback(std::bind(&CArtifactsOfHeroBase::clickPressedArtPlace, this, _1, _2)); for(const auto & [slot, artPlace] : artWorn) artPlace->setSelectionWidth(selectionWidth); for(auto artPlace : backpack) artPlace->setSelectionWidth(selectionWidth); }; -void CArtifactsOfHeroMarket::clickPrassedArtPlace(CArtPlace & artPlace, const Point & cursorPosition) +void CArtifactsOfHeroMarket::clickPressedArtPlace(CComponentHolder & artPlace, const Point & cursorPosition) { - if(artPlace.isLocked()) - return; - - if(const auto art = getArt(artPlace.slot)) + if(auto ownedPlace = getArtPlace(cursorPosition)) { - if(onSelectArtCallback && art->artType->isTradable()) + if(ownedPlace->isLocked()) + return; + + if(const auto art = getArt(ownedPlace->slot)) { - unmarkSlots(); - artPlace.selectSlot(true); - onSelectArtCallback(&artPlace); - } - else - { - if(onClickNotTradableCallback) - onClickNotTradableCallback(); + if(onSelectArtCallback && art->getType()->isTradable()) + { + unmarkSlots(); + artPlace.selectSlot(true); + onSelectArtCallback(ownedPlace.get()); + } + else + { + if(onClickNotTradableCallback) + onClickNotTradableCallback(); + } } } } diff --git a/client/widgets/CArtifactsOfHeroMarket.h b/client/widgets/CArtifactsOfHeroMarket.h index 87334c0d7..84a80a831 100644 --- a/client/widgets/CArtifactsOfHeroMarket.h +++ b/client/widgets/CArtifactsOfHeroMarket.h @@ -18,5 +18,5 @@ public: std::function onClickNotTradableCallback; CArtifactsOfHeroMarket(const Point & position, const int selectionWidth); - void clickPrassedArtPlace(CArtPlace & artPlace, const Point & cursorPosition) override; + void clickPressedArtPlace(CComponentHolder & artPlace, const Point & cursorPosition) override; }; diff --git a/client/widgets/CComponent.cpp b/client/widgets/CComponent.cpp index 2ece8d515..80b800492 100644 --- a/client/widgets/CComponent.cpp +++ b/client/widgets/CComponent.cpp @@ -12,9 +12,6 @@ #include "Images.h" -#include -#include - #include "../gui/CGuiHandler.h" #include "../gui/CursorHandler.h" #include "../gui/TextAlignment.h" @@ -29,7 +26,6 @@ #include "../CGameInfo.h" #include "../../lib/ArtifactUtils.h" -#include "../../lib/CHeroHandler.h" #include "../../lib/entities/building/CBuilding.h" #include "../../lib/entities/faction/CFaction.h" #include "../../lib/entities/faction/CTown.h" @@ -42,6 +38,11 @@ #include "../../lib/CArtHandler.h" #include "../../lib/CArtifactInstance.h" +#include +#include +#include +#include + CComponent::CComponent(ComponentType Type, ComponentSubType Subtype, std::optional Val, ESize imageSize, EFonts font) { init(Type, Subtype, Val, imageSize, font, ""); @@ -70,6 +71,7 @@ void CComponent::init(ComponentType Type, ComponentSubType Subtype, std::optiona customSubtitle = ValText; size = imageSize; font = fnt; + newLine = false; assert(size < sizeInvalid); @@ -471,7 +473,8 @@ void CComponentBox::placeComponents(bool selectable) //start next row if ((pos.w != 0 && rows.back().width + comp->pos.w + distance > pos.w) // row is full - || rows.back().comps >= componentsInRow) + || rows.back().comps >= componentsInRow + || (prevComp && prevComp->newLine)) { prevComp = nullptr; rows.push_back (RowData (0,0,0)); diff --git a/client/widgets/CComponent.h b/client/widgets/CComponent.h index f4d360460..d4c1acc3f 100644 --- a/client/widgets/CComponent.h +++ b/client/widgets/CComponent.h @@ -52,6 +52,7 @@ public: std::string customSubtitle; ESize size; //component size. EFonts font; //Font size of label + bool newLine; //Line break after component std::string getDescription() const; std::string getSubtitle() const; diff --git a/client/widgets/CComponentHolder.cpp b/client/widgets/CComponentHolder.cpp new file mode 100644 index 000000000..e0f4196a3 --- /dev/null +++ b/client/widgets/CComponentHolder.cpp @@ -0,0 +1,314 @@ +/* + * CComponentHolder.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 "CComponentHolder.h" + +#include "../gui/CGuiHandler.h" +#include "../gui/Shortcut.h" + +#include "CComponent.h" +#include "Images.h" + +#include "../render/Canvas.h" +#include "../render/Colors.h" +#include "../render/IRenderHandler.h" +#include "../CPlayerInterface.h" +#include "../CGameInfo.h" + +#include "../../CCallback.h" +#include "../../lib/texts/CGeneralTextHandler.h" +#include "../../lib/ArtifactUtils.h" +#include "../../lib/mapObjects/CGHeroInstance.h" +#include "../../lib/networkPacks/ArtifactLocation.h" +#include "../../lib/CConfigHandler.h" +#include "../../lib/CSkillHandler.h" + +CComponentHolder::CComponentHolder(const Rect & area, const Point & selectionOversize) + : SelectableSlot(area, selectionOversize) +{ + setClickPressedCallback([this](const CComponentHolder &, const Point & cursorPosition) + { + if(text.size()) + LRClickableAreaWTextComp::clickPressed(cursorPosition); + }); + setShowPopupCallback([this](const CComponentHolder &, const Point & cursorPosition) + { + if(text.size()) + LRClickableAreaWTextComp::showPopupWindow(cursorPosition); + }); +} + +void CComponentHolder::setClickPressedCallback(const ClickFunctor & callback) +{ + clickPressedCallback = callback; +} + +void CComponentHolder::setShowPopupCallback(const ClickFunctor & callback) +{ + showPopupCallback = callback; +} + +void CComponentHolder::setGestureCallback(const ClickFunctor & callback) +{ + gestureCallback = callback; +} + +void CComponentHolder::clickPressed(const Point & cursorPosition) +{ + if(clickPressedCallback) + clickPressedCallback(*this, cursorPosition); +} + +void CComponentHolder::showPopupWindow(const Point & cursorPosition) +{ + if(showPopupCallback) + showPopupCallback(*this, cursorPosition); +} + +void CComponentHolder::gesture(bool on, const Point & initialPosition, const Point & finalPosition) +{ + if(!on) + return; + + if(gestureCallback) + gestureCallback(*this, initialPosition); +} + +CArtPlace::CArtPlace(Point position, const ArtifactID & artId, const SpellID & spellId) + : CComponentHolder(Rect(position, Point(44, 44)), Point(1, 1)) + , locked(false) + , imageIndex(0) +{ + OBJECT_CONSTRUCTION; + + image = std::make_shared(AnimationPath::builtin("artifact"), 0); + setArtifact(artId, spellId); + moveSelectionForeground(); +} + +void CArtPlace::setArtifact(const SpellID & newSpellId) +{ + setArtifact(ArtifactID::SPELL_SCROLL, newSpellId); +} + +void CArtPlace::setArtifact(const ArtifactID & newArtId, const SpellID & newSpellId) +{ + artId = newArtId; + if(artId == ArtifactID::NONE) + { + image->disable(); + text.clear(); + lockSlot(false); + return; + } + + const auto artType = artId.toArtifact(); + imageIndex = artType->getIconIndex(); + if(artId == ArtifactID::SPELL_SCROLL) + { + spellId = newSpellId; + assert(spellId.num > 0); + + if(settings["general"]["enableUiEnhancements"].Bool()) + { + imageIndex = spellId.num; + if(component.type != ComponentType::SPELL_SCROLL) + { + image->setScale(Point(pos.w, 34)); + image->setAnimationPath(AnimationPath::builtin("spellscr"), imageIndex); + image->moveTo(Point(pos.x, pos.y + 4)); + } + } + // Add spell component info (used to provide a pic in r-click popup) + component.type = ComponentType::SPELL_SCROLL; + component.subType = spellId; + } + else + { + if(settings["general"]["enableUiEnhancements"].Bool() && component.type != ComponentType::ARTIFACT) + { + image->setScale(Point()); + image->setAnimationPath(AnimationPath::builtin("artifact"), imageIndex); + image->moveTo(Point(pos.x, pos.y)); + } + component.type = ComponentType::ARTIFACT; + component.subType = artId; + } + image->enable(); + lockSlot(locked); + + text = artType->getDescriptionTranslated(); + if(artType->isScroll()) + ArtifactUtils::insertScrrollSpellName(text, spellId); +} + +ArtifactID CArtPlace::getArtifactId() const +{ + return artId; +} + +CCommanderArtPlace::CCommanderArtPlace(Point position, const CGHeroInstance * commanderOwner, ArtifactPosition artSlot, + const ArtifactID & artId, const SpellID & spellId) + : CArtPlace(position, artId, spellId), + commanderOwner(commanderOwner), + commanderSlotID(artSlot.num) +{ +} + +void CCommanderArtPlace::returnArtToHeroCallback() +{ + ArtifactPosition artifactPos = commanderSlotID; + ArtifactPosition freeSlot = ArtifactUtils::getArtBackpackPosition(commanderOwner, getArtifactId()); + if(freeSlot == ArtifactPosition::PRE_FIRST) + { + LOCPLINT->showInfoDialog(CGI->generaltexth->translate("core.genrltxt.152")); + } + else + { + ArtifactLocation src(commanderOwner->id, artifactPos); + src.creature = SlotID::COMMANDER_SLOT_PLACEHOLDER; + ArtifactLocation dst(commanderOwner->id, freeSlot); + + if(getArtifactId().toArtifact()->canBePutAt(commanderOwner, freeSlot, true)) + { + LOCPLINT->cb->swapArtifacts(src, dst); + setArtifact(ArtifactID(ArtifactID::NONE)); + parent->redraw(); + } + } +} + +void CCommanderArtPlace::clickPressed(const Point & cursorPosition) +{ + if(getArtifactId() != ArtifactID::NONE && text.size()) + LOCPLINT->showYesNoDialog(CGI->generaltexth->translate("vcmi.commanderWindow.artifactMessage"), [this]() { returnArtToHeroCallback(); }, []() {}); +} + +void CCommanderArtPlace::showPopupWindow(const Point & cursorPosition) +{ + if(getArtifactId() != ArtifactID::NONE && text.size()) + CArtPlace::showPopupWindow(cursorPosition); +} + +void CArtPlace::lockSlot(bool on) +{ + locked = on; + if(on) + { + image->setFrame(ArtifactID::ART_LOCK); + hoverText = CGI->generaltexth->allTexts[507]; + } + else if(artId != ArtifactID::NONE) + { + image->setFrame(imageIndex); + auto hoverText = MetaString::createFromRawString(CGI->generaltexth->heroscrn[1]); + hoverText.replaceName(artId); + this->hoverText = hoverText.toString(); + } + else + { + hoverText = CGI->generaltexth->allTexts[507]; + } +} + +bool CArtPlace::isLocked() const +{ + return locked; +} + +void CArtPlace::addCombinedArtInfo(const std::map> & arts) +{ + for(auto [combinedId, availableArts] : arts) + { + const auto combinedArt = combinedId.toArtifact(); + MetaString info; + info.appendEOL(); + info.appendEOL(); + info.appendRawString("{"); + info.appendName(combinedArt->getId()); + info.appendRawString("}"); + info.appendRawString(" (%d/%d)"); + info.replaceNumber(availableArts.size()); + info.replaceNumber(combinedArt->getConstituents().size()); + for(const auto part : combinedArt->getConstituents()) + { + const auto found = std::find_if(availableArts.begin(), availableArts.end(), [part](const auto & availablePart) -> bool + { + return availablePart == part->getId() ? true : false; + }); + + info.appendEOL(); + if(found < availableArts.end()) + { + info.appendName(part->getId()); + availableArts.erase(found); + } + else + { + info.appendRawString("{#A9A9A9|"); + info.appendName(part->getId()); + info.appendRawString("}"); + } + } + text += info.toString(); + } +} + +CSecSkillPlace::CSecSkillPlace(const Point & position, const ImageSize & imageSize, const SecondarySkill & newSkillId, const uint8_t level) + : CComponentHolder(Rect(position, Point()), Point()) +{ + OBJECT_CONSTRUCTION; + + auto imagePath = AnimationPath::builtin("SECSK82"); + if(imageSize == ImageSize::MEDIUM) + imagePath = AnimationPath::builtin("SECSKILL"); + if(imageSize == ImageSize::SMALL) + imagePath = AnimationPath::builtin("SECSK32"); + + image = std::make_shared(imagePath, 0); + component.type = ComponentType::SEC_SKILL; + pos.w = image->pos.w; + pos.h = image->pos.h; + setSkill(newSkillId, level); +} + +void CSecSkillPlace::setSkill(const SecondarySkill & newSkillId, const uint8_t level) +{ + skillId = newSkillId; + component.subType = newSkillId; + setLevel(level); +} + +void CSecSkillPlace::setLevel(const uint8_t level) +{ + // 0 - none + // 1 - base + // 2 - advanced + // 3 - expert + assert(level <= 3); + if(skillId != SecondarySkill::NONE && level > 0) + { + const auto secSkill = skillId.toSkill(); + image->setFrame(secSkill->getIconIndex(level - 1)); + image->enable(); + auto hoverText = MetaString::createFromRawString(CGI->generaltexth->heroscrn[21]); + hoverText.replaceRawString(CGI->generaltexth->levels[level - 1]); + hoverText.replaceTextID(secSkill->getNameTextID()); + this->hoverText = hoverText.toString(); + component.value = level; + text = secSkill->getDescriptionTranslated(level); + } + else + { + image->disable(); + hoverText.clear(); + text.clear(); + } +} diff --git a/client/widgets/CArtPlace.h b/client/widgets/CComponentHolder.h similarity index 53% rename from client/widgets/CArtPlace.h rename to client/widgets/CComponentHolder.h index 95ff02b7e..9d364bd1d 100644 --- a/client/widgets/CArtPlace.h +++ b/client/widgets/CComponentHolder.h @@ -1,5 +1,5 @@ /* - * CArtPlace.h, part of VCMI engine + * CComponentHolder.h, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * @@ -13,37 +13,43 @@ class CAnimImage; -class CArtPlace : public SelectableSlot +class CComponentHolder : public SelectableSlot { public: - using ClickFunctor = std::function; + using ClickFunctor = std::function; - ArtifactPosition slot; - - CArtPlace(Point position, const CArtifactInstance * art = nullptr); - const CArtifactInstance * getArt() const; - void lockSlot(bool on); - bool isLocked() const; - void setArtifact(const CArtifactInstance * art); + ClickFunctor clickPressedCallback; + ClickFunctor showPopupCallback; + ClickFunctor gestureCallback; + std::shared_ptr image; + + CComponentHolder(const Rect & area, const Point & selectionOversize); void setClickPressedCallback(const ClickFunctor & callback); void setShowPopupCallback(const ClickFunctor & callback); void setGestureCallback(const ClickFunctor & callback); void clickPressed(const Point & cursorPosition) override; void showPopupWindow(const Point & cursorPosition) override; void gesture(bool on, const Point & initialPosition, const Point & finalPosition) override; +}; + +class CArtPlace : public CComponentHolder +{ +public: + ArtifactPosition slot; + + CArtPlace(Point position, const ArtifactID & newArtId = ArtifactID::NONE, const SpellID & newSpellId = SpellID::NONE); + void setArtifact(const SpellID & newSpellId); + void setArtifact(const ArtifactID & newArtId, const SpellID & newSpellId = SpellID::NONE); + ArtifactID getArtifactId() const; + void lockSlot(bool on); + bool isLocked() const; void addCombinedArtInfo(const std::map> & arts); private: - const CArtifactInstance * ourArt; + ArtifactID artId; + SpellID spellId; bool locked; - int imageIndex; - std::shared_ptr image; - ClickFunctor clickPressedCallback; - ClickFunctor showPopupCallback; - ClickFunctor gestureCallback; - -protected: - void setInternals(const CArtifactInstance * artInst); + int32_t imageIndex; }; class CCommanderArtPlace : public CArtPlace @@ -55,7 +61,26 @@ private: void returnArtToHeroCallback(); public: - CCommanderArtPlace(Point position, const CGHeroInstance * commanderOwner, ArtifactPosition artSlot, const CArtifactInstance * art = nullptr); + CCommanderArtPlace(Point position, const CGHeroInstance * commanderOwner, ArtifactPosition artSlot, + const ArtifactID & artId = ArtifactID::NONE, const SpellID & spellId = SpellID::NONE); void clickPressed(const Point & cursorPosition) override; void showPopupWindow(const Point & cursorPosition) override; }; + +class CSecSkillPlace : public CComponentHolder +{ +public: + enum class ImageSize + { + LARGE, + MEDIUM, + SMALL + }; + + CSecSkillPlace(const Point & position, const ImageSize & imageSize, const SecondarySkill & skillId = SecondarySkill::NONE, const uint8_t level = 0); + void setSkill(const SecondarySkill & newSkillId, const uint8_t level = 0); + void setLevel(const uint8_t level); + +private: + SecondarySkill skillId; +}; diff --git a/client/widgets/CGarrisonInt.cpp b/client/widgets/CGarrisonInt.cpp index 2880ffcaf..78a809e72 100644 --- a/client/widgets/CGarrisonInt.cpp +++ b/client/widgets/CGarrisonInt.cpp @@ -274,12 +274,12 @@ bool CGarrisonSlot::mustForceReselection() const if (!LOCPLINT->makingTurn) return true; - if (!creature || !selection->creature) - return false; - // Attempt to take creatures from ally (select theirs first) if (!selection->our()) return true; + + if (!creature || !selection->creature) + return false; // Attempt to swap creatures with ally (select ours first) if (selection->creature != creature && withAlly) @@ -370,10 +370,13 @@ void CGarrisonSlot::gesture(bool on, const Point & initialPosition, const Point if (!settings["input"]["radialWheelGarrisonSwipe"].Bool()) return; + if(GH.windows().topWindow()->isPopupWindow()) + return; + const auto * otherArmy = upg == EGarrisonType::UPPER ? owner->lowerArmy() : owner->upperArmy(); bool stackExists = myStack != nullptr; - bool hasSameUnit = stackExists && !owner->army(upg)->getCreatureSlots(myStack->type, ID).empty(); + bool hasSameUnit = stackExists && !owner->army(upg)->getCreatureSlots(myStack->getCreature(), ID).empty(); bool hasOwnEmptySlots = stackExists && owner->army(upg)->getFreeSlot() != SlotID(); bool exchangeMode = stackExists && owner->upperArmy() && owner->lowerArmy(); bool hasOtherEmptySlots = exchangeMode && otherArmy->getFreeSlot() != SlotID(); @@ -398,7 +401,7 @@ void CGarrisonSlot::update() { addUsedEvents(LCLICK | SHOW_POPUP | GESTURE | HOVER); myStack = getObj()->getStackPtr(ID); - creature = myStack ? myStack->type : nullptr; + creature = myStack ? myStack->getCreature() : nullptr; } else { @@ -426,7 +429,7 @@ CGarrisonSlot::CGarrisonSlot(CGarrisonInt * Owner, int x, int y, SlotID IID, EGa : ID(IID), owner(Owner), myStack(creature_), - creature(creature_ ? creature_->type : nullptr), + creature(creature_ ? creature_->getCreature() : nullptr), upg(Upg) { OBJECT_CONSTRUCTION; diff --git a/client/widgets/Images.cpp b/client/widgets/Images.cpp index 06a16a6d4..4cbb6be1a 100644 --- a/client/widgets/Images.cpp +++ b/client/widgets/Images.cpp @@ -20,6 +20,7 @@ #include "../render/CAnimation.h" #include "../render/Canvas.h" #include "../render/ColorFilter.h" +#include "../render/Colors.h" #include "../battle/BattleInterface.h" #include "../battle/BattleInterfaceClasses.h" @@ -194,12 +195,12 @@ CAnimImage::CAnimImage(const AnimationPath & name, size_t Frame, size_t Group, i { pos.x += x; pos.y += y; - anim = GH.renderHandler().loadAnimation(name, EImageBlitMode::COLORKEY); + anim = GH.renderHandler().loadAnimation(name, (Flags & CCreatureAnim::CREATURE_MODE) ? EImageBlitMode::WITH_SHADOW_AND_OVERLAY : EImageBlitMode::COLORKEY); init(); } CAnimImage::CAnimImage(const AnimationPath & name, size_t Frame, Rect targetPos, size_t Group, ui8 Flags): - anim(GH.renderHandler().loadAnimation(name, EImageBlitMode::COLORKEY)), + anim(GH.renderHandler().loadAnimation(name, (Flags & CCreatureAnim::CREATURE_MODE) ? EImageBlitMode::WITH_SHADOW_AND_OVERLAY : EImageBlitMode::COLORKEY)), frame(Frame), group(Group), flags(Flags), @@ -317,7 +318,7 @@ bool CAnimImage::isPlayerColored() const } CShowableAnim::CShowableAnim(int x, int y, const AnimationPath & name, ui8 Flags, ui32 frameTime, size_t Group, uint8_t alpha): - anim(GH.renderHandler().loadAnimation(name, (Flags & CREATURE_MODE) ? EImageBlitMode::ALPHA : EImageBlitMode::COLORKEY)), + anim(GH.renderHandler().loadAnimation(name, (Flags & CREATURE_MODE) ? EImageBlitMode::WITH_SHADOW_AND_OVERLAY : EImageBlitMode::COLORKEY)), group(Group), frame(0), first(0), @@ -430,9 +431,8 @@ void CShowableAnim::blitImage(size_t frame, size_t group, Canvas & to) auto img = anim->getImage(frame, group); if(img) { - if (flags & CREATURE_MODE) - img->setShadowEnabled(true); img->setAlpha(alpha); + img->setOverlayColor(Colors::TRANSPARENCY); to.draw(img, pos.topLeft(), src); } } diff --git a/client/widgets/MiscWidgets.cpp b/client/widgets/MiscWidgets.cpp index e7e7c655d..c6e888571 100644 --- a/client/widgets/MiscWidgets.cpp +++ b/client/widgets/MiscWidgets.cpp @@ -280,7 +280,7 @@ void CArmyTooltip::init(const InfoAboutArmy &army) continue; } - icons.push_back(std::make_shared(AnimationPath::builtin("CPRSMALL"), slot.second.type->getIconIndex(), 0, slotsPos[slot.first.getNum()].x, slotsPos[slot.first.getNum()].y)); + icons.push_back(std::make_shared(AnimationPath::builtin("CPRSMALL"), slot.second.getType()->getIconIndex(), 0, slotsPos[slot.first.getNum()].x, slotsPos[slot.first.getNum()].y)); std::string subtitle; if(army.army.isDetailed) @@ -468,8 +468,8 @@ void CInteractableTownTooltip::init(const CGTownInstance * town) LOCPLINT->showTavernWindow(town, nullptr, QueryID::NONE); } }, [town]{ - if(!town->town->faction->getDescriptionTranslated().empty()) - CRClickPopup::createAndPush(town->town->faction->getDescriptionTranslated()); + if(!town->getFaction()->getDescriptionTranslated().empty()) + CRClickPopup::createAndPush(town->getFaction()->getDescriptionTranslated()); }); fastMarket = std::make_shared(Rect(143, 31, 30, 34), []() { @@ -532,8 +532,7 @@ CreatureTooltip::CreatureTooltip(Point pos, const CGCreature * creature) { OBJECT_CONSTRUCTION; - auto creatureID = creature->getCreature(); - int32_t creatureIconIndex = CGI->creatures()->getById(creatureID)->getIconIndex(); + int32_t creatureIconIndex = creature->getCreature()->getIconIndex(); creatureImage = std::make_shared(AnimationPath::builtin("TWCRPORT"), creatureIconIndex); creatureImage->center(Point(parent->pos.x + parent->pos.w / 2, parent->pos.y + creatureImage->pos.h / 2 + 11)); @@ -574,7 +573,8 @@ void MoraleLuckBox::set(const AFactionMember * node) boost::algorithm::replace_first(text,"%s",CGI->generaltexth->arraytxt[neutralDescr[morale]-mrlt]); if (morale && node && (node->getBonusBearer()->hasBonusOfType(BonusType::UNDEAD) - || node->getBonusBearer()->hasBonusOfType(BonusType::NON_LIVING))) + || node->getBonusBearer()->hasBonusOfType(BonusType::NON_LIVING) + || node->getBonusBearer()->hasBonusOfType(BonusType::MECHANICAL))) { text += CGI->generaltexth->arraytxt[113]; //unaffected by morale component.value = 0; @@ -582,13 +582,13 @@ void MoraleLuckBox::set(const AFactionMember * node) else if(morale && node && node->getBonusBearer()->hasBonusOfType(BonusType::NO_MORALE)) { auto noMorale = node->getBonusBearer()->getBonus(Selector::type()(BonusType::NO_MORALE)); - text += "\n" + noMorale->Description(); + text += "\n" + noMorale->Description(LOCPLINT->cb.get()); component.value = 0; } else if (!morale && node && node->getBonusBearer()->hasBonusOfType(BonusType::NO_LUCK)) { auto noLuck = node->getBonusBearer()->getBonus(Selector::type()(BonusType::NO_LUCK)); - text += "\n" + noLuck->Description(); + text += "\n" + noLuck->Description(LOCPLINT->cb.get()); component.value = 0; } else @@ -597,7 +597,7 @@ void MoraleLuckBox::set(const AFactionMember * node) for(auto & bonus : * modifierList) { if(bonus->val) { - const std::string& description = bonus->Description(); + const std::string& description = bonus->Description(LOCPLINT->cb.get()); //arraytxt already contains \n if (description.size() && description[0] != '\n') addInfo += '\n'; @@ -633,7 +633,7 @@ CCreaturePic::CCreaturePic(int x, int y, const CCreature * cre, bool Big, bool A pos.x+=x; pos.y+=y; - auto faction = cre->getFaction(); + auto faction = cre->getFactionID(); assert(CGI->townh->size() > faction); diff --git a/client/widgets/ObjectLists.cpp b/client/widgets/ObjectLists.cpp index d2ad5c1ae..6470df4a5 100644 --- a/client/widgets/ObjectLists.cpp +++ b/client/widgets/ObjectLists.cpp @@ -116,12 +116,12 @@ void CListBox::updatePositions() (elem)->moveTo(itemPos); itemPos += itemOffset; } - if (isActive()) + if(slider) { - redraw(); - if (slider) - slider->scrollTo((int)first); + slider->scrollTo((int)first); + moveChildForeground(slider.get()); } + redraw(); } void CListBox::reset() diff --git a/client/widgets/TextControls.cpp b/client/widgets/TextControls.cpp index 8975fd469..16745214f 100644 --- a/client/widgets/TextControls.cpp +++ b/client/widgets/TextControls.cpp @@ -176,6 +176,11 @@ void CMultiLineLabel::setText(const std::string & Txt) CLabel::setText(Txt); } +std::vector CMultiLineLabel::getLines() +{ + return lines; +} + void CTextContainer::blitLine(Canvas & to, Rect destRect, std::string what) { const auto f = GH.renderHandler().loadFont(font); @@ -309,7 +314,7 @@ void CMultiLineLabel::splitText(const std::string & Txt, bool redrawAfter) lines.clear(); const auto & fontPtr = GH.renderHandler().loadFont(font); - int lineHeight = static_cast(fontPtr->getLineHeight()); + int lineHeight = fontPtr->getLineHeight(); lines = CMessage::breakText(Txt, pos.w, font); @@ -330,16 +335,16 @@ Rect CMultiLineLabel::getTextLocation() return pos; const auto & fontPtr = GH.renderHandler().loadFont(font); - Point textSize(pos.w, fontPtr->getLineHeight() * (int)lines.size()); - Point textOffset(pos.w - textSize.x, pos.h - textSize.y); + Point textSizeComputed(pos.w, fontPtr->getLineHeight() * lines.size()); //FIXME: how is this different from textSize member? + Point textOffset(pos.w - textSizeComputed.x, pos.h - textSizeComputed.y); switch(alignment) { - case ETextAlignment::TOPLEFT: return Rect(pos.topLeft(), textSize); - case ETextAlignment::TOPCENTER: return Rect(pos.topLeft(), textSize); - case ETextAlignment::CENTER: return Rect(pos.topLeft() + textOffset / 2, textSize); - case ETextAlignment::CENTERRIGHT: return Rect(pos.topLeft() + Point(textOffset.x, textOffset.y / 2), textSize); - case ETextAlignment::BOTTOMRIGHT: return Rect(pos.topLeft() + textOffset, textSize); + case ETextAlignment::TOPLEFT: return Rect(pos.topLeft(), textSizeComputed); + case ETextAlignment::TOPCENTER: return Rect(pos.topLeft(), textSizeComputed); + case ETextAlignment::CENTER: return Rect(pos.topLeft() + textOffset / 2, textSizeComputed); + case ETextAlignment::CENTERRIGHT: return Rect(pos.topLeft() + Point(textOffset.x, textOffset.y / 2), textSizeComputed); + case ETextAlignment::BOTTOMRIGHT: return Rect(pos.topLeft() + textOffset, textSizeComputed); } assert(0); return Rect(); diff --git a/client/widgets/TextControls.h b/client/widgets/TextControls.h index 6880e6b44..0c99c193b 100644 --- a/client/widgets/TextControls.h +++ b/client/widgets/TextControls.h @@ -96,6 +96,7 @@ public: CMultiLineLabel(Rect position, EFonts Font = FONT_SMALL, ETextAlignment Align = ETextAlignment::TOPLEFT, const ColorRGBA & Color = Colors::WHITE, const std::string & Text = ""); void setText(const std::string & Txt) override; + std::vector getLines(); void showAll(Canvas & to) override; void setVisibleSize(Rect visibleSize, bool redrawElement = true); diff --git a/client/widgets/VideoWidget.cpp b/client/widgets/VideoWidget.cpp index 0fa6570cf..4c8b786f3 100644 --- a/client/widgets/VideoWidget.cpp +++ b/client/widgets/VideoWidget.cpp @@ -9,12 +9,16 @@ */ #include "StdInc.h" #include "VideoWidget.h" +#include "TextControls.h" #include "../CGameInfo.h" #include "../gui/CGuiHandler.h" #include "../media/ISoundPlayer.h" #include "../media/IVideoPlayer.h" #include "../render/Canvas.h" +#include "../render/IScreenHandler.h" + +#include "../../lib/filesystem/Filesystem.h" VideoWidgetBase::VideoWidgetBase(const Point & position, const VideoPath & video, bool playAudio) : VideoWidgetBase(position, video, playAudio, 1.0) @@ -33,11 +37,41 @@ VideoWidgetBase::~VideoWidgetBase() = default; void VideoWidgetBase::playVideo(const VideoPath & fileToPlay) { - videoInstance = CCS->videoh->open(fileToPlay, scaleFactor); + OBJECT_CONSTRUCTION; + + JsonPath subTitlePath = fileToPlay.toType(); + JsonPath subTitlePathVideoDir = subTitlePath.addPrefix("VIDEO/"); + if(CResourceHandler::get()->existsResource(subTitlePath)) + subTitleData = JsonNode(subTitlePath); + else if(CResourceHandler::get()->existsResource(subTitlePathVideoDir)) + subTitleData = JsonNode(subTitlePathVideoDir); + + float preScaleFactor = 1; + VideoPath videoFile = fileToPlay; + if(GH.screenHandler().getScalingFactor() > 1) + { + std::vector factorsToCheck = {GH.screenHandler().getScalingFactor(), 4, 3, 2}; + for(auto factorToCheck : factorsToCheck) + { + std::string name = boost::algorithm::to_upper_copy(videoFile.getName()); + boost::replace_all(name, "VIDEO/", std::string("VIDEO") + std::to_string(factorToCheck) + std::string("X/")); + auto p = VideoPath::builtin(name).addPrefix("VIDEO" + std::to_string(factorToCheck) + "X/"); + if(CResourceHandler::get()->existsResource(p)) + { + preScaleFactor = 1.0 / static_cast(factorToCheck); + videoFile = p; + break; + } + } + } + + videoInstance = CCS->videoh->open(videoFile, scaleFactor * preScaleFactor); if (videoInstance) { pos.w = videoInstance->size().x; pos.h = videoInstance->size().y; + if(!subTitleData.isNull()) + subTitle = std::make_unique(Rect(0, (pos.h / 5) * 4, pos.w, pos.h / 5), EFonts::FONT_HIGH_SCORE, ETextAlignment::CENTER, Colors::WHITE); } if (playAudio) @@ -52,6 +86,8 @@ void VideoWidgetBase::show(Canvas & to) { if(videoInstance) videoInstance->show(pos.topLeft(), to); + if(subTitle) + subTitle->showAll(to); } void VideoWidgetBase::loadAudio(const VideoPath & fileToPlay) @@ -77,7 +113,7 @@ void VideoWidgetBase::startAudio() { this->audioHandle = -1; } - ); + ); } } @@ -91,22 +127,43 @@ void VideoWidgetBase::stopAudio() } } +std::string VideoWidgetBase::getSubTitleLine(double timestamp) +{ + if(subTitleData.isNull()) + return {}; + + for(auto & segment : subTitleData.Vector()) + if(timestamp > segment["timeStart"].Float() && timestamp < segment["timeEnd"].Float()) + return segment["text"].String(); + + return {}; +} + void VideoWidgetBase::activate() { CIntObject::activate(); - startAudio(); + if(audioHandle != -1) + CCS->soundh->resumeSound(audioHandle); + else + startAudio(); + if(videoInstance) + videoInstance->activate(); } void VideoWidgetBase::deactivate() { CIntObject::deactivate(); - stopAudio(); + CCS->soundh->pauseSound(audioHandle); + if(videoInstance) + videoInstance->deactivate(); } void VideoWidgetBase::showAll(Canvas & to) { if(videoInstance) videoInstance->show(pos.topLeft(), to); + if(subTitle) + subTitle->showAll(to); } void VideoWidgetBase::tick(uint32_t msPassed) @@ -122,6 +179,8 @@ void VideoWidgetBase::tick(uint32_t msPassed) onPlaybackFinished(); } } + if(subTitle && videoInstance) + subTitle->setText(getSubTitleLine(videoInstance->timeStamp())); } VideoWidget::VideoWidget(const Point & position, const VideoPath & prologue, const VideoPath & looped, bool playAudio) diff --git a/client/widgets/VideoWidget.h b/client/widgets/VideoWidget.h index b0264d58d..4e2672d68 100644 --- a/client/widgets/VideoWidget.h +++ b/client/widgets/VideoWidget.h @@ -12,21 +12,26 @@ #include "../gui/CIntObject.h" #include "../lib/filesystem/ResourcePath.h" +#include "../lib/json/JsonNode.h" class IVideoInstance; +class CMultiLineLabel; class VideoWidgetBase : public CIntObject { std::unique_ptr videoInstance; + std::unique_ptr subTitle; std::pair, si64> audioData = {nullptr, 0}; int audioHandle = -1; bool playAudio = false; float scaleFactor = 1.0; + JsonNode subTitleData; void loadAudio(const VideoPath & file); void startAudio(); void stopAudio(); + std::string getSubTitleLine(double timestamp); protected: VideoWidgetBase(const Point & position, const VideoPath & video, bool playAudio); diff --git a/client/widgets/markets/CAltarArtifacts.cpp b/client/widgets/markets/CAltarArtifacts.cpp index 4f57e543e..81ab7146f 100644 --- a/client/widgets/markets/CAltarArtifacts.cpp +++ b/client/widgets/markets/CAltarArtifacts.cpp @@ -58,7 +58,7 @@ CAltarArtifacts::CAltarArtifacts(const IMarket * market, const CGHeroInstance * CAltarArtifacts::onSlotClickPressed(altarSlot, offerTradePanel); }); offerTradePanel->updateSlotsCallback = std::bind(&CAltarArtifacts::updateAltarSlots, this); - offerTradePanel->moveTo(pos.topLeft() + Point(315, 52)); + offerTradePanel->moveTo(pos.topLeft() + Point(315, 53)); CMarketBase::updateShowcases(); CAltarArtifacts::deselect(); @@ -201,7 +201,7 @@ void CAltarArtifacts::onSlotClickPressed(const std::shared_ptr & { if(pickedArtInst->canBePutAt(altarArtifactsStorage)) { - if(pickedArtInst->artType->isTradable()) + if(pickedArtInst->getType()->isTradable()) { if(altarSlot->id == -1) tradeSlotsMap.try_emplace(altarSlot, pickedArtInst); diff --git a/client/widgets/markets/CAltarCreatures.cpp b/client/widgets/markets/CAltarCreatures.cpp index 896e0644b..c61906f18 100644 --- a/client/widgets/markets/CAltarCreatures.cpp +++ b/client/widgets/markets/CAltarCreatures.cpp @@ -162,9 +162,9 @@ void CAltarCreatures::makeDeal() for(int & units : unitsOnAltar) units = 0; - for(auto heroSlot : offerTradePanel->slots) + for(const auto & heroSlot : offerTradePanel->slots) { - heroSlot->setType(EType::CREATURE_PLACEHOLDER); + heroSlot->setID(CreatureID::NONE); heroSlot->subtitle->clear(); } deselect(); @@ -175,16 +175,16 @@ CMarketBase::MarketShowcasesParams CAltarCreatures::getShowcasesParams() const std::optional bidSelected = std::nullopt; std::optional offerSelected = std::nullopt; if(bidTradePanel->isHighlighted()) - bidSelected = ShowcaseParams {std::to_string(offerSlider->getValue()), CGI->creatures()->getByIndex(bidTradePanel->getSelectedItemId())->getIconIndex()}; + bidSelected = ShowcaseParams {std::to_string(offerSlider->getValue()), CGI->creatures()->getByIndex(bidTradePanel->getHighlightedItemId())->getIconIndex()}; if(offerTradePanel->isHighlighted() && offerSlider->getValue() > 0) - offerSelected = ShowcaseParams {offerTradePanel->highlightedSlot->subtitle->getText(), CGI->creatures()->getByIndex(offerTradePanel->getSelectedItemId())->getIconIndex()}; + offerSelected = ShowcaseParams {offerTradePanel->highlightedSlot->subtitle->getText(), CGI->creatures()->getByIndex(offerTradePanel->getHighlightedItemId())->getIconIndex()}; return MarketShowcasesParams {bidSelected, offerSelected}; } void CAltarCreatures::sacrificeAll() { std::optional lastSlot; - for(auto heroSlot : bidTradePanel->slots) + for(const auto & heroSlot : bidTradePanel->slots) { auto stackCount = hero->getStackCount(SlotID(heroSlot->serial)); if(stackCount > unitsOnAltar[heroSlot->serial]) @@ -211,7 +211,8 @@ void CAltarCreatures::sacrificeAll() void CAltarCreatures::updateAltarSlot(const std::shared_ptr & slot) { auto units = unitsOnAltar[slot->serial]; - slot->setType(units > 0 ? EType::CREATURE : EType::CREATURE_PLACEHOLDER); + const auto [oppositeSlot, oppositePanel] = getOpposite(slot); + slot->setID(units > 0 ? oppositeSlot->id : CreatureID::NONE); slot->subtitle->setText(units > 0 ? boost::str(boost::format(CGI->generaltexth->allTexts[122]) % std::to_string(hero->calculateXp(units * expPerUnit[slot->serial]))) : ""); } @@ -234,21 +235,9 @@ void CAltarCreatures::onSlotClickPressed(const std::shared_ptr & if(newSlot == curPanel->highlightedSlot) return; - auto oppositePanel = bidTradePanel; curPanel->onSlotClickPressed(newSlot); - if(curPanel->highlightedSlot == bidTradePanel->highlightedSlot) - { - oppositePanel = offerTradePanel; - } - std::shared_ptr oppositeNewSlot; - for(const auto & slot : oppositePanel->slots) - if(slot->serial == newSlot->serial) - { - oppositeNewSlot = slot; - break; - } - assert(oppositeNewSlot); - oppositePanel->onSlotClickPressed(oppositeNewSlot); + auto [oppositeSlot, oppositePanel] = getOpposite(newSlot); + oppositePanel->onSlotClickPressed(oppositeSlot); highlightingChanged(); redraw(); } @@ -258,7 +247,7 @@ std::string CAltarCreatures::getTraderText() if(bidTradePanel->isHighlighted() && offerTradePanel->isHighlighted()) { MetaString message = MetaString::createFromTextID("core.genrltxt.484"); - message.replaceNamePlural(CreatureID(bidTradePanel->getSelectedItemId())); + message.replaceNamePlural(CreatureID(bidTradePanel->getHighlightedItemId())); return message.toString(); } else @@ -266,3 +255,22 @@ std::string CAltarCreatures::getTraderText() return ""; } } + +std::tuple, std::shared_ptr> CAltarCreatures::getOpposite( + const std::shared_ptr & curSlot) +{ + assert(curSlot); + + auto oppositePanel = bidTradePanel; + if(vstd::contains(bidTradePanel->slots, curSlot)) + oppositePanel = offerTradePanel; + + std::shared_ptr oppositeSlot; + for(const auto & slot : oppositePanel->slots) + if (slot->serial == curSlot->serial) + { + oppositeSlot = slot; + break; + } + return std::make_tuple(oppositeSlot, oppositePanel); +} diff --git a/client/widgets/markets/CAltarCreatures.h b/client/widgets/markets/CAltarCreatures.h index bf5654919..a5407d74d 100644 --- a/client/widgets/markets/CAltarCreatures.h +++ b/client/widgets/markets/CAltarCreatures.h @@ -33,4 +33,5 @@ private: void onOfferSliderMoved(int newVal) override; void onSlotClickPressed(const std::shared_ptr & newSlot, std::shared_ptr & curPanel) override; std::string getTraderText() override; + std::tuple, std::shared_ptr> getOpposite(const std::shared_ptr & curSlot); }; diff --git a/client/widgets/markets/CArtifactsBuying.cpp b/client/widgets/markets/CArtifactsBuying.cpp index 853bc848b..0ae8a8312 100644 --- a/client/widgets/markets/CArtifactsBuying.cpp +++ b/client/widgets/markets/CArtifactsBuying.cpp @@ -46,7 +46,7 @@ CArtifactsBuying::CArtifactsBuying(const IMarket * market, const CGHeroInstance CArtifactsBuying::onSlotClickPressed(newSlot, offerTradePanel); }, [this]() { - CMarketBase::updateSubtitlesForBid(EMarketMode::RESOURCE_ARTIFACT, bidTradePanel->getSelectedItemId()); + CMarketBase::updateSubtitlesForBid(EMarketMode::RESOURCE_ARTIFACT, bidTradePanel->getHighlightedItemId()); }, market->availableItemsIds(EMarketMode::RESOURCE_ARTIFACT)); offerTradePanel->deleteSlotsCheck = [this](const std::shared_ptr & slot) { @@ -66,10 +66,10 @@ void CArtifactsBuying::deselect() void CArtifactsBuying::makeDeal() { - if(ArtifactID(offerTradePanel->getSelectedItemId()).toArtifact()->canBePutAt(hero)) + if(ArtifactID(offerTradePanel->getHighlightedItemId()).toArtifact()->canBePutAt(hero)) { - LOCPLINT->cb->trade(market->getObjInstanceID(), EMarketMode::RESOURCE_ARTIFACT, GameResID(bidTradePanel->getSelectedItemId()), - ArtifactID(offerTradePanel->getSelectedItemId()), offerQty, hero); + LOCPLINT->cb->trade(market->getObjInstanceID(), EMarketMode::RESOURCE_ARTIFACT, GameResID(bidTradePanel->getHighlightedItemId()), + ArtifactID(offerTradePanel->getHighlightedItemId()), offerQty, hero); CMarketTraderText::makeDeal(); deselect(); } @@ -84,8 +84,8 @@ CMarketBase::MarketShowcasesParams CArtifactsBuying::getShowcasesParams() const if(bidTradePanel->isHighlighted() && offerTradePanel->isHighlighted()) return MarketShowcasesParams { - ShowcaseParams {std::to_string(deal->isBlocked() ? 0 : bidQty), bidTradePanel->getSelectedItemId()}, - ShowcaseParams {std::to_string(deal->isBlocked() ? 0 : offerQty), CGI->artifacts()->getByIndex(offerTradePanel->getSelectedItemId())->getIconIndex()} + ShowcaseParams {std::to_string(deal->isBlocked() ? 0 : bidQty), bidTradePanel->getHighlightedItemId()}, + ShowcaseParams {std::to_string(deal->isBlocked() ? 0 : offerQty), CGI->artifacts()->getByIndex(offerTradePanel->getHighlightedItemId())->getIconIndex()} }; else return MarketShowcasesParams {std::nullopt, std::nullopt}; @@ -95,8 +95,8 @@ void CArtifactsBuying::highlightingChanged() { if(bidTradePanel->isHighlighted() && offerTradePanel->isHighlighted()) { - market->getOffer(bidTradePanel->getSelectedItemId(), offerTradePanel->getSelectedItemId(), bidQty, offerQty, EMarketMode::RESOURCE_ARTIFACT); - deal->block(LOCPLINT->cb->getResourceAmount(GameResID(bidTradePanel->getSelectedItemId())) < bidQty || !LOCPLINT->makingTurn); + market->getOffer(bidTradePanel->getHighlightedItemId(), offerTradePanel->getHighlightedItemId(), bidQty, offerQty, EMarketMode::RESOURCE_ARTIFACT); + deal->block(LOCPLINT->cb->getResourceAmount(GameResID(bidTradePanel->getHighlightedItemId())) < bidQty || !LOCPLINT->makingTurn); } CMarketBase::highlightingChanged(); CMarketTraderText::highlightingChanged(); @@ -107,10 +107,10 @@ std::string CArtifactsBuying::getTraderText() if(bidTradePanel->isHighlighted() && offerTradePanel->isHighlighted()) { MetaString message = MetaString::createFromTextID("core.genrltxt.267"); - message.replaceName(ArtifactID(offerTradePanel->getSelectedItemId())); + message.replaceName(ArtifactID(offerTradePanel->getHighlightedItemId())); message.replaceNumber(bidQty); message.replaceTextID(bidQty == 1 ? "core.genrltxt.161" : "core.genrltxt.160"); - message.replaceName(GameResID(bidTradePanel->getSelectedItemId())); + message.replaceName(GameResID(bidTradePanel->getHighlightedItemId())); return message.toString(); } else diff --git a/client/widgets/markets/CArtifactsSelling.cpp b/client/widgets/markets/CArtifactsSelling.cpp index dbbe41278..b31de19e7 100644 --- a/client/widgets/markets/CArtifactsSelling.cpp +++ b/client/widgets/markets/CArtifactsSelling.cpp @@ -79,7 +79,7 @@ void CArtifactsSelling::makeDeal() const auto art = hero->getArt(selectedHeroSlot); assert(art); LOCPLINT->cb->trade(market->getObjInstanceID(), EMarketMode::ARTIFACT_RESOURCE, art->getId(), - GameResID(offerTradePanel->getSelectedItemId()), offerQty, hero); + GameResID(offerTradePanel->getHighlightedItemId()), offerQty, hero); CMarketTraderText::makeDeal(); } @@ -129,7 +129,7 @@ CMarketBase::MarketShowcasesParams CArtifactsSelling::getShowcasesParams() const return MarketShowcasesParams { std::nullopt, - ShowcaseParams {std::to_string(offerQty), offerTradePanel->getSelectedItemId()} + ShowcaseParams {std::to_string(offerQty), offerTradePanel->getHighlightedItemId()} }; else return MarketShowcasesParams {std::nullopt, std::nullopt}; @@ -147,7 +147,7 @@ void CArtifactsSelling::highlightingChanged() const auto art = hero->getArt(selectedHeroSlot); if(art && offerTradePanel->isHighlighted()) { - market->getOffer(art->getTypeId(), offerTradePanel->getSelectedItemId(), bidQty, offerQty, EMarketMode::ARTIFACT_RESOURCE); + market->getOffer(art->getTypeId(), offerTradePanel->getHighlightedItemId(), bidQty, offerQty, EMarketMode::ARTIFACT_RESOURCE); deal->block(!LOCPLINT->makingTurn); } CMarketBase::highlightingChanged(); @@ -162,7 +162,7 @@ std::string CArtifactsSelling::getTraderText() MetaString message = MetaString::createFromTextID("core.genrltxt.268"); message.replaceNumber(offerQty); message.replaceRawString(offerQty == 1 ? CGI->generaltexth->allTexts[161] : CGI->generaltexth->allTexts[160]); - message.replaceName(GameResID(offerTradePanel->getSelectedItemId())); + message.replaceName(GameResID(offerTradePanel->getHighlightedItemId())); message.replaceName(art->getTypeId()); return message.toString(); } diff --git a/client/widgets/markets/CFreelancerGuild.cpp b/client/widgets/markets/CFreelancerGuild.cpp index 19c138d1b..cf2081ea2 100644 --- a/client/widgets/markets/CFreelancerGuild.cpp +++ b/client/widgets/markets/CFreelancerGuild.cpp @@ -29,7 +29,7 @@ CFreelancerGuild::CFreelancerGuild(const IMarket * market, const CGHeroInstance : CMarketBase(market, hero) , CResourcesBuying( [this](const std::shared_ptr & heroSlot){CFreelancerGuild::onSlotClickPressed(heroSlot, offerTradePanel);}, - [this](){CMarketBase::updateSubtitlesForBid(EMarketMode::CREATURE_RESOURCE, bidTradePanel->getSelectedItemId());}) + [this](){CMarketBase::updateSubtitlesForBid(EMarketMode::CREATURE_RESOURCE, bidTradePanel->getHighlightedItemId());}) , CMarketSlider([this](int newVal){CMarketSlider::onOfferSliderMoved(newVal);}) { OBJECT_CONSTRUCTION; @@ -69,7 +69,7 @@ void CFreelancerGuild::makeDeal() { if(auto toTrade = offerSlider->getValue(); toTrade != 0) { - LOCPLINT->cb->trade(market->getObjInstanceID(), EMarketMode::CREATURE_RESOURCE, SlotID(bidTradePanel->highlightedSlot->serial), GameResID(offerTradePanel->getSelectedItemId()), bidQty * toTrade, hero); + LOCPLINT->cb->trade(market->getObjInstanceID(), EMarketMode::CREATURE_RESOURCE, SlotID(bidTradePanel->highlightedSlot->serial), GameResID(offerTradePanel->getHighlightedItemId()), bidQty * toTrade, hero); CMarketTraderText::makeDeal(); deselect(); } @@ -80,8 +80,8 @@ CMarketBase::MarketShowcasesParams CFreelancerGuild::getShowcasesParams() const if(bidTradePanel->isHighlighted() && offerTradePanel->isHighlighted()) return MarketShowcasesParams { - ShowcaseParams {std::to_string(bidQty * offerSlider->getValue()), CGI->creatures()->getByIndex(bidTradePanel->getSelectedItemId())->getIconIndex()}, - ShowcaseParams {std::to_string(offerQty * offerSlider->getValue()), offerTradePanel->getSelectedItemId()} + ShowcaseParams {std::to_string(bidQty * offerSlider->getValue()), CGI->creatures()->getByIndex(bidTradePanel->getHighlightedItemId())->getIconIndex()}, + ShowcaseParams {std::to_string(offerQty * offerSlider->getValue()), offerTradePanel->getHighlightedItemId()} }; else return MarketShowcasesParams {std::nullopt, std::nullopt}; @@ -91,7 +91,7 @@ void CFreelancerGuild::highlightingChanged() { if(bidTradePanel->isHighlighted() && offerTradePanel->isHighlighted()) { - market->getOffer(bidTradePanel->getSelectedItemId(), offerTradePanel->getSelectedItemId(), bidQty, offerQty, EMarketMode::CREATURE_RESOURCE); + market->getOffer(bidTradePanel->getHighlightedItemId(), offerTradePanel->getHighlightedItemId(), bidQty, offerQty, EMarketMode::CREATURE_RESOURCE); offerSlider->setAmount((hero->getStackCount(SlotID(bidTradePanel->highlightedSlot->serial)) - (hero->stacksCount() == 1 && hero->needsLastStack() ? 1 : 0)) / bidQty); offerSlider->scrollTo(0); offerSlider->block(false); @@ -109,12 +109,12 @@ std::string CFreelancerGuild::getTraderText() MetaString message = MetaString::createFromTextID("core.genrltxt.269"); message.replaceNumber(offerQty); message.replaceRawString(offerQty == 1 ? CGI->generaltexth->allTexts[161] : CGI->generaltexth->allTexts[160]); - message.replaceName(GameResID(offerTradePanel->getSelectedItemId())); + message.replaceName(GameResID(offerTradePanel->getHighlightedItemId())); message.replaceNumber(bidQty); if(bidQty == 1) - message.replaceNameSingular(bidTradePanel->getSelectedItemId()); + message.replaceNameSingular(bidTradePanel->getHighlightedItemId()); else - message.replaceNamePlural(bidTradePanel->getSelectedItemId()); + message.replaceNamePlural(bidTradePanel->getHighlightedItemId()); return message.toString(); } else diff --git a/client/widgets/markets/CMarketBase.cpp b/client/widgets/markets/CMarketBase.cpp index 02574dc67..d7b75f1a6 100644 --- a/client/widgets/markets/CMarketBase.cpp +++ b/client/widgets/markets/CMarketBase.cpp @@ -23,9 +23,9 @@ #include "../../../CCallback.h" +#include "../../../lib/entities/hero/CHeroHandler.h" #include "../../../lib/texts/CGeneralTextHandler.h" #include "../../../lib/mapObjects/CGHeroInstance.h" -#include "../../../lib/CHeroHandler.h" #include "../../../lib/mapObjects/CGMarket.h" CMarketBase::CMarketBase(const IMarket * market, const CGHeroInstance * hero) @@ -93,7 +93,7 @@ void CMarketBase::updateSubtitlesForBid(EMarketMode marketMode, int bidId) void CMarketBase::updateShowcases() { - const auto updateSelectedBody = [](const std::shared_ptr & tradePanel, const std::optional & params) + const auto updateShowcase = [](const std::shared_ptr & tradePanel, const std::optional & params) { if(params.has_value()) { @@ -109,9 +109,9 @@ void CMarketBase::updateShowcases() const auto params = getShowcasesParams(); if(bidTradePanel) - updateSelectedBody(bidTradePanel, params.bidParams); + updateShowcase(bidTradePanel, params.bidParams); if(offerTradePanel) - updateSelectedBody(offerTradePanel, params.offerParams); + updateShowcase(offerTradePanel, params.offerParams); } void CMarketBase::highlightingChanged() diff --git a/client/widgets/markets/CMarketResources.cpp b/client/widgets/markets/CMarketResources.cpp index 4233d2f2a..0ddca24ab 100644 --- a/client/widgets/markets/CMarketResources.cpp +++ b/client/widgets/markets/CMarketResources.cpp @@ -60,7 +60,7 @@ void CMarketResources::makeDeal() { if(auto toTrade = offerSlider->getValue(); toTrade != 0) { - LOCPLINT->cb->trade(market->getObjInstanceID(), EMarketMode::RESOURCE_RESOURCE, GameResID(bidTradePanel->getSelectedItemId()), + LOCPLINT->cb->trade(market->getObjInstanceID(), EMarketMode::RESOURCE_RESOURCE, GameResID(bidTradePanel->getHighlightedItemId()), GameResID(offerTradePanel->highlightedSlot->id), bidQty * toTrade, hero); CMarketTraderText::makeDeal(); deselect(); @@ -69,11 +69,11 @@ void CMarketResources::makeDeal() CMarketBase::MarketShowcasesParams CMarketResources::getShowcasesParams() const { - if(bidTradePanel->highlightedSlot && offerTradePanel->highlightedSlot && bidTradePanel->getSelectedItemId() != offerTradePanel->getSelectedItemId()) + if(bidTradePanel->highlightedSlot && offerTradePanel->highlightedSlot && bidTradePanel->getHighlightedItemId() != offerTradePanel->getHighlightedItemId()) return MarketShowcasesParams { - ShowcaseParams {std::to_string(bidQty * offerSlider->getValue()), bidTradePanel->getSelectedItemId()}, - ShowcaseParams {std::to_string(offerQty * offerSlider->getValue()), offerTradePanel->getSelectedItemId()} + ShowcaseParams {std::to_string(bidQty * offerSlider->getValue()), bidTradePanel->getHighlightedItemId()}, + ShowcaseParams {std::to_string(offerQty * offerSlider->getValue()), offerTradePanel->getHighlightedItemId()} }; else return MarketShowcasesParams {std::nullopt, std::nullopt}; @@ -83,10 +83,10 @@ void CMarketResources::highlightingChanged() { if(bidTradePanel->isHighlighted() && offerTradePanel->isHighlighted()) { - market->getOffer(bidTradePanel->getSelectedItemId(), offerTradePanel->getSelectedItemId(), bidQty, offerQty, EMarketMode::RESOURCE_RESOURCE); - offerSlider->setAmount(LOCPLINT->cb->getResourceAmount(GameResID(bidTradePanel->getSelectedItemId())) / bidQty); + market->getOffer(bidTradePanel->getHighlightedItemId(), offerTradePanel->getHighlightedItemId(), bidQty, offerQty, EMarketMode::RESOURCE_RESOURCE); + offerSlider->setAmount(LOCPLINT->cb->getResourceAmount(GameResID(bidTradePanel->getHighlightedItemId())) / bidQty); offerSlider->scrollTo(0); - const bool isControlsBlocked = bidTradePanel->getSelectedItemId() != offerTradePanel->getSelectedItemId() ? false : true; + const bool isControlsBlocked = bidTradePanel->getHighlightedItemId() != offerTradePanel->getHighlightedItemId() ? false : true; offerSlider->block(isControlsBlocked); maxAmount->block(isControlsBlocked); deal->block(isControlsBlocked || !LOCPLINT->makingTurn); @@ -97,7 +97,7 @@ void CMarketResources::highlightingChanged() void CMarketResources::updateSubtitles() { - CMarketBase::updateSubtitlesForBid(EMarketMode::RESOURCE_RESOURCE, bidTradePanel->getSelectedItemId()); + CMarketBase::updateSubtitlesForBid(EMarketMode::RESOURCE_RESOURCE, bidTradePanel->getHighlightedItemId()); if(bidTradePanel->highlightedSlot) offerTradePanel->slots[bidTradePanel->highlightedSlot->serial]->subtitle->setText(CGI->generaltexth->allTexts[164]); // n/a } @@ -105,15 +105,15 @@ void CMarketResources::updateSubtitles() std::string CMarketResources::getTraderText() { if(bidTradePanel->isHighlighted() && offerTradePanel->isHighlighted() && - bidTradePanel->getSelectedItemId() != offerTradePanel->getSelectedItemId()) + bidTradePanel->getHighlightedItemId() != offerTradePanel->getHighlightedItemId()) { MetaString message = MetaString::createFromTextID("core.genrltxt.157"); message.replaceNumber(offerQty); message.replaceRawString(offerQty == 1 ? CGI->generaltexth->allTexts[161] : CGI->generaltexth->allTexts[160]); - message.replaceName(GameResID(bidTradePanel->getSelectedItemId())); + message.replaceName(GameResID(bidTradePanel->getHighlightedItemId())); message.replaceNumber(bidQty); message.replaceRawString(bidQty == 1 ? CGI->generaltexth->allTexts[161] : CGI->generaltexth->allTexts[160]); - message.replaceName(GameResID(offerTradePanel->getSelectedItemId())); + message.replaceName(GameResID(offerTradePanel->getHighlightedItemId())); return message.toString(); } else diff --git a/client/widgets/markets/CTransferResources.cpp b/client/widgets/markets/CTransferResources.cpp index f2871c869..3e58c816c 100644 --- a/client/widgets/markets/CTransferResources.cpp +++ b/client/widgets/markets/CTransferResources.cpp @@ -64,8 +64,8 @@ void CTransferResources::makeDeal() { if(auto toTrade = offerSlider->getValue(); toTrade != 0) { - LOCPLINT->cb->trade(market->getObjInstanceID(), EMarketMode::RESOURCE_PLAYER, GameResID(bidTradePanel->getSelectedItemId()), - PlayerColor(offerTradePanel->getSelectedItemId()), toTrade, hero); + LOCPLINT->cb->trade(market->getObjInstanceID(), EMarketMode::RESOURCE_PLAYER, GameResID(bidTradePanel->getHighlightedItemId()), + PlayerColor(offerTradePanel->getHighlightedItemId()), toTrade, hero); CMarketTraderText::makeDeal(); deselect(); } @@ -76,8 +76,8 @@ CMarketBase::MarketShowcasesParams CTransferResources::getShowcasesParams() cons if(bidTradePanel->isHighlighted() && offerTradePanel->isHighlighted()) return MarketShowcasesParams { - ShowcaseParams {std::to_string(offerSlider->getValue()), bidTradePanel->getSelectedItemId()}, - ShowcaseParams {CGI->generaltexth->capColors[offerTradePanel->getSelectedItemId()], offerTradePanel->getSelectedItemId()} + ShowcaseParams {std::to_string(offerSlider->getValue()), bidTradePanel->getHighlightedItemId()}, + ShowcaseParams {CGI->generaltexth->capColors[offerTradePanel->getHighlightedItemId()], offerTradePanel->getHighlightedItemId()} }; else return MarketShowcasesParams {std::nullopt, std::nullopt}; @@ -87,7 +87,7 @@ void CTransferResources::highlightingChanged() { if(bidTradePanel->isHighlighted() && offerTradePanel->isHighlighted()) { - offerSlider->setAmount(LOCPLINT->cb->getResourceAmount(GameResID(bidTradePanel->getSelectedItemId()))); + offerSlider->setAmount(LOCPLINT->cb->getResourceAmount(GameResID(bidTradePanel->getHighlightedItemId()))); offerSlider->scrollTo(0); offerSlider->block(false); maxAmount->block(false); @@ -102,8 +102,8 @@ std::string CTransferResources::getTraderText() if(bidTradePanel->isHighlighted() && offerTradePanel->isHighlighted()) { MetaString message = MetaString::createFromTextID("core.genrltxt.165"); - message.replaceName(GameResID(bidTradePanel->getSelectedItemId())); - message.replaceName(PlayerColor(offerTradePanel->getSelectedItemId())); + message.replaceName(GameResID(bidTradePanel->getHighlightedItemId())); + message.replaceName(PlayerColor(offerTradePanel->getHighlightedItemId())); return message.toString(); } else diff --git a/client/widgets/markets/TradePanels.cpp b/client/widgets/markets/TradePanels.cpp index cfea07353..119f6995a 100644 --- a/client/widgets/markets/TradePanels.cpp +++ b/client/widgets/markets/TradePanels.cpp @@ -23,11 +23,11 @@ #include "../../../lib/texts/CGeneralTextHandler.h" #include "../../../lib/mapObjects/CGHeroInstance.h" -CTradeableItem::CTradeableItem(const Rect & area, EType Type, int ID, int Serial) +CTradeableItem::CTradeableItem(const Rect & area, EType Type, int32_t ID, int32_t serial) : SelectableSlot(area, Point(1, 1)) , type(EType(-1)) // set to invalid, will be corrected in setType , id(ID) - , serial(Serial) + , serial(serial) { OBJECT_CONSTRUCTION; @@ -65,17 +65,14 @@ void CTradeableItem::setType(EType newType) subtitle->moveTo(pos.topLeft() + Point(35, 55)); image->moveTo(pos.topLeft() + Point(19, 8)); break; - case EType::CREATURE_PLACEHOLDER: case EType::CREATURE: subtitle->moveTo(pos.topLeft() + Point(30, 77)); break; case EType::PLAYER: subtitle->moveTo(pos.topLeft() + Point(31, 76)); break; - case EType::ARTIFACT_PLACEHOLDER: - case EType::ARTIFACT_INSTANCE: - image->moveTo(pos.topLeft() + Point(0, 1)); - subtitle->moveTo(pos.topLeft() + Point(21, 56)); + case EType::ARTIFACT: + subtitle->moveTo(pos.topLeft() + Point(21, 55)); break; case EType::ARTIFACT_TYPE: subtitle->moveTo(pos.topLeft() + Point(35, 57)); @@ -85,14 +82,14 @@ void CTradeableItem::setType(EType newType) } } -void CTradeableItem::setID(int newID) +void CTradeableItem::setID(int32_t newID) { if(id != newID) { id = newID; if(image) { - int index = getIndex(); + const auto index = getIndex(); if(index < 0) image->disable(); else @@ -121,8 +118,7 @@ AnimationPath CTradeableItem::getFilename() case EType::PLAYER: return AnimationPath::builtin("CREST58"); case EType::ARTIFACT_TYPE: - case EType::ARTIFACT_PLACEHOLDER: - case EType::ARTIFACT_INSTANCE: + case EType::ARTIFACT: return AnimationPath::builtin("artifact"); case EType::CREATURE: return AnimationPath::builtin("TWCRPORT"); @@ -142,8 +138,7 @@ int CTradeableItem::getIndex() case EType::PLAYER: return id; case EType::ARTIFACT_TYPE: - case EType::ARTIFACT_INSTANCE: - case EType::ARTIFACT_PLACEHOLDER: + case EType::ARTIFACT: return CGI->artifacts()->getByIndex(id)->getIconIndex(); case EType::CREATURE: return CGI->creatures()->getByIndex(id)->getIconIndex(); @@ -169,11 +164,10 @@ void CTradeableItem::hover(bool on) switch(type) { case EType::CREATURE: - case EType::CREATURE_PLACEHOLDER: GH.statusbar()->write(boost::str(boost::format(CGI->generaltexth->allTexts[481]) % CGI->creh->objects[id]->getNamePluralTranslated())); break; case EType::ARTIFACT_TYPE: - case EType::ARTIFACT_PLACEHOLDER: + case EType::ARTIFACT: if(id < 0) GH.statusbar()->write(CGI->generaltexth->zelp[582].first); else @@ -193,11 +187,9 @@ void CTradeableItem::showPopupWindow(const Point & cursorPosition) switch(type) { case EType::CREATURE: - case EType::CREATURE_PLACEHOLDER: break; case EType::ARTIFACT_TYPE: - case EType::ARTIFACT_PLACEHOLDER: - //TODO: it's would be better for market to contain actual CArtifactInstance and not just ids of certain artifact type so we can use getEffectiveDescription. + case EType::ARTIFACT: if (id >= 0) CRClickPopup::createAndPush(CGI->artifacts()->getByIndex(id)->getDescriptionTranslated()); break; @@ -241,7 +233,7 @@ void TradePanelBase::setShowcaseSubtitle(const std::string & text) showcaseSlot->subtitle->setText(text); } -int TradePanelBase::getSelectedItemId() const +int32_t TradePanelBase::getHighlightedItemId() const { if(highlightedSlot) return highlightedSlot->id; @@ -263,7 +255,7 @@ void TradePanelBase::onSlotClickPressed(const std::shared_ptr & bool TradePanelBase::isHighlighted() const { - return getSelectedItemId() != -1; + return highlightedSlot != nullptr; } ResourcesPanel::ResourcesPanel(const CTradeableItem::ClickPressedFunctor & clickPressedCallback, @@ -339,7 +331,7 @@ CreaturesPanel::CreaturesPanel(const CTradeableItem::ClickPressedFunctor & click for(const auto & [creatureId, slotId, creaturesNum] : initialSlots) { auto slot = slots.emplace_back(std::make_shared(Rect(slotsPos[slotId.num], slotDimension), - creaturesNum == 0 ? EType::CREATURE_PLACEHOLDER : EType::CREATURE, creatureId.num, slotId)); + EType::CREATURE, creaturesNum == 0 ? -1 : creatureId.num, slotId)); slot->clickPressedCallback = clickPressedCallback; if(creaturesNum != 0) slot->subtitle->setText(std::to_string(creaturesNum)); @@ -357,7 +349,7 @@ CreaturesPanel::CreaturesPanel(const CTradeableItem::ClickPressedFunctor & click for(const auto & srcSlot : srcSlots) { auto slot = slots.emplace_back(std::make_shared(Rect(slotsPos[srcSlot->serial], srcSlot->pos.dimensions()), - emptySlots ? EType::CREATURE_PLACEHOLDER : EType::CREATURE, srcSlot->id, srcSlot->serial)); + EType::CREATURE, emptySlots ? -1 : srcSlot->id, srcSlot->serial)); slot->clickPressedCallback = clickPressedCallback; slot->subtitle->setText(emptySlots ? "" : srcSlot->subtitle->getText()); slot->setSelectionWidth(selectionWidth); @@ -372,7 +364,7 @@ ArtifactsAltarPanel::ArtifactsAltarPanel(const CTradeableItem::ClickPressedFunct int slotNum = 0; for(auto & altarSlotPos : slotsPos) { - auto slot = slots.emplace_back(std::make_shared(Rect(altarSlotPos, Point(44, 44)), EType::ARTIFACT_PLACEHOLDER, -1, slotNum)); + auto slot = slots.emplace_back(std::make_shared(Rect(altarSlotPos, Point(44, 44)), EType::ARTIFACT, -1, slotNum)); slot->clickPressedCallback = clickPressedCallback; slot->subtitle->clear(); slot->subtitle->moveBy(Point(0, -1)); diff --git a/client/widgets/markets/TradePanels.h b/client/widgets/markets/TradePanels.h index a1927380a..3e2610106 100644 --- a/client/widgets/markets/TradePanels.h +++ b/client/widgets/markets/TradePanels.h @@ -16,7 +16,7 @@ enum class EType { - RESOURCE, PLAYER, ARTIFACT_TYPE, CREATURE, CREATURE_PLACEHOLDER, ARTIFACT_PLACEHOLDER, ARTIFACT_INSTANCE + RESOURCE, PLAYER, ARTIFACT_TYPE, CREATURE, ARTIFACT }; class CTradeableItem : public SelectableSlot, public std::enable_shared_from_this @@ -28,19 +28,19 @@ public: using ClickPressedFunctor = std::function&)>; EType type; - int id; - const int serial; + int32_t id; + const int32_t serial; std::shared_ptr subtitle; ClickPressedFunctor clickPressedCallback; void setType(EType newType); - void setID(int newID); + void setID(int32_t newID); void clear(); void showPopupWindow(const Point & cursorPosition) override; void hover(bool on) override; void clickPressed(const Point & cursorPosition) override; - CTradeableItem(const Rect & area, EType Type, int ID, int Serial); + CTradeableItem(const Rect & area, EType Type, int32_t ID, int32_t serial); }; class TradePanelBase : public CIntObject @@ -61,7 +61,7 @@ public: virtual void clearSubtitles(); void updateOffer(CTradeableItem & slot, int, int); void setShowcaseSubtitle(const std::string & text); - int getSelectedItemId() const; + int32_t getHighlightedItemId() const; void onSlotClickPressed(const std::shared_ptr & newSlot); bool isHighlighted() const; }; diff --git a/client/windows/CCastleInterface.cpp b/client/windows/CCastleInterface.cpp index e8086f830..3d917712e 100644 --- a/client/windows/CCastleInterface.cpp +++ b/client/windows/CCastleInterface.cpp @@ -82,7 +82,7 @@ CBuildingRect::CBuildingRect(CCastleBuildings * Par, const CGTownInstance * Town // special animation frame manipulation for castle shipyard with and without ship // done due to .def used in special way, not to animate building - first image is for shipyard without citadel moat, 2nd image is for including moat - if(Town->town->faction->getId() == FactionID::CASTLE && Str->building && + if(Town->getFactionID() == FactionID::CASTLE && Str->building && (Str->building->bid == BuildingID::SHIPYARD || Str->building->bid == BuildingID::SHIP)) { if(Town->hasBuilt(BuildingID::CITADEL)) @@ -98,7 +98,7 @@ CBuildingRect::CBuildingRect(CCastleBuildings * Par, const CGTownInstance * Town border = GH.renderHandler().loadImage(str->borderName, EImageBlitMode::COLORKEY); if(!str->areaName.empty()) - area = GH.renderHandler().loadImage(str->areaName, EImageBlitMode::ALPHA); + area = GH.renderHandler().loadImage(str->areaName, EImageBlitMode::SIMPLE); } const CBuilding * CBuildingRect::getBuilding() @@ -107,7 +107,7 @@ const CBuilding * CBuildingRect::getBuilding() return nullptr; if (str->hiddenUpgrade) // hidden upgrades, e.g. hordes - return base (dwelling for hordes) - return town->town->buildings.at(str->building->getBase()); + return town->getTown()->buildings.at(str->building->getBase()); return str->building; } @@ -156,7 +156,7 @@ void CBuildingRect::showPopupWindow(const Point & cursorPosition) return; BuildingID bid = getBuilding()->bid; - const CBuilding *bld = town->town->buildings.at(bid); + const CBuilding *bld = town->getTown()->buildings.at(bid); if (bid < BuildingID::DWELL_FIRST) { CRClickPopup::createAndPush(CInfoWindow::genText(bld->getNameTranslated(), bld->getDescriptionTranslated()), @@ -235,10 +235,11 @@ std::string CBuildingRect::getSubtitle()//hover text for building int bid = getBuilding()->bid; if (bid<30)//non-dwellings - only building name - return town->town->buildings.at(getBuilding()->bid)->getNameTranslated(); + return town->getTown()->buildings.at(getBuilding()->bid)->getNameTranslated(); else//dwellings - recruit %creature% { - auto & availableCreatures = town->creatures[(bid-30)%town->town->creatures.size()].second; + int level = BuildingID::getLevelFromDwelling(getBuilding()->bid); + auto & availableCreatures = town->creatures[level].second; if(availableCreatures.size()) { int creaID = availableCreatures.back();//taking last of available creatures @@ -455,7 +456,7 @@ void CHeroGSlot::showPopupWindow(const Point & cursorPosition) { if(hero) { - GH.windows().createAndPushWindow(Point(pos.x + 175, pos.y + 100), hero); + GH.windows().createAndPushWindow(pos.center(), hero); } } @@ -566,7 +567,7 @@ CCastleBuildings::CCastleBuildings(const CGTownInstance* Town): { OBJECT_CONSTRUCTION; - background = std::make_shared(town->town->clientInfo.townBackground); + background = std::make_shared(town->getTown()->clientInfo.townBackground); background->needRefresh = true; background->getSurface()->setBlitMode(EImageBlitMode::OPAQUE); pos.w = background->pos.w; @@ -602,7 +603,7 @@ void CCastleBuildings::recreate() } } - for(const CStructure * structure : town->town->clientInfo.structures) + for(const CStructure * structure : town->getTown()->clientInfo.structures) { if(!structure->building) { @@ -617,7 +618,7 @@ void CCastleBuildings::recreate() for(auto & entry : groups) { - const CBuilding * build = town->town->buildings.at(entry.first); + const CBuilding * build = town->getTown()->buildings.at(entry.first); const CStructure * toAdd = *boost::max_element(entry.second, [=](const CStructure * a, const CStructure * b) { @@ -648,7 +649,7 @@ void CCastleBuildings::recreate() void CCastleBuildings::addBuilding(BuildingID building) { //FIXME: implement faster method without complete recreation of town - BuildingID base = town->town->buildings.at(building)->getBase(); + BuildingID base = town->getTown()->buildings.at(building)->getBase(); recreate(); @@ -687,7 +688,7 @@ void CCastleBuildings::buildingClicked(BuildingID building) BuildingID buildingToEnter = building; for(;;) { - const CBuilding *b = town->town->buildings.find(buildingToEnter)->second; + const CBuilding *b = town->getTown()->buildings.find(buildingToEnter)->second; if (buildingTryActivateCustomUI(buildingToEnter, building)) return; @@ -705,7 +706,7 @@ void CCastleBuildings::buildingClicked(BuildingID building) bool CCastleBuildings::buildingTryActivateCustomUI(BuildingID buildingToTest, BuildingID buildingTarget) { logGlobal->trace("You've clicked on %d", (int)buildingToTest.toEnum()); - const CBuilding *b = town->town->buildings.at(buildingToTest); + const CBuilding *b = town->getTown()->buildings.at(buildingToTest); if (town->getWarMachineInBuilding(buildingToTest).hasValue()) { @@ -744,7 +745,7 @@ bool CCastleBuildings::buildingTryActivateCustomUI(BuildingID buildingToTest, Bu } } - if (town->rewardableBuildings.count(buildingToTest) && town->town->buildings.at(buildingToTest)->manualHeroVisit) + if (town->rewardableBuildings.count(buildingToTest) && town->getTown()->buildings.at(buildingToTest)->manualHeroVisit) { enterRewardable(buildingToTest); return true; @@ -820,10 +821,10 @@ bool CCastleBuildings::buildingTryActivateCustomUI(BuildingID buildingToTest, Bu return false; case BuildingSubID::PORTAL_OF_SUMMONING: - if (town->creatures[town->town->creatures.size()].second.empty())//No creatures + if (town->creatures[town->getTown()->creatures.size()].second.empty())//No creatures LOCPLINT->showInfoDialog(CGI->generaltexth->tcommands[30]); else - enterDwelling(town->town->creatures.size()); + enterDwelling(town->getTown()->creatures.size()); return true; case BuildingSubID::BANK: @@ -850,7 +851,7 @@ void CCastleBuildings::enterRewardable(BuildingID building) { MetaString message; message.appendTextID("core.genrltxt.273"); // only visiting heroes may visit %s - message.replaceTextID(town->town->buildings.at(building)->getNameTextID()); + message.replaceTextID(town->getTown()->buildings.at(building)->getNameTextID()); LOCPLINT->showInfoDialog(message.toString()); } @@ -868,7 +869,7 @@ void CCastleBuildings::enterBlacksmith(BuildingID building, ArtifactID artifactI const CGHeroInstance *hero = town->visitingHero; if(!hero) { - LOCPLINT->showInfoDialog(boost::str(boost::format(CGI->generaltexth->allTexts[273]) % town->town->buildings.find(building)->second->getNameTranslated())); + LOCPLINT->showInfoDialog(boost::str(boost::format(CGI->generaltexth->allTexts[273]) % town->getTown()->buildings.find(building)->second->getNameTranslated())); return; } auto art = artifactID.toArtifact(); @@ -897,8 +898,8 @@ void CCastleBuildings::enterBlacksmith(BuildingID building, ArtifactID artifactI void CCastleBuildings::enterBuilding(BuildingID building) { - std::vector> comps(1, std::make_shared(ComponentType::BUILDING, BuildingTypeUniqueID(town->getFaction(), building))); - LOCPLINT->showInfoDialog( town->town->buildings.find(building)->second->getDescriptionTranslated(), comps); + std::vector> comps(1, std::make_shared(ComponentType::BUILDING, BuildingTypeUniqueID(town->getFactionID(), building))); + LOCPLINT->showInfoDialog( town->getTown()->buildings.find(building)->second->getDescriptionTranslated(), comps); } void CCastleBuildings::enterCastleGate() @@ -915,20 +916,20 @@ void CCastleBuildings::enterCastleGate() { const CGTownInstance *t = Town; if (t->id != this->town->id && t->visitingHero == nullptr && //another town, empty and this is - t->town->faction->getId() == town->town->faction->getId() && //the town of the same faction + t->getFactionID() == town->getFactionID() && //the town of the same faction t->hasBuilt(BuildingSubID::CASTLE_GATE)) //and the town has a castle gate { availableTowns.push_back(t->id.getNum());//add to the list if(settings["general"]["enableUiEnhancements"].Bool()) { - auto image = GH.renderHandler().loadImage(AnimationPath::builtin("ITPA"), t->town->clientInfo.icons[t->hasFort()][false] + 2, 0, EImageBlitMode::OPAQUE); + auto image = GH.renderHandler().loadImage(AnimationPath::builtin("ITPA"), t->getTown()->clientInfo.icons[t->hasFort()][false] + 2, 0, EImageBlitMode::OPAQUE); image->scaleTo(Point(35, 23)); images.push_back(image); } } } - auto gateIcon = std::make_shared(town->town->clientInfo.buildingsIcons, BuildingID::CASTLE_GATE);//will be deleted by selection window + auto gateIcon = std::make_shared(town->getTown()->clientInfo.buildingsIcons, BuildingID::CASTLE_GATE);//will be deleted by selection window auto wnd = std::make_shared(availableTowns, gateIcon, CGI->generaltexth->jktexts[40], CGI->generaltexth->jktexts[41], std::bind (&CCastleInterface::castleTeleport, LOCPLINT->castleInt, _1), 0, images); wnd->onPopup = [availableTowns](int index) { CRClickPopup::createAndPush(LOCPLINT->cb->getObjInstance(ObjectInstanceID(availableTowns[index])), GH.getCursorPosition()); }; @@ -940,7 +941,7 @@ void CCastleBuildings::enterDwelling(int level) if (level < 0 || level >= town->creatures.size() || town->creatures[level].second.empty()) { assert(0); - logGlobal->error("Attempt to enter into invalid dwelling of level %d in town %s (%s)", level, town->getNameTranslated(), town->town->faction->getNameTranslated()); + logGlobal->error("Attempt to enter into invalid dwelling of level %d in town %s (%s)", level, town->getNameTranslated(), town->getFaction()->getNameTranslated()); return; } @@ -954,8 +955,8 @@ void CCastleBuildings::enterDwelling(int level) void CCastleBuildings::enterToTheQuickRecruitmentWindow() { const auto beginIt = town->creatures.cbegin(); - const auto afterLastIt = town->creatures.size() > town->town->creatures.size() - ? std::next(beginIt, town->town->creatures.size()) + const auto afterLastIt = town->creatures.size() > town->getTown()->creatures.size() + ? std::next(beginIt, town->getTown()->creatures.size()) : town->creatures.cend(); const auto hasSomeoneToRecruit = std::any_of(beginIt, afterLastIt, [](const auto & creatureInfo) { return creatureInfo.first > 0; }); @@ -967,8 +968,8 @@ void CCastleBuildings::enterToTheQuickRecruitmentWindow() void CCastleBuildings::enterFountain(const BuildingID & building, BuildingSubID::EBuildingSubID subID, BuildingID upgrades) { - std::vector> comps(1, std::make_shared(ComponentType::BUILDING, BuildingTypeUniqueID(town->getFaction(), building))); - std::string descr = town->town->buildings.find(building)->second->getDescriptionTranslated(); + std::vector> comps(1, std::make_shared(ComponentType::BUILDING, BuildingTypeUniqueID(town->getFactionID(), building))); + std::string descr = town->getTown()->buildings.find(building)->second->getDescriptionTranslated(); std::string hasNotProduced; std::string hasProduced; @@ -977,10 +978,10 @@ void CCastleBuildings::enterFountain(const BuildingID & building, BuildingSubID: bool isMysticPondOrItsUpgrade = subID == BuildingSubID::MYSTIC_POND || (upgrades != BuildingID::NONE - && town->town->buildings.find(BuildingID(upgrades))->second->subId == BuildingSubID::MYSTIC_POND); + && town->getTown()->buildings.find(BuildingID(upgrades))->second->subId == BuildingSubID::MYSTIC_POND); if(upgrades != BuildingID::NONE) - descr += "\n\n"+town->town->buildings.find(BuildingID(upgrades))->second->getDescriptionTranslated(); + descr += "\n\n"+town->getTown()->buildings.find(BuildingID(upgrades))->second->getDescriptionTranslated(); if(isMysticPondOrItsUpgrade) //for vanila Rampart like towns { @@ -1056,7 +1057,7 @@ void CCastleBuildings::enterTownHall() void CCastleBuildings::openMagesGuild() { - auto mageGuildBackground = LOCPLINT->castleInt->town->town->clientInfo.guildBackground; + auto mageGuildBackground = LOCPLINT->castleInt->town->getTown()->clientInfo.guildBackground; GH.windows().createAndPushWindow(LOCPLINT->castleInt, mageGuildBackground); } @@ -1247,7 +1248,7 @@ CTownInfo::CTownInfo(int posX, int posY, const CGTownInstance * Town, bool townH return;//FIXME: suspicious statement, fix or comment picture = std::make_shared(AnimationPath::builtin("ITMCL.DEF"), town->fortLevel()-1); } - building = town->town->buildings.at(BuildingID(buildID)); + building = town->getTown()->buildings.at(BuildingID(buildID)); pos = picture->pos; } @@ -1322,7 +1323,7 @@ CCastleInterface::CCastleInterface(const CGTownInstance * Town, const CGTownInst recreateIcons(); if (!from) adventureInt->onAudioPaused(); - CCS->musich->playMusicFromSet("faction", town->town->faction->getJsonKey(), true, false); + CCS->musich->playMusicFromSet("faction", town->getFaction()->getJsonKey(), true, false); } CCastleInterface::~CCastleInterface() @@ -1403,7 +1404,7 @@ void CCastleInterface::removeBuilding(BuildingID bid) void CCastleInterface::recreateIcons() { OBJECT_CONSTRUCTION; - size_t iconIndex = town->town->clientInfo.icons[town->hasFort()][town->built >= LOCPLINT->cb->getSettings().getInteger(EGameSettings::TOWNS_BUILDINGS_PER_TURN_CAP)]; + size_t iconIndex = town->getTown()->clientInfo.icons[town->hasFort()][town->built >= LOCPLINT->cb->getSettings().getInteger(EGameSettings::TOWNS_BUILDINGS_PER_TURN_CAP)]; icon->setFrame(iconIndex); TResources townIncome = town->dailyIncome(); @@ -1425,8 +1426,8 @@ void CCastleInterface::recreateIcons() if(town->hasBuilt(BuildingID::TAVERN)) LOCPLINT->showTavernWindow(town, nullptr, QueryID::NONE); }, [this]{ - if(!town->town->faction->getDescriptionTranslated().empty()) - CRClickPopup::createAndPush(town->town->faction->getDescriptionTranslated()); + if(!town->getFaction()->getDescriptionTranslated().empty()) + CRClickPopup::createAndPush(town->getFaction()->getDescriptionTranslated()); }); creainfo.clear(); @@ -1527,7 +1528,7 @@ CHallInterface::CBuildingBox::CBuildingBox(int x, int y, const CGTownInstance * -1, -1, -1, 0, 0, 1, 2, -1, 1, 1, -1, -1 }; - icon = std::make_shared(town->town->clientInfo.buildingsIcons, building->bid, 0, 2, 2); + icon = std::make_shared(town->getTown()->clientInfo.buildingsIcons, building->bid, 0, 2, 2); header = std::make_shared(AnimationPath::builtin("TPTHBAR"), panelIndex[static_cast(state)], 0, 1, 73); if(iconIndex[static_cast(state)] >=0) mark = std::make_shared(AnimationPath::builtin("TPTHCHK"), iconIndex[static_cast(state)], 0, 136, 56); @@ -1569,7 +1570,7 @@ void CHallInterface::CBuildingBox::showPopupWindow(const Point & cursorPosition) } CHallInterface::CHallInterface(const CGTownInstance * Town): - CWindowObject(PLAYER_COLORED | BORDERED, Town->town->clientInfo.hallBackground), + CWindowObject(PLAYER_COLORED | BORDERED, Town->getTown()->clientInfo.hallBackground), town(Town) { OBJECT_CONSTRUCTION; @@ -1581,10 +1582,10 @@ CHallInterface::CHallInterface(const CGTownInstance * Town): auto statusbarBackground = std::make_shared(background->getSurface(), barRect, 5, 556); statusbar = CGStatusBar::create(statusbarBackground); - title = std::make_shared(399, 12, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, town->town->buildings.at(BuildingID(town->hallLevel()+BuildingID::VILLAGE_HALL))->getNameTranslated()); + title = std::make_shared(399, 12, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, town->getTown()->buildings.at(BuildingID(town->hallLevel()+BuildingID::VILLAGE_HALL))->getNameTranslated()); exit = std::make_shared(Point(748, 556), AnimationPath::builtin("TPMAGE1.DEF"), CButton::tooltip(CGI->generaltexth->hcommands[8]), [&](){close();}, EShortcut::GLOBAL_RETURN); - auto & boxList = town->town->clientInfo.hallSlots; + auto & boxList = town->getTown()->clientInfo.hallSlots; boxes.resize(boxList.size()); for(size_t row=0; rowwarn("Invalid building ID found in hallSlots of town '%s'", town->town->faction->getJsonKey() ); + logMod->warn("Invalid building ID found in hallSlots of town '%s'", town->getFaction()->getJsonKey() ); continue; } - const CBuilding * current = town->town->buildings.at(buildingID); + const CBuilding * current = town->getTown()->buildings.at(buildingID); if(town->hasBuilt(buildingID)) { building = current; @@ -1629,7 +1630,7 @@ CBuildWindow::CBuildWindow(const CGTownInstance *Town, const CBuilding * Buildin { OBJECT_CONSTRUCTION; - icon = std::make_shared(town->town->clientInfo.buildingsIcons, building->bid, 0, 125, 50); + icon = std::make_shared(town->getTown()->clientInfo.buildingsIcons, building->bid, 0, 125, 50); auto statusbarBackground = std::make_shared(background->getSurface(), Rect(8, pos.h - 26, pos.w - 16, 19), 8, pos.h - 26); statusbar = CGStatusBar::create(statusbarBackground); @@ -1711,7 +1712,7 @@ std::string CBuildWindow::getTextForState(EBuildingState state) { auto toStr = [&](const BuildingID build) -> std::string { - return town->town->buildings.at(build)->getNameTranslated(); + return town->getTown()->buildings.at(build)->getNameTranslated(); }; ret = CGI->generaltexth->allTexts[52]; @@ -1721,7 +1722,7 @@ std::string CBuildWindow::getTextForState(EBuildingState state) case EBuildingState::MISSING_BASE: { std::string msg = CGI->generaltexth->translate("vcmi.townHall.missingBase"); - ret = boost::str(boost::format(msg) % town->town->buildings.at(building->upgrade)->getNameTranslated()); + ret = boost::str(boost::format(msg) % town->getTown()->buildings.at(building->upgrade)->getNameTranslated()); break; } } @@ -1780,10 +1781,11 @@ CFortScreen::CFortScreen(const CGTownInstance * town): { OBJECT_CONSTRUCTION; ui32 fortSize = static_cast(town->creatures.size()); - if(fortSize > town->town->creatures.size() && town->creatures.back().second.empty()) + if(fortSize > town->getTown()->creatures.size() && town->creatures.back().second.empty()) fortSize--; + fortSize = std::min(fortSize, static_cast(GameConstants::CREATURES_PER_TOWN)); // for 8 creatures + portal of summoning - const CBuilding * fortBuilding = town->town->buildings.at(BuildingID(town->fortLevel()+6)); + const CBuilding * fortBuilding = town->getTown()->buildings.at(BuildingID(town->fortLevel()+6)); title = std::make_shared(400, 12, FONT_BIG, ETextAlignment::CENTER, Colors::WHITE, fortBuilding->getNameTranslated()); std::string text = boost::str(boost::format(CGI->generaltexth->fcommands[6]) % fortBuilding->getNameTranslated()); @@ -1809,7 +1811,7 @@ CFortScreen::CFortScreen(const CGTownInstance * town): for(ui32 i=0; itown->creatures.size()) + if(fortSize == town->getTown()->creatures.size()) { BuildingID dwelling = BuildingID::getDwellingFromLevel(i, 1); @@ -1838,8 +1840,9 @@ CFortScreen::CFortScreen(const CGTownInstance * town): ImagePath CFortScreen::getBgName(const CGTownInstance * town) { ui32 fortSize = static_cast(town->creatures.size()); - if(fortSize > town->town->creatures.size() && town->creatures.back().second.empty()) + if(fortSize > town->getTown()->creatures.size() && town->creatures.back().second.empty()) fortSize--; + fortSize = std::min(fortSize, static_cast(GameConstants::CREATURES_PER_TOWN)); // for 8 creatures + portal of summoning if(fortSize == GameConstants::CREATURES_PER_TOWN) return ImagePath::builtin("TPCASTL8"); @@ -1875,7 +1878,7 @@ CFortScreen::RecruitArea::RecruitArea(int posX, int posY, const CGTownInstance * if(getMyBuilding() != nullptr) { - buildingIcon = std::make_shared(town->town->clientInfo.buildingsIcons, getMyBuilding()->bid, 0, 4, 21); + buildingIcon = std::make_shared(town->getTown()->clientInfo.buildingsIcons, getMyBuilding()->bid, 0, 4, 21); buildingName = std::make_shared(78, 101, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, getMyBuilding()->getNameTranslated(), 152); if(town->hasBuilt(getMyBuilding()->bid)) @@ -1911,8 +1914,8 @@ const CCreature * CFortScreen::RecruitArea::getMyCreature() { if(!town->creatures.at(level).second.empty()) // built return town->creatures.at(level).second.back().toCreature(); - if(!town->town->creatures.at(level).empty()) // there are creatures on this level - return town->town->creatures.at(level).front().toCreature(); + if(!town->getTown()->creatures.at(level).empty()) // there are creatures on this level + return town->getTown()->creatures.at(level).front().toCreature(); return nullptr; } @@ -1920,17 +1923,17 @@ const CBuilding * CFortScreen::RecruitArea::getMyBuilding() { BuildingID myID = BuildingID(BuildingID::getDwellingFromLevel(level, 0)); - if (level == town->town->creatures.size()) - return town->town->getSpecialBuilding(BuildingSubID::PORTAL_OF_SUMMONING); + if (level == town->getTown()->creatures.size()) + return town->getTown()->getSpecialBuilding(BuildingSubID::PORTAL_OF_SUMMONING); - if (!town->town->buildings.count(myID)) + if (!town->getTown()->buildings.count(myID)) return nullptr; - const CBuilding * build = town->town->buildings.at(myID); - while (town->town->buildings.count(myID)) + const CBuilding * build = town->getTown()->buildings.at(myID); + while (town->getTown()->buildings.count(myID)) { if (town->hasBuilt(myID)) - build = town->town->buildings.at(myID); + build = town->getTown()->buildings.at(myID); BuildingID::advanceDwelling(myID); } @@ -1966,11 +1969,11 @@ void CFortScreen::RecruitArea::showPopupWindow(const Point & cursorPosition) } CMageGuildScreen::CMageGuildScreen(CCastleInterface * owner, const ImagePath & imagename) - : CWindowObject(BORDERED, imagename) + : CWindowObject(BORDERED, imagename), townId(owner->town->id) { OBJECT_CONSTRUCTION; - window = std::make_shared(owner->town->town->clientInfo.guildWindow, 332, 76); + window = std::make_shared(owner->town->getTown()->clientInfo.guildWindow, 332, 76); resdatabar = std::make_shared(); resdatabar->moveBy(pos.topLeft(), true); @@ -1982,6 +1985,15 @@ CMageGuildScreen::CMageGuildScreen(CCastleInterface * owner, const ImagePath & i exit = std::make_shared(Point(748, 556), AnimationPath::builtin("TPMAGE1.DEF"), CButton::tooltip(CGI->generaltexth->allTexts[593]), [&](){ close(); }, EShortcut::GLOBAL_RETURN); + updateSpells(townId); +} + +void CMageGuildScreen::updateSpells(ObjectInstanceID tID) +{ + if(tID != townId) + return; + + OBJECT_CONSTRUCTION; static const std::vector > positions = { {Point(222,445), Point(312,445), Point(402,445), Point(520,445), Point(610,445), Point(700,445)}, @@ -1991,21 +2003,28 @@ CMageGuildScreen::CMageGuildScreen(CCastleInterface * owner, const ImagePath & i {Point(491,325), Point(591,325)} }; - for(size_t i=0; itown->town->mageLevel; i++) + spells.clear(); + emptyScrolls.clear(); + + const CGTownInstance * town = LOCPLINT->cb->getTown(townId); + + for(uint32_t i=0; igetTown()->mageLevel; i++) { - size_t spellCount = owner->town->spellsAtLevel((int)i+1,false); //spell at level with -1 hmmm? - for(size_t j=0; jspellsAtLevel(i+1,false); //spell at level with -1 hmmm? + for(uint32_t j=0; jtown->mageGuildLevel() && owner->town->spells[i].size()>j) - spells.push_back(std::make_shared(positions[i][j], owner->town->spells[i][j].toSpell())); + if(imageGuildLevel() && town->spells[i].size()>j) + spells.push_back(std::make_shared(positions[i][j], town->spells[i][j].toSpell(), townId)); else emptyScrolls.push_back(std::make_shared(AnimationPath::builtin("TPMAGES.DEF"), 1, 0, positions[i][j].x, positions[i][j].y)); } } + + redraw(); } -CMageGuildScreen::Scroll::Scroll(Point position, const CSpell *Spell) - : spell(Spell) +CMageGuildScreen::Scroll::Scroll(Point position, const CSpell *Spell, ObjectInstanceID townId) + : spell(Spell), townId(townId) { OBJECT_CONSTRUCTION; @@ -2017,7 +2036,64 @@ CMageGuildScreen::Scroll::Scroll(Point position, const CSpell *Spell) void CMageGuildScreen::Scroll::clickPressed(const Point & cursorPosition) { - LOCPLINT->showInfoDialog(spell->getDescriptionTranslated(0), std::make_shared(ComponentType::SPELL, spell->id)); + const CGTownInstance * town = LOCPLINT->cb->getTown(townId); + if(LOCPLINT->cb->getSettings().getBoolean(EGameSettings::TOWNS_SPELL_RESEARCH) && town->spellResearchAllowed) + { + int level = -1; + for(int i = 0; i < town->spells.size(); i++) + if(vstd::find_pos(town->spells[i], spell->id) != -1) + level = i; + + if(town->spellResearchCounterDay >= LOCPLINT->cb->getSettings().getValue(EGameSettings::TOWNS_SPELL_RESEARCH_PER_DAY).Vector()[level].Float()) + { + LOCPLINT->showInfoDialog(CGI->generaltexth->translate("vcmi.spellResearch.comeAgain")); + return; + } + + auto costBase = TResources(LOCPLINT->cb->getSettings().getValue(EGameSettings::TOWNS_SPELL_RESEARCH_COST).Vector()[level]); + auto costExponent = LOCPLINT->cb->getSettings().getValue(EGameSettings::TOWNS_SPELL_RESEARCH_COST_EXPONENT_PER_RESEARCH).Vector()[level].Float(); + auto cost = costBase * std::pow(town->spellResearchAcceptedCounter + 1, costExponent); + + std::vector> resComps; + + int index = town->spellsAtLevel(level, false); + if (index >= town->spells[level].size()) + { + LOCPLINT->showInfoDialog(CGI->generaltexth->translate("vcmi.spellResearch.noMoreSpells")); + return; + } + auto newSpell = town->spells[level].at(index); + resComps.push_back(std::make_shared(ComponentType::SPELL, spell->id)); + resComps.push_back(std::make_shared(ComponentType::SPELL, newSpell)); + resComps.back()->newLine = true; + for(TResources::nziterator i(cost); i.valid(); i++) + { + resComps.push_back(std::make_shared(ComponentType::RESOURCE, i->resType, i->resVal, CComponent::ESize::medium)); + } + + std::vector>> pom; + for(int i = 0; i < 3; i++) + pom.emplace_back(AnimationPath::builtin("settingsWindow/button80"), nullptr); + + auto text = CGI->generaltexth->translate(LOCPLINT->cb->getResourceAmount().canAfford(cost) ? "vcmi.spellResearch.pay" : "vcmi.spellResearch.canNotAfford"); + boost::replace_first(text, "%SPELL1", spell->id.toSpell()->getNameTranslated()); + boost::replace_first(text, "%SPELL2", newSpell.toSpell()->getNameTranslated()); + auto temp = std::make_shared(text, LOCPLINT->playerID, resComps, pom); + + temp->buttons[0]->setOverlay(std::make_shared(ImagePath::builtin("spellResearch/accept"))); + temp->buttons[0]->addCallback([this, town](){ LOCPLINT->cb->spellResearch(town, spell->id, true); }); + temp->buttons[0]->addPopupCallback([](){ CRClickPopup::createAndPush(CGI->generaltexth->translate("vcmi.spellResearch.research")); }); + temp->buttons[0]->setEnabled(LOCPLINT->cb->getResourceAmount().canAfford(cost)); + temp->buttons[1]->setOverlay(std::make_shared(ImagePath::builtin("spellResearch/reroll"))); + temp->buttons[1]->addCallback([this, town](){ LOCPLINT->cb->spellResearch(town, spell->id, false); }); + temp->buttons[1]->addPopupCallback([](){ CRClickPopup::createAndPush(CGI->generaltexth->translate("vcmi.spellResearch.skip")); }); + temp->buttons[2]->setOverlay(std::make_shared(ImagePath::builtin("spellResearch/close"))); + temp->buttons[2]->addPopupCallback([](){ CRClickPopup::createAndPush(CGI->generaltexth->translate("vcmi.spellResearch.abort")); }); + + GH.windows().pushWindow(temp); + } + else + LOCPLINT->showInfoDialog(spell->getDescriptionTranslated(0), std::make_shared(ComponentType::SPELL, spell->id)); } void CMageGuildScreen::Scroll::showPopupWindow(const Point & cursorPosition) diff --git a/client/windows/CCastleInterface.h b/client/windows/CCastleInterface.h index 3b8fb6037..717c1a748 100644 --- a/client/windows/CCastleInterface.h +++ b/client/windows/CCastleInterface.h @@ -379,9 +379,10 @@ class CMageGuildScreen : public CStatusbarWindow { const CSpell * spell; std::shared_ptr image; + ObjectInstanceID townId; public: - Scroll(Point position, const CSpell *Spell); + Scroll(Point position, const CSpell *Spell, ObjectInstanceID townId); void clickPressed(const Point & cursorPosition) override; void showPopupWindow(const Point & cursorPosition) override; void hover(bool on) override; @@ -393,8 +394,11 @@ class CMageGuildScreen : public CStatusbarWindow std::shared_ptr resdatabar; + ObjectInstanceID townId; + public: CMageGuildScreen(CCastleInterface * owner, const ImagePath & image); + void updateSpells(ObjectInstanceID tID); }; /// The blacksmith window where you can buy available in town war machine diff --git a/client/windows/CCreatureWindow.cpp b/client/windows/CCreatureWindow.cpp index 6c5612d5d..1ef1d08bb 100644 --- a/client/windows/CCreatureWindow.cpp +++ b/client/windows/CCreatureWindow.cpp @@ -17,21 +17,23 @@ #include "../CPlayerInterface.h" #include "../render/Canvas.h" #include "../widgets/Buttons.h" -#include "../widgets/CArtPlace.h" #include "../widgets/CComponent.h" +#include "../widgets/CComponentHolder.h" #include "../widgets/Images.h" #include "../widgets/TextControls.h" #include "../widgets/ObjectLists.h" +#include "../widgets/GraphicalPrimitiveCanvas.h" #include "../windows/InfoWindows.h" #include "../gui/CGuiHandler.h" #include "../gui/Shortcut.h" +#include "../battle/BattleInterface.h" #include "../../CCallback.h" #include "../../lib/ArtifactUtils.h" #include "../../lib/CStack.h" #include "../../lib/CBonusTypeHandler.h" #include "../../lib/IGameSettings.h" -#include "../../lib/CHeroHandler.h" +#include "../../lib/entities/hero/CHeroHandler.h" #include "../../lib/gameState/CGameState.h" #include "../../lib/networkPacks/ArtifactLocation.h" #include "../../lib/texts/CGeneralTextHandler.h" @@ -89,7 +91,7 @@ public: std::string getName() const { if(commander) - return commander->type->getNameSingularTranslated(); + return commander->getType()->getNameSingularTranslated(); else return creature->getNamePluralTranslated(); } @@ -249,6 +251,47 @@ CStackWindow::BonusLineSection::BonusLineSection(CStackWindow * owner, size_t li Point(214, 4) }; + auto drawBonusSource = [this](int leftRight, Point p, BonusInfo & bi) + { + std::map bonusColors = { + {BonusSource::ARTIFACT, Colors::GREEN}, + {BonusSource::ARTIFACT_INSTANCE, Colors::GREEN}, + {BonusSource::CREATURE_ABILITY, Colors::YELLOW}, + {BonusSource::SPELL_EFFECT, Colors::ORANGE}, + {BonusSource::SECONDARY_SKILL, Colors::PURPLE}, + {BonusSource::HERO_SPECIAL, Colors::PURPLE}, + {BonusSource::STACK_EXPERIENCE, Colors::CYAN}, + {BonusSource::COMMANDER, Colors::CYAN}, + }; + + std::map bonusNames = { + {BonusSource::ARTIFACT, CGI->generaltexth->translate("vcmi.bonusSource.artifact")}, + {BonusSource::ARTIFACT_INSTANCE, CGI->generaltexth->translate("vcmi.bonusSource.artifact")}, + {BonusSource::CREATURE_ABILITY, CGI->generaltexth->translate("vcmi.bonusSource.creature")}, + {BonusSource::SPELL_EFFECT, CGI->generaltexth->translate("vcmi.bonusSource.spell")}, + {BonusSource::SECONDARY_SKILL, CGI->generaltexth->translate("vcmi.bonusSource.hero")}, + {BonusSource::HERO_SPECIAL, CGI->generaltexth->translate("vcmi.bonusSource.hero")}, + {BonusSource::STACK_EXPERIENCE, CGI->generaltexth->translate("vcmi.bonusSource.commander")}, + {BonusSource::COMMANDER, CGI->generaltexth->translate("vcmi.bonusSource.commander")}, + }; + + auto c = bonusColors.count(bi.bonusSource) ? bonusColors[bi.bonusSource] : ColorRGBA(192, 192, 192); + std::string t = bonusNames.count(bi.bonusSource) ? bonusNames[bi.bonusSource] : CGI->generaltexth->translate("vcmi.bonusSource.other"); + int maxLen = 50; + EFonts f = FONT_TINY; + Point pText = p + Point(4, 38); + + // 1px Black border + bonusSource[leftRight].push_back(std::make_shared(pText.x - 1, pText.y, f, ETextAlignment::TOPLEFT, Colors::BLACK, t, maxLen)); + bonusSource[leftRight].push_back(std::make_shared(pText.x + 1, pText.y, f, ETextAlignment::TOPLEFT, Colors::BLACK, t, maxLen)); + bonusSource[leftRight].push_back(std::make_shared(pText.x, pText.y - 1, f, ETextAlignment::TOPLEFT, Colors::BLACK, t, maxLen)); + bonusSource[leftRight].push_back(std::make_shared(pText.x, pText.y + 1, f, ETextAlignment::TOPLEFT, Colors::BLACK, t, maxLen)); + bonusSource[leftRight].push_back(std::make_shared(pText.x, pText.y, f, ETextAlignment::TOPLEFT, c, t, maxLen)); + + frame[leftRight] = std::make_shared(Rect(p.x, p.y, 52, 52)); + frame[leftRight]->addRectangle(Point(0, 0), Point(52, 52), c); + }; + for(size_t leftRight : {0, 1}) { auto position = offset[leftRight]; @@ -258,8 +301,9 @@ CStackWindow::BonusLineSection::BonusLineSection(CStackWindow * owner, size_t li { BonusInfo & bi = parent->activeBonuses[bonusIndex]; icon[leftRight] = std::make_shared(bi.imagePath, position.x, position.y); - name[leftRight] = std::make_shared(position.x + 60, position.y + 2, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE, bi.name); - description[leftRight] = std::make_shared(Rect(position.x + 60, position.y + 17, 137, 30), FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE, bi.description); + name[leftRight] = std::make_shared(position.x + 60, position.y + 2, FONT_TINY, ETextAlignment::TOPLEFT, Colors::WHITE, bi.name, 137); + description[leftRight] = std::make_shared(Rect(position.x + 60, position.y + 20, 137, 26), FONT_TINY, ETextAlignment::TOPLEFT, Colors::WHITE, bi.description); + drawBonusSource(leftRight, Point(position.x - 1, position.y - 1), bi); } } } @@ -283,7 +327,7 @@ CStackWindow::BonusesSection::BonusesSection(CStackWindow * owner, int yOffset, return std::make_shared(owner, index); }; - lines = std::make_shared(onCreate, Point(0, 0), Point(0, itemHeight), visibleSize, totalSize, 0, 1, Rect(pos.w - 15, 0, pos.h, pos.h)); + lines = std::make_shared(onCreate, Point(0, 0), Point(0, itemHeight), visibleSize, totalSize, 0, totalSize > 3 ? 1 : 0, Rect(pos.w - 15, 0, pos.h, pos.h)); } CStackWindow::ButtonsSection::ButtonsSection(CStackWindow * owner, int yOffset) @@ -393,7 +437,7 @@ CStackWindow::CommanderMainSection::CommanderMainSection(CStackWindow * owner, i auto getSkillDescription = [this](int skillIndex) -> std::string { - return CGI->generaltexth->znpc00[152 + (12 * skillIndex) + (parent->info->commander->secondarySkills[skillIndex] * 2)]; + return parent->getCommanderSkillDescription(skillIndex, parent->info->commander->secondarySkills[skillIndex]); }; for(int index = ECommander::ATTACK; index <= ECommander::SPELL_POWER; ++index) @@ -432,7 +476,9 @@ CStackWindow::CommanderMainSection::CommanderMainSection(CStackWindow * owner, i for(auto equippedArtifact : parent->info->commander->artifactsWorn) { Point artPos = getArtifactPos(equippedArtifact.first); - auto artPlace = std::make_shared(artPos, parent->info->owner, equippedArtifact.first, equippedArtifact.second.artifact); + const auto commanderArt = equippedArtifact.second.artifact; + assert(commanderArt); + auto artPlace = std::make_shared(artPos, parent->info->owner, equippedArtifact.first, commanderArt->getTypeId()); artifacts.push_back(artPlace); } @@ -523,22 +569,31 @@ CStackWindow::MainSection::MainSection(CStackWindow * owner, int yOffset, bool s CRClickPopup::createAndPush(parent->info->creature->getDescriptionTranslated()); }); + if(parent->info->stackNode != nullptr && parent->info->commander == nullptr) { //normal stack, not a commander and not non-existing stack (e.g. recruitment dialog) animation->setAmount(parent->info->creatureCount); } - name = std::make_shared(215, 12, FONT_SMALL, ETextAlignment::CENTER, Colors::YELLOW, parent->info->getName()); + name = std::make_shared(215, 13, FONT_SMALL, ETextAlignment::CENTER, Colors::YELLOW, parent->info->getName()); + + const BattleInterface* battleInterface = LOCPLINT->battleInt.get(); + const CStack* battleStack = parent->info->stack; int dmgMultiply = 1; - if(parent->info->owner && parent->info->stackNode->hasBonusOfType(BonusType::SIEGE_WEAPON)) - dmgMultiply += parent->info->owner->getPrimSkillLevel(PrimarySkill::ATTACK); + if (battleInterface && battleInterface->getBattle() != nullptr && battleStack->hasBonusOfType(BonusType::SIEGE_WEAPON)) + { + // Determine the relevant hero based on the unit side + const auto hero = (battleStack->unitSide() == BattleSide::ATTACKER) + ? battleInterface->attackingHeroInstance + : battleInterface->defendingHeroInstance; + dmgMultiply += hero->getPrimSkillLevel(PrimarySkill::ATTACK); + } + icons = std::make_shared(ImagePath::builtin("stackWindow/icons"), 117, 32); - const CStack * battleStack = parent->info->stack; - morale = std::make_shared(true, Rect(Point(321, 110), Point(42, 42) )); luck = std::make_shared(false, Rect(Point(375, 110), Point(42, 42) )); @@ -566,7 +621,7 @@ CStackWindow::MainSection::MainSection(CStackWindow * owner, int yOffset, bool s addStatLabel(EStat::ATTACK, parent->info->creature->getAttack(shooter), parent->info->stackNode->getAttack(shooter)); addStatLabel(EStat::DEFENCE, parent->info->creature->getDefense(shooter), parent->info->stackNode->getDefense(shooter)); - addStatLabel(EStat::DAMAGE, parent->info->stackNode->getMinDamage(shooter) * dmgMultiply, parent->info->stackNode->getMaxDamage(shooter) * dmgMultiply); + addStatLabel(EStat::DAMAGE, parent->info->stackNode->getMinDamage(shooter), parent->info->stackNode->getMaxDamage(shooter)); addStatLabel(EStat::HEALTH, parent->info->creature->getMaxHealth(), parent->info->stackNode->getMaxHealth()); addStatLabel(EStat::SPEED, parent->info->creature->getMovementRange(), parent->info->stackNode->getMovementRange()); @@ -616,11 +671,11 @@ CStackWindow::MainSection::MainSection(CStackWindow * owner, int yOffset, bool s auto art = parent->info->stackNode->getArt(ArtifactPosition::CREATURE_SLOT); if(art) { - parent->stackArtifactIcon = std::make_shared(AnimationPath::builtin("ARTIFACT"), art->artType->getIconIndex(), 0, pos.x, pos.y); - parent->stackArtifactHelp = std::make_shared(Rect(pos, Point(44, 44)), ComponentType::ARTIFACT); - parent->stackArtifactHelp->component.subType = art->artType->getId(); - parent->stackArtifactHelp->text = art->getDescription(); - + parent->stackArtifact = std::make_shared(pos, art->getTypeId()); + parent->stackArtifact->setShowPopupCallback([](CComponentHolder & artPlace, const Point & cursorPosition) + { + artPlace.LRClickableAreaWTextComp::showPopupWindow(cursorPosition); + }); if(parent->info->owner) { parent->stackArtifactButton = std::make_shared( @@ -693,7 +748,7 @@ CStackWindow::CStackWindow(const CStackInstance * stack, bool popup) info(new UnitView()) { info->stackNode = stack; - info->creature = stack->type; + info->creature = stack->getCreature(); info->creatureCount = stack->count; info->popupWindow = popup; info->owner = dynamic_cast (stack->armyObj); @@ -705,7 +760,7 @@ CStackWindow::CStackWindow(const CStackInstance * stack, std::function d info(new UnitView()) { info->stackNode = stack; - info->creature = stack->type; + info->creature = stack->getCreature(); info->creatureCount = stack->count; info->upgradeInfo = std::make_optional(UnitView::StackUpgradeInfo()); @@ -722,7 +777,7 @@ CStackWindow::CStackWindow(const CCommanderInstance * commander, bool popup) info(new UnitView()) { info->stackNode = commander; - info->creature = commander->type; + info->creature = commander->getCreature(); info->commander = commander; info->creatureCount = 1; info->popupWindow = popup; @@ -735,7 +790,7 @@ CStackWindow::CStackWindow(const CCommanderInstance * commander, std::vectorstackNode = commander; - info->creature = commander->type; + info->creature = commander->getCreature(); info->commander = commander; info->creatureCount = 1; info->levelupInfo = std::make_optional(UnitView::CommanderLevelInfo()); @@ -774,6 +829,12 @@ void CStackWindow::initBonusesList() BonusList output; BonusList input; input = *(info->stackNode->getBonuses(CSelector(Bonus::Permanent), Selector::all)); + std::sort(input.begin(), input.end(), [this](std::shared_ptr v1, std::shared_ptr & v2){ + if (v1->source != v2->source) + return v1->source == BonusSource::CREATURE_ABILITY || (v1->source < v2->source); + else + return info->stackNode->bonusToString(v1, false) < info->stackNode->bonusToString(v2, false); + }); while(!input.empty()) { @@ -789,6 +850,7 @@ void CStackWindow::initBonusesList() bonusInfo.name = info->stackNode->bonusToString(b, false); bonusInfo.description = info->stackNode->bonusToString(b, true); bonusInfo.imagePath = info->stackNode->bonusToGraphics(b); + bonusInfo.bonusSource = b->source; //if it's possible to give any description or image for this kind of bonus //TODO: figure out why half of bonuses don't have proper description @@ -867,7 +929,7 @@ std::string CStackWindow::generateStackExpDescription() const CStackInstance * stack = info->stackNode; const CCreature * creature = info->creature; - int tier = stack->type->getLevel(); + int tier = stack->getType()->getLevel(); int rank = stack->getExpRank(); if (!vstd::iswithin(tier, 1, 7)) tier = 0; @@ -905,14 +967,30 @@ std::string CStackWindow::generateStackExpDescription() return expText; } +std::string CStackWindow::getCommanderSkillDescription(int skillIndex, int skillLevel) +{ + constexpr std::array skillNames = { + "attack", + "defence", + "health", + "damage", + "speed", + "magic" + }; + + std::string textID = TextIdentifier("vcmi", "commander", "skill", skillNames.at(skillIndex), skillLevel).get(); + + return CGI->generaltexth->translate(textID); +} + void CStackWindow::setSelection(si32 newSkill, std::shared_ptr newIcon) { auto getSkillDescription = [this](int skillIndex, bool selected) -> std::string { if(selected) - return CGI->generaltexth->znpc00[152 + (12 * skillIndex) + ((info->commander->secondarySkills[skillIndex] + 1) * 2)]; //upgrade description + return getCommanderSkillDescription(skillIndex, info->commander->secondarySkills[skillIndex] + 1); //upgrade description else - return CGI->generaltexth->znpc00[152 + (12 * skillIndex) + (info->commander->secondarySkills[skillIndex] * 2)]; + return getCommanderSkillDescription(skillIndex, info->commander->secondarySkills[skillIndex]); }; auto getSkillImage = [this](int skillIndex) @@ -987,8 +1065,7 @@ void CStackWindow::removeStackArtifact(ArtifactPosition pos) artLoc.creature = info->stackNode->armyObj->findStack(info->stackNode); LOCPLINT->cb->swapArtifacts(artLoc, ArtifactLocation(info->owner->id, slot)); stackArtifactButton.reset(); - stackArtifactHelp.reset(); - stackArtifactIcon.reset(); + stackArtifact.reset(); redraw(); } } diff --git a/client/windows/CCreatureWindow.h b/client/windows/CCreatureWindow.h index 4728b3b1f..acf43029a 100644 --- a/client/windows/CCreatureWindow.h +++ b/client/windows/CCreatureWindow.h @@ -28,8 +28,10 @@ class CTabbedInt; class CButton; class CMultiLineLabel; class CListBox; +class CArtPlace; class CCommanderArtPlace; class LRClickableArea; +class GraphicalPrimitiveCanvas; class CCommanderSkillIcon : public LRClickableAreaWText //TODO: maybe bring commander skill button initialization logic inside? { @@ -57,6 +59,7 @@ class CStackWindow : public CWindowObject std::string name; std::string description; ImagePath imagePath; + BonusSource bonusSource; }; class CWindowSection : public CIntObject @@ -83,6 +86,8 @@ class CStackWindow : public CWindowObject std::array, 2> icon; std::array, 2> name; std::array, 2> description; + std::array, 2> frame; + std::array>, 2> bonusSource; public: BonusLineSection(CStackWindow * owner, size_t lineIndex); }; @@ -156,8 +161,7 @@ class CStackWindow : public CWindowObject MainSection(CStackWindow * owner, int yOffset, bool showExp, bool showArt); }; - std::shared_ptr stackArtifactIcon; - std::shared_ptr stackArtifactHelp; + std::shared_ptr stackArtifact; std::shared_ptr stackArtifactButton; @@ -189,6 +193,7 @@ class CStackWindow : public CWindowObject void init(); std::string generateStackExpDescription(); + std::string getCommanderSkillDescription(int skillIndex, int skillLevel); public: // for battles diff --git a/client/windows/CExchangeWindow.cpp b/client/windows/CExchangeWindow.cpp index 4ddd045d9..35c5d8c7f 100644 --- a/client/windows/CExchangeWindow.cpp +++ b/client/windows/CExchangeWindow.cpp @@ -29,8 +29,8 @@ #include "../../CCallback.h" -#include "../lib/CHeroHandler.h" #include "../lib/CSkillHandler.h" +#include "../lib/entities/hero/CHeroHandler.h" #include "../lib/filesystem/Filesystem.h" #include "../lib/mapObjects/CGHeroInstance.h" #include "../lib/texts/CGeneralTextHandler.h" @@ -82,9 +82,10 @@ CExchangeWindow::CExchangeWindow(ObjectInstanceID hero1, ObjectInstanceID hero2, for(int m=0; m < hero->secSkills.size(); ++m) - secSkillIcons[leftRight].push_back(std::make_shared(AnimationPath::builtin("SECSK32"), 0, 0, 32 + 36 * m + 454 * leftRight, qeLayout ? 83 : 88)); + secSkills[leftRight].push_back(std::make_shared(Point(32 + 36 * m + 454 * leftRight, qeLayout ? 83 : 88), CSecSkillPlace::ImageSize::SMALL, + hero->secSkills[m].first, hero->secSkills[m].second)); - specImages[leftRight] = std::make_shared(AnimationPath::builtin("UN32"), hero->type->imageIndex, 0, 67 + 490 * leftRight, qeLayout ? 41 : 45); + specImages[leftRight] = std::make_shared(AnimationPath::builtin("UN32"), hero->getHeroType()->imageIndex, 0, 67 + 490 * leftRight, qeLayout ? 41 : 45); expImages[leftRight] = std::make_shared(AnimationPath::builtin("PSKIL32"), 4, 0, 103 + 490 * leftRight, qeLayout ? 41 : 45); expValues[leftRight] = std::make_shared(119 + 490 * leftRight, qeLayout ? 66 : 71, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE); @@ -94,12 +95,12 @@ CExchangeWindow::CExchangeWindow(ObjectInstanceID hero1, ObjectInstanceID hero2, } artifs[0] = std::make_shared(Point(-334, 151)); - artifs[0]->clickPressedCallback = [this, hero = heroInst[0]](const CArtPlace & artPlace, const Point & cursorPosition){clickPressedOnArtPlace(hero, artPlace.slot, true, false, false);}; + artifs[0]->clickPressedCallback = [this, hero = heroInst[0]](const CArtPlace & artPlace, const Point & cursorPosition){clickPressedOnArtPlace(hero, artPlace.slot, true, false, false, cursorPosition);}; artifs[0]->showPopupCallback = [this, heroArts = artifs[0]](CArtPlace & artPlace, const Point & cursorPosition){showArtifactAssembling(*heroArts, artPlace, cursorPosition);}; artifs[0]->gestureCallback = [this, hero = heroInst[0]](const CArtPlace & artPlace, const Point & cursorPosition){showQuickBackpackWindow(hero, artPlace.slot, cursorPosition);}; artifs[0]->setHero(heroInst[0]); artifs[1] = std::make_shared(Point(98, 151)); - artifs[1]->clickPressedCallback = [this, hero = heroInst[1]](const CArtPlace & artPlace, const Point & cursorPosition){clickPressedOnArtPlace(hero, artPlace.slot, true, false, false);}; + artifs[1]->clickPressedCallback = [this, hero = heroInst[1]](const CArtPlace & artPlace, const Point & cursorPosition){clickPressedOnArtPlace(hero, artPlace.slot, true, false, false, cursorPosition);}; artifs[1]->showPopupCallback = [this, heroArts = artifs[1]](CArtPlace & artPlace, const Point & cursorPosition){showArtifactAssembling(*heroArts, artPlace, cursorPosition);}; artifs[1]->gestureCallback = [this, hero = heroInst[1]](const CArtPlace & artPlace, const Point & cursorPosition){showQuickBackpackWindow(hero, artPlace.slot, cursorPosition);}; artifs[1]->setHero(heroInst[1]); @@ -126,21 +127,6 @@ CExchangeWindow::CExchangeWindow(ObjectInstanceID hero1, ObjectInstanceID hero2, { const CGHeroInstance * hero = heroInst.at(b); - //secondary skill's clickable areas - for(int g=0; gsecSkills.size(); ++g) - { - SecondarySkill skill = hero->secSkills[g].first; - int level = hero->secSkills[g].second; // <1, 3> - secSkillAreas[b].push_back(std::make_shared()); - secSkillAreas[b][g]->pos = Rect(Point(pos.x + 32 + g * 36 + b * 454 , pos.y + (qeLayout ? 83 : 88)), Point(32, 32) ); - secSkillAreas[b][g]->component = Component(ComponentType::SEC_SKILL, skill, level); - secSkillAreas[b][g]->text = CGI->skillh->getByIndex(skill)->getDescriptionTranslated(level); - - secSkillAreas[b][g]->hoverText = CGI->generaltexth->heroscrn[21]; - boost::algorithm::replace_first(secSkillAreas[b][g]->hoverText, "%s", CGI->generaltexth->levels[level - 1]); - boost::algorithm::replace_first(secSkillAreas[b][g]->hoverText, "%s", CGI->skillh->getByIndex(skill)->getNameTranslated()); - } - heroAreas[b] = std::make_shared(257 + 228 * b, 13, hero); heroAreas[b]->addClickCallback([this, hero]() -> void { @@ -151,7 +137,7 @@ CExchangeWindow::CExchangeWindow(ObjectInstanceID hero1, ObjectInstanceID hero2, specialtyAreas[b] = std::make_shared(); specialtyAreas[b]->pos = Rect(Point(pos.x + 69 + 490 * b, pos.y + (qeLayout ? 41 : 45)), Point(32, 32)); specialtyAreas[b]->hoverText = CGI->generaltexth->heroscrn[27]; - specialtyAreas[b]->text = hero->type->getSpecialtyDescriptionTranslated(); + specialtyAreas[b]->text = hero->getHeroType()->getSpecialtyDescriptionTranslated(); experienceAreas[b] = std::make_shared(); experienceAreas[b]->pos = Rect(Point(pos.x + 105 + 490 * b, pos.y + (qeLayout ? 41 : 45)), Point(32, 32)); @@ -192,18 +178,52 @@ CExchangeWindow::CExchangeWindow(ObjectInstanceID hero1, ObjectInstanceID hero2, if(qeLayout) { - buttonMoveUnitsFromLeftToRight = std::make_shared(Point(325, 118), AnimationPath::builtin("quick-exchange/armRight.DEF"), CButton::tooltip(CGI->generaltexth->qeModCommands[1]), [this](){ this->moveUnitsShortcut(true); }); - buttonMoveUnitsFromRightToLeft = std::make_shared(Point(425, 118), AnimationPath::builtin("quick-exchange/armLeft.DEF"), CButton::tooltip(CGI->generaltexth->qeModCommands[1]), [this](){ this->moveUnitsShortcut(false); }); - buttonMoveArtifactsFromLeftToRight = std::make_shared(Point(325, 154), AnimationPath::builtin("quick-exchange/artRight.DEF"), CButton::tooltip(CGI->generaltexth->qeModCommands[3]), [this](){ this->moveArtifactsCallback(true);}); - buttonMoveArtifactsFromRightToLeft = std::make_shared(Point(425, 154), AnimationPath::builtin("quick-exchange/artLeft.DEF"), CButton::tooltip(CGI->generaltexth->qeModCommands[3]), [this](){ this->moveArtifactsCallback(false);}); + buttonMoveUnitsFromLeftToRight = std::make_shared( + Point(325, 118), + AnimationPath::builtin("quick-exchange/armRight.DEF"), + CButton::tooltip(CGI->generaltexth->translate("vcmi.quickExchange.moveAllUnits")), + [this](){ this->moveUnitsShortcut(true); }); - exchangeUnitsButton = std::make_shared(Point(377, 118), AnimationPath::builtin("quick-exchange/swapAll.DEF"), CButton::tooltip(CGI->generaltexth->qeModCommands[2]), [this](){ controller.swapArmy(); }); - exchangeArtifactsButton = std::make_shared(Point(377, 154), AnimationPath::builtin("quick-exchange/swapAll.DEF"), CButton::tooltip(CGI->generaltexth->qeModCommands[4]), [this](){ this->swapArtifactsCallback(); }); + buttonMoveUnitsFromRightToLeft = std::make_shared( + Point(425, 118), + AnimationPath::builtin("quick-exchange/armLeft.DEF"), + CButton::tooltip(CGI->generaltexth->translate("vcmi.quickExchange.moveAllUnits")), + [this](){ this->moveUnitsShortcut(false); }); - backpackButtonLeft = std::make_shared(Point(325, 518), AnimationPath::builtin("heroBackpack"), CButton::tooltipLocalized("vcmi.heroWindow.openBackpack"), + buttonMoveArtifactsFromLeftToRight = std::make_shared( + Point(325, 154), AnimationPath::builtin("quick-exchange/artRight.DEF"), + CButton::tooltip(CGI->generaltexth->translate("vcmi.quickExchange.moveAllArtifacts")), + [this](){ this->moveArtifactsCallback(true);}); + + buttonMoveArtifactsFromRightToLeft = std::make_shared( + Point(425, 154), AnimationPath::builtin("quick-exchange/artLeft.DEF"), + CButton::tooltip(CGI->generaltexth->translate("vcmi.quickExchange.moveAllArtifacts")), + [this](){ this->moveArtifactsCallback(false);}); + + exchangeUnitsButton = std::make_shared( + Point(377, 118), + AnimationPath::builtin("quick-exchange/swapAll.DEF"), + CButton::tooltip(CGI->generaltexth->translate("vcmi.quickExchange.swapAllUnits")), + [this](){ controller.swapArmy(); }); + + exchangeArtifactsButton = std::make_shared( + Point(377, 154), + AnimationPath::builtin("quick-exchange/swapAll.DEF"), + CButton::tooltip(CGI->generaltexth->translate("vcmi.quickExchange.swapAllArtifacts")), + [this](){ this->swapArtifactsCallback(); }); + + backpackButtonLeft = std::make_shared( + Point(325, 518), + AnimationPath::builtin("heroBackpack"), + CButton::tooltipLocalized("vcmi.heroWindow.openBackpack"), [this](){ this->backpackShortcut(true); }); - backpackButtonRight = std::make_shared(Point(419, 518), AnimationPath::builtin("heroBackpack"), CButton::tooltipLocalized("vcmi.heroWindow.openBackpack"), + + backpackButtonRight = std::make_shared( + Point(419, 518), + AnimationPath::builtin("heroBackpack"), + CButton::tooltipLocalized("vcmi.heroWindow.openBackpack"), [this](){ this->backpackShortcut(false); }); + backpackButtonLeft->setOverlay(std::make_shared(ImagePath::builtin("heroWindow/backpackButtonIcon"))); backpackButtonRight->setOverlay(std::make_shared(ImagePath::builtin("heroWindow/backpackButtonIcon"))); @@ -227,7 +247,7 @@ CExchangeWindow::CExchangeWindow(ObjectInstanceID hero1, ObjectInstanceID hero2, std::make_shared( Point(484 + 35 * i, 154), AnimationPath::builtin("quick-exchange/unitLeft.DEF"), - CButton::tooltip(CGI->generaltexth->qeModCommands[1]), + CButton::tooltip(CGI->generaltexth->translate("vcmi.quickExchange.moveUnit")), std::bind(&CExchangeController::moveStack, &controller, false, SlotID(i)))); moveUnitFromRightToLeftButtons.back()->block(leftHeroBlock); @@ -235,7 +255,7 @@ CExchangeWindow::CExchangeWindow(ObjectInstanceID hero1, ObjectInstanceID hero2, std::make_shared( Point(66 + 35 * i, 154), AnimationPath::builtin("quick-exchange/unitRight.DEF"), - CButton::tooltip(CGI->generaltexth->qeModCommands[1]), + CButton::tooltip(CGI->generaltexth->translate("vcmi.quickExchange.moveUnit")), std::bind(&CExchangeController::moveStack, &controller, true, SlotID(i)))); moveUnitFromLeftToRightButtons.back()->block(rightHeroBlock); } @@ -362,7 +382,7 @@ void CExchangeWindow::update() int id = hero->secSkills[m].first; int level = hero->secSkills[m].second; - secSkillIcons[leftRight][m]->setFrame(2 + id * 3 + level); + secSkills[leftRight][m]->setSkill(id, level); } expValues[leftRight]->setText(TextOperations::formatMetric(hero->exp, 3)); diff --git a/client/windows/CExchangeWindow.h b/client/windows/CExchangeWindow.h index 97f40fffe..d19b27279 100644 --- a/client/windows/CExchangeWindow.h +++ b/client/windows/CExchangeWindow.h @@ -19,7 +19,6 @@ class CExchangeWindow : public CStatusbarWindow, public IGarrisonHolder, public std::array, 2> titles; std::vector> primSkillImages;//shared for both heroes std::array>, 2> primSkillValues; - std::array>, 2> secSkillIcons; std::array, 2> specImages; std::array, 2> expImages; std::array, 2> expValues; @@ -27,7 +26,7 @@ class CExchangeWindow : public CStatusbarWindow, public IGarrisonHolder, public std::array, 2> manaValues; std::vector> primSkillAreas; - std::array>, 2> secSkillAreas; + std::array>, 2> secSkills; std::array, 2> heroAreas; std::array, 2> specialtyAreas; diff --git a/client/windows/CHeroBackpackWindow.cpp b/client/windows/CHeroBackpackWindow.cpp index 361350a72..94b7a622f 100644 --- a/client/windows/CHeroBackpackWindow.cpp +++ b/client/windows/CHeroBackpackWindow.cpp @@ -20,6 +20,8 @@ #include "render/Canvas.h" #include "CPlayerInterface.h" +#include "../../CCallback.h" + #include "../../lib/mapObjects/CGHeroInstance.h" #include "../../lib/networkPacks/ArtifactLocation.h" @@ -33,7 +35,7 @@ CHeroBackpackWindow::CHeroBackpackWindow(const CGHeroInstance * hero, const std: arts->moveBy(Point(windowMargin, windowMargin)); arts->clickPressedCallback = [this](const CArtPlace & artPlace, const Point & cursorPosition) { - clickPressedOnArtPlace(arts->getHero(), artPlace.slot, true, false, true); + clickPressedOnArtPlace(arts->getHero(), artPlace.slot, true, false, true, cursorPosition); }; arts->showPopupCallback = [this](CArtPlace & artPlace, const Point & cursorPosition) { @@ -41,17 +43,38 @@ CHeroBackpackWindow::CHeroBackpackWindow(const CGHeroInstance * hero, const std: }; addSet(arts); arts->setHero(hero); - quitButton = std::make_shared(Point(), AnimationPath::builtin("IOKAY32.def"), CButton::tooltip(""), - [this]() { WindowBase::close(); }, EShortcut::GLOBAL_RETURN); + + buttons.emplace_back(std::make_unique(Point(), AnimationPath::builtin("ALTFILL.DEF"), + CButton::tooltipLocalized("vcmi.heroWindow.sortBackpackByCost"), + [hero]() { LOCPLINT->cb->sortBackpackArtifactsByCost(hero->id); })); + buttons.emplace_back(std::make_unique(Point(), AnimationPath::builtin("ALTFILL.DEF"), + CButton::tooltipLocalized("vcmi.heroWindow.sortBackpackBySlot"), + [hero]() { LOCPLINT->cb->sortBackpackArtifactsBySlot(hero->id); })); + buttons.emplace_back(std::make_unique(Point(), AnimationPath::builtin("ALTFILL.DEF"), + CButton::tooltipLocalized("vcmi.heroWindow.sortBackpackByClass"), + [hero]() { LOCPLINT->cb->sortBackpackArtifactsByClass(hero->id); })); + pos.w = stretchedBackground->pos.w = arts->pos.w + 2 * windowMargin; - pos.h = stretchedBackground->pos.h = arts->pos.h + quitButton->pos.h + 3 * windowMargin; - quitButton->moveTo(Point(pos.x + pos.w / 2 - quitButton->pos.w / 2, pos.y + arts->pos.h + 2 * windowMargin)); + pos.h = stretchedBackground->pos.h = arts->pos.h + buttons.back()->pos.h + 3 * windowMargin; + + auto buttonPos = Point(pos.x + windowMargin, pos.y + arts->pos.h + 2 * windowMargin); + for(const auto & button : buttons) + { + button->moveTo(buttonPos); + buttonPos += Point(button->pos.w + 10, 0); + } + statusbar = CGStatusBar::create(0, pos.h, ImagePath::builtin("ADROLLVR.bmp"), pos.w); pos.h += statusbar->pos.h; - + addUsedEvents(LCLICK); center(); } +void CHeroBackpackWindow::notFocusedClick() +{ + close(); +} + void CHeroBackpackWindow::showAll(Canvas & to) { CIntObject::showAll(to); @@ -70,10 +93,6 @@ CHeroQuickBackpackWindow::CHeroQuickBackpackWindow(const CGHeroInstance * hero, if(const auto curHero = arts->getHero()) swapArtifactAndClose(*arts, artPlace.slot, ArtifactLocation(curHero->id, arts->getFilterSlot())); }; - arts->showPopupCallback = [this](CArtPlace & artPlace, const Point & cursorPosition) - { - showArifactInfo(*arts, artPlace, cursorPosition); - }; addSet(arts); arts->setHero(hero); addUsedEvents(GESTURE); diff --git a/client/windows/CHeroBackpackWindow.h b/client/windows/CHeroBackpackWindow.h index 5dcd2ab1c..239a3fa0a 100644 --- a/client/windows/CHeroBackpackWindow.h +++ b/client/windows/CHeroBackpackWindow.h @@ -17,10 +17,11 @@ class CHeroBackpackWindow : public CStatusbarWindow, public CWindowWithArtifacts { public: CHeroBackpackWindow(const CGHeroInstance * hero, const std::vector & artsSets); + void notFocusedClick() override; protected: std::shared_ptr arts; - std::shared_ptr quitButton; + std::vector> buttons; std::shared_ptr stretchedBackground; const int windowMargin = 5; diff --git a/client/windows/CHeroOverview.cpp b/client/windows/CHeroOverview.cpp index b1719df4d..1c1f0bacf 100644 --- a/client/windows/CHeroOverview.cpp +++ b/client/windows/CHeroOverview.cpp @@ -18,15 +18,16 @@ #include "../render/Colors.h" #include "../render/IImage.h" #include "../renderSDL/RenderHandler.h" -#include "../widgets/CComponent.h" +#include "../widgets/CComponentHolder.h" #include "../widgets/Images.h" #include "../widgets/TextControls.h" #include "../widgets/GraphicalPrimitiveCanvas.h" #include "../../lib/IGameSettings.h" +#include "../../lib/entities/hero/CHeroHandler.h" +#include "../../lib/entities/hero/CHeroClass.h" #include "../../lib/texts/CGeneralTextHandler.h" #include "../../lib/CCreatureHandler.h" -#include "../../lib/CHeroHandler.h" #include "../../lib/CSkillHandler.h" #include "../../lib/spells/CSpellHandler.h" @@ -205,7 +206,8 @@ void CHeroOverview::genControls() i = 0; for(auto & skill : (*CGI->heroh)[heroIdx]->secSkillsInit) { - imageSecSkills.push_back(std::make_shared(AnimationPath::builtin("SECSK32"), (*CGI->skillh)[skill.first]->getIconIndex() * 3 + skill.second + 2, 0, 302, 7 * borderOffset + yOffset + 186 + i * (32 + borderOffset))); + secSkills.push_back(std::make_shared(Point(302, 7 * borderOffset + yOffset + 186 + i * (32 + borderOffset)), + CSecSkillPlace::ImageSize::SMALL, skill.first, skill.second)); labelSecSkillsNames.push_back(std::make_shared(334 + 2 * borderOffset, 8 * borderOffset + yOffset + 186 + i * (32 + borderOffset) - 5, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE, CGI->generaltexth->levels[skill.second - 1])); labelSecSkillsNames.push_back(std::make_shared(334 + 2 * borderOffset, 8 * borderOffset + yOffset + 186 + i * (32 + borderOffset) + 10, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE, (*CGI->skillh)[skill.first]->getNameTranslated())); i++; diff --git a/client/windows/CHeroOverview.h b/client/windows/CHeroOverview.h index 64c9b16e9..38e5fa683 100644 --- a/client/windows/CHeroOverview.h +++ b/client/windows/CHeroOverview.h @@ -19,6 +19,7 @@ class CComponentBox; class CTextBox; class TransparentFilledRectangle; class SimpleLine; +class CSecSkillPlace; class CHeroOverview : public CWindowObject { @@ -60,7 +61,7 @@ class CHeroOverview : public CWindowObject std::vector> labelSpellsNames; std::shared_ptr labelSecSkillTitle; - std::vector> imageSecSkills; + std::vector> secSkills; std::vector> labelSecSkillsNames; void genBackground(); diff --git a/client/windows/CHeroWindow.cpp b/client/windows/CHeroWindow.cpp index 36dd52d80..ad73c3960 100644 --- a/client/windows/CHeroWindow.cpp +++ b/client/windows/CHeroWindow.cpp @@ -35,11 +35,11 @@ #include "../lib/ArtifactUtils.h" #include "../lib/CArtHandler.h" #include "../lib/CConfigHandler.h" +#include "../lib/entities/hero/CHeroHandler.h" #include "../lib/texts/CGeneralTextHandler.h" -#include "../lib/CHeroHandler.h" #include "../lib/CSkillHandler.h" #include "../lib/mapObjects/CGHeroInstance.h" -#include "../../lib/networkPacks/ArtifactLocation.h" +#include "../lib/networkPacks/ArtifactLocation.h" void CHeroSwitcher::clickPressed(const Point & cursorPosition) { @@ -152,8 +152,7 @@ CHeroWindow::CHeroWindow(const CGHeroInstance * hero) for(int i = 0; i < std::min(hero->secSkills.size(), 8u); ++i) { Rect r = Rect(i%2 == 0 ? 18 : 162, 276 + 48 * (i/2), 136, 42); - secSkillAreas.push_back(std::make_shared(r, ComponentType::SEC_SKILL)); - secSkillImages.push_back(std::make_shared(AnimationPath::builtin("SECSKILL"), 0, 0, r.x, r.y)); + secSkills.emplace_back(std::make_shared(r.topLeft(), CSecSkillPlace::ImageSize::MEDIUM)); int x = (i % 2) ? 212 : 68; int y = 280 + 48 * (i/2); @@ -184,9 +183,9 @@ void CHeroWindow::update() name->setText(curHero->getNameTranslated()); title->setText((boost::format(CGI->generaltexth->allTexts[342]) % curHero->level % curHero->getClassNameTranslated()).str()); - specArea->text = curHero->type->getSpecialtyDescriptionTranslated(); - specImage->setFrame(curHero->type->imageIndex); - specName->setText(curHero->type->getSpecialtyNameTranslated()); + specArea->text = curHero->getHeroType()->getSpecialtyDescriptionTranslated(); + specImage->setFrame(curHero->getHeroType()->imageIndex); + specName->setText(curHero->getHeroType()->getSpecialtyNameTranslated()); tacticsButton = std::make_shared(Point(539, 483), AnimationPath::builtin("hsbtns8.def"), std::make_pair(heroscrn[26], heroscrn[31]), 0, EShortcut::HERO_TOGGLE_TACTICS); tacticsButton->addHoverText(EButtonState::HIGHLIGHTED, CGI->generaltexth->heroscrn[25]); @@ -200,17 +199,18 @@ void CHeroWindow::update() OBJECT_CONSTRUCTION; if(!garr) { + bool removableTroops = curHero->getOwner() == LOCPLINT->playerID; std::string helpBox = heroscrn[32]; boost::algorithm::replace_first(helpBox, "%s", CGI->generaltexth->allTexts[43]); - garr = std::make_shared(Point(15, 485), 8, Point(), curHero); + garr = std::make_shared(Point(15, 485), 8, Point(), curHero, nullptr, removableTroops); auto split = std::make_shared(Point(539, 519), AnimationPath::builtin("hsbtns9.def"), CButton::tooltip(CGI->generaltexth->allTexts[256], helpBox), [this](){ garr->splitClick(); }, EShortcut::HERO_ARMY_SPLIT); garr->addSplitBtn(split); } if(!arts) { arts = std::make_shared(Point(-65, -8)); - arts->clickPressedCallback = [this](const CArtPlace & artPlace, const Point & cursorPosition){clickPressedOnArtPlace(curHero, artPlace.slot, true, false, false);}; + arts->clickPressedCallback = [this](const CArtPlace & artPlace, const Point & cursorPosition){clickPressedOnArtPlace(curHero, artPlace.slot, true, false, false, cursorPosition);}; arts->showPopupCallback = [this](CArtPlace & artPlace, const Point & cursorPosition){showArtifactAssembling(*arts, artPlace, cursorPosition);}; arts->gestureCallback = [this](const CArtPlace & artPlace, const Point & cursorPosition){showQuickBackpackWindow(curHero, artPlace.slot, cursorPosition);}; arts->setHero(curHero); @@ -234,20 +234,16 @@ void CHeroWindow::update() } //secondary skills support - for(size_t g=0; g< secSkillAreas.size(); ++g) + for(size_t g=0; g< secSkills.size(); ++g) { SecondarySkill skill = curHero->secSkills[g].first; int level = curHero->getSecSkillLevel(skill); std::string skillName = CGI->skillh->getByIndex(skill)->getNameTranslated(); std::string skillValue = CGI->generaltexth->levels[level-1]; - secSkillAreas[g]->component.subType = skill; - secSkillAreas[g]->component.value = level; - secSkillAreas[g]->text = CGI->skillh->getByIndex(skill)->getDescriptionTranslated(level); - secSkillAreas[g]->hoverText = boost::str(boost::format(heroscrn[21]) % skillValue % skillName); - secSkillImages[g]->setFrame(skill*3 + level + 2); secSkillNames[g]->setText(skillName); secSkillValues[g]->setText(skillValue); + secSkills[g]->setSkill(skill, level); } std::ostringstream expstr; @@ -316,6 +312,7 @@ void CHeroWindow::dismissCurrent() arts->putBackPickedArtifact(); close(); LOCPLINT->cb->dismissHero(curHero); + arts->setHero(nullptr); }, nullptr); } diff --git a/client/windows/CHeroWindow.h b/client/windows/CHeroWindow.h index 75a3cff5a..fd33df05d 100644 --- a/client/windows/CHeroWindow.h +++ b/client/windows/CHeroWindow.h @@ -74,8 +74,7 @@ class CHeroWindow : public CStatusbarWindow, public IGarrisonHolder, public CWin std::shared_ptr specName; std::shared_ptr morale; std::shared_ptr luck; - std::vector> secSkillAreas; - std::vector> secSkillImages; + std::vector< std::shared_ptr> secSkills; std::vector> secSkillNames; std::vector> secSkillValues; diff --git a/client/windows/CKingdomInterface.cpp b/client/windows/CKingdomInterface.cpp index ab163ba62..61254a60a 100644 --- a/client/windows/CKingdomInterface.cpp +++ b/client/windows/CKingdomInterface.cpp @@ -33,8 +33,8 @@ #include "../../lib/CConfigHandler.h" #include "../../lib/CCreatureHandler.h" +#include "../../lib/entities/hero/CHeroHandler.h" #include "../../lib/texts/CGeneralTextHandler.h" -#include "../../lib/CHeroHandler.h" #include "../../lib/IGameSettings.h" #include "../../lib/CSkillHandler.h" #include "../../lib/StartInfo.h" @@ -300,7 +300,7 @@ int InfoBoxHeroData::getSubID() else return 0; case HERO_SPECIAL: - return hero->type->getIndex(); + return hero->getHeroTypeID().getNum(); case HERO_MANA: case HERO_EXPERIENCE: return 0; @@ -473,7 +473,7 @@ CKingdomInterface::CKingdomInterface() generateButtons(); statusbar = CGStatusBar::create(std::make_shared(ImagePath::builtin("KSTATBAR"), 10,pos.h - 45)); - resdatabar = std::make_shared(ImagePath::builtin("KRESBAR"), 7, 111+footerPos, 29, 5, 76, 81); + resdatabar = std::make_shared(ImagePath::builtin("KRESBAR"), 7, 111+footerPos, 29, 3, 76, 81); activateTab(persistentStorage["gui"]["lastKindomInterface"].Integer()); } @@ -552,7 +552,7 @@ std::shared_ptr CKingdomInterface::createMainTab(size_t index) { newHeroSet->clickPressedCallback = [this, newHeroSet](const CArtPlace & artPlace, const Point & cursorPosition) { - clickPressedOnArtPlace(newHeroSet->getHero(), artPlace.slot, false, false, false); + clickPressedOnArtPlace(newHeroSet->getHero(), artPlace.slot, false, false, false, cursorPosition); }; newHeroSet->showPopupCallback = [this, newHeroSet](CArtPlace & artPlace, const Point & cursorPosition) { @@ -583,28 +583,16 @@ void CKingdomInterface::generateMinesList(const std::vectorID == Obj::MINE || object->ID == Obj::ABANDONED_MINE) { const CGMine * mine = dynamic_cast(object); - assert(mine); minesCount[mine->producedResource]++; - totalIncome += mine->dailyIncome()[EGameResID::GOLD]; } } - //Heroes can produce gold as well - skill, specialty or arts - std::vector heroes = LOCPLINT->cb->getHeroesInfo(true); - auto * playerSettings = LOCPLINT->cb->getPlayerSettings(LOCPLINT->playerID); - for(auto & hero : heroes) - { - totalIncome += hero->dailyIncome()[EGameResID::GOLD]; - } - - //Add town income of all towns - std::vector towns = LOCPLINT->cb->getTownsInfo(true); - for(auto & town : towns) - { - totalIncome += town->dailyIncome()[EGameResID::GOLD]; - } + for(auto & mapObject : ownedObjects) + totalIncome += mapObject->asOwnable()->dailyIncome()[EGameResID::GOLD]; //if player has some modded boosts we want to show that as well + const auto * playerSettings = LOCPLINT->cb->getPlayerSettings(LOCPLINT->playerID); + const auto & towns = LOCPLINT->cb->getTownsInfo(true); totalIncome += LOCPLINT->cb->getPlayerState(LOCPLINT->playerID)->valOfBonuses(BonusType::RESOURCES_CONSTANT_BOOST, BonusSubtypeID(GameResID(EGameResID::GOLD))) * playerSettings->handicap.percentIncome / 100; totalIncome += LOCPLINT->cb->getPlayerState(LOCPLINT->playerID)->valOfBonuses(BonusType::RESOURCES_TOWN_MULTIPLYING_BOOST, BonusSubtypeID(GameResID(EGameResID::GOLD))) * towns.size() * playerSettings->handicap.percentIncome / 100; @@ -800,12 +788,12 @@ CTownItem::CTownItem(const CGTownInstance * Town) garr = std::make_shared(Point(313, 3), 4, Point(232,0), town->getUpperArmy(), town->visitingHero, true, true, CGarrisonInt::ESlotsLayout::TWO_ROWS); heroes = std::make_shared(town, Point(244,6), Point(475,6), garr, false); - size_t iconIndex = town->town->clientInfo.icons[town->hasFort()][town->built >= LOCPLINT->cb->getSettings().getInteger(EGameSettings::TOWNS_BUILDINGS_PER_TURN_CAP)]; + size_t iconIndex = town->getTown()->clientInfo.icons[town->hasFort()][town->built >= LOCPLINT->cb->getSettings().getInteger(EGameSettings::TOWNS_BUILDINGS_PER_TURN_CAP)]; picture = std::make_shared(AnimationPath::builtin("ITPT"), iconIndex, 0, 5, 6); openTown = std::make_shared(Rect(5, 6, 58, 64), town); - for(size_t i=0; icreatures.size(); i++) + for(size_t i=0; icreatures.size() && i(Point(401+37*(int)i, 78), town, (int)i, true, true)); available.push_back(std::make_shared(Point(48+37*(int)i, 78), town, (int)i, true, false)); @@ -823,8 +811,8 @@ CTownItem::CTownItem(const CGTownInstance * Town) if(town->hasBuilt(BuildingID::TAVERN)) LOCPLINT->showTavernWindow(town, nullptr, QueryID::NONE); }, [&]{ - if(!town->town->faction->getDescriptionTranslated().empty()) - CRClickPopup::createAndPush(town->town->faction->getDescriptionTranslated()); + if(!town->getTown()->faction->getDescriptionTranslated().empty()) + CRClickPopup::createAndPush(town->getFaction()->getDescriptionTranslated()); }); fastMarket = std::make_shared(Rect(153, 6, 65, 64), []() { @@ -866,7 +854,7 @@ void CTownItem::update() heroes->update(); - for (size_t i=0; icreatures.size(); i++) + for (size_t i=0; i(town->creatures.size()), GameConstants::CREATURES_PER_TOWN); i++) { growth[i]->update(); available[i]->update(); diff --git a/client/windows/CMapOverview.cpp b/client/windows/CMapOverview.cpp index 9e96ef8dc..c0845133d 100644 --- a/client/windows/CMapOverview.cpp +++ b/client/windows/CMapOverview.cpp @@ -67,9 +67,9 @@ Canvas CMapOverviewWidget::createMinimapForLayer(std::unique_ptr & map, in { TerrainTile & tile = map->getTile(int3(x, y, layer)); - ColorRGBA color = tile.terType->minimapUnblocked; - if (tile.blocked && (!tile.visitable)) - color = tile.terType->minimapBlocked; + ColorRGBA color = tile.getTerrain()->minimapUnblocked; + if (tile.blocked() && !tile.visitable()) + color = tile.getTerrain()->minimapBlocked; if(drawPlayerElements) // if object at tile is owned - it will be colored as its owner diff --git a/client/windows/CMarketWindow.cpp b/client/windows/CMarketWindow.cpp index bf7661696..1fbd750fd 100644 --- a/client/windows/CMarketWindow.cpp +++ b/client/windows/CMarketWindow.cpp @@ -192,7 +192,7 @@ std::string CMarketWindow::getMarketTitle(const ObjectInstanceID marketId, const { for(const auto & buildingId : town->getBuildings()) { - if(const auto building = town->town->buildings.at(buildingId); vstd::contains(building->marketModes, mode)) + if(const auto building = town->getTown()->buildings.at(buildingId); vstd::contains(building->marketModes, mode)) return building->getNameTranslated(); } } @@ -219,7 +219,6 @@ void CMarketWindow::createArtifactsSelling(const IMarket * market, const CGHeroI auto artsSellingMarket = std::make_shared(market, hero, getMarketTitle(market->getObjInstanceID(), EMarketMode::ARTIFACT_RESOURCE)); artSets.clear(); const auto heroArts = artsSellingMarket->getAOHset(); - heroArts->showPopupCallback = [this, heroArts](CArtPlace & artPlace, const Point & cursorPosition){showArifactInfo(*heroArts, artPlace, cursorPosition);}; addSet(heroArts); marketWidget = artsSellingMarket; initWidgetInternals(EMarketMode::ARTIFACT_RESOURCE, CGI->generaltexth->zelp[600]); @@ -263,7 +262,7 @@ void CMarketWindow::createAltarArtifacts(const IMarket * market, const CGHeroIns const auto heroArts = altarArtifactsStorage->getAOHset(); heroArts->clickPressedCallback = [this, heroArts](const CArtPlace & artPlace, const Point & cursorPosition) { - clickPressedOnArtPlace(heroArts->getHero(), artPlace.slot, true, true, false); + clickPressedOnArtPlace(heroArts->getHero(), artPlace.slot, true, true, false, cursorPosition); }; heroArts->showPopupCallback = [this, heroArts](CArtPlace & artPlace, const Point & cursorPosition) { diff --git a/client/windows/CMessage.cpp b/client/windows/CMessage.cpp index 51c0da65c..e5b208641 100644 --- a/client/windows/CMessage.cpp +++ b/client/windows/CMessage.cpp @@ -117,7 +117,13 @@ std::vector CMessage::breakText(std::string text, size_t maxLineWid color = ""; } else - printableString.append(text.data() + currPos, symbolSize); + { + std::string character = ""; + character.append(text.data() + currPos, symbolSize); + if(fontPtr->getStringWidth(printableString + character) > maxLineWidth) + break; + printableString += character; + } currPos += symbolSize; } @@ -129,7 +135,7 @@ std::vector CMessage::breakText(std::string text, size_t maxLineWid if(wordBreak != ui32(-1)) { currPos = wordBreak; - if(text.substr(0, currPos).find('{') == std::string::npos) + if(boost::count(text.substr(0, currPos), '{') == boost::count(text.substr(0, currPos), '}')) { opened = false; color = ""; diff --git a/client/windows/CQuestLog.cpp b/client/windows/CQuestLog.cpp index 2e18ef424..dbc39d439 100644 --- a/client/windows/CQuestLog.cpp +++ b/client/windows/CQuestLog.cpp @@ -78,7 +78,7 @@ void CQuestMinimap::addQuestMarks (const QuestInfo * q) int3 tile; if (q->obj) - tile = q->obj->pos; + tile = q->obj->visitablePos(); else tile = q->tile; @@ -104,7 +104,7 @@ void CQuestMinimap::update() void CQuestMinimap::iconClicked() { if(currentQuest->obj) - adventureInt->centerOnTile(currentQuest->obj->pos); + adventureInt->centerOnTile(currentQuest->obj->visitablePos()); //moveAdvMapSelection(); } diff --git a/client/windows/CSpellWindow.cpp b/client/windows/CSpellWindow.cpp index 778b7db6c..ff531c0f3 100644 --- a/client/windows/CSpellWindow.cpp +++ b/client/windows/CSpellWindow.cpp @@ -30,6 +30,7 @@ #include "../widgets/CTextInput.h" #include "../widgets/TextControls.h" #include "../widgets/Buttons.h" +#include "../widgets/VideoWidget.h" #include "../adventureMap/AdventureMapInterface.h" #include "../render/AssetGenerator.h" @@ -205,9 +206,9 @@ CSpellWindow::CSpellWindow(const CGHeroInstance * _myHero, CPlayerInterface * _m } } - selectedTab = battleSpellsOnly ? myInt->localState->spellbookSettings.spellbookLastTabBattle : myInt->localState->spellbookSettings.spellbookLastTabAdvmap; + selectedTab = battleSpellsOnly ? myInt->localState->getSpellbookSettings().spellbookLastTabBattle : myInt->localState->getSpellbookSettings().spellbookLastTabAdvmap; schoolTab->setFrame(selectedTab, 0); - int cp = battleSpellsOnly ? myInt->localState->spellbookSettings.spellbookLastPageBattle : myInt->localState->spellbookSettings.spellbookLastPageAdvmap; + int cp = battleSpellsOnly ? myInt->localState->getSpellbookSettings().spellbookLastPageBattle : myInt->localState->getSpellbookSettings().spellbookLastPageAdvmap; // spellbook last page battle index is not reset after battle, so this needs to stay here vstd::abetween(cp, 0, std::max(0, pagesWithinCurrentTab() - 1)); setCurrentPage(cp); @@ -313,8 +314,18 @@ void CSpellWindow::processSpells() void CSpellWindow::fexitb() { - (myInt->battleInt ? myInt->localState->spellbookSettings.spellbookLastTabBattle : myInt->localState->spellbookSettings.spellbookLastTabAdvmap) = selectedTab; - (myInt->battleInt ? myInt->localState->spellbookSettings.spellbookLastPageBattle : myInt->localState->spellbookSettings.spellbookLastPageAdvmap) = currentPage; + auto spellBookState = myInt->localState->getSpellbookSettings(); + if(myInt->battleInt) + { + spellBookState.spellbookLastTabBattle = selectedTab; + spellBookState.spellbookLastPageBattle = currentPage; + } + else + { + spellBookState.spellbookLastTabAdvmap = selectedTab; + spellBookState.spellbookLastPageAdvmap = currentPage; + } + myInt->localState->setSpellbookSettings(spellBookState); if(onSpellSelect) onSpellSelect(SpellID::NONE); @@ -385,6 +396,8 @@ void CSpellWindow::fRcornerb() void CSpellWindow::show(Canvas & to) { + if(video) + video->show(to); statusBar->show(to); } @@ -483,14 +496,22 @@ void CSpellWindow::setCurrentPage(int value) void CSpellWindow::turnPageLeft() { + OBJECT_CONSTRUCTION; if(settings["video"]["spellbookAnimation"].Bool() && !isBigSpellbook) - CCS->videoh->playSpellbookAnimation(VideoPath::builtin("PGTRNLFT.SMK"), pos.topLeft() + Point(13, 14)); + video = std::make_shared(Point(13, 14), VideoPath::builtin("PGTRNLFT.SMK"), false, [this](){ + video.reset(); + redraw(); + }); } void CSpellWindow::turnPageRight() { + OBJECT_CONSTRUCTION; if(settings["video"]["spellbookAnimation"].Bool() && !isBigSpellbook) - CCS->videoh->playSpellbookAnimation(VideoPath::builtin("PGTRNRGH.SMK"), pos.topLeft() + Point(13, 14)); + video = std::make_shared(Point(13, 14), VideoPath::builtin("PGTRNRGH.SMK"), false, [this](){ + video.reset(); + redraw(); + }); } void CSpellWindow::keyPressed(EShortcut key) @@ -619,8 +640,10 @@ void CSpellWindow::SpellArea::clickPressed(const Point & cursorPosition) auto guard = vstd::makeScopeGuard([this]() { - owner->myInt->localState->spellbookSettings.spellbookLastTabAdvmap = owner->selectedTab; - owner->myInt->localState->spellbookSettings.spellbookLastPageAdvmap = owner->currentPage; + auto spellBookState = owner->myInt->localState->getSpellbookSettings(); + spellBookState.spellbookLastTabAdvmap = owner->selectedTab; + spellBookState.spellbookLastPageAdvmap = owner->currentPage; + owner->myInt->localState->setSpellbookSettings(spellBookState); }); spells::detail::ProblemImpl problem; diff --git a/client/windows/CSpellWindow.h b/client/windows/CSpellWindow.h index 29293faa4..70b495055 100644 --- a/client/windows/CSpellWindow.h +++ b/client/windows/CSpellWindow.h @@ -28,6 +28,7 @@ class CSpellWindow; class CTextInput; class TransparentFilledRectangle; class CToggleButton; +class VideoWidgetOnce; /// The spell window class CSpellWindow : public CWindowObject @@ -86,6 +87,8 @@ class CSpellWindow : public CWindowObject std::shared_ptr showAllSpells; std::shared_ptr showAllSpellsDescription; + std::shared_ptr video; + bool isBigSpellbook; int spellsPerPage; int offL; diff --git a/client/windows/CWindowWithArtifacts.cpp b/client/windows/CWindowWithArtifacts.cpp index 0cd4f7032..67afb0c1c 100644 --- a/client/windows/CWindowWithArtifacts.cpp +++ b/client/windows/CWindowWithArtifacts.cpp @@ -62,19 +62,16 @@ const CGHeroInstance * CWindowWithArtifacts::getHeroPickedArtifact() const const CArtifactInstance * CWindowWithArtifacts::getPickedArtifact() const { - const CArtifactInstance * art = nullptr; - for(const auto & artSet : artSets) if(const auto pickedArt = artSet->getHero()->getArt(ArtifactPosition::TRANSITION_POS)) { - art = pickedArt; - break; + return pickedArt; } - return art; + return nullptr; } void CWindowWithArtifacts::clickPressedOnArtPlace(const CGHeroInstance * hero, const ArtifactPosition & slot, - bool allowExchange, bool altarTrading, bool closeWindow) + bool allowExchange, bool altarTrading, bool closeWindow, const Point & cursorPosition) { if(!LOCPLINT->makingTurn) return; @@ -88,8 +85,7 @@ void CWindowWithArtifacts::clickPressedOnArtPlace(const CGHeroInstance * hero, c } else if(GH.isKeyboardShiftDown()) { - if(ArtifactUtils::isSlotEquipment(slot)) - GH.windows().createAndPushWindow(hero, slot); + showQuickBackpackWindow(hero, slot, cursorPosition); } else if(auto art = hero->getArt(slot)) { @@ -131,18 +127,15 @@ void CWindowWithArtifacts::showArtifactAssembling(const CArtifactsOfHeroBase & a } } -void CWindowWithArtifacts::showArifactInfo(const CArtifactsOfHeroBase & artsInst, CArtPlace & artPlace, const Point & cursorPosition) const -{ - if(artsInst.getArt(artPlace.slot) && artPlace.text.size()) - artPlace.LRClickableAreaWTextComp::showPopupWindow(cursorPosition); -} - void CWindowWithArtifacts::showQuickBackpackWindow(const CGHeroInstance * hero, const ArtifactPosition & slot, const Point & cursorPosition) const { if(!settings["general"]["enableUiEnhancements"].Bool()) return; + if(!ArtifactUtils::isSlotEquipment(slot)) + return; + GH.windows().createAndPushWindow(hero, slot); auto backpackWindow = GH.windows().topWindow(); backpackWindow->moveTo(cursorPosition - Point(1, 1)); @@ -207,7 +200,7 @@ void CWindowWithArtifacts::markPossibleSlots() const continue; if(getHeroPickedArtifact() == hero || !std::dynamic_pointer_cast(artSet)) - artSet->markPossibleSlots(pickedArtInst, hero->tempOwner == LOCPLINT->playerID); + artSet->markPossibleSlots(pickedArtInst->getType(), hero->tempOwner == LOCPLINT->playerID); } } } @@ -228,7 +221,7 @@ bool CWindowWithArtifacts::checkSpecialArts(const CArtifactInstance & artInst, c std::vector>(1, std::make_shared(ComponentType::ARTIFACT, ArtifactID(ArtifactID::CATAPULT)))); return false; } - if(isTrade && !artInst.artType->isTradable()) + if(isTrade && !artInst.getType()->isTradable()) { LOCPLINT->showInfoDialog(CGI->generaltexth->allTexts[21], std::vector>(1, std::make_shared(ComponentType::ARTIFACT, artId))); @@ -249,7 +242,7 @@ void CWindowWithArtifacts::setCursorAnimation(const CArtifactInstance & artInst) } else { - CCS->curh->dragAndDropCursor(AnimationPath::builtin("artifact"), artInst.artType->getIconIndex()); + CCS->curh->dragAndDropCursor(AnimationPath::builtin("artifact"), artInst.getType()->getIconIndex()); } } @@ -262,10 +255,10 @@ void CWindowWithArtifacts::putPickedArtifact(const CGHeroInstance & curHero, con if(ArtifactUtils::isSlotBackpack(dstLoc.slot)) { - if(pickedArt->artType->isBig()) + if(pickedArt->getType()->isBig()) { // War machines cannot go to backpack - LOCPLINT->showInfoDialog(boost::str(boost::format(CGI->generaltexth->allTexts[153]) % pickedArt->artType->getNameTranslated())); + LOCPLINT->showInfoDialog(boost::str(boost::format(CGI->generaltexth->allTexts[153]) % pickedArt->getType()->getNameTranslated())); } else { diff --git a/client/windows/CWindowWithArtifacts.h b/client/windows/CWindowWithArtifacts.h index c6af89aba..cd6a42484 100644 --- a/client/windows/CWindowWithArtifacts.h +++ b/client/windows/CWindowWithArtifacts.h @@ -28,10 +28,9 @@ public: const CGHeroInstance * getHeroPickedArtifact() const; const CArtifactInstance * getPickedArtifact() const; void clickPressedOnArtPlace(const CGHeroInstance * hero, const ArtifactPosition & slot, - bool allowExchange, bool altarTrading, bool closeWindow); + bool allowExchange, bool altarTrading, bool closeWindow, const Point & cursorPosition); void swapArtifactAndClose(const CArtifactsOfHeroBase & artsInst, const ArtifactPosition & slot, const ArtifactLocation & dstLoc); void showArtifactAssembling(const CArtifactsOfHeroBase & artsInst, CArtPlace & artPlace, const Point & cursorPosition) const; - void showArifactInfo(const CArtifactsOfHeroBase & artsInst, CArtPlace & artPlace, const Point & cursorPosition) const; void showQuickBackpackWindow(const CGHeroInstance * hero, const ArtifactPosition & slot, const Point & cursorPosition) const; void activate() override; void deactivate() override; diff --git a/client/windows/CreaturePurchaseCard.cpp b/client/windows/CreaturePurchaseCard.cpp index b61adf9f1..50ddc9175 100644 --- a/client/windows/CreaturePurchaseCard.cpp +++ b/client/windows/CreaturePurchaseCard.cpp @@ -54,8 +54,8 @@ void CreaturePurchaseCard::switchCreatureLevel() auto index = vstd::find_pos(upgradesID, creatureOnTheCard->getId()); auto nextCreatureId = vstd::circularAt(upgradesID, ++index); creatureOnTheCard = nextCreatureId.toCreature(); - picture = std::make_shared(parent->pos.x, parent->pos.y, creatureOnTheCard); - creatureClickArea = std::make_shared(Point(parent->pos.x, parent->pos.y), picture, creatureOnTheCard); + picture = std::make_shared(picture->pos.x - pos.x, picture->pos.y - pos.y, creatureOnTheCard); + creatureClickArea = std::make_shared(Point(picture->pos.x - pos.x, picture->pos.y - pos.y), picture, creatureOnTheCard); parent->updateAllSliders(); cost->set(creatureOnTheCard->getFullRecruitCost() * slider->getValue()); } diff --git a/client/windows/GUIClasses.cpp b/client/windows/GUIClasses.cpp index 46a9775e5..e745653f6 100644 --- a/client/windows/GUIClasses.cpp +++ b/client/windows/GUIClasses.cpp @@ -44,6 +44,7 @@ #include "../lib/entities/building/CBuilding.h" #include "../lib/entities/faction/CTownHandler.h" +#include "../lib/entities/hero/CHeroHandler.h" #include "../lib/mapObjectConstructors/CObjectClassesHandler.h" #include "../lib/mapObjectConstructors/CommonConstructors.h" #include "../lib/mapObjects/CGHeroInstance.h" @@ -53,7 +54,6 @@ #include "../lib/gameState/SThievesGuildInfo.h" #include "../lib/gameState/TavernHeroesPool.h" #include "../lib/texts/CGeneralTextHandler.h" -#include "../lib/CHeroHandler.h" #include "../lib/IGameSettings.h" #include "ConditionalWait.h" #include "../lib/CRandomGenerator.h" @@ -522,9 +522,9 @@ CTavernWindow::CTavernWindow(const CGObjectInstance * TavernObj, const std::func recruit->block(true); } if(LOCPLINT->castleInt) - videoPlayer = std::make_shared(Point(70, 56), LOCPLINT->castleInt->town->town->clientInfo.tavernVideo, false); + videoPlayer = std::make_shared(Point(70, 56), LOCPLINT->castleInt->town->getTown()->clientInfo.tavernVideo, false); else if(const auto * townObj = dynamic_cast(TavernObj)) - videoPlayer = std::make_shared(Point(70, 56), townObj->town->clientInfo.tavernVideo, false); + videoPlayer = std::make_shared(Point(70, 56), townObj->getTown()->clientInfo.tavernVideo, false); else videoPlayer = std::make_shared(Point(70, 56), VideoPath::builtin("TAVERN.BIK"), false); @@ -548,11 +548,12 @@ void CTavernWindow::addInvite() if(!inviteableHeroes.empty()) { + int imageIndex = heroToInvite ? heroToInvite->getIconIndex() : 156; // 156 => special id for random if(!heroToInvite) heroToInvite = (*RandomGeneratorUtil::nextItem(inviteableHeroes, CRandomGenerator::getDefault())).second; inviteHero = std::make_shared(170, 444, EFonts::FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->translate("vcmi.tavernWindow.inviteHero")); - inviteHeroImage = std::make_shared(AnimationPath::builtin("PortraitsSmall"), (*CGI->heroh)[heroToInvite->getHeroType()]->imageIndex, 0, 245, 428); + inviteHeroImage = std::make_shared(AnimationPath::builtin("PortraitsSmall"), imageIndex, 0, 245, 428); inviteHeroImageArea = std::make_shared(Rect(245, 428, 48, 32), [this](){ GH.windows().createAndPushWindow(inviteableHeroes, [this](CGHeroInstance* h){ heroToInvite = h; addInvite(); }); }, [this](){ GH.windows().createAndPushWindow(std::make_shared(heroToInvite)); }); } } @@ -562,7 +563,7 @@ void CTavernWindow::recruitb() const CGHeroInstance *toBuy = (selected ? h2 : h1)->h; const CGObjectInstance *obj = tavernObj; - LOCPLINT->cb->recruitHero(obj, toBuy, heroToInvite ? heroToInvite->getHeroType() : HeroTypeID::NONE); + LOCPLINT->cb->recruitHero(obj, toBuy, heroToInvite ? heroToInvite->getHeroTypeID() : HeroTypeID::NONE); close(); } @@ -895,28 +896,18 @@ CUniversityWindow::CItem::CItem(CUniversityWindow * _parent, int _ID, int X, int pos.x += X; pos.y += Y; - icon = std::make_shared(AnimationPath::builtin("SECSKILL"), _ID * 3 + 3, 0); - - pos.h = icon->pos.h; - pos.w = icon->pos.w; + skill = std::make_shared(Point(), CSecSkillPlace::ImageSize::MEDIUM, _ID, 1); + skill->setClickPressedCallback([this](const CComponentHolder&, const Point& cursorPosition) + { + bool skillKnown = parent->hero->getSecSkillLevel(ID); + bool canLearn = parent->hero->canLearnSkill(ID); + if(!skillKnown && canLearn) + GH.windows().createAndPushWindow(parent, ID, LOCPLINT->cb->getResourceAmount(EGameResID::GOLD) >= 2000); + }); update(); } -void CUniversityWindow::CItem::clickPressed(const Point & cursorPosition) -{ - bool skillKnown = parent->hero->getSecSkillLevel(ID); - bool canLearn = parent->hero->canLearnSkill(ID); - - if (!skillKnown && canLearn) - GH.windows().createAndPushWindow(parent, ID, LOCPLINT->cb->getResourceAmount(EGameResID::GOLD) >= 2000); -} - -void CUniversityWindow::CItem::showPopupWindow(const Point & cursorPosition) -{ - CRClickPopup::createAndPush(CGI->skillh->getByIndex(ID)->getDescriptionTranslated(1), std::make_shared(ComponentType::SEC_SKILL, ID, 1)); -} - void CUniversityWindow::CItem::update() { bool skillKnown = parent->hero->getSecSkillLevel(ID); @@ -940,14 +931,6 @@ void CUniversityWindow::CItem::update() level = std::make_shared(22, 57, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->levels[0]); } -void CUniversityWindow::CItem::hover(bool on) -{ - if(on) - GH.statusbar()->write(ID.toEntity(VLC)->getNameTranslated()); - else - GH.statusbar()->clear(); -} - CUniversityWindow::CUniversityWindow(const CGHeroInstance * _hero, BuildingID building, const IMarket * _market, const std::function & onWindowClosed) : CWindowObject(PLAYER_COLORED, ImagePath::builtin("UNIVERS1")), hero(_hero), @@ -962,14 +945,14 @@ CUniversityWindow::CUniversityWindow(const CGHeroInstance * _hero, BuildingID bu if(auto town = dynamic_cast(_market)) { - auto faction = town->town->faction->getId(); + auto faction = town->getTown()->faction->getId(); titlePic = std::make_shared((*CGI->townh)[faction]->town->clientInfo.buildingsIcons, building); } else if(auto uni = dynamic_cast(_market); uni->appearance) { - titlePic = std::make_shared(uni->appearance->animationFile, 0); - titleStr = uni->title; - speechStr = uni->speech; + titlePic = std::make_shared(uni->appearance->animationFile, 0, 0, 0, 0, CShowableAnim::CREATURE_MODE); + titleStr = uni->getObjectName(); + speechStr = uni->getSpeechTranslated(); } else { @@ -1073,7 +1056,7 @@ CGarrisonWindow::CGarrisonWindow(const CArmedInstance * up, const CGHeroInstance if(up->Slots().size() > 0) { titleText = CGI->generaltexth->allTexts[35]; - boost::algorithm::replace_first(titleText, "%s", up->Slots().begin()->second->type->getNamePluralTranslated()); + boost::algorithm::replace_first(titleText, "%s", up->Slots().begin()->second->getType()->getNamePluralTranslated()); } else { @@ -1130,6 +1113,9 @@ CHillFortWindow::CHillFortWindow(const CGHeroInstance * visitor, const CGObjectI statusbar = CGStatusBar::create(std::make_shared(background->getSurface(), Rect(8, pos.h - 26, pos.w - 16, 19), 8, pos.h - 26)); garr = std::make_shared(Point(108, 60), 18, Point(), hero, nullptr); + + statusbar->write(VLC->generaltexth->translate(dynamic_cast(fort)->getDescriptionToolTip())); + updateGarrisons(); } @@ -1147,45 +1133,59 @@ void CHillFortWindow::updateGarrisons() TResources totalSum; // totalSum[resource ID] = value + auto getImgIdx = [](CHillFortWindow::State st) -> std::size_t + { + switch (st) + { + case State::EMPTY: + return 0; + case State::UNAVAILABLE: + case State::ALREADY_UPGRADED: + return 1; + default: + return static_cast(st); + } + }; + for(int i=0; icb->fillUpgradeInfo(hero, SlotID(i), info); if(info.newID.size())//we have upgrades here - update costs { - costs[i] = info.cost[0] * hero->getStackCount(SlotID(i)); + costs[i] = info.cost.back() * hero->getStackCount(SlotID(i)); totalSum += costs[i]; } } currState[i] = newState; - upgrade[i]->setImage(AnimationPath::builtin(currState[i] == -1 ? slotImages[0] : slotImages[currState[i]])); - upgrade[i]->block(currState[i] == -1); + upgrade[i]->setImage(AnimationPath::builtin(slotImages[getImgIdx(currState[i])])); + upgrade[i]->block(currState[i] == State::EMPTY); upgrade[i]->addHoverText(EButtonState::NORMAL, getTextForSlot(SlotID(i))); } //"Upgrade all" slot - int newState = 2; + State newState = State::MAKE_UPGRADE; { TResources myRes = LOCPLINT->cb->getResourceAmount(); bool allUpgraded = true;//All creatures are upgraded? for(int i=0; isetImage(AnimationPath::builtin(allImages[newState])); + upgradeAll->setImage(AnimationPath::builtin(allImages[static_cast(newState)])); garr->recreateSlots(); @@ -1198,7 +1198,7 @@ void CHillFortWindow::updateGarrisons() slotLabels[i][j]->setText(""); } //if can upgrade or can not afford, draw cost - if(currState[i] == 0 || currState[i] == 2) + if(currState[i] == State::UNAFFORDABLE || currState[i] == State::MAKE_UPGRADE) { if(costs[i].nonZero()) { @@ -1243,24 +1243,30 @@ void CHillFortWindow::updateGarrisons() void CHillFortWindow::makeDeal(SlotID slot) { - assert(slot.getNum()>=0); - int offset = (slot.getNum() == slotsCount)?2:0; + assert(slot.getNum() >= 0); + int offset = (slot.getNum() == slotsCount) ? 2 : 0; switch(currState[slot.getNum()]) { - case 0: - LOCPLINT->showInfoDialog(CGI->generaltexth->allTexts[314 + offset], std::vector>(), soundBase::sound_todo); - break; - case 1: + case State::ALREADY_UPGRADED: LOCPLINT->showInfoDialog(CGI->generaltexth->allTexts[313 + offset], std::vector>(), soundBase::sound_todo); break; - case 2: - for(int i=0; ishowInfoDialog(CGI->generaltexth->allTexts[314 + offset], std::vector>(), soundBase::sound_todo); + break; + case State::UNAVAILABLE: + { + std::string message = VLC->generaltexth->translate(dynamic_cast(fort)->getUnavailableUpgradeMessage()); + LOCPLINT->showInfoDialog(message, std::vector>(), soundBase::sound_todo); + break; + } + case State::MAKE_UPGRADE: + for(int i = 0; i < slotsCount; i++) { - if(slot.getNum() ==i || ( slot.getNum() == slotsCount && currState[i] == 2 ))//this is activated slot or "upgrade all" + if(slot.getNum() == i || ( slot.getNum() == slotsCount && currState[i] == State::MAKE_UPGRADE ))//this is activated slot or "upgrade all" { UpgradeInfo info; LOCPLINT->cb->fillUpgradeInfo(hero, SlotID(i), info); - LOCPLINT->cb->upgradeCreature(hero, SlotID(i), info.newID[0]); + LOCPLINT->cb->upgradeCreature(hero, SlotID(i), info.newID.back()); } } break; @@ -1282,22 +1288,28 @@ std::string CHillFortWindow::getTextForSlot(SlotID slot) return str; } -int CHillFortWindow::getState(SlotID slot) +CHillFortWindow::State CHillFortWindow::getState(SlotID slot) { TResources myRes = LOCPLINT->cb->getResourceAmount(); - if(hero->slotEmpty(slot))//no creature here - return -1; + if(hero->slotEmpty(slot)) + return State::EMPTY; UpgradeInfo info; LOCPLINT->cb->fillUpgradeInfo(hero, slot, info); - if(!info.newID.size())//already upgraded - return 1; + if (info.newID.empty()) + { + // Hill Fort may limit level of upgradeable creatures, e.g. mini Hill Fort from HOTA + if (hero->getCreature(slot)->hasUpgrades()) + return State::UNAVAILABLE; - if(!(info.cost[0] * hero->getStackCount(slot)).canBeAfforded(myRes)) - return 0; + return State::ALREADY_UPGRADED; + } - return 2;//can upgrade + if(!(info.cost.back() * hero->getStackCount(slot)).canBeAfforded(myRes)) + return State::UNAFFORDABLE; + + return State::MAKE_UPGRADE; } CThievesGuildWindow::CThievesGuildWindow(const CGObjectInstance * _owner): @@ -1479,39 +1491,47 @@ void CObjectListWindow::CItem::showPopupWindow(const Point & cursorPosition) parent->onPopup(index); } -CObjectListWindow::CObjectListWindow(const std::vector & _items, std::shared_ptr titleWidget_, std::string _title, std::string _descr, std::function Callback, size_t initialSelection, std::vector> images) +CObjectListWindow::CObjectListWindow(const std::vector & _items, std::shared_ptr titleWidget_, std::string _title, std::string _descr, std::function Callback, size_t initialSelection, std::vector> images, bool searchBoxEnabled) : CWindowObject(PLAYER_COLORED, ImagePath::builtin("TPGATE")), onSelect(Callback), selected(initialSelection), images(images) { OBJECT_CONSTRUCTION; + + addUsedEvents(KEYBOARD); + items.reserve(_items.size()); for(int id : _items) - { items.push_back(std::make_pair(id, LOCPLINT->cb->getObjInstance(ObjectInstanceID(id))->getObjectName())); - } + itemsVisible = items; - init(titleWidget_, _title, _descr); + init(titleWidget_, _title, _descr, searchBoxEnabled); + list->scrollTo(std::min(static_cast(initialSelection + 4), static_cast(items.size() - 1))); // 4 is for centering (list have 9 elements) } -CObjectListWindow::CObjectListWindow(const std::vector & _items, std::shared_ptr titleWidget_, std::string _title, std::string _descr, std::function Callback, size_t initialSelection, std::vector> images) +CObjectListWindow::CObjectListWindow(const std::vector & _items, std::shared_ptr titleWidget_, std::string _title, std::string _descr, std::function Callback, size_t initialSelection, std::vector> images, bool searchBoxEnabled) : CWindowObject(PLAYER_COLORED, ImagePath::builtin("TPGATE")), onSelect(Callback), selected(initialSelection), images(images) { OBJECT_CONSTRUCTION; + + addUsedEvents(KEYBOARD); + items.reserve(_items.size()); for(size_t i=0; i<_items.size(); i++) items.push_back(std::make_pair(int(i), _items[i])); + itemsVisible = items; - init(titleWidget_, _title, _descr); + init(titleWidget_, _title, _descr, searchBoxEnabled); + list->scrollTo(std::min(static_cast(initialSelection + 4), static_cast(items.size() - 1))); // 4 is for centering (list have 9 elements) } -void CObjectListWindow::init(std::shared_ptr titleWidget_, std::string _title, std::string _descr) +void CObjectListWindow::init(std::shared_ptr titleWidget_, std::string _title, std::string _descr, bool searchBoxEnabled) { titleWidget = titleWidget_; @@ -1526,24 +1546,51 @@ void CObjectListWindow::init(std::shared_ptr titleWidget_, std::stri titleWidget->pos.y =75 + pos.y - titleWidget->pos.h/2; } list = std::make_shared(std::bind(&CObjectListWindow::genItem, this, _1), - Point(14, 151), Point(0, 25), 9, items.size(), 0, 1, Rect(262, -32, 256, 256) ); + Point(14, 151), Point(0, 25), 9, itemsVisible.size(), 0, 1, Rect(262, -32, 256, 256) ); list->setRedrawParent(true); ok = std::make_shared(Point(15, 402), AnimationPath::builtin("IOKAY.DEF"), CButton::tooltip(), std::bind(&CObjectListWindow::elementSelected, this), EShortcut::GLOBAL_ACCEPT); ok->block(!list->size()); + + if(!searchBoxEnabled) + return; + + Rect r(50, 90, pos.w - 100, 16); + const ColorRGBA rectangleColor = ColorRGBA(0, 0, 0, 75); + const ColorRGBA borderColor = ColorRGBA(128, 100, 75); + const ColorRGBA grayedColor = ColorRGBA(158, 130, 105); + searchBoxRectangle = std::make_shared(r.resize(1), rectangleColor, borderColor); + searchBoxDescription = std::make_shared(r.center().x, r.center().y, FONT_SMALL, ETextAlignment::CENTER, grayedColor, CGI->generaltexth->translate("vcmi.spellBook.search")); + + searchBox = std::make_shared(r, FONT_SMALL, ETextAlignment::CENTER, true); + searchBox->setCallback([this](const std::string & text){ + searchBoxDescription->setEnabled(text.empty()); + + itemsVisible.clear(); + for(auto & item : items) + if(boost::algorithm::contains(boost::algorithm::to_lower_copy(item.second), boost::algorithm::to_lower_copy(text))) + itemsVisible.push_back(item); + + selected = 0; + list->resize(itemsVisible.size()); + list->scrollTo(0); + ok->block(!itemsVisible.size()); + + redraw(); + }); } std::shared_ptr CObjectListWindow::genItem(size_t index) { - if(index < items.size()) - return std::make_shared(this, index, items[index].second); + if(index < itemsVisible.size()) + return std::make_shared(this, index, itemsVisible[index].second); return std::shared_ptr(); } void CObjectListWindow::elementSelected() { std::function toCall = onSelect;//save - int where = items[selected].first; //required variables + int where = itemsVisible[selected].first; //required variables close();//then destroy window toCall(where);//and send selected object } @@ -1599,18 +1646,18 @@ void CObjectListWindow::keyPressed(EShortcut key) sel = 0; break; case EShortcut::MOVE_LAST: - sel = static_cast(items.size()); + sel = static_cast(itemsVisible.size()); break; default: return; } - vstd::abetween(sel, 0, items.size()-1); + vstd::abetween(sel, 0, itemsVisible.size()-1); list->scrollTo(sel); changeSelection(sel); } -VideoWindow::VideoWindow(VideoPath video, ImagePath rim, bool showBackground, float scaleFactor, std::function closeCb) +VideoWindow::VideoWindow(const VideoPath & video, const ImagePath & rim, bool showBackground, float scaleFactor, const std::function & closeCb) : CWindowObject(BORDERED | SHADOW_DISABLED | NEEDS_ANIMATED_BACKGROUND), closeCb(closeCb) { OBJECT_CONSTRUCTION; @@ -1656,7 +1703,7 @@ void VideoWindow::keyPressed(EShortcut key) exit(true); } -bool VideoWindow::receiveEvent(const Point & position, int eventType) const +void VideoWindow::notFocusedClick() { - return true; // capture click also outside of window + exit(true); } diff --git a/client/windows/GUIClasses.h b/client/windows/GUIClasses.h index 5e58ad6d2..f3c8a044c 100644 --- a/client/windows/GUIClasses.h +++ b/client/windows/GUIClasses.h @@ -45,6 +45,8 @@ class IImage; class VideoWidget; class VideoWidgetOnce; class GraphicalPrimitiveCanvas; +class TransparentFilledRectangle; +class CSecSkillPlace; enum class EUserEvent; @@ -186,9 +188,14 @@ class CObjectListWindow : public CWindowObject std::shared_ptr ok; std::shared_ptr exit; - std::vector< std::pair > items;//all items present in list + std::shared_ptr searchBox; + std::shared_ptr searchBoxRectangle; + std::shared_ptr searchBoxDescription; - void init(std::shared_ptr titleWidget_, std::string _title, std::string _descr); + std::vector< std::pair > items; //all items present in list + std::vector< std::pair > itemsVisible; //visible items present in list + + void init(std::shared_ptr titleWidget_, std::string _title, std::string _descr, bool searchBoxEnabled); void exitPressed(); public: size_t selected;//index of currently selected item @@ -200,8 +207,8 @@ public: /// Callback will be called when OK button is pressed, returns id of selected item. initState = initially selected item /// Image can be nullptr ///item names will be taken from map objects - CObjectListWindow(const std::vector &_items, std::shared_ptr titleWidget_, std::string _title, std::string _descr, std::function Callback, size_t initialSelection = 0, std::vector> images = {}); - CObjectListWindow(const std::vector &_items, std::shared_ptr titleWidget_, std::string _title, std::string _descr, std::function Callback, size_t initialSelection = 0, std::vector> images = {}); + CObjectListWindow(const std::vector &_items, std::shared_ptr titleWidget_, std::string _title, std::string _descr, std::function Callback, size_t initialSelection = 0, std::vector> images = {}, bool searchBoxEnabled = false); + CObjectListWindow(const std::vector &_items, std::shared_ptr titleWidget_, std::string _title, std::string _descr, std::function Callback, size_t initialSelection = 0, std::vector> images = {}, bool searchBoxEnabled = false); std::shared_ptr genItem(size_t index); void elementSelected();//call callback and close this window @@ -364,7 +371,7 @@ class CUniversityWindow final : public CStatusbarWindow, public IMarketHolder { class CItem final : public CIntObject { - std::shared_ptr icon; + std::shared_ptr skill; std::shared_ptr topBar; std::shared_ptr bottomBar; std::shared_ptr name; @@ -373,9 +380,6 @@ class CUniversityWindow final : public CStatusbarWindow, public IMarketHolder SecondarySkill ID;//id of selected skill CUniversityWindow * parent; - void clickPressed(const Point & cursorPosition) override; - void showPopupWindow(const Point & cursorPosition) override; - void hover(bool on) override; void update(); CItem(CUniversityWindow * _parent, int _ID, int X, int Y); }; @@ -445,9 +449,11 @@ public: class CHillFortWindow : public CStatusbarWindow, public IGarrisonHolder { private: - static const int slotsCount = 7; + + enum class State { UNAFFORDABLE, ALREADY_UPGRADED, MAKE_UPGRADE, EMPTY, UNAVAILABLE }; + static constexpr std::size_t slotsCount = 7; //todo: mithril support - static const int resCount = 7; + static constexpr std::size_t resCount = 7; const CGObjectInstance * fort; const CGHeroInstance * hero; @@ -459,7 +465,7 @@ private: std::array, resCount> totalLabels; std::array, slotsCount> upgrade;//upgrade single creature - std::array currState;//current state of slot - to avoid calls to getState or updating buttons + std::array currState;//current state of slot - to avoid calls to getState or updating buttons //there is a place for only 2 resources per slot std::array< std::array, 2>, slotsCount> slotIcons; @@ -474,7 +480,7 @@ private: std::string getTextForSlot(SlotID slot); void makeDeal(SlotID slot);//-1 for upgrading all creatures - int getState(SlotID slot); //-1 = no creature 0=can't upgrade, 1=upgraded, 2=can upgrade + State getState(SlotID slot); public: CHillFortWindow(const CGHeroInstance * visitor, const CGObjectInstance * object); void updateGarrisons() override;//update buttons after garrison changes @@ -513,9 +519,9 @@ class VideoWindow : public CWindowObject void exit(bool skipped); public: - VideoWindow(VideoPath video, ImagePath rim, bool showBackground, float scaleFactor, std::function closeCb); + VideoWindow(const VideoPath & video, const ImagePath & rim, bool showBackground, float scaleFactor, const std::function & closeCb); void clickPressed(const Point & cursorPosition) override; void keyPressed(EShortcut key) override; - bool receiveEvent(const Point & position, int eventType) const override; + void notFocusedClick() override; }; diff --git a/client/windows/InfoWindows.cpp b/client/windows/InfoWindows.cpp index cabeeef13..494192e4c 100644 --- a/client/windows/InfoWindows.cpp +++ b/client/windows/InfoWindows.cpp @@ -187,11 +187,6 @@ bool CRClickPopup::isPopupWindow() const return true; } -void CRClickPopup::close() -{ - WindowBase::close(); -} - void CRClickPopup::createAndPush(const std::string & txt, const CInfoWindow::TCompsInfo & comps) { PlayerColor player = LOCPLINT ? LOCPLINT->playerID : PlayerColor(1); //if no player, then use blue @@ -271,16 +266,6 @@ void CRClickPopupInt::mouseDraggedPopup(const Point & cursorPosition, const Poin close(); } -Point CInfoBoxPopup::toScreen(Point p) -{ - auto bounds = adventureInt->terrainAreaPixels(); - - vstd::abetween(p.x, bounds.top() + 100, bounds.bottom() - 100); - vstd::abetween(p.y, bounds.left() + 100, bounds.right() - 100); - - return p; -} - void CInfoBoxPopup::mouseDraggedPopup(const Point & cursorPosition, const Point & lastUpdateDistance) { if(!settings["adventure"]["rightButtonDrag"].Bool()) @@ -294,7 +279,7 @@ void CInfoBoxPopup::mouseDraggedPopup(const Point & cursorPosition, const Point CInfoBoxPopup::CInfoBoxPopup(Point position, const CGTownInstance * town) - : CWindowObject(RCLICK_POPUP | PLAYER_COLORED, ImagePath::builtin("TOWNQVBK"), toScreen(position)) + : CWindowObject(RCLICK_POPUP | PLAYER_COLORED, ImagePath::builtin("TOWNQVBK"), position) { InfoAboutTown iah; LOCPLINT->cb->getTownInfo(town, iah, LOCPLINT->localState->getCurrentArmy()); //todo: should this be nearest hero? @@ -303,10 +288,12 @@ CInfoBoxPopup::CInfoBoxPopup(Point position, const CGTownInstance * town) tooltip = std::make_shared(Point(9, 10), iah); addUsedEvents(DRAG_POPUP); + + fitToScreen(10); } CInfoBoxPopup::CInfoBoxPopup(Point position, const CGHeroInstance * hero) - : CWindowObject(RCLICK_POPUP | PLAYER_COLORED, ImagePath::builtin("HEROQVBK"), toScreen(position)) + : CWindowObject(RCLICK_POPUP | PLAYER_COLORED, ImagePath::builtin("HEROQVBK"), position) { InfoAboutHero iah; LOCPLINT->cb->getHeroInfo(hero, iah, LOCPLINT->localState->getCurrentArmy()); //todo: should this be nearest hero? @@ -315,10 +302,12 @@ CInfoBoxPopup::CInfoBoxPopup(Point position, const CGHeroInstance * hero) tooltip = std::make_shared(Point(9, 10), iah); addUsedEvents(DRAG_POPUP); + + fitToScreen(10); } CInfoBoxPopup::CInfoBoxPopup(Point position, const CGGarrison * garr) - : CWindowObject(RCLICK_POPUP | PLAYER_COLORED, ImagePath::builtin("TOWNQVBK"), toScreen(position)) + : CWindowObject(RCLICK_POPUP | PLAYER_COLORED, ImagePath::builtin("TOWNQVBK"), position) { InfoAboutTown iah; LOCPLINT->cb->getTownInfo(garr, iah); @@ -327,15 +316,19 @@ CInfoBoxPopup::CInfoBoxPopup(Point position, const CGGarrison * garr) tooltip = std::make_shared(Point(9, 10), iah); addUsedEvents(DRAG_POPUP); + + fitToScreen(10); } CInfoBoxPopup::CInfoBoxPopup(Point position, const CGCreature * creature) - : CWindowObject(RCLICK_POPUP | BORDERED, ImagePath::builtin("DIBOXBCK"), toScreen(position)) + : CWindowObject(RCLICK_POPUP | BORDERED, ImagePath::builtin("DIBOXBCK"), position) { OBJECT_CONSTRUCTION; tooltip = std::make_shared(Point(9, 10), creature); addUsedEvents(DRAG_POPUP); + + fitToScreen(10); } std::shared_ptr diff --git a/client/windows/InfoWindows.h b/client/windows/InfoWindows.h index a06416934..468088dd2 100644 --- a/client/windows/InfoWindows.h +++ b/client/windows/InfoWindows.h @@ -64,7 +64,6 @@ public: class CRClickPopup : public WindowBase { public: - void close() override; bool isPopupWindow() const override; static std::shared_ptr createCustomInfoWindow(Point position, const CGObjectInstance * specific); @@ -91,7 +90,6 @@ public: class CInfoBoxPopup : public CWindowObject { std::shared_ptr tooltip; - Point toScreen(Point pos); Point dragDistance; diff --git a/client/windows/QuickRecruitmentWindow.cpp b/client/windows/QuickRecruitmentWindow.cpp index ea512efc8..cdbb80121 100644 --- a/client/windows/QuickRecruitmentWindow.cpp +++ b/client/windows/QuickRecruitmentWindow.cpp @@ -51,9 +51,9 @@ void QuickRecruitmentWindow::setCreaturePurchaseCards() { int availableAmount = getAvailableCreatures(); Point position = Point((pos.w - 100*availableAmount - 8*(availableAmount-1))/2,64); - for (int i = 0; i < town->town->creatures.size(); i++) + for (int i = 0; i < town->getTown()->creatures.size(); i++) { - if(!town->town->creatures.at(i).empty() && !town->creatures.at(i).second.empty() && town->creatures[i].first) + if(!town->getTown()->creatures.at(i).empty() && !town->creatures.at(i).second.empty() && town->creatures[i].first) { cards.push_back(std::make_shared(town->creatures[i].second, position, town->creatures[i].first, this)); position.x += 108; @@ -108,7 +108,7 @@ void QuickRecruitmentWindow::purchaseUnits() { int level = 0; int i = 0; - for(auto c : town->town->creatures) + for(auto c : town->getTown()->creatures) { for(auto c2 : c) if(c2 == selected->creatureOnTheCard->getId()) @@ -129,8 +129,8 @@ void QuickRecruitmentWindow::purchaseUnits() int QuickRecruitmentWindow::getAvailableCreatures() { int creaturesAmount = 0; - for (int i=0; i< town->town->creatures.size(); i++) - if(!town->town->creatures.at(i).empty() && !town->creatures.at(i).second.empty() && town->creatures[i].first) + for (int i=0; i< town->getTown()->creatures.size(); i++) + if(!town->getTown()->creatures.at(i).empty() && !town->creatures.at(i).second.empty() && town->creatures[i].first) creaturesAmount++; return creaturesAmount; } @@ -166,4 +166,6 @@ QuickRecruitmentWindow::QuickRecruitmentWindow(const CGTownInstance * townd, Rec setButtons(); setCreaturePurchaseCards(); maxAllCards(cards); + + center(); } diff --git a/client/windows/settings/GeneralOptionsTab.cpp b/client/windows/settings/GeneralOptionsTab.cpp index d8ead20f8..6571a3422 100644 --- a/client/windows/settings/GeneralOptionsTab.cpp +++ b/client/windows/settings/GeneralOptionsTab.cpp @@ -194,10 +194,8 @@ GeneralOptionsTab::GeneralOptionsTab() build(config); - const auto & currentResolution = settings["video"]["resolution"]; - std::shared_ptr scalingLabel = widget("scalingLabel"); - scalingLabel->setText(scalingToLabelString(currentResolution["scaling"].Integer())); + scalingLabel->setText(scalingToLabelString(GH.screenHandler().getInterfaceScalingPercentage())); std::shared_ptr longTouchLabel = widget("longTouchLabel"); if (longTouchLabel) diff --git a/clientapp/CMakeLists.txt b/clientapp/CMakeLists.txt index edb0cb873..40061748d 100644 --- a/clientapp/CMakeLists.txt +++ b/clientapp/CMakeLists.txt @@ -56,6 +56,11 @@ if(WIN32) endif() target_compile_definitions(vcmiclient PRIVATE WINDOWS_IGNORE_PACKING_MISMATCH) + if(NOT ffmpeg_LIBRARIES) + target_compile_definitions(vcmiclient PRIVATE DISABLE_VIDEO) + endif() + + # TODO: very hacky, find proper solution to copy AI dlls into bin dir if(MSVC) add_custom_command(TARGET vcmiclient POST_BUILD diff --git a/clientapp/icons/vcmiclient.desktop b/clientapp/icons/vcmiclient.desktop index 68d446c47..3a94d8deb 100644 --- a/clientapp/icons/vcmiclient.desktop +++ b/clientapp/icons/vcmiclient.desktop @@ -1,8 +1,11 @@ [Desktop Entry] Type=Application Name=VCMI Client +Name[cs]=VCMI Klient GenericName=Strategy Game Engine +GenericName[cs]=Engine strategické hry Comment=Open engine for Heroes of Might and Magic 3 +Comment[cs]=Open-source engine pro Heroes of Might and Magic III Icon=vcmiclient Exec=vcmiclient Categories=Game;StrategyGame; diff --git a/conanfile.py b/conanfile.py index c9c5df23d..302aba724 100644 --- a/conanfile.py +++ b/conanfile.py @@ -15,10 +15,11 @@ class VCMI(ConanFile): "minizip/[~1.2.12]", ] _clientRequires = [ - "sdl/[~2.26.1 || >=2.0.20 <=2.22.0]", # versions in between have broken sound - "sdl_image/[~2.0.5]", - "sdl_mixer/[~2.0.4]", - "sdl_ttf/[~2.0.18]", + # Versions between 2.5-2.8 have broken loading of palette sdl images which a lot of mods use + # there is workaround that require disabling cmake flag which is not available in conan recipes. + # Bug is fixed in version 2.8, however it is not available in conan at the moment + "sdl_image/2.0.5", + "sdl_ttf/[>=2.0.18]", "onetbb/[^2021.7 <2021.10]", # 2021.10+ breaks mobile builds due to added hwloc dependency "xz_utils/[>=5.2.5]", # Required for innoextract ] @@ -46,21 +47,41 @@ class VCMI(ConanFile): self.options["freetype"].shared = self.settings.os == "Android" # SDL_image and Qt depend on it, in iOS both are static - # Enable static libpng due to https://github.com/conan-io/conan-center-index/issues/15440, - # which leads to VCMI crashes of MinGW - self.options["libpng"].shared = not (self.settings.os == "Windows" and cross_building(self)) and self.settings.os != "iOS" + self.options["libpng"].shared = self.settings.os != "iOS" # static Qt for iOS is the only viable option at the moment self.options["qt"].shared = self.settings.os != "iOS" - if self.settings.os == "Android": - self.options["qt"].android_sdk = tools.get_env("ANDROID_HOME", default="") - # TODO: enable for all platforms if self.settings.os == "Android": self.options["bzip2"].shared = True self.options["libiconv"].shared = True self.options["zlib"].shared = True + # TODO: enable for all platforms? + if self.settings.os == "Windows": + self.options["sdl"].shared = True + self.options["sdl_image"].shared = True + self.options["sdl_mixer"].shared = True + self.options["sdl_ttf"].shared = True + + if self.settings.os == "iOS": + # TODO: ios - newer sdl fails to link + self.requires("sdl/2.26.1") + self.requires("sdl_mixer/2.0.4") + elif self.settings.os == "Android": + # On Android SDL version must be same as version of Java wrapper for SDL in VCMI source code + # Wrapper can be found in following directory: android/vcmi-app/src/main/java/org/libsdl/app + self.requires("sdl/2.26.5") + self.requires("sdl_mixer/2.0.4") + else: + # upcoming SDL version 3.0+ is not supported at the moment due to API breakage + # SDL versions between 2.22-2.26.1 have broken sound + self.requires("sdl/[^2.26 || >=2.0.20 <=2.22.0]") + self.requires("sdl_mixer/[>=2.0.4]") + + if self.settings.os == "Android": + self.options["qt"].android_sdk = tools.get_env("ANDROID_HOME", default="") + if self.options.default_options_of_requirements: return @@ -86,6 +107,7 @@ class VCMI(ConanFile): self.options["boost"].without_timer = True self.options["boost"].without_type_erasure = True self.options["boost"].without_wave = True + self.options["boost"].without_url = True self.options["ffmpeg"].disable_all_bitstream_filters = True self.options["ffmpeg"].disable_all_decoders = True @@ -149,8 +171,13 @@ class VCMI(ConanFile): self.options["sdl"].sdl2main = self.settings.os != "iOS" self.options["sdl"].vulkan = False + # bmp, png are the only ones that needs to be supported + # dds support may be useful for HD edition, but not supported by sdl_image at the moment + self.options["sdl_image"].gif = False self.options["sdl_image"].lbm = False self.options["sdl_image"].pnm = False + self.options["sdl_image"].pcx = False + #self.options["sdl_image"].qoi = False # sdl_image >=2.6 self.options["sdl_image"].svg = False self.options["sdl_image"].tga = False self.options["sdl_image"].with_libjpeg = False @@ -162,13 +189,17 @@ class VCMI(ConanFile): if is_apple_os(self): self.options["sdl_image"].imageio = True + # mp3, ogg and wav are the only ones that needs to be supported + # opus is nice to have, but fails to build in CI + # flac can be considered, but generally unnecessary self.options["sdl_mixer"].flac = False - self.options["sdl_mixer"].mad = False - self.options["sdl_mixer"].mikmod = False self.options["sdl_mixer"].modplug = False - self.options["sdl_mixer"].nativemidi = False self.options["sdl_mixer"].opus = False - self.options["sdl_mixer"].wav = False + if self.settings.os == "iOS" or self.settings.os == "Android": + # only available in older sdl_mixer version, removed in newer version + self.options["sdl_mixer"].mad = False + self.options["sdl_mixer"].mikmod = False + self.options["sdl_mixer"].nativemidi = False def _disableQtOptions(disableFlag, options): return " ".join([f"-{disableFlag}-{tool}" for tool in options]) diff --git a/config/ai/nkai/nkai-settings.json b/config/ai/nkai/nkai-settings.json index f597be497..7c29e2f27 100644 --- a/config/ai/nkai/nkai-settings.json +++ b/config/ai/nkai/nkai-settings.json @@ -1,10 +1,86 @@ { - "maxRoamingHeroes" : 8, - "maxpass" : 30, - "mainHeroTurnDistanceLimit" : 10, - "scoutHeroTurnDistanceLimit" : 5, - "maxGoldPressure" : 0.3, - "useTroopsFromGarrisons" : true, - "openMap": true, - "allowObjectGraph": true + "pawn" : { + "maxRoamingHeroes" : 8, + "maxpass" : 30, + "mainHeroTurnDistanceLimit" : 10, + "scoutHeroTurnDistanceLimit" : 5, + "maxGoldPressure" : 0.3, + "useTroopsFromGarrisons" : true, + "openMap": false, + "allowObjectGraph": false, + "pathfinderBucketsCount" : 1, // old value: 3, + "pathfinderBucketSize" : 32, // old value: 7, + "retreatThresholdRelative" : 0.3, + "retreatThresholdAbsolute" : 10000, + "safeAttackRatio" : 1.1, + "useFuzzy" : false + }, + + "knight" : { + "maxRoamingHeroes" : 8, + "maxpass" : 30, + "mainHeroTurnDistanceLimit" : 10, + "scoutHeroTurnDistanceLimit" : 5, + "maxGoldPressure" : 0.3, + "useTroopsFromGarrisons" : true, + "openMap": false, + "allowObjectGraph": false, + "pathfinderBucketsCount" : 1, // old value: 3, + "pathfinderBucketSize" : 32, // old value: 7, + "retreatThresholdRelative" : 0.3, + "retreatThresholdAbsolute" : 10000, + "safeAttackRatio" : 1.1, + "useFuzzy" : false + }, + + "rook" : { + "maxRoamingHeroes" : 8, + "maxpass" : 30, + "mainHeroTurnDistanceLimit" : 10, + "scoutHeroTurnDistanceLimit" : 5, + "maxGoldPressure" : 0.3, + "useTroopsFromGarrisons" : true, + "openMap": false, + "allowObjectGraph": false, + "pathfinderBucketsCount" : 1, // old value: 3, + "pathfinderBucketSize" : 32, // old value: 7, + "retreatThresholdRelative" : 0.3, + "retreatThresholdAbsolute" : 10000, + "safeAttackRatio" : 1.1, + "useFuzzy" : false + }, + + "queen" : { + "maxRoamingHeroes" : 8, + "maxpass" : 30, + "mainHeroTurnDistanceLimit" : 10, + "scoutHeroTurnDistanceLimit" : 5, + "maxGoldPressure" : 0.3, + "useTroopsFromGarrisons" : true, + "openMap": true, + "allowObjectGraph": false, + "pathfinderBucketsCount" : 1, // old value: 3, + "pathfinderBucketSize" : 32, // old value: 7, + "retreatThresholdRelative" : 0.3, + "retreatThresholdAbsolute" : 10000, + "safeAttackRatio" : 1.1, + "useFuzzy" : false + }, + + "king" : { + "maxRoamingHeroes" : 8, + "maxpass" : 30, + "mainHeroTurnDistanceLimit" : 10, + "scoutHeroTurnDistanceLimit" : 5, + "maxGoldPressure" : 0.3, + "useTroopsFromGarrisons" : true, + "openMap": true, + "allowObjectGraph": false, + "pathfinderBucketsCount" : 1, // old value: 3, + "pathfinderBucketSize" : 32, // old value: 7, + "retreatThresholdRelative" : 0.3, + "retreatThresholdAbsolute" : 10000, + "safeAttackRatio" : 1.1, + "useFuzzy" : false + } } \ No newline at end of file diff --git a/config/artifacts.json b/config/artifacts.json index ae5501f30..1c4cb65b8 100644 --- a/config/artifacts.json +++ b/config/artifacts.json @@ -1995,9 +1995,17 @@ "type" : "HAS_ANOTHER_BONUS_LIMITER", "parameters" : [ "NON_LIVING" ] }, + { + "type" : "HAS_ANOTHER_BONUS_LIMITER", + "parameters" : [ "MECHANICAL" ] + }, { "type" : "HAS_ANOTHER_BONUS_LIMITER", "parameters" : [ "GARGOYLE" ] + }, + { + "type" : "HAS_ANOTHER_BONUS_LIMITER", + "parameters" : [ "SIEGE_WEAPON" ] } ] }, @@ -2012,6 +2020,10 @@ "type" : "HAS_ANOTHER_BONUS_LIMITER", "parameters" : [ "NON_LIVING" ] }, + { + "type" : "HAS_ANOTHER_BONUS_LIMITER", + "parameters" : [ "MECHANICAL" ] + }, { "type" : "HAS_ANOTHER_BONUS_LIMITER", "parameters" : [ "GARGOYLE" ] diff --git a/config/battlefields.json b/config/battlefields.json index 9d9b12fa3..9f032e603 100644 --- a/config/battlefields.json +++ b/config/battlefields.json @@ -162,5 +162,5 @@ 159, 160, 161, 162, 163, 176, 177, 178, 179, 180] }, - "ship": { "graphics" : "CMBKDECK.BMP" } + "ship": { "graphics" : "CMBKDECK.BMP", "isSpecial" : true} } diff --git a/config/bonuses.json b/config/bonuses.json index 897e0a9b9..2765bd15e 100644 --- a/config/bonuses.json +++ b/config/bonuses.json @@ -399,6 +399,14 @@ } }, + "MECHANICAL": + { + "graphics": + { + "icon": "zvs/Lib1.res/Mechanical" + } + }, + "RANDOM_SPELLCASTER": { "graphics": @@ -548,6 +556,14 @@ } }, + "PRISM_HEX_ATTACK_BREATH": + { + "graphics": + { + "icon": "zvs/Lib1.res/PrismBreath" + } + }, + "THREE_HEADED_ATTACK": { "graphics": diff --git a/config/campaignMedia.json b/config/campaignMedia.json index c54185837..c0a78ce61 100644 --- a/config/campaignMedia.json +++ b/config/campaignMedia.json @@ -70,7 +70,7 @@ //Playing with Fire "H3ABpf1.smk", //PlayingWithFire_a "H3ABpf2.smk", //PlayingWithFire_b - "3ABpf3.smk", //PlayingWithFire_c + "H3ABpf3.smk", //PlayingWithFire_c "H3ABpf4.smk", //PlayingWithFire_end //Shadow of Death Campaigns //Birth of a Barbarian diff --git a/config/campaignSets.json b/config/campaignSets.json index eccafe67d..ce118a7fc 100644 --- a/config/campaignSets.json +++ b/config/campaignSets.json @@ -47,5 +47,21 @@ { "id": 6, "x":34, "y":417, "file":"DATA/FINAL", "image":"CAMPUA1", "video":"UNHOLY", "requires": [4] }, { "id": 7, "x":404, "y":414, "file":"DATA/SECRET", "image":"CAMPSP1", "video":"SPECTRE", "requires": [6] } ] + }, + "chr": + { + "images" : [ {"x": 0, "y": 0, "name":"data/CampaignBackground8"} ], + "exitbutton" : {"x": 658, "y": 482, "name":"CMPSCAN" }, + "items": + [ + { "id": 1, "x":40, "y":72, "file":"Maps/Chronicles/Hc1_Main", "image":"CampaignHc1Image", "video":"", "requires": [], "optional": true }, + { "id": 2, "x":310, "y":72, "file":"Maps/Chronicles/Hc2_Main", "image":"CampaignHc2Image", "video":"", "requires": [], "optional": true }, + { "id": 3, "x":590, "y":72, "file":"Maps/Chronicles/Hc3_Main", "image":"CampaignHc3Image", "video":"", "requires": [], "optional": true }, + { "id": 4, "x":43, "y":245, "file":"Maps/Chronicles/Hc4_Main", "image":"CampaignHc4Image", "video":"", "requires": [], "optional": true }, + { "id": 5, "x":313, "y":244, "file":"Maps/Chronicles/Hc5_Main", "image":"CampaignHc5Image", "video":"", "requires": [], "optional": true }, + { "id": 6, "x":586, "y":244, "file":"Maps/Chronicles/Hc6_Main", "image":"CampaignHc6Image", "video":"", "requires": [], "optional": true }, + { "id": 7, "x":34, "y":413, "file":"Maps/Chronicles/Hc7_Main", "image":"CampaignHc7Image", "video":"", "requires": [], "optional": true }, + { "id": 8, "x":404, "y":414, "file":"Maps/Chronicles/Hc8_Main", "image":"CampaignHc8Image", "video":"", "requires": [], "optional": true } + ] } } diff --git a/config/creatures/castle.json b/config/creatures/castle.json index e45c185db..d769f8679 100644 --- a/config/creatures/castle.json +++ b/config/creatures/castle.json @@ -57,6 +57,13 @@ "extraNames": [ "lightCrossbowman" ], "faction": "castle", "upgrades": ["marksman"], + "shots" : 12, + "abilities" : + { + "shooter" : { + "type" : "SHOOTER" + } + }, "graphics" : { "animation": "CLCBOW.DEF", @@ -80,7 +87,12 @@ "index": 3, "level": 2, "faction": "castle", - "abilities": { + "shots" : 24, + "abilities": + { + "shooter" : { + "type" : "SHOOTER" + }, "extraAttack" : { "type": "ADDITIONAL_ATTACK", @@ -111,8 +123,12 @@ "index": 4, "level": 3, "faction": "castle", + "doubleWide": true, "abilities": { + "canFly" : { + "type" : "FLYING" + }, "extraRetaliation" : { "type" : "ADDITIONAL_RETALIATION", @@ -139,8 +155,12 @@ "index": 5, "level": 3, "faction": "castle", + "doubleWide": true, "abilities": { + "canFly" : { + "type" : "FLYING" + }, "unlimitedRetaliation" : { "type" : "UNLIMITED_RETALIATIONS" @@ -210,6 +230,13 @@ "level": 5, "faction": "castle", "upgrades": ["zealot"], + "shots" : 12, + "abilities" : + { + "shooter" : { + "type" : "SHOOTER" + } + }, "graphics" : { "animation": "CMONKK.DEF", @@ -233,6 +260,16 @@ "index": 9, "level": 5, "faction": "castle", + "shots" : 24, + "abilities" : + { + "shooter" : { + "type" : "SHOOTER" + }, + "noMeleePenalty" : { + "type" : "NO_MELEE_PENALTY" + } + }, "graphics" : { "animation": "CZEALT.DEF", @@ -256,7 +293,16 @@ "index": 10, "level": 6, "faction": "castle", + "doubleWide": true, "upgrades": ["champion"], + "abilities" : + { + "jousting": + { + "type": "JOUSTING", + "val": 5 + } + }, "graphics" : { "animation": "CCAVLR.DEF" @@ -275,6 +321,15 @@ "index": 11, "level": 6, "faction": "castle", + "doubleWide": true, + "abilities" : + { + "jousting": + { + "type": "JOUSTING", + "val": 5 + } + }, "graphics" : { "animation": "CCHAMP.DEF" @@ -295,6 +350,22 @@ "faction": "castle", "abilities": { + "canFly" : + { + "type" : "FLYING" + }, + "raisesMorale" : + { + "type" : "MORALE", + "val" : 1, + "propagator" : "HERO", + "stacking" : "Angels" + }, + "KING_2" : // Will be affected by Advanced Slayer or better + { + "type" : "KING", + "val" : 2 + }, "hateDevils" : { "type" : "HATE", @@ -306,10 +377,6 @@ "type" : "HATE", "subtype" : "creature.archDevil", "val" : 50 - }, - "const_raises_morale" : - { - "stacking" : "Angels" } }, "upgrades": ["archangel"], @@ -331,13 +398,12 @@ "index": 13, "level": 7, "faction": "castle", + "doubleWide" : true, "abilities": { - "resurrection100hp" : + "canFly" : { - "type" : "SPECIFIC_SPELL_POWER", - "subtype" : "spell.resurrection", - "val" : 100 + "type" : "FLYING" }, "resurrects" : { @@ -345,11 +411,28 @@ "subtype" : "spell.resurrection", "val" : 3 }, + "resurrection100hp" : + { + "type" : "SPECIFIC_SPELL_POWER", + "subtype" : "spell.resurrection", + "val" : 100 + }, "spellpoints" : { "type" : "CASTS", "val" : 1 }, + "raisesMorale" : { + "type" : "MORALE", + "val" : 1, + "propagator" : "HERO", + "stacking" : "Angels" + }, + "KING_2" : // Will be affected by Advanced Slayer or better + { + "type" : "KING", + "val" : 2 + }, "hateDevils" : { "type" : "HATE", @@ -361,10 +444,6 @@ "type" : "HATE", "subtype" : "creature.archDevil", "val" : 50 - }, - "const_raises_morale" : - { - "stacking" : "Angels" } }, "graphics" : diff --git a/config/creatures/conflux.json b/config/creatures/conflux.json index 397d9e154..78cb70ec1 100755 --- a/config/creatures/conflux.json +++ b/config/creatures/conflux.json @@ -1,4 +1,60 @@ { + "pixie" : + { + "index": 118, + "level": 1, + "extraNames": [ "pixies" ], + "faction": "conflux", + "upgrades": ["sprite"], + "abilities" : + { + "canFly" : + { + "type" : "FLYING" + } + }, + "graphics" : + { + "animation": "CPIXIE.DEF" + }, + "sound" : + { + "attack": "PIXIATTK.wav", + "defend": "PIXIDFND.wav", + "killed": "PIXIKILL.wav", + "move": "PIXIMOVE.wav", + "wince": "PIXIWNCE.wav" + } + }, + "sprite" : + { + "index": 119, + "level": 1, + "faction": "conflux", + "abilities" : + { + "canFly" : + { + "type" : "FLYING" + }, + "noRetaliation" : + { + "type" : "BLOCKS_RETALIATION" + } + }, + "graphics" : + { + "animation": "CSPRITE.DEF" + }, + "sound" : + { + "attack": "SPRTATTK.wav", + "defend": "SPRTDFND.wav", + "killed": "SPRTKILL.wav", + "move": "SPRTMOVE.wav", + "wince": "SPRTWNCE.wav" + } + }, "airElemental" : { "index": 112, @@ -55,10 +111,6 @@ "upgrades": ["stormElemental"], "graphics" : { - "animationTime" : - { - "idle" : 0 - }, "animation": "CAELEM.DEF" }, "sound" : @@ -70,525 +122,21 @@ "wince": "AELMWNCE.wav" } }, - "earthElemental" : - { - "index": 113, - "level": 5, - "faction": "conflux", - "abilities": - { - "nonLiving" : - { - "type" : "NON_LIVING" - }, - "immuneToMind" : - { - "type" : "MIND_IMMUNITY" - }, - "meteorShowerVulnerability" : - { - "type" : "MORE_DAMAGE_FROM_SPELL", - "subtype" : "spell.meteorShower", - "val" : 100 - }, - "lightingImmunity" : - { - "type" : "SPELL_IMMUNITY", - "subtype" : "spell.lightningBolt" - }, - "chainLightingImmunity" : - { - "type" : "SPELL_IMMUNITY", - "subtype" : "spell.chainLightning" - }, - "armageddonImmunity" : - { - "type" : "SPELL_IMMUNITY", - "subtype" : "spell.armageddon" - }, - "oppositeAir" : - { - "type" : "HATE", - "subtype" : "creature.airElemental", - "val" : 100 - }, - "oppositeStorm" : - { - "type" : "HATE", - "subtype" : "creature.stormElemental", - "val" : 100 - } - }, - "upgrades": ["magmaElemental"], - "graphics" : - { - "animationTime" : - { - "idle" : 0 - }, - "animation": "CEELEM.DEF" - }, - "sound" : - { - "attack": "EELMATTK.wav", - "defend": "EELMDFND.wav", - "killed": "EELMKILL.wav", - "move": "EELMMOVE.wav", - "wince": "EELMWNCE.wav" - } - }, - "fireElemental" : - { - "index": 114, - "level": 4, - "faction": "conflux", - "abilities": - { - "nonLiving" : - { - "type" : "NON_LIVING" - }, - "immuneToMind" : - { - "type" : "MIND_IMMUNITY" - }, - "immuneToFire" : - { - "type" : "SPELL_SCHOOL_IMMUNITY", - "subtype" : "spellSchool.fire" - }, - "frostRingVulnerablity" : - { - "type" : "MORE_DAMAGE_FROM_SPELL", - "subtype" : "spell.frostRing", - "val" : 100 - }, - "iceBoltVulnerablity" : - { - "type" : "MORE_DAMAGE_FROM_SPELL", - "subtype" : "spell.iceBolt", - "val" : 100 - }, - "oppositeWater" : - { - "type" : "HATE", - "subtype" : "creature.waterElemental", - "val" : 100 - }, - "oppositeIce" : - { - "type" : "HATE", - "subtype" : "creature.iceElemental", - "val" : 100 - } - }, - "upgrades": ["energyElemental"], - "graphics" : - { - "animationTime" : - { - "idle" : 0 - }, - "animation": "CFELEM.DEF" - }, - "sound" : - { - "attack": "FELMATTK.wav", - "defend": "FELMDFND.wav", - "killed": "FELMKILL.wav", - "move": "FELMMOVE.wav", - "wince": "FELMWNCE.wav" - } - }, - "waterElemental" : - { - "index": 115, - "level": 3, - "extraNames": [ "waterElementals" ], - "faction": "conflux", - "abilities": - { - "nonLiving" : - { - "type" : "NON_LIVING" - }, - "immuneToMind" : - { - "type" : "MIND_IMMUNITY" - }, - "fireShieldVulnerablity" : - { - "type" : "MORE_DAMAGE_FROM_SPELL", - "subtype" : "spell.fireShield", - "val" : 100 - }, - "infernoVulnerablity" : - { - "type" : "MORE_DAMAGE_FROM_SPELL", - "subtype" : "spell.inferno", - "val" : 100 - }, - "fireballVulnerablity" : - { - "type" : "MORE_DAMAGE_FROM_SPELL", - "subtype" : "spell.fireball", - "val" : 100 - }, - "fireWallVulnerablity" : - { - "type" : "MORE_DAMAGE_FROM_SPELL", - "subtype" : "spell.fireWallTrigger", - "val" : 100 - }, - "armageddonVulnerablity" : - { - "type" : "MORE_DAMAGE_FROM_SPELL", - "subtype" : "spell.armageddon", - "val" : 100 - }, - "immuneToIceBolt" : - { - "type" : "SPELL_IMMUNITY", - "subtype" : "spell.iceBolt" - }, - "immuneToFrostRing" : - { - "type" : "SPELL_IMMUNITY", - "subtype" : "spell.frostRing" - }, - "oppositeFire" : - { - "type" : "HATE", - "subtype" : "creature.fireElemental", - "val" : 100 - }, - "oppositeEnergy" : - { - "type" : "HATE", - "subtype" : "creature.energyElemental", - "val" : 100 - } - }, - "doubleWide" : true, - "upgrades": ["iceElemental"], - "graphics" : - { - "animationTime" : - { - "idle" : 0 - }, - "animation": "CWELEM.DEF" - }, - "sound" : - { - "attack": "WELMATTK.wav", - "defend": "WELMDFND.wav", - "killed": "WELMKILL.wav", - "move": "WELMMOVE.wav", - "wince": "WELMWNCE.wav" - } - }, - "pixie" : - { - "index": 118, - "level": 1, - "extraNames": [ "pixies" ], - "faction": "conflux", - "upgrades": ["sprite"], - "graphics" : - { - "animation": "CPIXIE.DEF" - }, - "sound" : - { - "attack": "PIXIATTK.wav", - "defend": "PIXIDFND.wav", - "killed": "PIXIKILL.wav", - "move": "PIXIMOVE.wav", - "wince": "PIXIWNCE.wav" - } - }, - "sprite" : - { - "index": 119, - "level": 1, - "faction": "conflux", - "graphics" : - { - "animation": "CSPRITE.DEF" - }, - "sound" : - { - "attack": "SPRTATTK.wav", - "defend": "SPRTDFND.wav", - "killed": "SPRTKILL.wav", - "move": "SPRTMOVE.wav", - "wince": "SPRTWNCE.wav" - } - }, - "psychicElemental" : - { - "index": 120, - "level": 6, - "faction": "conflux", - "abilities": - { - "nonLiving" : - { - "type" : "NON_LIVING" - } - }, - "doubleWide" : false, - "upgrades": ["magicElemental"], - "graphics" : - { - "animation": "CPSYEL.DEF" - }, - "sound" : - { - "attack": "PSYCATTK.wav", - "defend": "PSYCDFND.wav", - "killed": "PSYCKILL.wav", - "move": "PSYCMOVE.wav", - "wince": "PSYCWNCE.wav" - } - }, - "magicElemental" : - { - "index": 121, - "level": 6, - "faction": "conflux", - "abilities": - { - "nonLiving" : - { - "type" : "NON_LIVING" - }, - "magicImmunity" : - { - "type" : "LEVEL_SPELL_IMMUNITY", - "val" : 5 - } - }, - "doubleWide" : false, - "graphics" : - { - "animation": "CMAGEL.DEF" - }, - "sound" : - { - "attack": "MGELATTK.wav", - "defend": "MGELDFND.wav", - "killed": "MGELKILL.wav", - "move": "MGELMOVE.wav", - "wince": "MGELWNCE.wav" - } - }, - "iceElemental" : - { - "index": 123, - "level": 3, - "faction": "conflux", - "abilities": - { - "nonLiving" : - { - "type" : "NON_LIVING" - }, - "spellPower" : - { - "type" : "CREATURE_ENCHANT_POWER", - "val" : 6 - }, - "spellPoints" : - { - "type" : "CASTS", - "val" : 3 - }, - "spellcaster": - { - "type" : "SPELLCASTER", - "subtype" : "spell.protectWater", - "val" : 2 - }, - "immuneToMind" : - { - "type" : "MIND_IMMUNITY" - }, - "fireShieldVulnerablity" : - { - "type" : "MORE_DAMAGE_FROM_SPELL", - "subtype" : "spell.fireShield", - "val" : 100 - }, - "infernoVulnerablity" : - { - "type" : "MORE_DAMAGE_FROM_SPELL", - "subtype" : "spell.inferno", - "val" : 100 - }, - "fireballVulnerablity" : - { - "type" : "MORE_DAMAGE_FROM_SPELL", - "subtype" : "spell.fireball", - "val" : 100 - }, - "fireWallVulnerablity" : - { - "type" : "MORE_DAMAGE_FROM_SPELL", - "subtype" : "spell.fireWallTrigger", - "val" : 100 - }, - "armageddonVulnerablity" : - { - "type" : "MORE_DAMAGE_FROM_SPELL", - "subtype" : "spell.armageddon", - "val" : 100 - }, - "immuneToIceBolt" : - { - "type" : "SPELL_IMMUNITY", - "subtype" : "spell.iceBolt" - }, - "immuneToFrostRing" : - { - "type" : "SPELL_IMMUNITY", - "subtype" : "spell.frostRing" - }, - "oppositeFire" : - { - "type" : "HATE", - "subtype" : "creature.fireElemental", - "val" : 100 - }, - "oppositeEnergy" : - { - "type" : "HATE", - "subtype" : "creature.energyElemental", - "val" : 100 - } - }, - "doubleWide" : true, - "graphics" : - { - "animation": "CICEE.DEF", - "animationTime" : - { - "idle" : 0 - }, - "missile" : - { - "projectile": "PICEE.DEF" - } - }, - "sound" : - { - "attack": "ICELATTK.wav", - "defend": "ICELDFND.wav", - "killed": "ICELKILL.wav", - "move": "ICELMOVE.wav", - "shoot": "ICELSHOT.wav", - "wince": "ICELWNCE.wav" - } - }, - "magmaElemental" : - { - "index": 125, - "level": 5, - "faction": "conflux", - "abilities": - { - "nonLiving" : - { - "type" : "NON_LIVING" - }, - "spellPower" : - { - "type" : "CREATURE_ENCHANT_POWER", - "val" : 6 - }, - "spellPoints" : - { - "type" : "CASTS", - "val" : 3 - }, - "spellcaster": - { - "type" : "SPELLCASTER", - "subtype" : "spell.protectEarth", - "val" : 2 - }, - "meteorShowerVulnerability" : - { - "type" : "MORE_DAMAGE_FROM_SPELL", - "subtype" : "spell.meteorShower", - "val" : 100 - }, - "lightingImmunity" : - { - "type" : "SPELL_IMMUNITY", - "subtype" : "spell.lightningBolt" - }, - "chainLightingImmunity" : - { - "type" : "SPELL_IMMUNITY", - "subtype" : "spell.chainLightning" - }, - "armageddonImmunity" : - { - "type" : "SPELL_IMMUNITY", - "subtype" : "spell.armageddon" - }, - "oppositeAir" : - { - "type" : "HATE", - "subtype" : "creature.airElemental", - "val" : 100 - }, - "oppositeStorm" : - { - "type" : "HATE", - "subtype" : "creature.stormElemental", - "val" : 100 - } - }, - "graphics" : - { - "animationTime" : - { - "idle" : 0 - }, - "animation": "CSTONE.DEF" - }, - "sound" : - { - "attack": "MAGMATTK.wav", - "defend": "MAGMDFND.wav", - "killed": "MAGMKILL.wav", - "move": "MAGMMOVE.wav", - "wince": "MAGMWNCE.wav" - } - }, "stormElemental" : { "index": 127, "level": 2, "faction": "conflux", + "shots" : 24, "abilities": { "nonLiving" : { "type" : "NON_LIVING" }, - "spellPower" : + "shooter" : { - "type" : "CREATURE_ENCHANT_POWER", - "val" : 6 - }, - "spellPoints" : - { - "type" : "CASTS", - "val" : 3 + "type" : "SHOOTER" }, "spellcaster": { @@ -596,6 +144,20 @@ "subtype" : "spell.protectAir", "val" : 2 }, + "spellPower" : + { + "type" : "CREATURE_ENCHANT_POWER", + "val" : 6 + }, + "spellPoints" : + { + "type" : "CASTS", + "val" : 3 + }, + "immuneToMind" : + { + "type" : "MIND_IMMUNITY" + }, "meteorShowerImmunity" : { "type" : "SPELL_IMMUNITY", @@ -635,10 +197,6 @@ "graphics" : { "animation": "CSTORM.DEF", - "animationTime" : - { - "idle" : 0 - }, "missile" : { "projectile": "CPRGTIX.DEF" @@ -654,9 +212,201 @@ "wince": "STORWNCE.wav" } }, - "energyElemental" : + "waterElemental" : { - "index": 129, + "index": 115, + "level": 3, + "extraNames": [ "waterElementals" ], + "faction": "conflux", + "doubleWide" : true, + "abilities": + { + "nonLiving" : + { + "type" : "NON_LIVING" + }, + "immuneToMind" : + { + "type" : "MIND_IMMUNITY" + }, + "immuneToIceBolt" : + { + "type" : "SPELL_IMMUNITY", + "subtype" : "spell.iceBolt" + }, + "immuneToFrostRing" : + { + "type" : "SPELL_IMMUNITY", + "subtype" : "spell.frostRing" + }, + "fireballVulnerablity" : + { + "type" : "MORE_DAMAGE_FROM_SPELL", + "subtype" : "spell.fireball", + "val" : 100 + }, + "infernoVulnerablity" : + { + "type" : "MORE_DAMAGE_FROM_SPELL", + "subtype" : "spell.inferno", + "val" : 100 + }, + "armageddonVulnerablity" : + { + "type" : "MORE_DAMAGE_FROM_SPELL", + "subtype" : "spell.armageddon", + "val" : 100 + }, + "fireShieldVulnerablity" : + { + "type" : "MORE_DAMAGE_FROM_SPELL", + "subtype" : "spell.fireShield", + "val" : 100 + }, + "fireWallVulnerablity" : + { + "type" : "MORE_DAMAGE_FROM_SPELL", + "subtype" : "spell.fireWallTrigger", + "val" : 100 + }, + "oppositeFire" : + { + "type" : "HATE", + "subtype" : "creature.fireElemental", + "val" : 100 + }, + "oppositeEnergy" : + { + "type" : "HATE", + "subtype" : "creature.energyElemental", + "val" : 100 + } + }, + "upgrades": ["iceElemental"], + "graphics" : + { + "animation": "CWELEM.DEF" + }, + "sound" : + { + "attack": "WELMATTK.wav", + "defend": "WELMDFND.wav", + "killed": "WELMKILL.wav", + "move": "WELMMOVE.wav", + "wince": "WELMWNCE.wav" + } + }, + "iceElemental" : + { + "index": 123, + "level": 3, + "faction": "conflux", + "doubleWide" : true, + "shots" : 24, + "abilities": + { + "nonLiving" : + { + "type" : "NON_LIVING" + }, + "shooter" : + { + "type" : "SHOOTER" + }, + "spellcaster": + { + "type" : "SPELLCASTER", + "subtype" : "spell.protectWater", + "val" : 2 + }, + "spellPower" : + { + "type" : "CREATURE_ENCHANT_POWER", + "val" : 6 + }, + "spellPoints" : + { + "type" : "CASTS", + "val" : 3 + }, + "immuneToMind" : + { + "type" : "MIND_IMMUNITY" + }, + "immuneToIceBolt" : + { + "type" : "SPELL_IMMUNITY", + "subtype" : "spell.iceBolt" + }, + "immuneToFrostRing" : + { + "type" : "SPELL_IMMUNITY", + "subtype" : "spell.frostRing" + }, + "fireballVulnerablity" : + { + "type" : "MORE_DAMAGE_FROM_SPELL", + "subtype" : "spell.fireball", + "val" : 100 + }, + "infernoVulnerablity" : + { + "type" : "MORE_DAMAGE_FROM_SPELL", + "subtype" : "spell.inferno", + "val" : 100 + }, + "armageddonVulnerablity" : + { + "type" : "MORE_DAMAGE_FROM_SPELL", + "subtype" : "spell.armageddon", + "val" : 100 + }, + "fireShieldVulnerablity" : + { + "type" : "MORE_DAMAGE_FROM_SPELL", + "subtype" : "spell.fireShield", + "val" : 100 + }, + "fireWallVulnerablity" : + { + "type" : "MORE_DAMAGE_FROM_SPELL", + "subtype" : "spell.fireWallTrigger", + "val" : 100 + }, + "oppositeFire" : + { + "type" : "HATE", + "subtype" : "creature.fireElemental", + "val" : 100 + }, + "oppositeEnergy" : + { + "type" : "HATE", + "subtype" : "creature.energyElemental", + "val" : 100 + } + }, + "graphics" : + { + "animation": "CICEE.DEF", + "missile" : + { + "projectile": "PICEE.DEF" + } + }, + "sound" : + { + "attack": "ICELATTK.wav", + "defend": "ICELDFND.wav", + "killed": "ICELKILL.wav", + "move": "ICELMOVE.wav", + "shoot": "ICELSHOT.wav", + "wince": "ICELWNCE.wav" + } + }, + "fireElemental" : + { + "index": 114, "level": 4, "faction": "conflux", "abilities": @@ -674,18 +424,66 @@ "type" : "SPELL_SCHOOL_IMMUNITY", "subtype" : "spellSchool.fire" }, + "iceBoltVulnerablity" : + { + "type" : "MORE_DAMAGE_FROM_SPELL", + "subtype" : "spell.iceBolt", + "val" : 100 + }, "frostRingVulnerablity" : { "type" : "MORE_DAMAGE_FROM_SPELL", "subtype" : "spell.frostRing", "val" : 100 }, - "iceBoltVulnerablity" : + "oppositeWater" : { - "type" : "MORE_DAMAGE_FROM_SPELL", - "subtype" : "spell.iceBolt", + "type" : "HATE", + "subtype" : "creature.waterElemental", "val" : 100 }, + "oppositeIce" : + { + "type" : "HATE", + "subtype" : "creature.iceElemental", + "val" : 100 + } + }, + "upgrades": ["energyElemental"], + "graphics" : + { + "animation": "CFELEM.DEF" + }, + "sound" : + { + "attack": "FELMATTK.wav", + "defend": "FELMDFND.wav", + "killed": "FELMKILL.wav", + "move": "FELMMOVE.wav", + "wince": "FELMWNCE.wav" + } + }, + "energyElemental" : + { + "index": 129, + "level": 4, + "faction": "conflux", + "abilities": + { + "nonLiving" : + { + "type" : "NON_LIVING" + }, + "energizes" : + { + "type" : "FLYING" + }, + "spellcaster" : + { + "type" : "SPELLCASTER", + "subtype" : "spell.protectFire", + "val" : 2 + }, "spellPower" : { "type" : "CREATURE_ENCHANT_POWER", @@ -696,11 +494,26 @@ "type" : "CASTS", "val" : 3 }, - "spellcaster": + "immuneToMind" : { - "type" : "SPELLCASTER", - "subtype" : "spell.protectFire", - "val" : 2 + "type" : "MIND_IMMUNITY" + }, + "immuneToFire" : + { + "type" : "SPELL_SCHOOL_IMMUNITY", + "subtype" : "spellSchool.fire" + }, + "iceBoltVulnerablity" : + { + "type" : "MORE_DAMAGE_FROM_SPELL", + "subtype" : "spell.iceBolt", + "val" : 100 + }, + "frostRingVulnerablity" : + { + "type" : "MORE_DAMAGE_FROM_SPELL", + "subtype" : "spell.frostRing", + "val" : 100 }, "oppositeWater" : { @@ -717,10 +530,6 @@ }, "graphics" : { - "animationTime" : - { - "idle" : 0 - }, "animation": "CNRG.DEF" }, "sound" : @@ -732,18 +541,251 @@ "wince": "ENERWNCE.wav" } }, + "earthElemental" : + { + "index": 113, + "level": 5, + "faction": "conflux", + "abilities": + { + "nonLiving" : + { + "type" : "NON_LIVING" + }, + "immuneToMind" : + { + "type" : "MIND_IMMUNITY" + }, + "lightingImmunity" : + { + "type" : "SPELL_IMMUNITY", + "subtype" : "spell.lightningBolt" + }, + "chainLightingImmunity" : + { + "type" : "SPELL_IMMUNITY", + "subtype" : "spell.chainLightning" + }, + "armageddonImmunity" : + { + "type" : "SPELL_IMMUNITY", + "subtype" : "spell.armageddon" + }, + "meteorShowerVulnerability" : + { + "type" : "MORE_DAMAGE_FROM_SPELL", + "subtype" : "spell.meteorShower", + "val" : 100 + }, + "oppositeAir" : + { + "type" : "HATE", + "subtype" : "creature.airElemental", + "val" : 100 + }, + "oppositeStorm" : + { + "type" : "HATE", + "subtype" : "creature.stormElemental", + "val" : 100 + } + }, + "upgrades": ["magmaElemental"], + "graphics" : + { + "animation": "CEELEM.DEF" + }, + "sound" : + { + "attack": "EELMATTK.wav", + "defend": "EELMDFND.wav", + "killed": "EELMKILL.wav", + "move": "EELMMOVE.wav", + "wince": "EELMWNCE.wav" + } + }, + "magmaElemental" : + { + "index": 125, + "level": 5, + "faction": "conflux", + "abilities": + { + "nonLiving" : + { + "type" : "NON_LIVING" + }, + "spellcaster": + { + "type" : "SPELLCASTER", + "subtype" : "spell.protectEarth", + "val" : 2 + }, + "spellPower" : + { + "type" : "CREATURE_ENCHANT_POWER", + "val" : 6 + }, + "spellPoints" : + { + "type" : "CASTS", + "val" : 3 + }, + "immuneToMind" : + { + "type" : "MIND_IMMUNITY" + }, + "lightingImmunity" : + { + "type" : "SPELL_IMMUNITY", + "subtype" : "spell.lightningBolt" + }, + "chainLightingImmunity" : + { + "type" : "SPELL_IMMUNITY", + "subtype" : "spell.chainLightning" + }, + "armageddonImmunity" : + { + "type" : "SPELL_IMMUNITY", + "subtype" : "spell.armageddon" + }, + "meteorShowerVulnerability" : + { + "type" : "MORE_DAMAGE_FROM_SPELL", + "subtype" : "spell.meteorShower", + "val" : 100 + }, + "oppositeAir" : + { + "type" : "HATE", + "subtype" : "creature.airElemental", + "val" : 100 + }, + "oppositeStorm" : + { + "type" : "HATE", + "subtype" : "creature.stormElemental", + "val" : 100 + } + }, + "graphics" : + { + "animation": "CSTONE.DEF" + }, + "sound" : + { + "attack": "MAGMATTK.wav", + "defend": "MAGMDFND.wav", + "killed": "MAGMKILL.wav", + "move": "MAGMMOVE.wav", + "wince": "MAGMWNCE.wav" + } + }, + "psychicElemental" : + { + "index": 120, + "level": 6, + "faction": "conflux", + "abilities": + { + "nonLiving" : + { + "type" : "NON_LIVING" + }, + "attackAllAdjacent" : + { + "type" : "ATTACKS_ALL_ADJACENT" + }, + "noRetaliation" : + { + "type" : "BLOCKS_RETALIATION" + }, + "immuneToMind" : + { + "type" : "MIND_IMMUNITY" + } + }, + "doubleWide" : false, + "upgrades": ["magicElemental"], + "graphics" : + { + "animation": "CPSYEL.DEF" + }, + "sound" : + { + "attack": "PSYCATTK.wav", + "defend": "PSYCDFND.wav", + "killed": "PSYCKILL.wav", + "move": "PSYCMOVE.wav", + "wince": "PSYCWNCE.wav" + } + }, + "magicElemental" : + { + "index": 121, + "level": 6, + "faction": "conflux", + "abilities": + { + "nonLiving" : + { + "type" : "NON_LIVING" + }, + "attackAllAdjacent" : + { + "type" : "ATTACKS_ALL_ADJACENT" + }, + "noRetaliation" : + { + "type" : "BLOCKS_RETALIATION" + }, + "magicImmunity" : + { + "type" : "LEVEL_SPELL_IMMUNITY", + "val" : 5 + } + }, + "doubleWide" : false, + "graphics" : + { + "animation": "CMAGEL.DEF" + }, + "sound" : + { + "attack": "MGELATTK.wav", + "defend": "MGELDFND.wav", + "killed": "MGELKILL.wav", + "move": "MGELMOVE.wav", + "wince": "MGELWNCE.wav" + } + }, "firebird" : { "index": 130, "level": 7, "faction": "conflux", "upgrades": ["phoenix"], + "doubleWide" : true, "abilities": { + "canFly" : + { + "type" : "FLYING" + }, + "twoHexAttackBreath" : + { + "type" : "TWO_HEX_ATTACK_BREATH" + }, "immuneToFire" : { "type" : "SPELL_SCHOOL_IMMUNITY", "subtype" : "spellSchool.fire" + }, + "KING_1" : // Will be affected by Slayer with no expertise + { + "type" : "KING", + "val" : 0 } }, "graphics" : @@ -764,22 +806,36 @@ "index": 131, "level": 7, "faction": "conflux", + "doubleWide" : true, "abilities": { + "canFly" : + { + "type" : "FLYING" + }, + "twoHexAttackBreath" : + { + "type" : "TWO_HEX_ATTACK_BREATH" + }, "rebirthOnce" : { "type" : "CASTS", "val" : 1 }, + "rebirth" : + { + "type" : "REBIRTH", + "val" : 20 + }, "immuneToFire" : { "type" : "SPELL_SCHOOL_IMMUNITY", "subtype" : "spellSchool.fire" }, - "rebirth" : + "KING_1" : // Will be affected by Slayer with no expertise { - "type" : "REBIRTH", - "val" : 20 + "type" : "KING", + "val" : 0 } }, "graphics" : diff --git a/config/creatures/dungeon.json b/config/creatures/dungeon.json index e3e03e9b1..4d7492983 100644 --- a/config/creatures/dungeon.json +++ b/config/creatures/dungeon.json @@ -74,6 +74,10 @@ "faction": "dungeon", "abilities": { + "canFly" : + { + "type" : "FLYING" + }, "strikeAndReturn" : { "type" : "RETURN_AFTER_STRIKE" @@ -101,6 +105,10 @@ "faction": "dungeon", "abilities": { + "canFly" : + { + "type" : "FLYING" + }, "strikeAndReturn" : { "type" : "RETURN_AFTER_STRIKE" @@ -130,6 +138,18 @@ "level": 3, "faction": "dungeon", "upgrades": ["evilEye"], + "shots" : 12, + "abilities" : + { + "shooter" : + { + "type" : "SHOOTER" + }, + "noMeleePenalty" : + { + "type" : "NO_MELEE_PENALTY" + } + }, "graphics" : { "animation": "CBEHOL.DEF", @@ -160,6 +180,18 @@ "index": 75, "level": 3, "faction": "dungeon", + "shots" : 24, + "abilities" : + { + "shooter" : + { + "type" : "SHOOTER" + }, + "noMeleePenalty" : + { + "type" : "NO_MELEE_PENALTY" + } + }, "graphics" : { "animation": "CEVEYE.DEF", @@ -190,8 +222,18 @@ "index": 76, "level": 4, "faction": "dungeon", + "doubleWide" : true, + "shots" : 4, "abilities": { + "shooter" : + { + "type" : "SHOOTER" + }, + "noMeleePenalty" : + { + "type" : "NO_MELEE_PENALTY" + }, "petrification" : { "type" : "SPELL_AFTER_ATTACK", @@ -224,8 +266,18 @@ "index": 77, "level": 4, "faction": "dungeon", + "doubleWide" : true, + "shots" : 8, "abilities": { + "shooter" : + { + "type" : "SHOOTER" + }, + "noMeleePenalty" : + { + "type" : "NO_MELEE_PENALTY" + }, "petrification" : { "type" : "SPELL_AFTER_ATTACK", @@ -314,6 +366,14 @@ "level": 6, "faction": "dungeon", "upgrades": ["scorpicore"], + "doubleWide" : true, + "abilities" : + { + "canFly" : + { + "type" : "FLYING" + } + }, "graphics" : { "animation": "CMCORE.DEF" @@ -333,8 +393,13 @@ "index": 81, "level": 6, "faction": "dungeon", + "doubleWide" : true, "abilities": { + "canFly" : + { + "type" : "FLYING" + }, "paralize" : { "type" : "SPELL_AFTER_ATTACK", @@ -361,13 +426,18 @@ "index": 82, "level": 7, "faction": "dungeon", + "doubleWide" : true, "abilities": { "dragon" : { "type" : "DRAGON_NATURE" }, - "fireBreath" : + "canFly" : + { + "type" : "FLYING" + }, + "twoHexAttackBreath" : { "type" : "TWO_HEX_ATTACK_BREATH" }, @@ -375,6 +445,11 @@ { "type" : "LEVEL_SPELL_IMMUNITY", "val" : 3 + }, + "KING_1" : // Will be affected by Slayer with no expertise + { + "type" : "KING", + "val" : 0 } }, "upgrades": ["blackDragon"], @@ -396,13 +471,18 @@ "index": 83, "level": 7, "faction": "dungeon", + "doubleWide" : true, "abilities": { "dragon" : { "type" : "DRAGON_NATURE" }, - "fireBreath" : + "canFly" : + { + "type" : "FLYING" + }, + "twoHexAttackBreath" : { "type" : "TWO_HEX_ATTACK_BREATH" }, @@ -411,6 +491,11 @@ "type" : "LEVEL_SPELL_IMMUNITY", "val" : 5 }, + "KING_1" : // Will be affected by Slayer with no expertise + { + "type" : "KING", + "val" : 0 + }, "hateTitans" : { "type" : "HATE", diff --git a/config/creatures/fortress.json b/config/creatures/fortress.json index 1b3169052..f834f993a 100644 --- a/config/creatures/fortress.json +++ b/config/creatures/fortress.json @@ -44,6 +44,14 @@ "faction": "fortress", "upgrades": ["lizardWarrior"], "hasDoubleWeek": true, + "shots" : 12, + "abilities" : + { + "shooter" : + { + "type" : "SHOOTER" + } + }, "graphics" : { "animation": "CPLIZA.DEF", @@ -67,6 +75,14 @@ "index": 101, "level": 2, "faction": "fortress", + "shots" : 24, + "abilities" : + { + "shooter" : + { + "type" : "SHOOTER" + } + }, "graphics" : { "animation": "CALIZA.DEF", @@ -85,52 +101,6 @@ "wince": "ALIZWNCE.wav" } }, - "gorgon" : - { - "index": 102, - "level": 5, - "faction": "fortress", - "upgrades": ["mightyGorgon"], - "graphics" : - { - "animation": "CCGORG.DEF" - }, - "sound" : - { - "attack": "CGORATTK.wav", - "defend": "CGORDFND.wav", - "killed": "CGORKILL.wav", - "move": "CGORMOVE.wav", - "wince": "CGORWNCE.wav" - } - }, - "mightyGorgon" : - { - "index": 103, - "level": 5, - "faction": "fortress", - "abilities": - { - "deathStare" : - { - "type" : "DEATH_STARE", - "subtype" : "deathStareGorgon", - "val" : 10 - } - }, - "graphics" : - { - "animation": "CBGOG.DEF" - }, - "sound" : - { - "attack": "BGORATTK.wav", - "defend": "BGORDFND.wav", - "killed": "BGORKILL.wav", - "move": "BGORMOVE.wav", - "wince": "BGORWNCE.wav" - } - }, "serpentFly" : { "index": 104, @@ -139,6 +109,10 @@ "faction": "fortress", "abilities": { + "canFly" : + { + "type" : "FLYING" + }, "dispellHelpful" : { "type" : "SPELL_AFTER_ATTACK", @@ -168,6 +142,10 @@ "faction": "fortress", "abilities": { + "canFly" : + { + "type" : "FLYING" + }, "dispellHelpful" : { "type" : "SPELL_AFTER_ATTACK", @@ -200,6 +178,7 @@ "index": 106, "level": 4, "faction": "fortress", + "doubleWide" : true, "abilities": { "petrify" : @@ -228,6 +207,7 @@ "index": 107, "level": 4, "faction": "fortress", + "doubleWide" : true, "abilities": { "petrify" : @@ -250,12 +230,68 @@ "wince": "GBASWNCE.wav" } }, + "gorgon" : + { + "index": 102, + "level": 5, + "faction": "fortress", + "upgrades": ["mightyGorgon"], + "doubleWide" : true, + "graphics" : + { + "animation": "CCGORG.DEF" + }, + "sound" : + { + "attack": "CGORATTK.wav", + "defend": "CGORDFND.wav", + "killed": "CGORKILL.wav", + "move": "CGORMOVE.wav", + "wince": "CGORWNCE.wav" + } + }, + "mightyGorgon" : + { + "index": 103, + "level": 5, + "faction": "fortress", + "doubleWide" : true, + "abilities": + { + "deathStare" : + { + "type" : "DEATH_STARE", + "subtype" : "deathStareGorgon", + "val" : 10 + } + }, + "graphics" : + { + "animation": "CBGOG.DEF" + }, + "sound" : + { + "attack": "BGORATTK.wav", + "defend": "BGORDFND.wav", + "killed": "BGORKILL.wav", + "move": "BGORMOVE.wav", + "wince": "BGORWNCE.wav" + } + }, "wyvern" : { "index": 108, "level": 6, "faction": "fortress", "upgrades": ["wyvernMonarch"], + "doubleWide" : true, + "abilities" : + { + "canFly" : + { + "type" : "FLYING" + } + }, "graphics" : { "animation": "CWYVER.DEF" @@ -274,9 +310,14 @@ "index": 109, "level": 6, "faction": "fortress", + "doubleWide" : true, "abilities": { - "petrify" : + "canFly" : + { + "type" : "FLYING" + }, + "poison" : { "type" : "SPELL_AFTER_ATTACK", "subtype" : "spell.poison", @@ -301,15 +342,21 @@ "index": 110, "level": 7, "faction": "fortress", + "doubleWide" : true, "abilities": { - "noRetaliate" : + "attackAllAdjacent" : + { + "type" : "ATTACKS_ALL_ADJACENT" + }, + "noRetaliation" : { "type" : "BLOCKS_RETALIATION" }, - "attackAll" : + "KING_1" : // Will be affected by Slayer with no expertise { - "type" : "ATTACKS_ALL_ADJACENT" + "type" : "KING", + "val" : 0 } }, "upgrades": ["chaosHydra"], @@ -331,15 +378,21 @@ "index": 111, "level": 7, "faction": "fortress", + "doubleWide" : true, "abilities": { - "noRetaliate" : + "attackAllAdjacent" : + { + "type" : "ATTACKS_ALL_ADJACENT" + }, + "noRetaliation" : { "type" : "BLOCKS_RETALIATION" }, - "attackAll" : + "KING_1" : // Will be affected by Slayer with no expertise { - "type" : "ATTACKS_ALL_ADJACENT" + "type" : "KING", + "val" : 0 } }, "graphics" : diff --git a/config/creatures/inferno.json b/config/creatures/inferno.json index 204432246..cbea72a91 100755 --- a/config/creatures/inferno.json +++ b/config/creatures/inferno.json @@ -51,6 +51,14 @@ "faction": "inferno", "upgrades": ["magog"], "hasDoubleWeek": true, + "shots" : 12, + "abilities" : + { + "shooter" : + { + "type" : "SHOOTER" + } + }, "graphics" : { "animation": "CGOG.DEF", @@ -74,8 +82,13 @@ "index": 45, "level": 2, "faction": "inferno", + "shots" : 24, "abilities": { + "shooter" : + { + "type" : "SHOOTER" + }, "fireball" : { "type" : "SPELL_LIKE_ATTACK", @@ -106,10 +119,7 @@ "level": 3, "faction": "inferno", "upgrades": ["cerberus"], - "abilities": - { - "FLYING_ARMY" : null //hell hound doesn't fly - }, + "doubleWide" : true, "graphics" : { "animation": "CHHOUN.DEF" @@ -128,17 +138,17 @@ "index": 47, "level": 3, "faction": "inferno", + "doubleWide" : true, "abilities": { - "threeHeads" : - { - "type" : "THREE_HEADED_ATTACK" - }, "noRetaliation" : { "type" : "BLOCKS_RETALIATION" }, - "FLYING_ARMY" : null //cerberus doesn't fly + "threeHeads" : + { + "type" : "THREE_HEADED_ATTACK" + } }, "graphics" : { @@ -254,6 +264,15 @@ "faction": "inferno", "abilities": { + "canFly" : + { + "type" : "FLYING" + }, + "immuneToFire" : + { + "type" : "SPELL_SCHOOL_IMMUNITY", + "subtype" : "spellSchool.fire" + }, "hateGenies" : { "type" : "HATE", @@ -265,15 +284,6 @@ "type" : "HATE", "subtype" : "creature.masterGenie", "val" : 50 - }, - "canFly" : - { - "type" : "FLYING" - }, - "immuneToFire" : - { - "type" : "SPELL_SCHOOL_IMMUNITY", - "subtype" : "spellSchool.fire" } }, "upgrades": ["efreetSultan"], @@ -297,6 +307,20 @@ "faction": "inferno", "abilities": { + "canFly" : + { + "type" : "FLYING" + }, + "fireShield" : + { + "type" : "FIRE_SHIELD", + "val" : 20 + }, + "immuneToFire" : + { + "type" : "SPELL_SCHOOL_IMMUNITY", + "subtype" : "spellSchool.fire" + }, "hateGenies" : { "type" : "HATE", @@ -308,20 +332,6 @@ "type" : "HATE", "subtype" : "creature.masterGenie", "val" : 50 - }, - "canFly" : - { - "type" : "FLYING" - }, - "immuneToFire" : - { - "type" : "SPELL_SCHOOL_IMMUNITY", - "subtype" : "spellSchool.fire" - }, - "fireShield" : - { - "type" : "FIRE_SHIELD", - "val" : 20 } }, "graphics" : @@ -345,6 +355,29 @@ "faction": "inferno", "abilities": { + "teleports" : + { + "type" : "FLYING", + "subtype" : "movementTeleporting" + }, + "noRetaliation" : + { + "type" : "BLOCKS_RETALIATION" + }, + "descreaseLuck" : + { + "type" : "LUCK", + "val" : -1, + "stacking" : "Devils", + "propagator": "BATTLE_WIDE", + "propagationUpdater" : "BONUS_OWNER_UPDATER", + "limiters" : [ "OPPOSITE_SIDE" ] + }, + "KING_2" : // Will be affected by Advanced Slayer or better + { + "type" : "KING", + "val" : 2 + }, "hateAngels" : { "type" : "HATE", @@ -357,24 +390,6 @@ "subtype" : "creature.archangel", "val" : 50, "description" : "Devil -1" - }, - "FLYING_ARMY" : - { - // type loaded from crtraits - "subtype" : "movementTeleporting" - }, - "descreaseLuck" : - { - "type" : "LUCK", - "val" : -1, - "stacking" : "Devils", - "propagator": "BATTLE_WIDE", - "propagationUpdater" : "BONUS_OWNER_UPDATER", - "limiters" : [ "OPPOSITE_SIDE" ] - }, - "blockRetaliation" : - { - "type" : "BLOCKS_RETALIATION" } }, "upgrades": ["archDevil"], @@ -400,6 +415,29 @@ "faction": "inferno", "abilities" : { + "teleports" : + { + "type" : "FLYING", + "subtype" : "movementTeleporting" + }, + "noRetaliation" : + { + "type" : "BLOCKS_RETALIATION" + }, + "descreaseLuck" : + { + "type" : "LUCK", + "val" : -1, + "stacking" : "Devils", + "propagator": "BATTLE_WIDE", + "propagationUpdater" : "BONUS_OWNER_UPDATER", + "limiters" : [ "OPPOSITE_SIDE" ] + }, + "KING_2" : // Will be affected by Advanced Slayer or better + { + "type" : "KING", + "val" : 2 + }, "hateAngels" : { "type" : "HATE", @@ -411,24 +449,6 @@ "type" : "HATE", "subtype" : "creature.archangel", "val" : 50 - }, - "FLYING_ARMY" : - { - // type loaded from crtraits - "subtype" : "movementTeleporting" - }, - "descreaseLuck" : - { - "type" : "LUCK", - "val" : -1, - "stacking" : "Devils", - "propagator": "BATTLE_WIDE", - "propagationUpdater" : "BONUS_OWNER_UPDATER", - "limiters" : [ "OPPOSITE_SIDE" ] - }, - "blockRetaliation" : - { - "type" : "BLOCKS_RETALIATION" } }, "graphics" : diff --git a/config/creatures/necropolis.json b/config/creatures/necropolis.json index 54f1e8cec..06a71382d 100644 --- a/config/creatures/necropolis.json +++ b/config/creatures/necropolis.json @@ -5,6 +5,13 @@ "level": 1, "faction": "necropolis", "upgrades": ["skeletonWarrior"], + "abilities" : + { + "undead" : + { + "type" : "UNDEAD" + } + }, "graphics" : { "animation": "CSKELE.DEF" @@ -23,6 +30,13 @@ "index": 57, "level": 1, "faction": "necropolis", + "abilities" : + { + "undead" : + { + "type" : "UNDEAD" + } + }, "graphics" : { "animation": "CWSKEL.DEF" @@ -43,6 +57,13 @@ "extraNames": [ "zombie" ], //FIXME: zombie is a name of upgrade but not in HOTRAITS "faction" : "necropolis", "upgrades": ["zombieLord"], + "abilities" : + { + "undead" : + { + "type" : "UNDEAD" + } + }, "graphics" : { "animation": "CZOMBI.DEF" @@ -75,6 +96,10 @@ }, "abilities": { + "undead" : + { + "type" : "UNDEAD" + }, "castDisease" : { "type" : "SPELL_AFTER_ATTACK", @@ -90,6 +115,14 @@ "faction": "necropolis", "abilities": { + "undead" : + { + "type" : "UNDEAD" + }, + "canFly" : + { + "type" : "FLYING" + }, "regenerate" : { "type" : "HP_REGENERATION", @@ -118,6 +151,14 @@ "faction": "necropolis", "abilities": { + "undead" : + { + "type" : "UNDEAD" + }, + "canFly" : + { + "type" : "FLYING" + }, "regenerate" : { "type" : "HP_REGENERATION", @@ -149,6 +190,14 @@ "faction": "necropolis", "abilities": { + "undead" : + { + "type" : "UNDEAD" + }, + "canFly" : + { + "type" : "FLYING" + }, "noRetalitation" : { "type" : "BLOCKS_RETALIATION" @@ -177,6 +226,14 @@ "faction": "necropolis", "abilities": { + "undead" : + { + "type" : "UNDEAD" + }, + "canFly" : + { + "type" : "FLYING" + }, "noRetalitation" : { "type" : "BLOCKS_RETALIATION" @@ -208,8 +265,17 @@ "index": 64, "level": 5, "faction": "necropolis", + "shots" : 12, "abilities": { + "undead" : + { + "type" : "UNDEAD" + }, + "shooter" : + { + "type" : "SHOOTER" + }, "deathCloud" : { "type" : "SPELL_LIKE_ATTACK", @@ -240,8 +306,17 @@ "index": 65, "level": 5, "faction": "necropolis", + "shots" : 24, "abilities": { + "undead" : + { + "type" : "UNDEAD" + }, + "shooter" : + { + "type" : "SHOOTER" + }, "deathCloud" : { "type" : "SPELL_LIKE_ATTACK", @@ -271,8 +346,13 @@ "index": 66, "level": 6, "faction": "necropolis", + "doubleWide" : true, "abilities": { + "undead" : + { + "type" : "UNDEAD" + }, "curses" : { "type" : "SPELL_AFTER_ATTACK", @@ -299,8 +379,13 @@ "index": 67, "level": 6, "faction": "necropolis", + "doubleWide" : true, "abilities": { + "undead" : + { + "type" : "UNDEAD" + }, "curses" : { "type" : "SPELL_AFTER_ATTACK", @@ -331,12 +416,21 @@ "index": 68, "level": 7, "faction": "necropolis", + "doubleWide" : true, "abilities" : { + "undead" : + { + "type" : "UNDEAD" + }, "dragon" : { "type" : "DRAGON_NATURE" }, + "canFly" : + { + "type" : "FLYING" + }, "decreaseMorale" : { "type" : "MORALE", @@ -345,6 +439,11 @@ "propagator": "BATTLE_WIDE", "propagationUpdater" : "BONUS_OWNER_UPDATER", "limiters" : [ "OPPOSITE_SIDE" ] + }, + "KING_1" : // Will be affected by Slayer with no expertise + { + "type" : "KING", + "val" : 0 } }, "upgrades": ["ghostDragon"], @@ -366,12 +465,27 @@ "index": 69, "level": 7, "faction": "necropolis", + "doubleWide" : true, "abilities": { + "undead" : + { + "type" : "UNDEAD" + }, "dragon" : { "type" : "DRAGON_NATURE" }, + "canFly" : + { + "type" : "FLYING" + }, + "age" : + { + "type" : "SPELL_AFTER_ATTACK", + "subtype" : "spell.age", + "val" : 20 + }, "decreaseMorale" : { "type" : "MORALE", @@ -381,11 +495,10 @@ "propagationUpdater" : "BONUS_OWNER_UPDATER", "limiters" : [ "OPPOSITE_SIDE" ] }, - "age" : + "KING_1" : // Will be affected by Slayer with no expertise { - "type" : "SPELL_AFTER_ATTACK", - "subtype" : "spell.age", - "val" : 20 + "type" : "KING", + "val" : 0 } }, "graphics" : diff --git a/config/creatures/neutral.json b/config/creatures/neutral.json index 8977afa7d..0e25c5231 100644 --- a/config/creatures/neutral.json +++ b/config/creatures/neutral.json @@ -7,15 +7,15 @@ "faction": "neutral", "abilities": { + "nonliving" : + { + "type" : "NON_LIVING" + }, "magicResistance" : { "type" : "SPELL_DAMAGE_REDUCTION", "subtype" : "spellSchool.any", "val" : 85 - }, - "nonliving" : - { - "type" : "NON_LIVING" } }, "graphics" : @@ -38,15 +38,15 @@ "faction": "neutral", "abilities": { + "nonliving" : + { + "type" : "NON_LIVING" + }, "magicResistance" : { "type" : "SPELL_DAMAGE_REDUCTION", "subtype" : "spellSchool.any", "val" : 95 - }, - "nonliving" : - { - "type" : "NON_LIVING" } }, "graphics" : @@ -68,28 +68,38 @@ "level": 10, "faction": "neutral", "excludeFromRandomization" : true, + "doubleWide" : true, "abilities": { "dragon" : { "type" : "DRAGON_NATURE" }, - "fireBreath" : + "canFly" : + { + "type" : "FLYING" + }, + "twoHexAttackBreath" : { "type" : "TWO_HEX_ATTACK_BREATH" }, + "fear" : + { + "type" : "FEAR" + }, + "fearless" : + { + "type" : "FEARLESS" + }, "spellImmunity" : { "type" : "LEVEL_SPELL_IMMUNITY", "val" : 3 }, - "fearless" : + "KING_1" : // Will be affected by Slayer with no expertise { - "type" : "FEARLESS" - }, - "fear" : - { - "type" : "FEAR" + "type" : "KING", + "val" : 0 } }, "graphics" : @@ -111,22 +121,27 @@ "level": 10, "faction": "neutral", "excludeFromRandomization" : true, + "doubleWide" : true, "abilities": { "dragon" : { "type" : "DRAGON_NATURE" }, + "crystals" : + { + "type" : "SPECIAL_CRYSTAL_GENERATION" + }, "magicResistance" : { "type" : "MAGIC_RESISTANCE", "val" : 20 }, - "crystals" : + "KING_1" : // Will be affected by Slayer with no expertise { - "type" : "SPECIAL_CRYSTAL_GENERATION" - }, - "FLYING_ARMY" : null + "type" : "KING", + "val" : 0 + } }, "graphics" : { @@ -147,12 +162,17 @@ "level": 8, "faction": "neutral", "excludeFromRandomization" : true, + "doubleWide" : true, "abilities": { "dragon" : { "type" : "DRAGON_NATURE" }, + "canFly" : + { + "type" : "FLYING" + }, "mirror" : { "type" : "MAGIC_MIRROR", @@ -223,6 +243,11 @@ "subtype" : "spell.meteorShower", "addInfo" : 5, "val" : 2 + }, + "KING_1" : // Will be affected by Slayer with no expertise + { + "type" : "KING", + "val" : 0 } }, "graphics" : @@ -245,27 +270,37 @@ "level": 10, "faction": "neutral", "excludeFromRandomization" : true, + "doubleWide" : true, "abilities": { "dragon" : { "type" : "DRAGON_NATURE" }, + "canFly" : + { + "type" : "FLYING" + }, + "twoHexAttackBreath" : + { + "type" : "TWO_HEX_ATTACK_BREATH" + }, "acidBreath" : { "type" : "ACID_BREATH", "val" : 25, "addInfo" : 30 }, - "fireBreath" : - { - "type" : "TWO_HEX_ATTACK_BREATH" - }, "reduceDefence" : { "type" : "SPELL_AFTER_ATTACK", "subtype" : "spell.acidBreath", "val" : 100 + }, + "KING_1" : // Will be affected by Slayer with no expertise + { + "type" : "KING", + "val" : 0 } }, "graphics" : @@ -288,9 +323,18 @@ "extraNames": [ "enchanters" ], "faction": "neutral", "excludeFromRandomization" : true, + "shots" : 32, "abilities": { - "noPenalty" : + "shooter" : + { + "type" : "SHOOTER" + }, + "noMeleePenalty" : + { + "type" : "NO_MELEE_PENALTY" + }, + "noWallPenalty" : { "type" : "NO_WALL_PENALTY" }, @@ -299,10 +343,24 @@ "type" : "CASTS", "val" : 5 }, - "castsAirShield" : + "castsHaste" : { "type" : "ENCHANTER", - "subtype" : "spell.airShield", + "subtype" : "spell.haste", + "val" : 3, + "addInfo" : 3 + }, + "castsSlow" : + { + "type" : "ENCHANTER", + "subtype" : "spell.slow", + "val" : 3, + "addInfo" : 3 + }, + "castsStoneSkin" : + { + "type" : "ENCHANTER", + "subtype" : "spell.stoneSkin", "val" : 3, "addInfo" : 3 }, @@ -320,28 +378,13 @@ "val" : 3, "addInfo" : 3 }, - "castsStoneSkin" : + "castsAirShield" : { "type" : "ENCHANTER", - "subtype" : "spell.stoneSkin", - "val" : 3, - "addInfo" : 3 - }, - "castsSlow" : - { - "type" : "ENCHANTER", - "subtype" : "spell.slow", - "val" : 3, - "addInfo" : 3 - }, - "castsHaste" : - { - "type" : "ENCHANTER", - "subtype" : "spell.haste", + "subtype" : "spell.airShield", "val" : 3, "addInfo" : 3 } - }, "graphics" : { @@ -368,15 +411,20 @@ "extraNames": [ "sharpshooters" ], "faction": "neutral", "excludeFromRandomization" : true, + "shots" : 32, "abilities": { - "noPenalty" : + "shooter" : { - "type" : "NO_WALL_PENALTY" + "type" : "SHOOTER" }, "noDistancePenalty" : { "type" : "NO_DISTANCE_PENALTY" + }, + "noWallPenalty" : + { + "type" : "NO_WALL_PENALTY" } }, "graphics" : @@ -402,8 +450,13 @@ "index": 138, "level": 1, "faction": "neutral", + "shots" : 24, "abilities": { + "shooter" : + { + "type" : "SHOOTER" + }, "lucky" : { "type" : "LUCK", @@ -482,7 +535,7 @@ "type" : "SPELL_AFTER_ATTACK", "subtype" : "spell.curse", "val" : 50 - } + } }, "graphics" : { @@ -502,6 +555,7 @@ "index": 142, "level": 3, "faction": "neutral", + "doubleWide" : true, "abilities": { "sandWalker" : @@ -511,7 +565,6 @@ "propagator" : "HERO" } }, - "doubleWide" : true, "graphics" : { "animation": "CNOMAD.DEF" @@ -546,7 +599,7 @@ "subtype" : "visionsHeroes", "val" : 3, "valueType" : "INDEPENDENT_MAX", - "propagator" : "HERO" + "propagator" : "HERO" }, "visionsTowns" : { @@ -554,7 +607,7 @@ "subtype" : "visionsTowns", "val" : 3, "valueType" : "INDEPENDENT_MAX", - "propagator" : "HERO" + "propagator" : "HERO" } }, "graphics" : diff --git a/config/creatures/rampart.json b/config/creatures/rampart.json index 157fe24ae..3b28901d7 100644 --- a/config/creatures/rampart.json +++ b/config/creatures/rampart.json @@ -5,6 +5,7 @@ "level": 1, "faction": "rampart", "upgrades": ["centaurCaptain"], + "doubleWide" : true, "hasDoubleWeek": true, "graphics" : { @@ -26,6 +27,7 @@ "index": 15, "level": 1, "faction": "rampart", + "doubleWide" : true, "graphics" : { "missile" : null, @@ -99,6 +101,14 @@ "level": 3, "faction": "rampart", "upgrades": ["grandElf"], + "shots" : 24, + "abilities" : + { + "shooter" : + { + "type" : "SHOOTER" + } + }, "graphics" : { "animation": "CELF.DEF", @@ -122,8 +132,13 @@ "index": 19, "level": 3, "faction": "rampart", - "abilities": + "shots" : 24, + "abilities" : { + "shooter" : + { + "type" : "SHOOTER" + }, "doubleShot" : { "type": "ADDITIONAL_ATTACK", @@ -154,8 +169,13 @@ "index": 20, "level": 4, "faction": "rampart", + "doubleWide" : true, "abilities": { + "canFly" : + { + "type" : "FLYING" + }, "increaseManaCost" : { "type" : "CHANGES_SPELL_COST_FOR_ENEMY", @@ -182,8 +202,13 @@ "index": 21, "level": 4, "faction": "rampart", + "doubleWide" : true, "abilities": { + "canFly" : + { + "type" : "FLYING" + }, "increaseManaCost" : { "type" : "CHANGES_SPELL_COST_FOR_ENEMY", @@ -263,19 +288,20 @@ "index": 24, "level": 6, "faction": "rampart", + "doubleWide" : true, "abilities": { - "spellResistAura" : - { - "type" : "SPELL_RESISTANCE_AURA", - "val" : 20 - }, "blinds" : { "type" : "SPELL_AFTER_ATTACK", "subtype" : "spell.blind", "val" : 20, "addInfo" : [3,0] + }, + "spellResistAura" : + { + "type" : "SPELL_RESISTANCE_AURA", + "val" : 20 } }, "upgrades": ["warUnicorn"], @@ -297,19 +323,20 @@ "index": 25, "level": 6, "faction": "rampart", + "doubleWide" : true, "abilities": { - "spellResistAura" : - { - "type" : "SPELL_RESISTANCE_AURA", - "val" : 20 - }, "blinds" : { "type" : "SPELL_AFTER_ATTACK", "subtype" : "spell.blind", "val" : 20, "addInfo" : [3,0] + }, + "spellResistAura" : + { + "type" : "SPELL_RESISTANCE_AURA", + "val" : 20 } }, "graphics" : @@ -331,13 +358,18 @@ "index": 26, "level": 7, "faction": "rampart", + "doubleWide" : true, "abilities": { "dragon" : { "type" : "DRAGON_NATURE" }, - "fireBreath" : + "canFly" : + { + "type" : "FLYING" + }, + "twoHexAttackBreath" : { "type" : "TWO_HEX_ATTACK_BREATH" }, @@ -345,6 +377,11 @@ { "type" : "LEVEL_SPELL_IMMUNITY", "val" : 3 + }, + "KING_1" : // Will be affected by Slayer with no expertise + { + "type" : "KING", + "val" : 0 } }, "upgrades": ["goldDragon"], @@ -366,13 +403,18 @@ "index": 27, "level": 7, "faction": "rampart", + "doubleWide" : true, "abilities": { "dragon" : { "type" : "DRAGON_NATURE" }, - "fireBreath" : + "canFly" : + { + "type" : "FLYING" + }, + "twoHexAttackBreath" : { "type" : "TWO_HEX_ATTACK_BREATH" }, @@ -380,6 +422,11 @@ { "type" : "LEVEL_SPELL_IMMUNITY", "val" : 4 + }, + "KING_1" : // Will be affected by Slayer with no expertise + { + "type" : "KING", + "val" : 0 } }, "graphics" : diff --git a/config/creatures/special.json b/config/creatures/special.json index b99c7867d..4e528117a 100644 --- a/config/creatures/special.json +++ b/config/creatures/special.json @@ -36,8 +36,23 @@ "index": 145, "level": 0, "faction": "neutral", - "abilities" : { - "siegeMachine" : { "type" : "CATAPULT", "subtype" : "spell.catapultShot" } + "doubleWide" : true, + "shots" : 24, + "abilities" : + { + "siegeWeapon" : + { + "type" : "SIEGE_WEAPON" + }, + "shooter" : + { + "type" : "SHOOTER" + }, + "siegeMachine" : + { + "type" : "CATAPULT", + "subtype" : "spell.catapultShot" + } }, "graphics" : { @@ -60,6 +75,19 @@ "index": 146, "level": 0, "faction": "neutral", + "doubleWide" : true, + "shots" : 24, + "abilities" : + { + "siegeWeapon" : + { + "type" : "SIEGE_WEAPON" + }, + "shooter" : + { + "type" : "SHOOTER" + } + }, "graphics" : { "animation": "SMBAL.DEF", @@ -81,9 +109,15 @@ "index": 147, "level": 0, "faction": "neutral", + "doubleWide" : true, "abilities": { - "heals" : { + "siegeWeapon" : + { + "type" : "SIEGE_WEAPON" + }, + "heals" : + { "type" : "HEALER" , "subtype" : "spell.firstAid" } @@ -104,7 +138,17 @@ "index": 148, "level": 0, "faction": "neutral", - "abilities": { "inactive" : { "type" : "NOT_ACTIVE" } }, + "abilities": + { + "siegeWeapon" : + { + "type" : "SIEGE_WEAPON" + }, + "inactive" : + { + "type" : "NOT_ACTIVE" + } + }, "graphics" : { "animation": "SMCART.DEF" @@ -121,8 +165,10 @@ "index": 149, "level": 0, "faction": "neutral", + "shots" : 99, "abilities": { + "siegeWeapon" : { "type" : "SIEGE_WEAPON" }, "shooter" : { "type" : "SHOOTER" }, "ignoreDefence" : { "type" : "ENEMY_DEFENCE_REDUCTION", "val" : 100 }, "noWallPenalty" : { "type" : "NO_WALL_PENALTY" }, diff --git a/config/creatures/stronghold.json b/config/creatures/stronghold.json index bbdb2e110..7824a2b58 100644 --- a/config/creatures/stronghold.json +++ b/config/creatures/stronghold.json @@ -44,6 +44,7 @@ "level": 2, "faction": "stronghold", "upgrades": ["hobgoblinWolfRider"], + "doubleWide" : true, "hasDoubleWeek": true, "graphics" : { @@ -63,6 +64,7 @@ "index": 87, "level": 2, "faction": "stronghold", + "doubleWide" : true, "abilities": { "extraAttack" : @@ -90,6 +92,14 @@ "level": 3, "faction": "stronghold", "upgrades": ["orcChieftain"], + "shots" : 12, + "abilities" : + { + "shooter" : + { + "type" : "SHOOTER" + } + }, "graphics" : { "animation": "CORC.DEF", @@ -113,6 +123,14 @@ "index": 89, "level": 3, "faction": "stronghold", + "shots" : 24, + "abilities" : + { + "shooter" : + { + "type" : "SHOOTER" + } + }, "graphics" : { "animation": "CORCCH.DEF", @@ -194,6 +212,14 @@ "level": 5, "faction": "stronghold", "upgrades": ["thunderbird"], + "doubleWide" : true, + "abilities" : + { + "canFly" : + { + "type" : "FLYING" + } + }, "graphics" : { "animation": "CROC.DEF" @@ -212,19 +238,24 @@ "index": 93, "level": 5, "faction": "stronghold", + "doubleWide" : true, "abilities": { - "thunderStrength" : + "canFly" : { - "type" : "SPECIFIC_SPELL_POWER", - "subtype" : "spell.thunderbolt", - "val" : 10 + "type" : "FLYING" }, "thunderOnAttack" : { "type" : "SPELL_AFTER_ATTACK", "subtype" : "spell.thunderbolt", "val" : 20 + }, + "thunderStrength" : + { + "type" : "SPECIFIC_SPELL_POWER", + "subtype" : "spell.thunderbolt", + "val" : 10 } }, "graphics" : @@ -245,11 +276,17 @@ "index": 94, "level": 6, "faction": "stronghold", + "shots" : 16, "abilities" : { - "siege" : { - "subtype" : "spell.cyclopsShot", - "type" : "CATAPULT" + "shooter" : + { + "type" : "SHOOTER" + }, + "canShootWalls" : + { + "type" : "CATAPULT", + "subtype" : "spell.cyclopsShot" } }, "upgrades": ["cyclopKing"], @@ -276,16 +313,22 @@ "index": 95, "level": 6, "faction": "stronghold", + "shots" : 24, "abilities": { - "siege" : { - "subtype" : "spell.cyclopsShot", - "type" : "CATAPULT" - }, - "siegeLevel" : + "shooter" : + { + "type" : "SHOOTER" + }, + "canShootWalls" : + { + "type" : "CATAPULT", + "subtype" : "spell.cyclopsShot" + }, + "canShootWallsTimes" : { - "subtype" : "spell.cyclopsShot", "type" : "CATAPULT_EXTRA_SHOTS", + "subtype" : "spell.cyclopsShot", "valueType" : "BASE_NUMBER", "val" : 1 } @@ -313,12 +356,18 @@ "index": 96, "level": 7, "faction": "stronghold", + "doubleWide" : true, "abilities": { "reduceDefence" : { "type" : "ENEMY_DEFENCE_REDUCTION", "val" : 40 + }, + "KING_1" : // Will be affected by Slayer with no expertise + { + "type" : "KING", + "val" : 0 } }, "upgrades": ["ancientBehemoth"], @@ -340,12 +389,18 @@ "index": 97, "level": 7, "faction": "stronghold", + "doubleWide" : true, "abilities": { "reduceDefence" : { "type" : "ENEMY_DEFENCE_REDUCTION", "val" : 80 + }, + "KING_1" : // Will be affected by Slayer with no expertise + { + "type" : "KING", + "val" : 0 } }, "graphics" : diff --git a/config/creatures/tower.json b/config/creatures/tower.json index 2c5c10f7c..99781a907 100644 --- a/config/creatures/tower.json +++ b/config/creatures/tower.json @@ -26,6 +26,14 @@ "index": 29, "level": 1, "faction": "tower", + "shots" : 8, + "abilities" : + { + "shooter" : + { + "type" : "SHOOTER" + } + }, "graphics" : { "animation": "CGREMM.DEF", @@ -54,6 +62,10 @@ "gargoyle" : { "type" : "GARGOYLE" + }, + "canFly" : + { + "type" : "FLYING" } }, "upgrades": ["obsidianGargoyle"], @@ -80,6 +92,10 @@ "gargoyle" : { "type" : "GARGOYLE" + }, + "canFly" : + { + "type" : "FLYING" } }, "graphics" : @@ -102,15 +118,15 @@ "faction": "tower", "abilities": { + "nonliving" : + { + "type" : "NON_LIVING" + }, "magicResistance" : { "type" : "SPELL_DAMAGE_REDUCTION", "subtype" : "spellSchool.any", "val" : 50 - }, - "nonliving" : - { - "type" : "NON_LIVING" } }, "upgrades": ["stoneGolem"], @@ -134,15 +150,15 @@ "faction": "tower", "abilities" : { + "nonliving" : + { + "type" : "NON_LIVING" + }, "magicResistance" : { "type" : "SPELL_DAMAGE_REDUCTION", "subtype" : "spellSchool.any", "val" : 75 - }, - "nonliving" : - { - "type" : "NON_LIVING" } }, "graphics" : @@ -163,8 +179,17 @@ "index": 34, "level": 4, "faction": "tower", + "shots" : 24, "abilities": { + "shooter" : + { + "type" : "SHOOTER" + }, + "noMeleePenalty" : + { + "type" : "NO_MELEE_PENALTY" + }, "reduceSpellCost" : { "type" : "CHANGES_SPELL_COST_FOR_ALLY", @@ -195,8 +220,21 @@ "index": 35, "level": 4, "faction": "tower", + "shots" : 24, "abilities": { + "shooter" : + { + "type" : "SHOOTER" + }, + "noMeleePenalty" : + { + "type" : "NO_MELEE_PENALTY" + }, + "noWallPenalty" : + { + "type" : "NO_WALL_PENALTY" + }, "reduceSpellCost" : { "type" : "CHANGES_SPELL_COST_FOR_ALLY", @@ -236,13 +274,17 @@ "faction": "tower", "abilities": { - "hateAngels" : + "canFly" : + { + "type" : "FLYING" + }, + "hateEfreet" : { "type" : "HATE", "subtype" : "creature.efreet", "val" : 50 }, - "hateArchAngels" : + "hateEfreetSultans" : { "type" : "HATE", "subtype" : "creature.efreetSultan", @@ -270,17 +312,14 @@ "faction": "tower", "abilities": { - "hateAngels" : + "canFly" : { - "type" : "HATE", - "subtype" : "creature.efreet", - "val" : 50 + "type" : "FLYING" }, - "hateArchAngels" : + "casts" : { - "type" : "HATE", - "subtype" : "creature.efreetSultan", - "val" : 50 + "type" : "CASTS", + "val" : 3 }, "spellsLength" : { @@ -292,10 +331,17 @@ "type" : "RANDOM_SPELLCASTER", "val" : 2 }, - "casts" : + "hateEfreet" : { - "type" : "CASTS", - "val" : 3 + "type" : "HATE", + "subtype" : "creature.efreet", + "val" : 50 + }, + "hateEfreetSultans" : + { + "type" : "HATE", + "subtype" : "creature.efreetSultan", + "val" : 50 } }, "graphics" : @@ -317,6 +363,7 @@ "index": 38, "level": 6, "faction": "tower", + "doubleWide" : true, "abilities" : { "noRetaliation" : @@ -343,6 +390,7 @@ "index": 39, "level": 6, "faction": "tower", + "doubleWide" : true, "abilities" : { "noRetaliation" : @@ -373,6 +421,11 @@ "immuneToMind" : { "type" : "MIND_IMMUNITY" + }, + "KING_3" : // Will be affected by Expert Slayer only + { + "type" : "KING", + "val" : 3 } }, "upgrades": ["titan"], @@ -394,13 +447,27 @@ "index": 41, "level": 7, "faction": "tower", + "shots" : 24, "abilities" : { + "shooter" : + { + "type" : "SHOOTER" + }, + "noMeleePenalty" : + { + "type" : "NO_MELEE_PENALTY" + }, "immuneToMind" : { "type" : "MIND_IMMUNITY" }, - "hateArchAngels" : + "KING_3" : // Will be affected by Expert Slayer only + { + "type" : "KING", + "val" : 3 + }, + "hateBlackDragons" : { "type" : "HATE", "subtype" : "creature.blackDragon", diff --git a/config/factions/fortress.json b/config/factions/fortress.json index 62f3d1d51..d238f9d52 100644 --- a/config/factions/fortress.json +++ b/config/factions/fortress.json @@ -187,7 +187,7 @@ "bonuses": [ { "type": "PRIMARY_SKILL", - "subtype": "primarySkill.defence", + "subtype": "primarySkill.attack", "val": 2 } ], diff --git a/config/factions/rampart.json b/config/factions/rampart.json index 2b4c12f85..42bd462f9 100644 --- a/config/factions/rampart.json +++ b/config/factions/rampart.json @@ -199,7 +199,7 @@ "dwellingLvl3": { "id" : 32, "requires" : [ "dwellingLvl1" ] }, "dwellingLvl4": { "id" : 33, "requires" : [ "dwellingLvl3" ] }, "dwellingLvl5": { "id" : 34, "requires" : [ "dwellingLvl3" ] }, - "dwellingLvl6": { "id" : 35, "requires" : [ "allOf", [ "dwellingLvl3" ], [ "dwellingLvl4" ] ] }, + "dwellingLvl6": { "id" : 35, "requires" : [ "allOf", [ "dwellingLvl4" ], [ "dwellingLvl5" ] ] }, "dwellingLvl7": { "id" : 36, "requires" : [ "allOf", [ "dwellingLvl6" ], [ "mageGuild2" ] ] }, "dwellingUpLvl1": { "id" : 37, "upgrades" : "dwellingLvl1" }, diff --git a/config/fonts.json b/config/fonts.json index 0ba3f076f..e40da48f6 100644 --- a/config/fonts.json +++ b/config/fonts.json @@ -29,14 +29,14 @@ // "noShadow" - if set, this font will not drop any shadow "trueType": { - "BIGFONT" : { "file" : "NotoSerif-Bold.ttf", "size" : [ 19, 39, 58, 78] }, + "BIGFONT" : { "file" : "NotoSerif-Bold.ttf", "size" : [ 18, 38, 57, 76] }, "CALLI10R" : { "file" : "NotoSerif-Bold.ttf", "size" : [ 12, 24, 36, 48] }, // TODO: find better matching font? This is likely non-free 'Callisto MT' font "CREDITS" : { "file" : "NotoSerif-Black.ttf", "size" : [ 22, 44, 66, 88], "outline" : true }, "HISCORE" : { "file" : "NotoSerif-Black.ttf", "size" : [ 18, 36, 54, 72], "outline" : true }, - "MEDFONT" : { "file" : "NotoSerif-Bold.ttf", "size" : [ 15, 31, 46, 62] }, - "SMALFONT" : { "file" : "NotoSerif-Medium.ttf", "size" : [ 12, 24, 36, 48] }, + "MEDFONT" : { "file" : "NotoSerif-Bold.ttf", "size" : [ 13, 26, 39, 52] }, + "SMALFONT" : { "file" : "NotoSerif-Medium.ttf", "size" : [ 11, 22, 33, 44] }, "TIMES08R" : { "file" : "NotoSerif-Medium.ttf", "size" : [ 8, 16, 24, 32] }, - "TINY" : { "file" : "NotoSans-Medium.ttf", "size" : [ 9, 19, 28, 38], "noShadow" : true }, // The only H3 font without shadow + "TINY" : { "file" : "NotoSans-Medium.ttf", "size" : [ 9, 18, 28, 38], "noShadow" : true }, // The only H3 font without shadow "VERD10B" : { "file" : "NotoSans-Medium.ttf", "size" : [ 13, 26, 39, 52] } } } diff --git a/config/gameConfig.json b/config/gameConfig.json index 714309b2b..ac40a419f 100644 --- a/config/gameConfig.json +++ b/config/gameConfig.json @@ -53,8 +53,10 @@ "config/objects/creatureBanks.json", "config/objects/dwellings.json", "config/objects/generic.json", + "config/objects/lighthouse.json", "config/objects/magicSpring.json", "config/objects/magicWell.json", + "config/objects/markets.json", "config/objects/moddables.json", "config/objects/observatory.json", "config/objects/pyramid.json", @@ -302,7 +304,7 @@ "backpackSize" : -1, // if heroes are invitable in tavern "tavernInvite" : false, - // minimai primary skills for heroes + // minimal primary skills for heroes "minimalPrimarySkills": [ 0, 0, 1, 1] }, @@ -311,7 +313,21 @@ // How many new building can be built in a town per day "buildingsPerTurnCap" : 1, // Chances for a town with default buildings to receive corresponding dwelling level built in start - "startingDwellingChances": [100, 50] + "startingDwellingChances": [100, 50], + // Enable spell research in mage guild + "spellResearch": false, + // Cost for an spell research (array index is spell tier) + "spellResearchCost": [ + { "gold": 1000, "wood" : 2, "mercury": 2, "ore": 2, "sulfur": 2, "crystal": 2, "gems": 2 }, + { "gold": 1000, "wood" : 4, "mercury": 4, "ore": 4, "sulfur": 4, "crystal": 4, "gems": 4 }, + { "gold": 1000, "wood" : 6, "mercury": 6, "ore": 6, "sulfur": 6, "crystal": 6, "gems": 6 }, + { "gold": 1000, "wood" : 8, "mercury": 8, "ore": 8, "sulfur": 8, "crystal": 8, "gems": 8 }, + { "gold": 1000, "wood" : 10, "mercury": 10, "ore": 10, "sulfur": 10, "crystal": 10, "gems": 10 } + ], + // How much researchs/skips per day are possible? (array index is spell tier) + "spellResearchPerDay": [ 2, 2, 2, 2, 1 ], + // Exponent for increasing cost for each research (factor 1 disables this; array index is spell tier) + "spellResearchCostExponentPerResearch": [ 1.25, 1.25, 1.25, 1.25, 1.25 ] }, "combat": @@ -472,6 +488,20 @@ // if enabled flying will work like in original game, otherwise nerf similar to HotA flying is applied "originalFlyRules" : true }, + + "resources" : { + // H3 mechanics - AI receives bonus (or malus, on easy) to his resource income + // AI will receive specified values as percentage of his weekly income + // So, "gems" : 200 will give AI player 200% of his daily income of gems over week, or, in other words, + // giving AI player 2 additional gems per week for every owned Gem Pond + "weeklyBonusesAI" : { + "pawn" : { "gold" : -175 }, + "knight": {}, + "rook" : {}, + "queen" : { "wood" : 275 , "mercury" : 100, "ore" : 275, "sulfur" : 100, "crystal" : 100, "gems" : 100, "gold" : 175}, + "king" : { "wood" : 375 , "mercury" : 200, "ore" : 375, "sulfur" : 200, "crystal" : 200, "gems" : 200, "gold" : 350} + } + }, "spells": { @@ -555,6 +585,22 @@ "valueType" : "BASE_NUMBER" } } + }, + + "interface" : + { + // Color transform to make color of brown DIBOX.PCX texture match color of specified player + "playerColoredBackground" : + { + "red" : [ 0.25, 0, 0, 1.25, 0.00, 0.00 ], + "blue" : [ 0, 0, 0, 0.45, 1.20, 4.50 ], + "tan" : [ 0.40, 0.27, 0.23, 1.10, 1.20, 1.15 ], + "green" : [ -0.27, 0.10, -0.27, 0.70, 1.70, 0.70 ], + "orange" : [ 0.47, 0.17, -0.27, 1.60, 1.20, 0.70 ], + "purple" : [ 0.12, -0.1, 0.25, 1.15, 1.20, 2.20 ], + "teal" : [ -0.13, 0.23, 0.23, 0.90, 1.20, 2.20 ], + "pink" : [ 0.44, 0.15, 0.25, 1.00, 1.00, 1.75 ] + } } } } diff --git a/config/heroClasses.json b/config/heroClasses.json index 6fa79b64a..f4e8d2116 100644 --- a/config/heroClasses.json +++ b/config/heroClasses.json @@ -106,7 +106,7 @@ "defaultTavern" : 5, "affinity" : "might", "commander" : "medusaQueen", - "mapObject" : { "templates" : { "default" : { "animation" : "AH11_.def", "editorAnimation": "AH11_E.def" } } }, + "mapObject" : { "templates" : { "default" : { "animation" : "AH10_.def", "editorAnimation": "AH10_E.def" } } }, "animation": { "battle" : { "male" : "CH010.DEF", "female" : "CH11.DEF" } } }, "warlock" : @@ -116,7 +116,7 @@ "defaultTavern" : 5, "affinity" : "magic", "commander" : "medusaQueen", - "mapObject" : { "templates" : { "default" : { "animation" : "AH10_.def", "editorAnimation": "AH10_E.def" } } }, + "mapObject" : { "templates" : { "default" : { "animation" : "AH11_.def", "editorAnimation": "AH11_E.def" } } }, "animation": { "battle" : { "male" : "CH010.DEF", "female" : "CH11.DEF" } } }, "barbarian" : diff --git a/config/heroes/dungeon.json b/config/heroes/dungeon.json index 04fb2d3c4..44f3b245d 100644 --- a/config/heroes/dungeon.json +++ b/config/heroes/dungeon.json @@ -198,7 +198,7 @@ { "index": 91, "class" : "warlock", - "female": true, + "female": false, "spellbook": [ "resurrection" ], "skills": [ diff --git a/config/heroes/special.json b/config/heroes/special.json index b6b945461..d6846093f 100644 --- a/config/heroes/special.json +++ b/config/heroes/special.json @@ -176,7 +176,7 @@ "mutare": { "index": 151, - "class" : "warlock", + "class" : "overlord", "female": true, "special" : true, "spellbook": [ "magicArrow" ], @@ -220,7 +220,7 @@ "mutareDrake": { "index": 153, - "class" : "warlock", + "class" : "overlord", "female": true, "special" : true, "spellbook": [ "magicArrow" ], diff --git a/config/objects/creatureBanks.json b/config/objects/creatureBanks.json index 8624e8e4d..dc69e117b 100644 --- a/config/objects/creatureBanks.json +++ b/config/objects/creatureBanks.json @@ -261,7 +261,8 @@ } ] }, - "inpCache" : { + "impCache" : { + "compatibilityIdentifiers" : [ "inpCache" ], "index" : 3, "name" : "Imp Cache", "aiValue" : 1500, diff --git a/config/objects/generic.json b/config/objects/generic.json index 23240bbd5..3be342df6 100644 --- a/config/objects/generic.json +++ b/config/objects/generic.json @@ -18,115 +18,6 @@ } }, - "altarOfSacrifice" : { - "index" :2, - "handler" : "market", - "base" : { - "sounds" : { - "visit" : ["MYSTERY"] - } - }, - "types" : { - "object" : { - "index" : 0, - "aiValue" : 100, - "rmg" : { - "zoneLimit" : 1, - "value" : 100, - "rarity" : 20 - }, - "modes" : ["creature-experience", "artifact-experience"] - } - } - }, - "tradingPost" : { - "index" :221, - "handler" : "market", - "base" : { - "sounds" : { - "ambient" : ["LOOPMARK"], - "visit" : ["STORE"] - } - }, - "types" : { - "object" : { - "index" : 0, - "aiValue" : 100, - "rmg" : { - "zoneLimit" : 1, - "value" : 100, - "rarity" : 100 - }, - "modes" : ["resource-resource", "resource-player"], - "efficiency" : 5, - "title" : "core.genrltxt.159" - } - } - }, - "tradingPostDUPLICATE" : { - "index" :99, - "handler" : "market", - "base" : { - "sounds" : { - "ambient" : ["LOOPMARK"], - "visit" : ["STORE"] - } - }, - "types" : { - "object" : { - "index" : 0, - "aiValue" : 100, - "rmg" : { - "zoneLimit" : 1, - "value" : 100, - "rarity" : 100 - }, - "modes" : ["resource-resource", "resource-player"], - "efficiency" : 5, - "title" : "core.genrltxt.159" - } - } - }, - "freelancersGuild" : { - "index" :213, - "handler" : "market", - "types" : { - "object" : { - "index" : 0, - "aiValue" : 100, - "rmg" : { - "zoneLimit" : 1, - "value" : 100, - "rarity" : 100 - }, - "modes" : ["creature-resource"] - } - } - }, - - "blackMarket" : { - "index" :7, - "handler" : "market", - "base" : { - "sounds" : { - "ambient" : ["LOOPMARK"], - "visit" : ["MYSTERY"] - } - }, - "types" : { - "object" : { - "index" : 0, - "aiValue" : 8000, - "rmg" : { - "value" : 8000, - "rarity" : 20 - }, - "modes" : ["resource-artifact"], - "title" : "core.genrltxt.349" - } - } - }, - "pandoraBox" : { "index" :6, "handler" : "pandora", @@ -222,7 +113,8 @@ "sounds" : { "ambient" : ["LOOPFACT"], "visit" : ["MILITARY"] - } + }, + "creatures": [ ["ballista"], ["firstAidTent"], ["ammoCart"] ] }, "types" : { "object" : { @@ -270,23 +162,6 @@ } } }, - "lighthouse" : { - "index" :42, - "handler" : "lighthouse", - "base" : { - "sounds" : { - "visit" : ["LIGHTHOUSE"] - } - }, - "types" : { - "object" : { - "index" : 0, - "aiValue" : 500, - "rmg" : { - } - } - } - }, "obelisk" : { "index" :57, "handler" : "obelisk", @@ -409,35 +284,6 @@ } } }, - "university" : { - "index" :104, - "handler" : "market", - "base" : { - "sounds" : { - "visit" : ["GAZEBO"] - } - }, - "types" : { - "object" : { - "index" : 0, - "aiValue" : 2500, - "rmg" : { - "value" : 2500, - "rarity" : 20 - }, - "modes" : ["resource-skill"], - "title" : "core.genrltxt.602", - "speech" : "core.genrltxt.603", - "offer": - [ - { "noneOf" : ["necromancy"] }, - { "noneOf" : ["necromancy"] }, - { "noneOf" : ["necromancy"] }, - { "noneOf" : ["necromancy"] } - ] - } - } - }, "questGuard" : { "index" :215, "handler" : "questGuard", @@ -683,6 +529,7 @@ "object" : { "index" : 0, "aiValue" : 7000, + "description" : "", "rmg" : { "zoneLimit" : 1, "value" : 7000, diff --git a/config/objects/lighthouse.json b/config/objects/lighthouse.json new file mode 100644 index 000000000..cecabfc0b --- /dev/null +++ b/config/objects/lighthouse.json @@ -0,0 +1,29 @@ +{ + "lighthouse" : { + "index" :42, + "handler" : "flaggable", + "base" : { + "sounds" : { + "visit" : ["LIGHTHOUSE"] + } + }, + "types" : { + "lighthouse" : { + "compatibilityIdentifiers" : [ "object" ], + "index" : 0, + "aiValue" : 500, + "rmg" : { + }, + + "message" : "@core.advevent.69", + "bonuses" : { + "seaMovement" : { + "type" : "MOVEMENT", + "subtype" : "heroMovementSea", + "val" : 500 + } + } + } + } + } +} \ No newline at end of file diff --git a/config/objects/markets.json b/config/objects/markets.json new file mode 100644 index 000000000..ea5c517b9 --- /dev/null +++ b/config/objects/markets.json @@ -0,0 +1,138 @@ +{ + "altarOfSacrifice" : { + "index" :2, + "handler" : "market", + "base" : { + "sounds" : { + "visit" : ["MYSTERY"] + } + }, + "types" : { + "object" : { + "index" : 0, + "aiValue" : 100, + "rmg" : { + "zoneLimit" : 1, + "value" : 100, + "rarity" : 20 + }, + "modes" : ["creature-experience", "artifact-experience"] + } + } + }, + + "tradingPost" : { + "index" :221, + "handler" : "market", + "base" : { + "sounds" : { + "ambient" : ["LOOPMARK"], + "visit" : ["STORE"] + } + }, + "types" : { + "object" : { + "index" : 0, + "aiValue" : 100, + "rmg" : { + "zoneLimit" : 1, + "value" : 100, + "rarity" : 100 + }, + "modes" : ["resource-resource", "resource-player"], + "efficiency" : 5 + } + } + }, + + "tradingPostDUPLICATE" : { + "index" :99, + "handler" : "market", + "base" : { + "sounds" : { + "ambient" : ["LOOPMARK"], + "visit" : ["STORE"] + } + }, + "types" : { + "object" : { + "index" : 0, + "aiValue" : 100, + "rmg" : { + "zoneLimit" : 1, + "value" : 100, + "rarity" : 100 + }, + "modes" : ["resource-resource", "resource-player"], + "efficiency" : 5 + } + } + }, + + "freelancersGuild" : { + "index" :213, + "handler" : "market", + "types" : { + "object" : { + "index" : 0, + "aiValue" : 100, + "rmg" : { + "zoneLimit" : 1, + "value" : 100, + "rarity" : 100 + }, + "modes" : ["creature-resource"] + } + } + }, + + "blackMarket" : { + "index" :7, + "handler" : "market", + "base" : { + "sounds" : { + "ambient" : ["LOOPMARK"], + "visit" : ["MYSTERY"] + } + }, + "types" : { + "object" : { + "index" : 0, + "aiValue" : 8000, + "rmg" : { + "value" : 8000, + "rarity" : 20 + }, + "modes" : ["resource-artifact"] + } + } + }, + "university" : { + "index" :104, + "handler" : "market", + "base" : { + "sounds" : { + "visit" : ["GAZEBO"] + } + }, + "types" : { + "object" : { + "index" : 0, + "aiValue" : 2500, + "rmg" : { + "value" : 2500, + "rarity" : 20 + }, + "modes" : ["resource-skill"], + "speech" : "@core.genrltxt.603", + "offer": + [ + { "noneOf" : ["necromancy"] }, + { "noneOf" : ["necromancy"] }, + { "noneOf" : ["necromancy"] }, + { "noneOf" : ["necromancy"] } + ] + } + } + } +} \ No newline at end of file diff --git a/config/objects/pyramid.json b/config/objects/pyramid.json index cc8b06a93..e1efd7b85 100644 --- a/config/objects/pyramid.json +++ b/config/objects/pyramid.json @@ -46,7 +46,7 @@ "spells" : [ "@gainedSpell" ], - "message" : [ 106, "%s." ], // Upon defeating monsters, you learn new spell + "message" : [ 106, "{%s}." ], // Upon defeating monsters, you learn new spell "guards" : [ { "amount" : 40, "type" : "goldGolem" }, { "amount" : 10, "type" : "diamondGolem" }, @@ -63,10 +63,10 @@ } ] }, - "message" : [ 106, "%s.", 108 ] // No Wisdom + "message" : [ 106, "{%s}. ", 108 ] // No Wisdom }, { - "message" : [ 106, "%s.", 109 ] // No spellbook + "message" : [ 106, "{%s}. ", 109 ] // No spellbook } ] diff --git a/config/objects/shrine.json b/config/objects/shrine.json index 44bd2a952..8216e9874 100644 --- a/config/objects/shrine.json +++ b/config/objects/shrine.json @@ -46,10 +46,10 @@ "@gainedSpell" ], "description" : "@core.genrltxt.355", - "message" : [ 127, "%s." ] // You learn new spell + "message" : [ 127, "{%s}." ] // You learn new spell } ], - "onVisitedMessage" : [ 127, "%s.", 174 ], // You already known this spell + "onVisitedMessage" : [ 127, "{%s}. ", 174 ], // You already known this spell "onEmpty" : [ { "limiter" : { @@ -59,10 +59,10 @@ } ] }, - "message" : [ 127, "%s.", 130 ] // No Wisdom + "message" : [ 127, "{%s}. ", 130 ] // No Wisdom }, { - "message" : [ 127, "%s.", 131 ] // No spellbook + "message" : [ 127, "{%s}. ", 131 ] // No spellbook } ] } @@ -115,10 +115,10 @@ "@gainedSpell" ], "description" : "@core.genrltxt.355", - "message" : [ 128, "%s." ] // You learn new spell + "message" : [ 128, "{%s}." ] // You learn new spell } ], - "onVisitedMessage" : [ 128, "%s.", 174 ], // You already known this spell + "onVisitedMessage" : [ 128, "{%s}. ", 174 ], // You already known this spell "onEmpty" : [ { "limiter" : { @@ -128,10 +128,10 @@ } ] }, - "message" : [ 128, "%s.", 130 ] // No Wisdom + "message" : [ 128, "{%s}. ", 130 ] // No Wisdom }, { - "message" : [ 128, "%s.", 131 ] // No spellbook + "message" : [ 128, "{%s}. ", 131 ] // No spellbook } ] } @@ -184,10 +184,10 @@ "@gainedSpell" ], "description" : "@core.genrltxt.355", - "message" : [ 129, "%s." ] // You learn new spell + "message" : [ 129, "{%s}." ] // You learn new spell } ], - "onVisitedMessage" : [ 129, "%s.", 174 ], // You already known this spell + "onVisitedMessage" : [ 129, "{%s}. ", 174 ], // You already known this spell "onEmpty" : [ { "limiter" : { @@ -197,10 +197,10 @@ } ] }, - "message" : [ 129, "%s.", 130 ] // No Wisdom + "message" : [ 129, "{%s}. ", 130 ] // No Wisdom }, { - "message" : [ 129, "%s.", 131 ] // No spellbook + "message" : [ 129, "{%s}. ", 131 ] // No spellbook } ] } diff --git a/config/obstacles.json b/config/obstacles.json index 52c8de672..b9d5bc57d 100644 --- a/config/obstacles.json +++ b/config/obstacles.json @@ -538,7 +538,7 @@ }, "55": { - "allowedTerrains" : ["water"], + "specialBattlefields" : ["ship"], "width" : 3, "height" : 3, "blockedTiles" : [-15, -16, -33], diff --git a/config/schemas/artifact.json b/config/schemas/artifact.json index ab335a5ca..7143ddecc 100644 --- a/config/schemas/artifact.json +++ b/config/schemas/artifact.json @@ -61,6 +61,10 @@ "description" : "Optional, list of components for combinational artifacts", "items" : { "type" : "string" } }, + "fusedComponents" : { + "type" : "boolean", + "description" : "Used together with components fild. Marks the artifact as fused. Cannot be disassembled." + }, "bonuses" : { "type" : "array", "description" : "Bonuses provided by this artifact using bonus system", diff --git a/config/schemas/flaggable.json b/config/schemas/flaggable.json new file mode 100644 index 000000000..534c9297f --- /dev/null +++ b/config/schemas/flaggable.json @@ -0,0 +1,64 @@ +{ + "type" : "object", + "$schema" : "http://json-schema.org/draft-04/schema", + "title" : "VCMI map object format", + "description" : "Description of map object class", + "required" : [ "message" ], + "anyOf" : [ //NOTE: strictly speaking, not required - buidling can function without it, but won't do anythin + { + "required" : [ "bonuses" ] + }, + { + "required" : [ "dailyIncome" ] + } + ], + "additionalProperties" : false, + + "properties" : { + "bonuses" : { + "type" : "object", + "description" : "List of bonuses provided by this map object. See bonus format for more details", + "additionalProperties" : { "$ref" : "bonus.json" } + }, + + "message" : { + "description" : "Message that will be shown to player on capturing this object", + "anyOf" : [ + { + "type" : "string", + }, + { + "type" : "number", + } + ] + }, + + "dailyIncome" : { + "type" : "object", + "additionalProperties" : false, + "description" : "Daily income that this building provides to owner, if any", + "properties" : { + "gold" : { "type" : "number"}, + "wood" : { "type" : "number"}, + "ore" : { "type" : "number"}, + "mercury" : { "type" : "number"}, + "sulfur" : { "type" : "number"}, + "crystal" : { "type" : "number"}, + "gems" : { "type" : "number"} + } + }, + + // Properties that might appear since this node is shared with object config + "compatibilityIdentifiers" : { }, + "blockedVisitable" : { }, + "removable" : { }, + "aiValue" : { }, + "index" : { }, + "base" : { }, + "name" : { }, + "rmg" : { }, + "templates" : { }, + "battleground" : { }, + "sounds" : { } + } +} diff --git a/config/schemas/gameSettings.json b/config/schemas/gameSettings.json index 0eec56189..9d1b55b1e 100644 --- a/config/schemas/gameSettings.json +++ b/config/schemas/gameSettings.json @@ -51,8 +51,12 @@ "type" : "object", "additionalProperties" : false, "properties" : { - "buildingsPerTurnCap" : { "type" : "number" }, - "startingDwellingChances" : { "type" : "array" } + "buildingsPerTurnCap" : { "type" : "number" }, + "startingDwellingChances" : { "type" : "array" }, + "spellResearch" : { "type" : "boolean" }, + "spellResearchCost" : { "type" : "array" }, + "spellResearchPerDay" : { "type" : "array" }, + "spellResearchCostExponentPerResearch" : { "type" : "array" } } }, "combat": { @@ -129,6 +133,14 @@ "originalFlyRules" : { "type" : "boolean" } } }, + "resources": { + "type" : "object", + "additionalProperties" : false, + "properties" : { + "weeklyBonusesAI" : { "type" : "object" } + } + }, + "spells": { "type" : "object", "additionalProperties" : false, @@ -148,5 +160,12 @@ "perHero" : { "type" : "object" } } }, + "interface": { + "type" : "object", + "additionalProperties" : false, + "properties" : { + "playerColoredBackground" : { "type" : "object" } + } + } } } diff --git a/config/schemas/hero.json b/config/schemas/hero.json index 8d533387b..c1ccc548a 100644 --- a/config/schemas/hero.json +++ b/config/schemas/hero.json @@ -4,7 +4,7 @@ "title" : "VCMI hero format", "description" : "Format used to define new heroes in VCMI", "required" : [ "class", "army", "skills", "texts" ], - "oneOf" : [ + "anyOf" : [ { "required" : [ "images" ] }, diff --git a/config/schemas/heroClass.json b/config/schemas/heroClass.json index b6f5db342..93a8607a8 100644 --- a/config/schemas/heroClass.json +++ b/config/schemas/heroClass.json @@ -60,9 +60,7 @@ "properties" : { "filters" : { "type" : "object", - "additionalProperties" : { - "type" : "array" - } + "additionalProperties" : true } } }, diff --git a/config/schemas/market.json b/config/schemas/market.json new file mode 100644 index 000000000..7554b453a --- /dev/null +++ b/config/schemas/market.json @@ -0,0 +1,50 @@ +{ + "type" : "object", + "$schema" : "http://json-schema.org/draft-04/schema", + "title" : "VCMI map object format", + "description" : "Description of map object class", + "required" : [ "modes" ], + + "additionalProperties" : false, + + "properties" : { + "description" : { + "description" : "Message that will be shown on right-clicking this object", + "type" : "string" + }, + + "speech" : { + "description" : "Message that will be shown to player on visiting this object", + "type" : "string" + }, + + "modes" : { + "type" : "array", + "items" : { + "enum" : [ "resource-resource", "resource-player", "creature-resource", "resource-artifact", "artifact-resource", "artifact-experience", "creature-experience", "creature-undead", "resource-skill" ], + "type" : "string" + } + }, + "efficiency" : { + "type" : "number", + "minimum" : 1, + "maximum" : 9 + }, + "offer" : { + "type" : "array" + }, + + // Properties that might appear since this node is shared with object config + "compatibilityIdentifiers" : { }, + "blockedVisitable" : { }, + "removable" : { }, + "aiValue" : { }, + "index" : { }, + "base" : { }, + "name" : { }, + "rmg" : { }, + "templates" : { }, + "battleground" : { }, + "sounds" : { } + } +} diff --git a/config/schemas/mod.json b/config/schemas/mod.json index 5c09ef74c..d98cb4ab3 100644 --- a/config/schemas/mod.json +++ b/config/schemas/mod.json @@ -5,6 +5,17 @@ "description" : "Format used to define main mod file (mod.json) in VCMI", "required" : [ "name", "description", "modType", "version", "author", "contact" ], "definitions" : { + "fileListOrObject" : { + "oneOf" : [ + { + "type" : "array", + "items" : { "type" : "string", "format" : "textFile" } + }, + { + "type" : "object" + } + ] + }, "localizable" : { "type" : "object", "additionalProperties" : false, @@ -35,9 +46,8 @@ "description" : "If set to true, vcmi will skip validation of current translation json files" }, "translations" : { - "type" : "array", "description" : "List of files with translations for this language", - "items" : { "type" : "string", "format" : "textFile" } + "$ref" : "#/definitions/fileListOrObject" } } } @@ -112,6 +122,11 @@ "description" : "List of mods that are required to run this one", "items" : { "type" : "string" } }, + "softDepends" : { + "type" : "array", + "description" : "List of mods if they are enabled, should be loaded before this one. This mod will overwrite any conflicting items from its soft dependency mods", + "items" : { "type" : "string" } + }, "conflicts" : { "type" : "array", "description" : "List of mods that can't be enabled in the same time as this one", @@ -122,9 +137,17 @@ "description" : "If set to true, mod will not be enabled automatically on install" }, "settings" : { - "type" : "object", "description" : "List of changed game settings by mod", - "$ref" : "gameSettings.json" + "oneOf" : [ + { + "type" : "object", + "$ref" : "gameSettings.json" + }, + { + "type" : "array", + "items" : { "type" : "string", "format" : "textFile" } + }, + ] }, "filesystem" : { "type" : "object", @@ -206,94 +229,76 @@ "$ref" : "#/definitions/localizable" }, "translations" : { - "type" : "array", "description" : "List of files with translations for this language", - "items" : { "type" : "string", "format" : "textFile" } + "$ref" : "#/definitions/fileListOrObject" }, "factions" : { - "type" : "array", "description" : "List of configuration files for towns/factions", - "items" : { "type" : "string", "format" : "textFile" } + "$ref" : "#/definitions/fileListOrObject" }, "heroClasses" : { - "type" : "array", "description" : "List of configuration files for hero classes", - "items" : { "type" : "string", "format" : "textFile" } + "$ref" : "#/definitions/fileListOrObject" }, "heroes" : { - "type" : "array", "description" : "List of configuration files for heroes", - "items" : { "type" : "string", "format" : "textFile" } + "$ref" : "#/definitions/fileListOrObject" }, "skills" : { - "type" : "array", "description" : "List of configuration files for skills", - "items" : { "type" : "string", "format" : "textFile" } + "$ref" : "#/definitions/fileListOrObject" }, "creatures" : { - "type" : "array", "description" : "List of configuration files for creatures", - "items" : { "type" : "string", "format" : "textFile" } + "$ref" : "#/definitions/fileListOrObject" }, "artifacts" : { - "type" : "array", "description" : "List of configuration files for artifacts", - "items" : { "type" : "string", "format" : "textFile" } + "$ref" : "#/definitions/fileListOrObject" }, "spells" : { - "type" : "array", "description" : "List of configuration files for spells", - "items" : { "type" : "string", "format" : "textFile" } + "$ref" : "#/definitions/fileListOrObject" }, "objects" : { - "type" : "array", "description" : "List of configuration files for objects", - "items" : { "type" : "string", "format" : "textFile" } + "$ref" : "#/definitions/fileListOrObject" }, "biomes" : { - "type" : "array", "description" : "List of configuration files for biomes", - "items" : { "type" : "string", "format" : "textFile" } + "$ref" : "#/definitions/fileListOrObject" }, "bonuses" : { - "type" : "array", "description" : "List of configuration files for bonuses", - "items" : { "type" : "string", "format" : "textFile" } + "$ref" : "#/definitions/fileListOrObject" }, "terrains" : { - "type" : "array", "description" : "List of configuration files for terrains", - "items" : { "type" : "string", "format" : "textFile" } + "$ref" : "#/definitions/fileListOrObject" }, "roads" : { - "type" : "array", "description" : "List of configuration files for roads", - "items" : { "type" : "string", "format" : "textFile" } + "$ref" : "#/definitions/fileListOrObject" }, "rivers" : { - "type" : "array", "description" : "List of configuration files for rivers", - "items" : { "type" : "string", "format" : "textFile" } + "$ref" : "#/definitions/fileListOrObject" }, "battlefields" : { - "type" : "array", "description" : "List of configuration files for battlefields", - "items" : { "type" : "string", "format" : "textFile" } + "$ref" : "#/definitions/fileListOrObject" }, "obstacles" : { - "type" : "array", "description" : "List of configuration files for obstacles", - "items" : { "type" : "string", "format" : "textFile" } + "$ref" : "#/definitions/fileListOrObject" }, "templates" : { - "type" : "array", "description" : "List of configuration files for RMG templates", - "items" : { "type" : "string", "format" : "textFile" } + "$ref" : "#/definitions/fileListOrObject" }, "scripts" : { - "type" : "array", "description" : "List of configuration files for scripts", - "items" : { "type" : "string", "format" : "textFile" } + "$ref" : "#/definitions/fileListOrObject" } } } diff --git a/config/schemas/object.json b/config/schemas/object.json index f74dffce3..59d913440 100644 --- a/config/schemas/object.json +++ b/config/schemas/object.json @@ -21,7 +21,7 @@ "enum" : [ "configurable", "dwelling", "hero", "town", "boat", "market", "hillFort", "shipyard", "monster", "resource", "static", "randomArtifact", "randomHero", "randomResource", "randomTown", "randomMonster", "randomDwelling", "generic", "artifact", "borderGate", "borderGuard", "denOfThieves", - "event", "garrison", "heroPlaceholder", "keymaster", "lighthouse", "magi", "mine", "obelisk", "pandora", "prison", "questGuard", "seerHut", "sign", + "event", "garrison", "heroPlaceholder", "keymaster", "flaggable", "magi", "mine", "obelisk", "pandora", "prison", "questGuard", "seerHut", "sign", "siren", "monolith", "subterraneanGate", "whirlpool", "terrain" ] }, diff --git a/config/schemas/settings.json b/config/schemas/settings.json index 3cc99dc28..94c95923f 100644 --- a/config/schemas/settings.json +++ b/config/schemas/settings.json @@ -3,7 +3,7 @@ { "type" : "object", "$schema" : "http://json-schema.org/draft-04/schema", - "required" : [ "general", "video", "adventure", "battle", "input", "server", "logging", "launcher", "lobby", "gameTweaks" ], + "required" : [ "general", "video", "adventure", "battle", "input", "server", "logging", "launcher", "lobby", "gameTweaks", "mods" ], "definitions" : { "logLevelEnum" : { "type" : "string", @@ -149,6 +149,23 @@ } } }, + + "mods" : { + "type" : "object", + "additionalProperties" : false, + "default" : {}, + "required" : [ + "validation" + ], + "properties" : { + "validation" : { + "type" : "string", + "enum" : [ "off", "basic", "full" ], + "default" : "basic" + } + } + }, + "video" : { "type" : "object", "additionalProperties" : false, @@ -167,6 +184,7 @@ "targetfps", "vsync", "fontsType", + "cursorScalingFactor", "fontScalingFactor", "upscalingFilter", "fontUpscalingFilter", @@ -178,22 +196,19 @@ "additionalProperties" : false, "required" : [ "width", "height", "scaling" ], "properties" : { - "width" : { "type" : "number" }, - "height" : { "type" : "number" }, - "scaling" : { "type" : "number" } - }, - "defaultIOS" : {"width" : 800, "height" : 600, "scaling" : 200 }, - "defaultAndroid" : {"width" : 800, "height" : 600, "scaling" : 200 }, - "default" : {"width" : 800, "height" : 600, "scaling" : 100 } + "width" : { "type" : "number", "default" : 1280 }, + "height" : { "type" : "number", "default" : 720 }, + "scaling" : { "type" : "number", "default" : 0 } + } }, "reservedWidth" : { "type" : "number", - "defaultIOS" : 0.1, // iOS camera cutout / notch is excluded from available area by SDL + "defaultIOS" : 0.1, // iOS camera cutout / notch is not excluded from available area by SDL, handle it this way "default" : 0 }, "fullscreen" : { "type" : "boolean", - "default" : false + "default" : true }, "realFullscreen" : { "type" : "boolean", @@ -239,6 +254,10 @@ "enum" : [ "auto", "original", "scalable" ], "default" : "auto" }, + "cursorScalingFactor" : { + "type" : "number", + "default" : 1 + }, "fontScalingFactor" : { "type" : "number", "default" : 1 @@ -509,7 +528,7 @@ }, "neutralAI" : { "type" : "string", - "default" : "StupidAI" + "default" : "BattleAI" }, "enemyAI" : { "type" : "string", @@ -601,7 +620,6 @@ "defaultRepositoryURL", "extraRepositoryURL", "extraRepositoryEnabled", - "enableInstalledMods", "autoCheckRepositories", "ignoreSslErrors", "updateOnStartup", @@ -628,10 +646,6 @@ "type" : "boolean", "default" : false }, - "enableInstalledMods" : { - "type" : "boolean", - "default" : true - }, "ignoreSslErrors" : { "type" : "boolean", "default" : false diff --git a/config/schemas/skill.json b/config/schemas/skill.json index 162f84de6..2332f0eaf 100644 --- a/config/schemas/skill.json +++ b/config/schemas/skill.json @@ -94,5 +94,9 @@ "onlyOnWaterMap" : { "type" : "boolean", "description" : "It true, skill won't be available on a map without water" + }, + "special" : { + "type" : "boolean", + "description" : "If true, skill is not available on maps at random" } } diff --git a/config/schemas/spell.json b/config/schemas/spell.json index d4b00487b..816595068 100644 --- a/config/schemas/spell.json +++ b/config/schemas/spell.json @@ -22,7 +22,8 @@ "properties" : { "verticalPosition" : {"type" : "string", "enum" :["top","bottom"]}, "defName" : {"type" : "string", "format" : "animationFile"}, - "effectName" : { "type" : "string" } + "effectName" : { "type" : "string" }, + "transparency" : {"type" : "number", "minimum" : 0, "maximum" : 1} }, "additionalProperties" : false } @@ -171,6 +172,10 @@ "type" : "boolean", "description" : "If used as creature spell, unit can cast this spell on itself" }, + "canCastOnlyOnSelf" : { + "type" : "boolean", + "description" : "If used as creature spell, unit can cast this spell only on itself" + }, "canCastWithoutSkip" : { "type" : "boolean", "description" : "If used the creature will not skip the turn after casting a spell." diff --git a/config/schemas/template.json b/config/schemas/template.json index fc185815c..4ed948449 100644 --- a/config/schemas/template.json +++ b/config/schemas/template.json @@ -12,7 +12,7 @@ "properties" : { "type" : { "type" : "string", - "enum" : ["playerStart", "cpuStart", "treasure", "junction"] + "enum" : ["playerStart", "cpuStart", "treasure", "junction", "sealed"] }, "size" : { "type" : "number", "minimum" : 1 }, "owner" : {}, @@ -22,6 +22,7 @@ "minesLikeZone" : { "type" : "number" }, "terrainTypeLikeZone" : { "type" : "number" }, "treasureLikeZone" : { "type" : "number" }, + "customObjectsLikeZone" : { "type" : "number" }, "terrainTypes": {"$ref" : "#/definitions/stringArray"}, "bannedTerrains": {"$ref" : "#/definitions/stringArray"}, @@ -49,6 +50,51 @@ }, "additionalProperties" : false } + }, + "customObjects" : { + "type" : "object", + "properties": { + "bannedCategories": { + "type": "array", + "items": { + "type": "string", + "enum": ["all", "dwelling", "creatureBank", "randomArtifact", "bonus", "resource", "resourceGenerator", "spellScroll", "pandorasBox", "questArtifact", "seerHut"] + } + }, + "bannedObjects": { + "type": "array", + "items": { + "type": "string" + } + }, + "commonObjects": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "rmg": { + "type": "object", + "properties": { + "value": { + "type": "integer" + }, + "rarity": { + "type": "integer" + }, + "zoneLimit": { + "type": "integer" + } + }, + "required": ["value", "rarity"] + } + }, + "required": ["id", "rmg"] + } + } + } } } }, diff --git a/config/shortcutsConfig.json b/config/shortcutsConfig.json index 41ee66f22..e6c91b12c 100644 --- a/config/shortcutsConfig.json +++ b/config/shortcutsConfig.json @@ -56,6 +56,8 @@ "adventureZoomIn": "Keypad +", "adventureZoomOut": "Keypad -", "adventureZoomReset": "Backspace", + "adventureSearch": "Ctrl+F", + "adventureSearchContinue": "Alt+F", "battleAutocombat": "A", "battleAutocombatEnd": "Q", "battleCastSpell": "C", diff --git a/config/spells/ability.json b/config/spells/ability.json index d9e945722..4364312c1 100644 --- a/config/spells/ability.json +++ b/config/spells/ability.json @@ -88,6 +88,7 @@ "targetCondition" : { "noneOf" : { "bonus.NON_LIVING" : "absolute", + "bonus.MECHANICAL" : "absolute", "bonus.SIEGE_WEAPON" : "absolute", "bonus.UNDEAD" : "absolute", "bonus.GARGOYLE" : "absolute" @@ -159,6 +160,7 @@ "targetCondition" : { "noneOf" : { "bonus.NON_LIVING" : "normal", + "bonus.MECHANICAL" : "normal", "bonus.SIEGE_WEAPON" : "absolute", "bonus.UNDEAD" : "normal" } @@ -238,6 +240,7 @@ "targetCondition" : { "noneOf" : { "bonus.NON_LIVING" : "absolute", + "bonus.MECHANICAL" : "absolute", "bonus.SIEGE_WEAPON" : "absolute", "bonus.UNDEAD" : "absolute", "bonus.GARGOYLE" : "absolute" @@ -249,7 +252,7 @@ "targetType": "NO_TARGET", "animation":{ - "hit":["SP04_"] + "hit":[{ "defName" : "SP04_", "transparency" : 0.5}] }, "sounds": { "cast": "DEATHCLD" @@ -270,6 +273,7 @@ "targetCondition" : { "noneOf" : { "bonus.NON_LIVING" : "absolute", + "bonus.MECHANICAL" : "absolute", "bonus.SIEGE_WEAPON" : "absolute", "bonus.UNDEAD" : "absolute", "bonus.GARGOYLE" : "absolute" @@ -357,6 +361,7 @@ "targetCondition" : { "noneOf" : { "bonus.NON_LIVING" : "absolute", + "bonus.MECHANICAL" : "absolute", "bonus.SIEGE_WEAPON" : "absolute", "bonus.UNDEAD" : "absolute", "bonus.GARGOYLE" : "absolute" diff --git a/config/spells/moats.json b/config/spells/moats.json index 71eea7fe8..5b864d9d2 100644 --- a/config/spells/moats.json +++ b/config/spells/moats.json @@ -1,732 +1,732 @@ { - "castleMoatTrigger" : - { - "targetType" : "CREATURE", - "type": "ability", - "name": "Moat", - "school": {}, - "level": 0, - "power": 0, - "gainChance": {}, - "levels" : { - "base": { - "power" : 0, - "range" : "0", - "description" : "", //For validation - "cost" : 0, //For validation - "aiValue" : 0, //For validation - "battleEffects" : { - "directDamage" : { - "type":"core:damage" - } - }, - "targetModifier":{"smart":false} - }, - "none" : { - }, - "basic" : { - }, - "advanced" : { - }, - "expert" : { - } - }, - "flags" : { - "damage": true, - "negative": true, - "nonMagical" : true, - "special": true - }, - "targetCondition" : { - } - }, - "castleMoat": { - "targetType" : "NO_TARGET", - "type": "ability", - "name": "Moat", - "school" : {}, - "level": 0, - "power": 0, - "defaultGainChance": 0, - "gainChance": {}, - "levels" : { - "base":{ - "description" : "", - "aiValue" : 0, - "power" : 0, - "cost" : 0, - "targetModifier":{"smart":false}, - "battleEffects":{ - "moat":{ - "type":"core:moat", - "hidden" : false, - "trap" : true, - "triggerAbility" : "core:castleMoatTrigger", - "dispellable" : false, - "removeOnTrigger" : false, - "moatDamage" : 70, - "moatHexes" : [[11, 28, 44, 61, 77, 111, 129, 146, 164, 181]], - "defender" :{ - }, - "bonus" :{ - "primarySkill" : { - "val" : -3, - "type" : "PRIMARY_SKILL", - "subtype" : "primarySkill.defence", - "valueType" : "ADDITIVE_VALUE" - } - } - } - }, - "range" : "X" - }, - "none" :{ - }, - "basic" :{ - }, - "advanced" :{ - }, - "expert" :{ - } - }, - "flags" : { - "nonMagical" : true, - "indifferent": true - }, - "targetCondition" : { - } - }, - "rampartMoatTrigger" : - { - "targetType" : "CREATURE", - "type": "ability", - "name": "Brambles", - "school": {}, - "level": 0, - "power": 0, - "gainChance": {}, - "levels" : { - "base": { - "power" : 0, - "range" : "0", - "description" : "", //For validation - "cost" : 0, //For validation - "aiValue" : 0, //For validation - "battleEffects" : { - "directDamage" : { - "type":"core:damage" - } - }, - "targetModifier":{"smart":false} - }, - "none" : { - }, - "basic" : { - }, - "advanced" : { - }, - "expert" : { - } - }, - "flags" : { - "damage": true, - "negative": true, - "nonMagical" : true, - "special": true - }, - "targetCondition" : { - } - }, - "rampartMoat": { - "targetType" : "NO_TARGET", - "type": "ability", - "name": "Brambles", - "school" : {}, - "level": 0, - "power": 0, - "defaultGainChance": 0, - "gainChance": {}, - "levels" : { - "base":{ - "description" : "", - "aiValue" : 0, - "power" : 0, - "cost" : 0, - "targetModifier":{"smart":false}, - "battleEffects":{ - "moat":{ - "type":"core:moat", - "hidden" : false, - "trap" : true, - "triggerAbility" : "core:rampartMoatTrigger", - "dispellable" : false, - "removeOnTrigger" : false, - "moatDamage" : 70, - "moatHexes" : [[11, 28, 44, 61, 77, 111, 129, 146, 164, 181]], - "defender" :{ - }, - "bonus" :{ - "primarySkill" : { - "val" : -3, - "type" : "PRIMARY_SKILL", - "subtype" : "primarySkill.defence", - "valueType" : "ADDITIVE_VALUE" - } - } - } - }, - "range" : "X" - }, - "none" :{ - }, - "basic" :{ - }, - "advanced" :{ - }, - "expert" :{ - } - }, - "flags" : { - "nonMagical" : true, - "indifferent": true - }, - "targetCondition" : { - } - }, - "towerMoat": { - "targetType" : "NO_TARGET", - "type": "ability", - "name": "Land Mine", - "school" : {}, - "level": 3, - "power": 0, - "defaultGainChance": 0, - "gainChance": {}, - "levels" : { - "base":{ - "description" : "", - "aiValue" : 0, - "power" : 0, - "cost" : 0, - "targetModifier":{"smart":false}, - "battleEffects":{ - "moat":{ - "type":"core:moat", - "hidden" : true, - "trap" : false, - "triggerAbility" : "core:landMineTrigger", - "dispellable" : true, - "removeOnTrigger" : true, - "moatDamage" : 150, - "moatHexes" : [[11], [28], [44], [61], [77], [111], [129], [146], [164], [181]], - "defender" :{ - "animation" : "C09SPF1", - "appearAnimation" : "C09SPF0", - "appearSound" : "LANDMINE" - } - } - }, - "range" : "X" - }, - "none" :{ - }, - "basic" :{ - }, - "advanced" :{ - }, - "expert" :{ - } - }, - "flags" : { - "nonMagical" : true, - "indifferent": true - }, - "targetCondition" : { - } - }, - "infernoMoatTrigger" : - { - "targetType" : "CREATURE", - "type": "ability", - "name": "Lava", - "school": {}, - "level": 0, - "power": 0, - "gainChance": {}, - "levels" : { - "base": { - "power" : 0, - "range" : "0", - "description" : "", //For validation - "cost" : 0, //For validation - "aiValue" : 0, //For validation - "battleEffects" : { - "directDamage" : { - "type":"core:damage" - } - }, - "targetModifier":{"smart":false} - }, - "none" : { - }, - "basic" : { - }, - "advanced" : { - }, - "expert" : { - } - }, - "flags" : { - "damage": true, - "negative": true, - "nonMagical" : true, - "special": true - }, - "targetCondition" : { - } - }, - "infernoMoat": { - "targetType" : "NO_TARGET", - "type": "ability", - "name": "Lava", - "school" : {}, - "level": 0, - "power": 0, - "defaultGainChance": 0, - "gainChance": {}, - "levels" : { - "base":{ - "description" : "", - "aiValue" : 0, - "power" : 0, - "cost" : 0, - "targetModifier":{"smart":false}, - "battleEffects":{ - "moat":{ - "type":"core:moat", - "hidden" : false, - "trap" : true, - "triggerAbility" : "core:infernoMoatTrigger", - "dispellable" : false, - "removeOnTrigger" : false, - "moatDamage" : 90, - "moatHexes" : [[11, 28, 44, 61, 77, 111, 129, 146, 164, 181]], - "defender" :{ - }, - "bonus" :{ - "primarySkill" : { - "val" : -3, - "type" : "PRIMARY_SKILL", - "subtype" : "primarySkill.defence", - "valueType" : "ADDITIVE_VALUE" - } - } - } - }, - "range" : "X" - }, - "none" :{ - }, - "basic" :{ - }, - "advanced" :{ - }, - "expert" :{ - } - }, - "flags" : { - "nonMagical" : true, - "indifferent": true - }, - "targetCondition" : { - } - }, - "necropolisMoatTrigger" : - { - "targetType" : "CREATURE", - "type": "ability", - "name": "Boneyard", - "school": {}, - "level": 0, - "power": 0, - "gainChance": {}, - "levels" : { - "base": { - "power" : 0, - "range" : "0", - "description" : "", //For validation - "cost" : 0, //For validation - "aiValue" : 0, //For validation - "battleEffects" : { - "directDamage" : { - "type":"core:damage" - } - }, - "targetModifier":{"smart":false} - }, - "none" : { - }, - "basic" : { - }, - "advanced" : { - }, - "expert" : { - } - }, - "flags" : { - "damage": true, - "negative": true, - "nonMagical" : true, - "special": true - }, - "targetCondition" : { - } - }, - "necropolisMoat": { - "targetType" : "NO_TARGET", - "type": "ability", - "name": "Boneyard", - "school" : {}, - "level": 0, - "power": 0, - "defaultGainChance": 0, - "gainChance": {}, - "levels" : { - "base":{ - "description" : "", - "aiValue" : 0, - "power" : 0, - "cost" : 0, - "targetModifier":{"smart":false}, - "battleEffects":{ - "moat":{ - "type":"core:moat", - "hidden" : false, - "trap" : true, - "triggerAbility" : "core:necropolisMoatTrigger", - "dispellable" : false, - "removeOnTrigger" : false, - "moatDamage" : 70, - "moatHexes" : [[11, 28, 44, 61, 77, 111, 129, 146, 164, 181]], - "defender" :{ - }, - "bonus" :{ - "primarySkill" : { - "val" : -3, - "type" : "PRIMARY_SKILL", - "subtype" : "primarySkill.defence", - "valueType" : "ADDITIVE_VALUE" - } - } - } - }, - "range" : "X" - }, - "none" :{ - }, - "basic" :{ - }, - "advanced" :{ - }, - "expert" :{ - } - }, - "flags" : { - "nonMagical" : true, - "indifferent": true - }, - "targetCondition" : { - } - }, - "dungeonMoatTrigger" : - { - "targetType" : "CREATURE", - "type": "ability", - "name": "Boiling Oil", - "school": {}, - "level": 0, - "power": 0, - "gainChance": {}, - "levels" : { - "base": { - "power" : 0, - "range" : "0", - "description" : "", //For validation - "cost" : 0, //For validation - "aiValue" : 0, //For validation - "battleEffects" : { - "directDamage" : { - "type":"core:damage" - } - }, - "targetModifier":{"smart":false} - }, - "none" : { - }, - "basic" : { - }, - "advanced" : { - }, - "expert" : { - } - }, - "flags" : { - "damage": true, - "negative": true, - "nonMagical" : true, - "special": true - }, - "targetCondition" : { - } - }, - "dungeonMoat": { - "targetType" : "NO_TARGET", - "type": "ability", - "name": "Boiling Oil", - "school" : {}, - "level": 0, - "power": 0, - "defaultGainChance": 0, - "gainChance": {}, - "levels" : { - "base":{ - "description" : "", - "aiValue" : 0, - "power" : 0, - "cost" : 0, - "targetModifier":{"smart":false}, - "battleEffects":{ - "moat":{ - "type":"core:moat", - "hidden" : false, - "trap" : true, - "triggerAbility" : "core:dungeonMoatTrigger", - "dispellable" : false, - "removeOnTrigger" : false, - "moatDamage" : 90, - "moatHexes" : [[11, 28, 44, 61, 77, 111, 129, 146, 164, 181]], - "defender" :{ - }, - "bonus" :{ - "primarySkill" : { - "val" : -3, - "type" : "PRIMARY_SKILL", - "subtype" : "primarySkill.defence", - "valueType" : "ADDITIVE_VALUE" - } - } - } - }, - "range" : "X" - }, - "none" :{ - }, - "basic" :{ - }, - "advanced" :{ - }, - "expert" :{ - } - }, - "flags" : { - "nonMagical" : true, - "indifferent": true - }, - "targetCondition" : { - } - }, - "strongholdMoatTrigger" : - { - "targetType" : "CREATURE", - "type": "ability", - "name": "Wooden Spikes", - "school": {}, - "level": 0, - "power": 0, - "gainChance": {}, - "levels" : { - "base": { - "power" : 0, - "range" : "0", - "description" : "", //For validation - "cost" : 0, //For validation - "aiValue" : 0, //For validation - "battleEffects" : { - "directDamage" : { - "type":"core:damage" - } - }, - "targetModifier":{"smart":false} - }, - "none" : { - }, - "basic" : { - }, - "advanced" : { - }, - "expert" : { - } - }, - "flags" : { - "damage": true, - "negative": true, - "nonMagical" : true, - "special": true - }, - "targetCondition" : { - } - }, - "strongholdMoat": { - "targetType" : "NO_TARGET", - "type": "ability", - "name": "Wooden Spikes", - "school" : {}, - "level": 0, - "power": 0, - "defaultGainChance": 0, - "gainChance": {}, - "levels" : { - "base":{ - "description" : "", - "aiValue" : 0, - "power" : 0, - "cost" : 0, - "targetModifier":{"smart":false}, - "battleEffects":{ - "moat":{ - "type":"core:moat", - "hidden" : false, - "trap" : true, - "triggerAbility" : "core:strongholdMoatTrigger", - "dispellable" : false, - "removeOnTrigger" : false, - "moatDamage" : 70, - "moatHexes" : [[11, 28, 44, 61, 77, 111, 129, 146, 164, 181]], - "defender" :{ - }, - "bonus" :{ - "primarySkill" : { - "val" : -3, - "type" : "PRIMARY_SKILL", - "subtype" : "primarySkill.defence", - "valueType" : "ADDITIVE_VALUE" - } - } - } - }, - "range" : "X" - }, - "none" :{ - }, - "basic" :{ - }, - "advanced" :{ - }, - "expert" :{ - } - }, - "flags" : { - "nonMagical" : true, - "indifferent": true - }, - "targetCondition" : { - } - }, - "fortressMoatTrigger" : - { - "targetType" : "CREATURE", - "type": "ability", - "name": "Boiling Tar", - "school": {}, - "level": 0, - "power": 0, - "gainChance": {}, - "levels" : { - "base": { - "power" : 0, - "range" : "0", - "description" : "", //For validation - "cost" : 0, //For validation - "aiValue" : 0, //For validation - "battleEffects" : { - "directDamage" : { - "type":"core:damage" - } - }, - "targetModifier":{"smart":false} - }, - "none" : { - }, - "basic" : { - }, - "advanced" : { - }, - "expert" : { - } - }, - "flags" : { - "damage": true, - "negative": true, - "nonMagical" : true, - "special": true - }, - "targetCondition" : { - } - }, - "fortressMoat": { - "targetType" : "NO_TARGET", - "type": "ability", - "name": "Boiling Tar", - "school" : {}, - "level": 0, - "power": 0, - "defaultGainChance": 0, - "gainChance": {}, - "levels" : { - "base":{ - "description" : "", - "aiValue" : 0, - "power" : 0, - "cost" : 0, - "targetModifier":{"smart":false}, - "battleEffects":{ - "moat":{ - "type":"core:moat", - "hidden" : false, - "trap" : true, - "triggerAbility" : "core:fortressMoatTrigger", - "dispellable" : false, - "removeOnTrigger" : false, - "moatDamage" : 90, - "moatHexes" : [[10, 11, 27, 28, 43, 44, 60, 61, 76, 77, 94, 110, 111, 128, 129, 145, 146, 163, 164, 180, 181]], - "defender" :{ - }, - "bonus" :{ - "primarySkill" : { - "val" : -3, - "type" : "PRIMARY_SKILL", - "subtype" : "primarySkill.defence", - "valueType" : "ADDITIVE_VALUE" - } - } - } - }, - "range" : "X" - }, - "none" :{ - }, - "basic" :{ - }, - "advanced" :{ - }, - "expert" :{ - } - }, - "flags" : { - "nonMagical" : true, - "indifferent": true - }, - "targetCondition" : { - } - } + "castleMoatTrigger" : + { + "targetType" : "CREATURE", + "type": "ability", + "name": "", + "school": {}, + "level": 0, + "power": 0, + "gainChance": {}, + "levels" : { + "base": { + "power" : 0, + "range" : "0", + "description" : "", //For validation + "cost" : 0, //For validation + "aiValue" : 0, //For validation + "battleEffects" : { + "directDamage" : { + "type":"core:damage" + } + }, + "targetModifier":{"smart":false} + }, + "none" : { + }, + "basic" : { + }, + "advanced" : { + }, + "expert" : { + } + }, + "flags" : { + "damage": true, + "negative": true, + "nonMagical" : true, + "special": true + }, + "targetCondition" : { + } + }, + "castleMoat": { + "targetType" : "NO_TARGET", + "type": "ability", + "name": "", + "school" : {}, + "level": 0, + "power": 0, + "defaultGainChance": 0, + "gainChance": {}, + "levels" : { + "base":{ + "description" : "", + "aiValue" : 0, + "power" : 0, + "cost" : 0, + "targetModifier":{"smart":false}, + "battleEffects":{ + "moat":{ + "type":"core:moat", + "hidden" : false, + "trap" : true, + "triggerAbility" : "core:castleMoatTrigger", + "dispellable" : false, + "removeOnTrigger" : false, + "moatDamage" : 70, + "moatHexes" : [[11, 28, 44, 61, 77, 111, 129, 146, 164, 181]], + "defender" :{ + }, + "bonus" :{ + "primarySkill" : { + "val" : -3, + "type" : "PRIMARY_SKILL", + "subtype" : "primarySkill.defence", + "valueType" : "ADDITIVE_VALUE" + } + } + } + }, + "range" : "X" + }, + "none" :{ + }, + "basic" :{ + }, + "advanced" :{ + }, + "expert" :{ + } + }, + "flags" : { + "nonMagical" : true, + "indifferent": true + }, + "targetCondition" : { + } + }, + "rampartMoatTrigger" : + { + "targetType" : "CREATURE", + "type": "ability", + "name": "", + "school": {}, + "level": 0, + "power": 0, + "gainChance": {}, + "levels" : { + "base": { + "power" : 0, + "range" : "0", + "description" : "", //For validation + "cost" : 0, //For validation + "aiValue" : 0, //For validation + "battleEffects" : { + "directDamage" : { + "type":"core:damage" + } + }, + "targetModifier":{"smart":false} + }, + "none" : { + }, + "basic" : { + }, + "advanced" : { + }, + "expert" : { + } + }, + "flags" : { + "damage": true, + "negative": true, + "nonMagical" : true, + "special": true + }, + "targetCondition" : { + } + }, + "rampartMoat": { + "targetType" : "NO_TARGET", + "type": "ability", + "name": "", + "school" : {}, + "level": 0, + "power": 0, + "defaultGainChance": 0, + "gainChance": {}, + "levels" : { + "base":{ + "description" : "", + "aiValue" : 0, + "power" : 0, + "cost" : 0, + "targetModifier":{"smart":false}, + "battleEffects":{ + "moat":{ + "type":"core:moat", + "hidden" : false, + "trap" : true, + "triggerAbility" : "core:rampartMoatTrigger", + "dispellable" : false, + "removeOnTrigger" : false, + "moatDamage" : 70, + "moatHexes" : [[11, 28, 44, 61, 77, 111, 129, 146, 164, 181]], + "defender" :{ + }, + "bonus" :{ + "primarySkill" : { + "val" : -3, + "type" : "PRIMARY_SKILL", + "subtype" : "primarySkill.defence", + "valueType" : "ADDITIVE_VALUE" + } + } + } + }, + "range" : "X" + }, + "none" :{ + }, + "basic" :{ + }, + "advanced" :{ + }, + "expert" :{ + } + }, + "flags" : { + "nonMagical" : true, + "indifferent": true + }, + "targetCondition" : { + } + }, + "towerMoat": { + "targetType" : "NO_TARGET", + "type": "ability", + "name": "", + "school" : {}, + "level": 3, + "power": 0, + "defaultGainChance": 0, + "gainChance": {}, + "levels" : { + "base":{ + "description" : "", + "aiValue" : 0, + "power" : 0, + "cost" : 0, + "targetModifier":{"smart":false}, + "battleEffects":{ + "moat":{ + "type":"core:moat", + "hidden" : true, + "trap" : false, + "triggerAbility" : "core:landMineTrigger", + "dispellable" : true, + "removeOnTrigger" : true, + "moatDamage" : 150, + "moatHexes" : [[11], [28], [44], [61], [77], [111], [129], [146], [164], [181]], + "defender" :{ + "animation" : "C09SPF1", + "appearAnimation" : "C09SPF0", + "appearSound" : "LANDMINE" + } + } + }, + "range" : "X" + }, + "none" :{ + }, + "basic" :{ + }, + "advanced" :{ + }, + "expert" :{ + } + }, + "flags" : { + "nonMagical" : true, + "indifferent": true + }, + "targetCondition" : { + } + }, + "infernoMoatTrigger" : + { + "targetType" : "CREATURE", + "type": "ability", + "name": "", + "school": {}, + "level": 0, + "power": 0, + "gainChance": {}, + "levels" : { + "base": { + "power" : 0, + "range" : "0", + "description" : "", //For validation + "cost" : 0, //For validation + "aiValue" : 0, //For validation + "battleEffects" : { + "directDamage" : { + "type":"core:damage" + } + }, + "targetModifier":{"smart":false} + }, + "none" : { + }, + "basic" : { + }, + "advanced" : { + }, + "expert" : { + } + }, + "flags" : { + "damage": true, + "negative": true, + "nonMagical" : true, + "special": true + }, + "targetCondition" : { + } + }, + "infernoMoat": { + "targetType" : "NO_TARGET", + "type": "ability", + "name": "", + "school" : {}, + "level": 0, + "power": 0, + "defaultGainChance": 0, + "gainChance": {}, + "levels" : { + "base":{ + "description" : "", + "aiValue" : 0, + "power" : 0, + "cost" : 0, + "targetModifier":{"smart":false}, + "battleEffects":{ + "moat":{ + "type":"core:moat", + "hidden" : false, + "trap" : true, + "triggerAbility" : "core:infernoMoatTrigger", + "dispellable" : false, + "removeOnTrigger" : false, + "moatDamage" : 90, + "moatHexes" : [[11, 28, 44, 61, 77, 111, 129, 146, 164, 181]], + "defender" :{ + }, + "bonus" :{ + "primarySkill" : { + "val" : -3, + "type" : "PRIMARY_SKILL", + "subtype" : "primarySkill.defence", + "valueType" : "ADDITIVE_VALUE" + } + } + } + }, + "range" : "X" + }, + "none" :{ + }, + "basic" :{ + }, + "advanced" :{ + }, + "expert" :{ + } + }, + "flags" : { + "nonMagical" : true, + "indifferent": true + }, + "targetCondition" : { + } + }, + "necropolisMoatTrigger" : + { + "targetType" : "CREATURE", + "type": "ability", + "name": "", + "school": {}, + "level": 0, + "power": 0, + "gainChance": {}, + "levels" : { + "base": { + "power" : 0, + "range" : "0", + "description" : "", //For validation + "cost" : 0, //For validation + "aiValue" : 0, //For validation + "battleEffects" : { + "directDamage" : { + "type":"core:damage" + } + }, + "targetModifier":{"smart":false} + }, + "none" : { + }, + "basic" : { + }, + "advanced" : { + }, + "expert" : { + } + }, + "flags" : { + "damage": true, + "negative": true, + "nonMagical" : true, + "special": true + }, + "targetCondition" : { + } + }, + "necropolisMoat": { + "targetType" : "NO_TARGET", + "type": "ability", + "name": "", + "school" : {}, + "level": 0, + "power": 0, + "defaultGainChance": 0, + "gainChance": {}, + "levels" : { + "base":{ + "description" : "", + "aiValue" : 0, + "power" : 0, + "cost" : 0, + "targetModifier":{"smart":false}, + "battleEffects":{ + "moat":{ + "type":"core:moat", + "hidden" : false, + "trap" : true, + "triggerAbility" : "core:necropolisMoatTrigger", + "dispellable" : false, + "removeOnTrigger" : false, + "moatDamage" : 70, + "moatHexes" : [[11, 28, 44, 61, 77, 111, 129, 146, 164, 181]], + "defender" :{ + }, + "bonus" :{ + "primarySkill" : { + "val" : -3, + "type" : "PRIMARY_SKILL", + "subtype" : "primarySkill.defence", + "valueType" : "ADDITIVE_VALUE" + } + } + } + }, + "range" : "X" + }, + "none" :{ + }, + "basic" :{ + }, + "advanced" :{ + }, + "expert" :{ + } + }, + "flags" : { + "nonMagical" : true, + "indifferent": true + }, + "targetCondition" : { + } + }, + "dungeonMoatTrigger" : + { + "targetType" : "CREATURE", + "type": "ability", + "name": "", + "school": {}, + "level": 0, + "power": 0, + "gainChance": {}, + "levels" : { + "base": { + "power" : 0, + "range" : "0", + "description" : "", //For validation + "cost" : 0, //For validation + "aiValue" : 0, //For validation + "battleEffects" : { + "directDamage" : { + "type":"core:damage" + } + }, + "targetModifier":{"smart":false} + }, + "none" : { + }, + "basic" : { + }, + "advanced" : { + }, + "expert" : { + } + }, + "flags" : { + "damage": true, + "negative": true, + "nonMagical" : true, + "special": true + }, + "targetCondition" : { + } + }, + "dungeonMoat": { + "targetType" : "NO_TARGET", + "type": "ability", + "name": "", + "school" : {}, + "level": 0, + "power": 0, + "defaultGainChance": 0, + "gainChance": {}, + "levels" : { + "base":{ + "description" : "", + "aiValue" : 0, + "power" : 0, + "cost" : 0, + "targetModifier":{"smart":false}, + "battleEffects":{ + "moat":{ + "type":"core:moat", + "hidden" : false, + "trap" : true, + "triggerAbility" : "core:dungeonMoatTrigger", + "dispellable" : false, + "removeOnTrigger" : false, + "moatDamage" : 90, + "moatHexes" : [[11, 28, 44, 61, 77, 111, 129, 146, 164, 181]], + "defender" :{ + }, + "bonus" :{ + "primarySkill" : { + "val" : -3, + "type" : "PRIMARY_SKILL", + "subtype" : "primarySkill.defence", + "valueType" : "ADDITIVE_VALUE" + } + } + } + }, + "range" : "X" + }, + "none" :{ + }, + "basic" :{ + }, + "advanced" :{ + }, + "expert" :{ + } + }, + "flags" : { + "nonMagical" : true, + "indifferent": true + }, + "targetCondition" : { + } + }, + "strongholdMoatTrigger" : + { + "targetType" : "CREATURE", + "type": "ability", + "name": "", + "school": {}, + "level": 0, + "power": 0, + "gainChance": {}, + "levels" : { + "base": { + "power" : 0, + "range" : "0", + "description" : "", //For validation + "cost" : 0, //For validation + "aiValue" : 0, //For validation + "battleEffects" : { + "directDamage" : { + "type":"core:damage" + } + }, + "targetModifier":{"smart":false} + }, + "none" : { + }, + "basic" : { + }, + "advanced" : { + }, + "expert" : { + } + }, + "flags" : { + "damage": true, + "negative": true, + "nonMagical" : true, + "special": true + }, + "targetCondition" : { + } + }, + "strongholdMoat": { + "targetType" : "NO_TARGET", + "type": "ability", + "name": "", + "school" : {}, + "level": 0, + "power": 0, + "defaultGainChance": 0, + "gainChance": {}, + "levels" : { + "base":{ + "description" : "", + "aiValue" : 0, + "power" : 0, + "cost" : 0, + "targetModifier":{"smart":false}, + "battleEffects":{ + "moat":{ + "type":"core:moat", + "hidden" : false, + "trap" : true, + "triggerAbility" : "core:strongholdMoatTrigger", + "dispellable" : false, + "removeOnTrigger" : false, + "moatDamage" : 70, + "moatHexes" : [[11, 28, 44, 61, 77, 111, 129, 146, 164, 181]], + "defender" :{ + }, + "bonus" :{ + "primarySkill" : { + "val" : -3, + "type" : "PRIMARY_SKILL", + "subtype" : "primarySkill.defence", + "valueType" : "ADDITIVE_VALUE" + } + } + } + }, + "range" : "X" + }, + "none" :{ + }, + "basic" :{ + }, + "advanced" :{ + }, + "expert" :{ + } + }, + "flags" : { + "nonMagical" : true, + "indifferent": true + }, + "targetCondition" : { + } + }, + "fortressMoatTrigger" : + { + "targetType" : "CREATURE", + "type": "ability", + "name": "", + "school": {}, + "level": 0, + "power": 0, + "gainChance": {}, + "levels" : { + "base": { + "power" : 0, + "range" : "0", + "description" : "", //For validation + "cost" : 0, //For validation + "aiValue" : 0, //For validation + "battleEffects" : { + "directDamage" : { + "type":"core:damage" + } + }, + "targetModifier":{"smart":false} + }, + "none" : { + }, + "basic" : { + }, + "advanced" : { + }, + "expert" : { + } + }, + "flags" : { + "damage": true, + "negative": true, + "nonMagical" : true, + "special": true + }, + "targetCondition" : { + } + }, + "fortressMoat": { + "targetType" : "NO_TARGET", + "type": "ability", + "name": "", + "school" : {}, + "level": 0, + "power": 0, + "defaultGainChance": 0, + "gainChance": {}, + "levels" : { + "base":{ + "description" : "", + "aiValue" : 0, + "power" : 0, + "cost" : 0, + "targetModifier":{"smart":false}, + "battleEffects":{ + "moat":{ + "type":"core:moat", + "hidden" : false, + "trap" : true, + "triggerAbility" : "core:fortressMoatTrigger", + "dispellable" : false, + "removeOnTrigger" : false, + "moatDamage" : 90, + "moatHexes" : [[10, 11, 27, 28, 43, 44, 60, 61, 76, 77, 94, 110, 111, 128, 129, 145, 146, 163, 164, 180, 181]], + "defender" :{ + }, + "bonus" :{ + "primarySkill" : { + "val" : -3, + "type" : "PRIMARY_SKILL", + "subtype" : "primarySkill.defence", + "valueType" : "ADDITIVE_VALUE" + } + } + } + }, + "range" : "X" + }, + "none" :{ + }, + "basic" :{ + }, + "advanced" :{ + }, + "expert" :{ + } + }, + "flags" : { + "nonMagical" : true, + "indifferent": true + }, + "targetCondition" : { + } + } } \ No newline at end of file diff --git a/config/spells/offensive.json b/config/spells/offensive.json index dd8583432..8f952527d 100644 --- a/config/spells/offensive.json +++ b/config/spells/offensive.json @@ -44,7 +44,7 @@ {"minimumAngle": 1.20 ,"defName":"C08SPW1"}, {"minimumAngle": 1.50 ,"defName":"C08SPW0"} ], - "hit":["C08SPW5"] + "hit":[ {"defName" : "C08SPW5", "transparency" : 0.5 }] }, "sounds": { "cast": "ICERAY" @@ -309,7 +309,7 @@ "targetType" : "CREATURE", "animation":{ - "affect":["C14SPA0"] + "affect":[{"defName" : "C14SPA0", "transparency" : 0.5}] }, "sounds": { "cast": "SACBRETH" diff --git a/config/spells/other.json b/config/spells/other.json index 633332d8d..5f6aa02fc 100644 --- a/config/spells/other.json +++ b/config/spells/other.json @@ -55,7 +55,7 @@ { "targetType" : "CREATURE", "type": "combat", - "name": "Land Mine", + "name": "", "school": { "air": false, @@ -237,7 +237,7 @@ "fireWallTrigger" : { "targetType" : "CREATURE", "type": "combat", - "name": "Fire Wall", + "name": "", "school": { "air": false, @@ -483,7 +483,7 @@ "targetType" : "CREATURE", "animation":{ - "affect":["C01SPE0"] + "affect":[{ "defName" : "C01SPE0", "transparency" : 0.5}] }, "sounds": { "cast": "RESURECT" @@ -531,6 +531,7 @@ "targetCondition" : { "noneOf" : { "bonus.NON_LIVING" : "absolute", + "bonus.MECHANICAL" : "absolute", "bonus.SIEGE_WEAPON" : "absolute", "bonus.UNDEAD" : "absolute", "bonus.GARGOYLE" : "absolute" @@ -602,6 +603,7 @@ "targetCondition" : { "noneOf" : { "bonus.NON_LIVING" : "absolute", + "bonus.MECHANICAL" : "absolute", "bonus.SIEGE_WEAPON" : "absolute", "bonus.UNDEAD" : "absolute", "bonus.GARGOYLE" : "absolute" diff --git a/config/spells/timed.json b/config/spells/timed.json index 77edbabcd..76db6315b 100644 --- a/config/spells/timed.json +++ b/config/spells/timed.json @@ -652,7 +652,7 @@ "targetType" : "CREATURE", "animation":{ - "affect":["C07SPA1"], + "affect":[{"defName" : "C07SPA1", "transparency" : 0.5}], "projectile":[{"defName":"C07SPA0"}]//??? }, "sounds": { @@ -696,7 +696,7 @@ "targetType" : "CREATURE", "animation":{ - "affect":[{"defName":"C10SPW", "verticalPosition":"bottom"}] + "affect":[{"defName":"C10SPW", "verticalPosition":"bottom", "transparency" : 0.5}] }, "sounds": { "cast": "PRAYER" @@ -806,6 +806,7 @@ "noneOf" : { "bonus.MIND_IMMUNITY" : "normal", "bonus.NON_LIVING" : "normal", + "bonus.MECHANICAL" : "normal", "bonus.SIEGE_WEAPON" : "absolute", "bonus.UNDEAD" : "absolute" } @@ -859,6 +860,7 @@ "noneOf" : { "bonus.MIND_IMMUNITY" : "normal", "bonus.NON_LIVING" : "normal", + "bonus.MECHANICAL" : "normal", "bonus.SIEGE_WEAPON" : "absolute", "bonus.UNDEAD" : "absolute" } @@ -1155,6 +1157,7 @@ "noneOf" : { "bonus.MIND_IMMUNITY" : "absolute", "bonus.NON_LIVING" : "absolute", + "bonus.MECHANICAL" : "absolute", "bonus.SIEGE_WEAPON" : "absolute", "bonus.UNDEAD" : "absolute" } @@ -1245,6 +1248,7 @@ "noneOf" : { "bonus.MIND_IMMUNITY" : "normal", "bonus.NON_LIVING" : "normal", + "bonus.MECHANICAL" : "normal", "bonus.SIEGE_WEAPON" : "absolute", "bonus.UNDEAD" : "normal" } @@ -1311,7 +1315,8 @@ "bonus.SIEGE_WEAPON":"absolute", "bonus.MIND_IMMUNITY":"normal", "bonus.UNDEAD":"normal", - "bonus.NON_LIVING":"normal" + "bonus.NON_LIVING":"normal", + "bonus.MECHANICAL":"normal" } }, "flags" : { @@ -1379,6 +1384,7 @@ "noneOf" : { "bonus.MIND_IMMUNITY" : "normal", "bonus.NON_LIVING" : "normal", + "bonus.MECHANICAL" : "normal", "bonus.SIEGE_WEAPON" : "absolute", "bonus.UNDEAD" : "normal" } @@ -1437,6 +1443,7 @@ "noneOf" : { "bonus.MIND_IMMUNITY" : "normal", "bonus.NON_LIVING" : "normal", + "bonus.MECHANICAL" : "normal", "bonus.SIEGE_WEAPON" : "absolute", "bonus.UNDEAD" : "absolute" } diff --git a/config/spells/vcmiAbility.json b/config/spells/vcmiAbility.json index db1212b7e..39cfb20d3 100644 --- a/config/spells/vcmiAbility.json +++ b/config/spells/vcmiAbility.json @@ -1,8 +1,8 @@ { - "summonDemons" : { + "summonDemons" : { "type": "ability", "targetType" : "CREATURE", - "name": "Summon Demons", + "name": "", "school" : {}, "level": 2, "power": 50, @@ -41,16 +41,17 @@ "targetCondition" : { "noneOf" : { "bonus.NON_LIVING" : "absolute", + "bonus.MECHANICAL" : "absolute", "bonus.SIEGE_WEAPON" : "absolute", "bonus.UNDEAD" : "absolute", "bonus.GARGOYLE" : "absolute" } } - }, - "firstAid" : { + }, + "firstAid" : { "targetType" : "CREATURE", "type": "ability", - "name": "First Aid", + "name": "", "school" : {}, "level": 1, "power": 10, @@ -106,7 +107,7 @@ "catapultShot" : { "targetType" : "LOCATION", "type": "ability", - "name": "Catapult shot", + "name": "", "school" : {}, "level": 1, "power": 1, @@ -187,7 +188,7 @@ "cyclopsShot" : { "targetType" : "LOCATION", "type": "ability", - "name": "Siege shot", + "name": "", "school" : {}, "level": 1, "power": 1, diff --git a/docs/Readme.md b/docs/Readme.md index b50df62b8..68f3118cd 100644 --- a/docs/Readme.md +++ b/docs/Readme.md @@ -1,28 +1,27 @@ +# 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.5.0/total)](https://github.com/vcmi/vcmi/releases/tag/1.5.0) [![Github Downloads](https://img.shields.io/github/downloads/vcmi/vcmi/1.5.6/total)](https://github.com/vcmi/vcmi/releases/tag/1.5.6) [![Github Downloads](https://img.shields.io/github/downloads/vcmi/vcmi/1.5.7/total)](https://github.com/vcmi/vcmi/releases/tag/1.5.7) [![Github Downloads](https://img.shields.io/github/downloads/vcmi/vcmi/total)](https://github.com/vcmi/vcmi/releases) -# VCMI Project - VCMI is an open-source recreation of Heroes of Might & Magic III engine, giving it new and extended possibilities.

-Vanilla town siege in extended window -Vanilla town view with radial menu for touchscreen devices -Large Spellbook with German translation -New widget for Hero selection, featuring Pavillon Town + Vanilla town siege in extended window + Vanilla town view with radial menu for touchscreen devices + Large Spellbook with German translation + New widget for Hero selection, featuring Pavillon Town

- ## Links - * Homepage: https://vcmi.eu/ - * Forums: https://forum.vcmi.eu/ - * Bugtracker: https://github.com/vcmi/vcmi/issues - * Discord: https://discord.gg/chBT42V - * GPT Store: https://chat.openai.com/g/g-1kNhX0mlO-vcmi-assistant +* Homepage: +* Forums: +* Bugtracker: +* Discord: +* GPT Store: ## Latest release @@ -31,16 +30,19 @@ Loading saves made with different major version of VCMI is usually **not** suppo Please see corresponding installation guide articles for details for your platform. ## Installation guides + - [Windows](players/Installation_Windows.md) - [macOS](players/Installation_macOS.md) - [Linux](players/Installation_Linux.md) - [Android](players/Installation_Android.md) - [iOS](players/Installation_iOS.md) +See also installation guide for [Heroes Chronicles](players/Heroes_Chronicles.md). +

-Forge Town in battle -Asylum town with new creature dialog -Ruins town siege + Forge Town in battle + Asylum town with new creature dialog + Ruins town siege Map editor

@@ -68,6 +70,7 @@ Please see corresponding installation guide articles for details for your platfo ## Documentation and guidelines for developers Development environment setup instructions: + - [Building VCMI for Android](developers/Building_Android.md) - [Building VCMI for iOS](developers/Building_iOS.md) - [Building VCMI for Linux](developers/Building_Linux.md) @@ -76,6 +79,7 @@ Development environment setup instructions: - [Conan](developers/Conan.md) Engine documentation: (NOTE: may be outdated) + - [Development with Qt Creator](developers/Development_with_Qt_Creator.md) - [Coding Guidelines](developers/Coding_Guidelines.md) - [Bonus System](developers/Bonus_System.md) @@ -93,6 +97,6 @@ Engine documentation: (NOTE: may be outdated) ## Copyright and license VCMI Project source code is licensed under GPL version 2 or later. -VCMI Project assets are licensed under CC-BY-SA 4.0. Assets sources and information about contributors are available under following link: https://github.com/vcmi/vcmi-assets +VCMI Project assets are licensed under CC-BY-SA 4.0. Assets sources and information about contributors are available under following link: Copyright (C) 2007-2024 VCMI Team (check AUTHORS file for the contributors list) diff --git a/docs/developers/AI.md b/docs/developers/AI.md index b75ba539a..6fd7910ab 100644 --- a/docs/developers/AI.md +++ b/docs/developers/AI.md @@ -6,18 +6,19 @@ There are two types of AI: adventure and battle. **Battle AIs** are responsible for fighting, i.e. moving stacks on the battlefield We have 3 battle AIs so far: + * BattleAI - strongest * StupidAI - for neutrals, should be simple so that experienced players can abuse it * Empty AI - should do nothing at all. If needed another battle AI can be introduced. -Each battle AI consist of a few classes, but the main class, kind of entry point usually has the same name as the package itself. In BattleAI it is the BattleAI class. It implements some battle specific interface, do not remember. Main method there is activeStack(battle::Unit* stack). It is invoked by the system when it's time to move your stack. The thing you use to interact with the game and receive the gamestate is usually referenced in the code as cb. CPlayerSpecificCallback it should be. It has a lot of methods and can do anything. For instance it has battleGetUnitsIf(), which returns all units on the battlefield matching some lambda condition. -Each side in a battle is represented by an CArmedInstance object. CHeroInstance and CGDwelling, CGMonster and more are subclasses of CArmedInstance. CArmedInstance contains a set of stacks. When the battle starts, these stacks are converted to battle stacks. Usually Battle AIs reference them using the interface battle::Unit *. +Each battle AI consist of a few classes, but the main class, kind of entry point usually has the same name as the package itself. In BattleAI it is the BattleAI class. It implements some battle specific interface, do not remember. Main method there is `activeStack(battle::Unit * stack)`. It is invoked by the system when it's time to move your stack. The thing you use to interact with the game and receive the gamestate is usually referenced in the code as `cb`. `CPlayerSpecificCallback` it should be. It has a lot of methods and can do anything. For instance it has battleGetUnitsIf(), which returns all units on the battlefield matching some lambda condition. +Each side in a battle is represented by an `CArmedInstance` object. `CHeroInstance` and `CGDwelling`, `CGMonster` and more are subclasses of `CArmedInstance`. `CArmedInstance` contains a set of stacks. When the battle starts, these stacks are converted to battle stacks. Usually Battle AIs reference them using the interface `battle::Unit *`. Units have bonuses. Nearly everything aspect of a unit is configured in the form of bonuses. Attack, defense, health, retaliation, shooter or not, initial count of shots and so on. -When you call unit->getAttack() it summarizes all these bonuses and returns the resulting value. +When you call `unit->getAttack()` it summarizes all these bonuses and returns the resulting value. -One important class is HypotheticBattle. It is used to evaluate the effects of an action without changing the actual gamestate. It is a wrapper around CPlayerSpecificCallback or another HypotheticBattle so it can provide you data, Internally it has a set of modified unit states and intercepts some calls to underlying callback and returns these internal states instead. These states in turn are wrappers around original units and contain modified bonuses (CStackWithBonuses). So if you need to emulate an attack you can call hypotheticbattle.getforupdate() and it will return the CStackWithBonuses which you can safely change. +One important class is `HypotheticBattle`. It is used to evaluate the effects of an action without changing the actual gamestate. It is a wrapper around `CPlayerSpecificCallback` or another `HypotheticBattle` so it can provide you data, Internally it has a set of modified unit states and intercepts some calls to underlying callback and returns these internal states instead. These states in turn are wrappers around original units and contain modified bonuses (`CStackWithBonuses`). So if you need to emulate an attack you can call `hypotheticbattle.getforupdate()` and it will return the `CStackWithBonuses` which you can safely change. -## BattleAI +## BattleAI BattleAI's most important classes are the following: @@ -38,17 +39,20 @@ BattleAI itself handles all the rest and issues actual commands Adventure AI responsible for moving heroes on map, gathering things, developing town. Main idea is to gather all possible tasks on map, prioritize them and select the best one for each heroes. Initially was a fork of VCAI ### Parts + Gateway - a callback for server used to invoke AI actions when server thinks it is time to do something. Through this callback AI is informed about various events like hero level up, tile revialed, blocking dialogs and so on. In order to do this Gaateway implements specific interface. The interface is exactly the same for human and AI Another important actor for server interaction is CCallback * cb. This one is used to retrieve gamestate information and ask server to do things like hero moving, spell casting and so on. Each AI has own instance of Gateway and it is a root object which holds all AI state. Gateway has an event method yourTurn which invokes makeTurn in another thread. The last passes control to Nullkiller engine. Nullkiller engine - place where actual AI logic is organized. It contains a main loop for gathering and prioritizing things. Its algorithm: + * reset AI state, it avoids keeping some memory about the game in general to reduce amount of things serialized into savefile state. The only serialized things are in nullkiller->memory. This helps reducing save incompatibility. It should be mostly enough for AI to analyze data avaialble in CCallback * main loop, loop iteration is called a pass -** update AI state, some state is lazy and updates once per day to avoid performance hit, some state is recalculated each loop iteration. At this stage analysers and pathfidner work -** gathering goals, prioritizing and decomposing them -** execute selected best goals + * update AI state, some state is lazy and updates once per day to avoid performance hit, some state is recalculated each loop iteration. At this stage analysers and pathfidner work + * gathering goals, prioritizing and decomposing them + * execute selected best goals Analyzer - a module gathering data from CCallback *. Its goal to make some statistics and avoid making any significant decissions. + * HeroAnalyser - decides upong which hero suits better to be main (army carrier and fighter) and which is better to be a scout (gathering unguarded resources, exploring) * BuildAnalyzer - prepares information on what we can build in our towns, and what resources we need to do this * DangerHitMapAnalyser - checks if enemy hero can rich each tile, how fast and what is their army strangth @@ -61,9 +65,11 @@ Analyzer - a module gathering data from CCallback *. Its goal to make some stati * PriorityEvaluator - gathers information on task rewards, evaluates their priority using Fuzzy Light library (fuzzy logic) ### Goals + Units of activity in AI. Can be AbstractGoal, Task, Marker and Behavior Task - simple thing which can be done right away in order to gain some reward. Or a composition of simple things in case if more than one action is needed to gain the reward. + * AdventureSpellCast - town portal, water walk, air walk, summon boat * BuildBoat - builds a boat in a specific shipyard * BuildThis - builds a building in a specified town @@ -78,6 +84,7 @@ Task - simple thing which can be done right away in order to gain some reward. O * StayAtTown - stay at town for the rest of the day (to regain mana) Behavior - a core game activity + * CaptureObjectsBehavior - generally it is about visiting map objects which give reward. It can capture any object, even those which are behind monsters and so on. But due to performance considerations it is not allowed to handle monsters and quests now. * ClusterBehavior - uses information of ObjectClusterizer to unblock objects hidden behind various blockers. It kills guards, completes quests, captures garrisons. * BuildingBehavior - develops our towns @@ -89,6 +96,7 @@ Behavior - a core game activity * DefenceBehavior - defend towns by eliminating treatening heroes or hiding in town garrison AbstractGoal - some goals can not be completed because it is not clear how to do this. They express desire to do something, not exact plan. DeepDecomposer is used to refine such goals until they are turned into such plan or discarded. Some examples: + * CaptureObject - you need to visit some object (flag a shipyard for instance) but do not know how * CompleteQuest - you need to bypass bordergate or borderguard or questguard but do not know how AbstractGoal usually comes in form of composition with some elementar task blocked by abstract objective. For instance CaptureObject(Shipyard), ExecuteHeroChain(visit x, build boat, visit enemy town). When such composition is decomposed it can turn into either a pair of herochains or into another abstract composition if path to shipyard is also blocked with something. diff --git a/docs/developers/Bonus_System.md b/docs/developers/Bonus_System.md index e594fff8e..191acd4a0 100644 --- a/docs/developers/Bonus_System.md +++ b/docs/developers/Bonus_System.md @@ -6,8 +6,8 @@ The bonus system of VCMI is a set of mechanisms that make handling of different Each bonus originates from some node in the bonus system, and may have propagator and limiter objects attached to it. Bonuses are shared around as follows: -1. Bonuses with propagator are propagated to "matching" descendants in the red DAG - which descendants match is determined by the propagator. Bonuses without a propagator will not be propagated. -2. Bonuses without limiters are inherited by all descendants in the black DAG. If limiters are present, they can restrict inheritance to certain nodes. +1. Bonuses with propagator are propagated to "matching" descendants in the red DAG - which descendants match is determined by the propagator. Bonuses without a propagator will not be propagated. +2. Bonuses without limiters are inherited by all descendants in the black DAG. If limiters are present, they can restrict inheritance to certain nodes. Inheritance is the default means of sharing bonuses. A typical example is an artefact granting a bonus to attack/defense stat, which is inherited by the hero wearing it, and then by creatures in the hero's army. A common limiter is by creature - e.g. the hero Eric has a specialty that grants bonuses to attack, defense and speed, but only to griffins. @@ -15,9 +15,9 @@ Propagation is used when bonuses need to be shared in a different direction than ### Technical Details -- Propagation is done by copying bonuses to the target nodes. This happens when bonuses are added. -- Inheritance is done on-the-fly when needed, by traversing the black DAG. Results are cached to improve performance. -- Whenever a node changes (e.g. bonus added), a global counter gets increased which is used to check whether cached results are still current. +- Propagation is done by copying bonuses to the target nodes. This happens when bonuses are added. +- Inheritance is done on-the-fly when needed, by traversing the black DAG. Results are cached to improve performance. +- Whenever a node changes (e.g. bonus added), a global counter gets increased which is used to check whether cached results are still current. ## Operations on the graph @@ -26,6 +26,7 @@ There are two basic types of operations that can be performed on the graph: ### Adding a new node When node is attached to a new black parent (the only possibility - adding parent is the same as adding a child to it), the propagation system is triggered and works as follows: + - For the attached node and its all red ancestors - For every bonus - Call propagator giving the new descendant - then attach appropriately bonuses to the red descendant of attached node (or the node itself). @@ -54,7 +55,7 @@ Updaters are objects attached to bonuses. They can modify a bonus (typically by The following example shows an artifact providing a bonus based on the level of the hero that wears it: -```javascript +```json5 "core:greaterGnollsFlail": { "text" : { "description" : "This mighty flail increases the attack of all gnolls under the hero's command by twice the hero's level." }, diff --git a/docs/developers/Building_Android.md b/docs/developers/Building_Android.md index 5ce865f7b..5d1af915e 100644 --- a/docs/developers/Building_Android.md +++ b/docs/developers/Building_Android.md @@ -1,26 +1,26 @@ # Building Android -The following instructions apply to **v1.2 and later**. For earlier versions the best documentation is https://github.com/vcmi/vcmi-android/blob/master/building.txt (and reading scripts in that repo), however very limited to no support will be provided from our side if you wish to go down that rabbit hole. +The following instructions apply to **v1.2 and later**. For earlier versions the best documentation is (and reading scripts in that repo), however very limited to no support will be provided from our side if you wish to go down that rabbit hole. *Note*: building has been tested only on Linux and macOS. It may or may not work on Windows out of the box. ## Requirements -1. CMake 3.20+: download from your package manager or from https://cmake.org/download/ +1. CMake 3.20+: download from your package manager or from 2. JDK 11, not necessarily from Oracle -3. Android command line tools or Android Studio for your OS: https://developer.android.com/studio/ +3. Android command line tools or Android Studio for your OS: 4. Android NDK version **r25c (25.2.9519653)**, there're multiple ways to obtain it: - install with Android Studio - install with `sdkmanager` command line tool - - download from https://developer.android.com/ndk/downloads + - download from - download with Conan, see [#NDK and Conan](#ndk-and-conan) 5. Optional: - - Ninja: download from your package manager or from https://github.com/ninja-build/ninja/releases - - Ccache: download from your package manager or from https://github.com/ccache/ccache/releases + - Ninja: download from your package manager or from + - Ccache: download from your package manager or from ## Obtaining source code -Clone https://github.com/vcmi/vcmi with submodules. Example for command line: +Clone with submodules. Example for command line: ``` git clone --recurse-submodules https://github.com/vcmi/vcmi.git @@ -31,6 +31,7 @@ git clone --recurse-submodules https://github.com/vcmi/vcmi.git We use Conan package manager to build/consume dependencies, find detailed usage instructions [here](./Conan.md). Note that the link points to the state of the current branch, for the latest release check the same document in the [master branch](https://github.com/vcmi/vcmi/blob/master/docs/developers/Сonan.md). On the step where you need to replace **PROFILE**, choose: + - `android-32` to build for 32-bit architecture (armeabi-v7a) - `android-64` to build for 64-bit architecture (aarch64-v8a) @@ -38,7 +39,7 @@ On the step where you need to replace **PROFILE**, choose: Conan must be aware of the NDK location when you execute `conan install`. There're multiple ways to achieve that as written in the [Conan docs](https://docs.conan.io/1/integrations/cross_platform/android.html): -- the easiest is to download NDK from Conan (option 1 in the docs), then all the magic happens automatically. On the step where you need to replace **PROFILE**, choose _android-**X**-ndk_ where _**X**_ is either `32` or `64`. +- the easiest is to download NDK from Conan (option 1 in the docs), then all the magic happens automatically. On the step where you need to replace **PROFILE**, choose *android-**X**-ndk* where ***X*** is either `32` or `64`. - to use an already installed NDK, you can simply pass it on the command line to `conan install`: (note that this will work only when consuming the pre-built binaries) ``` diff --git a/docs/developers/Building_Linux.md b/docs/developers/Building_Linux.md index 4a9b42cf1..d4cabb57b 100644 --- a/docs/developers/Building_Linux.md +++ b/docs/developers/Building_Linux.md @@ -11,15 +11,15 @@ Older distributions and compilers might work, but they aren't tested by Github C To compile, the following packages (and their development counterparts) are needed to build: -- CMake -- SDL2 with devel packages: mixer, image, ttf -- zlib and zlib-devel -- Boost C++ libraries v1.48+: program-options, filesystem, system, thread, locale -- Recommended, if you want to build launcher or map editor: Qt 5, widget and network modules -- Recommended, FFmpeg libraries, if you want to watch in-game videos: libavformat and libswscale. Their name could be libavformat-devel and libswscale-devel, or ffmpeg-libs-devel or similar names. -- Optional: - - if you want to build scripting modules: LuaJIT - - to speed up recompilation: Ccache +- CMake +- SDL2 with devel packages: mixer, image, ttf +- zlib and zlib-devel +- Boost C++ libraries v1.48+: program-options, filesystem, system, thread, locale +- Recommended, if you want to build launcher or map editor: Qt 5, widget and network modules +- Recommended, FFmpeg libraries, if you want to watch in-game videos: libavformat and libswscale. Their name could be libavformat-devel and libswscale-devel, or ffmpeg-libs-devel or similar names. +- Optional: + - if you want to build scripting modules: LuaJIT + - to speed up recompilation: Ccache ### On Debian-based systems (e.g. Ubuntu) @@ -41,7 +41,7 @@ NOTE: `fuzzylite-devel` package is no longer available in recent version of Fedo On Arch-based distributions, there is a development package available for VCMI on the AUR. -It can be found at https://aur.archlinux.org/packages/vcmi-git/ +It can be found at Information about building packages from the Arch User Repository (AUR) can be found at the Arch wiki. @@ -109,9 +109,9 @@ This will generate `vcmiclient`, `vcmiserver`, `vcmilauncher` as well as .so lib ### RPM package -The first step is to prepare a RPM build environment. On Fedora systems you can follow this guide: http://fedoraproject.org/wiki/How_to_create_an_RPM_package#SPEC_file_overview +The first step is to prepare a RPM build environment. On Fedora systems you can follow this guide: -0. Enable RPMFusion free repo to access to ffmpeg libs: +1. Enable RPMFusion free repo to access to ffmpeg libs: ```sh sudo dnf install https://download1.rpmfusion.org/free/fedora/rpmfusion-free-release-$(rpm -E %fedora).noarch.rpm @@ -120,33 +120,34 @@ sudo dnf install https://download1.rpmfusion.org/free/fedora/rpmfusion-free-rele > [!NOTE] > The stock ffmpeg from Fedora repo is no good as it lacks a lots of codecs -1. Perform a git clone from a tagged branch for the right Fedora version from https://github.com/rpmfusion/vcmi; for example for Fedora 38:
git clone -b f38 --single-branch https://github.com/rpmfusion/vcmi.git
+2. Perform a git clone from a tagged branch for the right Fedora version from ; for example for Fedora 38:
git clone -b f38 --single-branch https://github.com/rpmfusion/vcmi.git
-2. Copy all files to ~/rpmbuild/SPECS with command:
cp vcmi/*  ~/rpmbuild/SPECS
+3. Copy all files to ~/rpmbuild/SPECS with command:
cp vcmi/*  ~/rpmbuild/SPECS
-3. Fetch all sources by using spectool: +4. Fetch all sources by using spectool: ```sh sudo dnf install rpmdevtools spectool -g -R ~/rpmbuild/SPECS/vcmi.spec ``` -4. Fetch all dependencies required to build the RPM: +5. Fetch all dependencies required to build the RPM: ```sh sudo dnf install dnf-plugins-core sudo dnf builddep ~/rpmbuild/SPECS/vcmi.spec ``` -4. Go to ~/rpmbuild/SPECS and open terminal in this folder and type: +6. Go to ~/rpmbuild/SPECS and open terminal in this folder and type: + ```sh rpmbuild -ba ~/rpmbuild/SPECS/vcmi.spec ``` -5. Generated RPM is in folder ~/rpmbuild/RPMS +7. Generated RPM is in folder ~/rpmbuild/RPMS If you want to package the generated RPM above for different processor architectures and operating systems you can use the tool mock. -Moreover, it is necessary to install mock-rpmfusion_free due to the packages ffmpeg-devel and ffmpeg-libs which aren't available in the standard RPM repositories(at least for Fedora). Go to ~/rpmbuild/SRPMS in terminal and type: +Moreover, it is necessary to install mock-rpmfusion_free due to the packages ffmpeg-devel and ffmpeg-libs which aren't available in the standard RPM repositories(at least for Fedora). Go to ~/rpmbuild/SRPMS in terminal and type: ```sh mock -r fedora-38-aarch64-rpmfusion_free path_to_source_RPM diff --git a/docs/developers/Building_Windows.md b/docs/developers/Building_Windows.md index 91f7a1fd2..8df20783d 100644 --- a/docs/developers/Building_Windows.md +++ b/docs/developers/Building_Windows.md @@ -12,8 +12,8 @@ Windows builds can be made in more than one way and with more than one tool. Thi - CMake [download link](https://cmake.org/download/). During install after accepting license agreement make sure to check "Add CMake to the system PATH for all users". - To unpack pre-build Vcpkg: [7-zip](http://www.7-zip.org/download.html) - Optional: - - To create installer: [NSIS](http://nsis.sourceforge.net/Main_Page) - - To speed up recompilation: [CCache](https://github.com/ccache/ccache/releases) + - To create installer: [NSIS](http://nsis.sourceforge.net/Main_Page) + - To speed up recompilation: [CCache](https://github.com/ccache/ccache/releases) ### Choose an installation directory @@ -21,12 +21,14 @@ Create a directory for VCMI development, eg. `C:\VCMI` We will call this directo Warning! Replace `%VCMI_DIR%` with path you've chosen for VCMI installation in the following commands. -It is recommended to avoid non-ascii characters in the path to your working folders. The folder should not be write-protected by system. +It is recommended to avoid non-ascii characters in the path to your working folders. The folder should not be write-protected by system. Good locations: + - `C:\VCMI` Bad locations: + - `C:\Users\Michał\VCMI (non-ascii character)` - `C:\Program Files (x86)\VCMI (write protection)` @@ -38,13 +40,14 @@ You have two options: to use pre-built libraries or build your own. We strongly #### Download and unpack archive -Vcpkg Archives are available at our GitHub: https://github.com/vcmi/vcmi-deps-windows/releases +Vcpkg Archives are available at our GitHub: - Download latest version available. EG: v1.6 assets - [vcpkg-export-x64-windows-v143.7z](https://github.com/vcmi/vcmi-deps-windows/releases/download/v1.6/vcpkg-export-x64-windows-v143.7z) - Extract archive by right clicking on it and choosing "7-zip -> Extract Here". #### Move dependencies to target directory + Once extracted, a `vcpkg` directory will appear with `installed` and `scripts` subfolders inside. Move extracted `vcpkg` directory into your `%VCMI_DIR%` @@ -57,19 +60,21 @@ Be aware that building Vcpkg might take a lot of time depend on your CPU model a #### Clone vcpkg -1. open SourceTree -2. File -\> Clone -3. select **** as source -4. select **%VCMI_DIR%/vcpkg** as destination -5. click **Clone** +1. open SourceTree +2. File -\> Clone +3. select **** as source +4. select **%VCMI_DIR%/vcpkg** as destination +5. click **Clone** From command line use: - git clone https://github.com/microsoft/vcpkg.git %VCMI_DIR%/vcpkg +```sh +git clone https://github.com/microsoft/vcpkg.git %VCMI_DIR%/vcpkg +``` #### Build vcpkg and dependencies -- Run +- Run `%VCMI_DIR%/vcpkg/bootstrap-vcpkg.bat` - For 32-bit build run: `%VCMI_DIR%/vcpkg/vcpkg.exe install tbb:x64-windows fuzzylite:x64-windows sdl2:x64-windows sdl2-image:x64-windows sdl2-ttf:x64-windows sdl2-mixer[mpg123]:x64-windows boost:x64-windows qt5-base:x64-windows ffmpeg:x64-windows luajit:x64-windows` @@ -85,6 +90,7 @@ Extract `ccache` to a folder of your choosing, add the folder to the `PATH` envi ## Build VCMI #### From GIT GUI + - Open SourceTree - File -> Clone - select `https://github.com/vcmi/vcmi/` as source @@ -94,28 +100,32 @@ Extract `ccache` to a folder of your choosing, add the folder to the `PATH` envi - click Clone #### From command line + - `git clone --recursive https://github.com/vcmi/vcmi.git %VCMI_DIR%/source` ### Generate solution for VCMI + - Create `%VCMI_DIR%/build` folder - Open a command line prompt at `%VCMI_DIR%/build` -- Execute `cd %VCMI_DIR%/build` +- Execute `cd %VCMI_DIR%/build` - Create solution (Visual Studio 2022 64-bit) `cmake %VCMI_DIR%/source -DCMAKE_TOOLCHAIN_FILE=%VCMI_DIR%/vcpkg/scripts/buildsystems/vcpkg.cmake -G "Visual Studio 17 2022" -A x64` ### Compile VCMI with Visual Studio + - Open `%VCMI_DIR%/build/VCMI.sln` in Visual Studio - Select `Release` build type in the combobox - If you want to use ccache: - - Select `Manage Configurations...` in the combobox - - Specify the following CMake variable: `ENABLE_CCACHE=ON` - - See the [Visual Studio documentation](https://learn.microsoft.com/en-us/cpp/build/customize-cmake-settings?view=msvc-170#cmake-variables-and-cache) for details + - Select `Manage Configurations...` in the combobox + - Specify the following CMake variable: `ENABLE_CCACHE=ON` + - See the [Visual Studio documentation](https://learn.microsoft.com/en-us/cpp/build/customize-cmake-settings?view=msvc-170#cmake-variables-and-cache) for details - Right click on `BUILD_ALL` project. This `BUILD_ALL` project should be in `CMakePredefinedTargets` tree in Solution Explorer. - VCMI will be built in `%VCMI_DIR%/build/bin` folder! ### Compile VCMI with MinGW via MSYS2 -- Install MSYS2 from https://www.msys2.org/ + +- Install MSYS2 from - Start the `MSYS MinGW x64`-shell -- Install dependencies: `pacman -S mingw-w64-x86_64-SDL2 mingw-w64-x86_64-SDL2_image mingw-w64-x86_64-SDL2_mixer mingw-w64-x86_64-SDL2_ttf mingw-w64-x86_64-boost mingw-w64-x86_64-gcc mingw-w64-x86_64-ninja mingw-w64-x86_64-qt5-static` +- Install dependencies: `pacman -S mingw-w64-x86_64-SDL2 mingw-w64-x86_64-SDL2_image mingw-w64-x86_64-SDL2_mixer mingw-w64-x86_64-SDL2_ttf mingw-w64-x86_64-boost mingw-w64-x86_64-gcc mingw-w64-x86_64-ninja mingw-w64-x86_64-qt5-static mingw-w64-x86_64-qt5-tools mingw-w64-x86_64-tbb` - Generate and build solution from VCMI-root dir: `cmake --preset windows-mingw-release && cmake --build --preset windows-mingw-release` **NOTE:** This will link Qt5 statically to `VCMI_launcher.exe` and `VCMI_Mapeditor.exe`. See [PR #3421](https://github.com/vcmi/vcmi/pull/3421) for some background. @@ -134,8 +144,10 @@ Vcpkg might be very unstable due to limited popularity and fact of using bleedin Pre-built version we provide is always manually tested with all supported versions of MSVC for both Release and Debug builds and all known quirks are listed below. -#$# Build is successful but can not start new game +### Build is successful but can not start new game + Make sure you have: + * Installed Heroes III from disk or using GOG installer * Copied `Data`, `Maps` and `Mp3` folders from Heroes III to: `%USERPROFILE%\Documents\My Games\vcmi\` diff --git a/docs/developers/Building_iOS.md b/docs/developers/Building_iOS.md index a922e44a1..d9e98073b 100644 --- a/docs/developers/Building_iOS.md +++ b/docs/developers/Building_iOS.md @@ -6,7 +6,7 @@ 2. Xcode: 3. CMake 3.21+: `brew install --cask cmake` or get from 4. Optional: - - CCache to speed up recompilation: `brew install ccache` + - CCache to speed up recompilation: `brew install ccache` ## Obtaining source code diff --git a/docs/developers/Building_macOS.md b/docs/developers/Building_macOS.md index 71a1ae644..0f3c40a43 100644 --- a/docs/developers/Building_macOS.md +++ b/docs/developers/Building_macOS.md @@ -91,7 +91,7 @@ Open `VCMI.xcodeproj` from the build directory, select `vcmiclient` scheme and h ## Packaging project into DMG file -After building, run `cpack` from the build directory. If using Xcode generator, also pass `-C ` with the same configuration that you used to build the project. +After building, run `cpack` from the build directory. If using Xcode generator, also pass `-C ` with the same configuration that you used to build the project. If you use Conan, it's expected that you use **conan-generated** directory at step 4 of [Conan package manager](Conan.md). diff --git a/docs/developers/CMake.md b/docs/developers/CMake.md index 140d7ef89..4295bef1b 100644 --- a/docs/developers/CMake.md +++ b/docs/developers/CMake.md @@ -1,23 +1,21 @@ # CMake options * `-D CMAKE_BUILD_TYPE=Debug` - * Enables debug info and disables optimizations + * Enables debug info and disables optimizations * `-D CMAKE_EXPORT_COMPILE_COMMANDS=ON` - * Creates `compile_commands.json` for [clangd](https://clangd.llvm.org/) language server. - - For clangd to find the JSON, create a file named `.clangd` with this content - ``` - CompileFlags: - CompilationDatabase: build - ``` - and place it here: - ``` - . - ├── vcmi -> contains sources and is under git control - ├── build -> contains build output, makefiles, object files,... - └── .clangd - ``` + * Creates `compile_commands.json` for [clangd](https://clangd.llvm.org/) language server. For clangd to find the JSON, create a file named `.clangd` with this content + ``` + CompileFlags: + CompilationDatabase: build + ``` + and place it here: + ``` + . + ├── vcmi -> contains sources and is under git control + ├── build -> contains build output, makefiles, object files,... + └── .clangd + ``` * `-D ENABLE_CCACHE:BOOL=ON` - * Speeds up recompilation + * Speeds up recompilation * `-G Ninja` - * Use Ninja build system instead of Make, which speeds up the build and doesn't require a `-j` flag \ No newline at end of file + * Use Ninja build system instead of Make, which speeds up the build and doesn't require a `-j` flag diff --git a/docs/developers/Code_Structure.md b/docs/developers/Code_Structure.md index 9f3e2c802..ee3384670 100644 --- a/docs/developers/Code_Structure.md +++ b/docs/developers/Code_Structure.md @@ -29,9 +29,10 @@ Most of VCMI configuration files uses Json format and located in "config" direct ### Main purposes of client Client is responsible for: -- displaying state of game to human player -- capturing player's actions and sending requests to server -- displaying changes in state of game indicated by server + +- displaying state of game to human player +- capturing player's actions and sending requests to server +- displaying changes in state of game indicated by server ### Rendering of graphics @@ -44,9 +45,9 @@ In rendering, Interface object system is quite helpful. Its base is CIntObject c Server is responsible for: -- maintaining state of the game -- handling requests from all clients participating in game -- informing all clients about changes in state of the game that are +- maintaining state of the game +- handling requests from all clients participating in game +- informing all clients about changes in state of the game that are visible to them ## Lib @@ -59,11 +60,11 @@ iOS platform pioneered single process build, where server is a static library an Lib contains code responsible for: -- handling most of Heroes III files (.lod, .txt setting files) -- storing information common to server and client like state of the game -- managing armies, buildings, artifacts, spells, bonuses and other game objects -- handling general game mechanics and related actions (only adventure map objects; it's an unwanted remnant of past development - all game mechanics should be handled by the server) -- networking and serialization +- handling most of Heroes III files (.lod, .txt setting files) +- storing information common to server and client like state of the game +- managing armies, buildings, artifacts, spells, bonuses and other game objects +- handling general game mechanics and related actions (only adventure map objects; it's an unwanted remnant of past development - all game mechanics should be handled by the server) +- networking and serialization #### Serialization @@ -94,7 +95,6 @@ Forward declarations of the lib in headers of other parts of the project need to `` `` - ##### New project part If you're creating new project part, place `VCMI_LIB_USING_NAMESPACE` in its `StdInc.h` to be able to use lib classes without explicit namespace in implementation files. Example: @@ -121,4 +121,4 @@ VCMI includes [FuzzyLite](http://code.google.com/p/fuzzy-lite/) library to make ### Duels -### ERM parser \ No newline at end of file +### ERM parser diff --git a/docs/developers/Coding_Guidelines.md b/docs/developers/Coding_Guidelines.md index 89bad05a6..5c1706cc3 100644 --- a/docs/developers/Coding_Guidelines.md +++ b/docs/developers/Coding_Guidelines.md @@ -4,7 +4,7 @@ VCMI implementation bases on C++17 standard. Any feature is acceptable as long as it's will pass build on our CI, but there is list below on what is already being used. -Any compiler supporting C++17 should work, but this has not been thoroughly tested. You can find information about extensions and compiler support at http://en.cppreference.com/w/cpp/compiler_support +Any compiler supporting C++17 should work, but this has not been thoroughly tested. You can find information about extensions and compiler support at ## Style Guidelines @@ -20,7 +20,7 @@ Inside a code block put the opening brace on the next line after the current sta Good: -``` cpp +```cpp if(a) { code(); @@ -30,7 +30,7 @@ if(a) Bad: -``` cpp +```cpp if(a) { code(); code(); @@ -41,14 +41,14 @@ Avoid using unnecessary open/close braces, vertical space is usually limited: Good: -``` cpp +```cpp if(a) code(); ``` Bad: -``` cpp +```cpp if(a) { code(); } @@ -58,7 +58,7 @@ Unless there are either multiple hierarchical conditions being used or that the Good: -``` cpp +```cpp if(a) { if(b) @@ -68,7 +68,7 @@ if(a) Bad: -``` cpp +```cpp if(a) if(b) code(); @@ -78,7 +78,7 @@ If there are brackets inside the body, outside brackets are required. Good: -``` cpp +```cpp if(a) { for(auto elem : list) @@ -90,7 +90,7 @@ if(a) Bad: -``` cpp +```cpp if(a) for(auto elem : list) { @@ -102,7 +102,7 @@ If "else" branch has brackets then "if" should also have brackets even if it is Good: -``` cpp +```cpp if(a) { code(); @@ -118,7 +118,7 @@ else Bad: -``` cpp +```cpp if(a) code(); else @@ -134,7 +134,7 @@ If you intentionally want to avoid usage of "else if" and keep if body indent ma Good: -``` cpp +```cpp if(a) { code(); @@ -148,7 +148,7 @@ else Bad: -``` cpp +```cpp if(a) code(); else @@ -160,7 +160,7 @@ When defining a method, use a new line for the brace, like this: Good: -``` cpp +```cpp void method() { } @@ -168,7 +168,7 @@ void method() Bad: -``` cpp +```cpp void Method() { } ``` @@ -179,14 +179,14 @@ Use white space in expressions liberally, except in the presence of parenthesis. **Good:** -``` cpp +```cpp if(a + 5 > method(blah('a') + 4)) foo += 24; ``` **Bad:** -``` cpp +```cpp if(a+5>method(blah('a')+4)) foo+=24; ``` @@ -199,13 +199,13 @@ Use a space before and after the address or pointer character in a pointer decla Good: -``` cpp +```cpp CIntObject * images[100]; ``` Bad: -``` cpp +```cpp CIntObject* images[100]; or CIntObject *images[100]; ``` @@ -214,14 +214,14 @@ Do not use spaces before parentheses. Good: -``` cpp +```cpp if(a) code(); ``` Bad: -``` cpp +```cpp if (a) code(); ``` @@ -230,7 +230,7 @@ Do not use extra spaces around conditions inside parentheses. Good: -``` cpp +```cpp if(a && b) code(); @@ -240,7 +240,7 @@ if(a && (b || c)) Bad: -``` cpp +```cpp if( a && b ) code(); @@ -252,14 +252,14 @@ Do not use more than one space between operators. Good: -``` cpp +```cpp if((a && b) || (c + 1 == d)) code(); ``` Bad: -``` cpp +```cpp if((a && b) || (c + 1 == d)) code(); @@ -273,14 +273,14 @@ When allocating objects, don't use parentheses for creating stack-based objects Good: -``` cpp +```cpp std::vector v; CGBoat btn = new CGBoat(); ``` Bad: -``` cpp +```cpp std::vector v(); // shouldn't compile anyway CGBoat btn = new CGBoat; ``` @@ -289,14 +289,14 @@ Avoid overuse of parentheses: Good: -``` cpp +```cpp if(a && (b + 1)) return c == d; ``` Bad: -``` cpp +```cpp if((a && (b + 1))) return (c == d); ``` @@ -305,7 +305,7 @@ if((a && (b + 1))) Base class list must be on same line with class name. -``` cpp +```cpp class CClass : public CClassBaseOne, public CClassBaseOne { int id; @@ -321,7 +321,7 @@ When 'private:', 'public:' and other labels are not on the line after opening br Good: -``` cpp +```cpp class CClass { int id; @@ -333,7 +333,7 @@ public: Bad: -``` cpp +```cpp class CClass { int id; @@ -344,7 +344,7 @@ public: Good: -``` cpp +```cpp class CClass { protected: @@ -357,7 +357,7 @@ public: Bad: -``` cpp +```cpp class CClass { @@ -373,7 +373,7 @@ public: Constructor member and base class initialization must be on new line, indented with tab with leading colon. -``` cpp +```cpp CClass::CClass() : CClassBaseOne(true, nullptr), id(0), bool parameters(false) { @@ -387,7 +387,7 @@ Switch statements have the case at the same indentation as the switch. Good: -``` cpp +```cpp switch(alignment) { case EAlignment::EVIL: @@ -407,7 +407,7 @@ default: Bad: -``` cpp +```cpp switch(alignment) { case EAlignment::EVIL: @@ -447,7 +447,7 @@ break; Good: -``` cpp +```cpp auto lambda = [this, a, &b](int3 & tile, int index) -> bool { do_that(); @@ -456,7 +456,7 @@ auto lambda = [this, a, &b](int3 & tile, int index) -> bool Bad: -``` cpp +```cpp auto lambda = [this,a,&b](int3 & tile, int index)->bool{do_that();}; ``` @@ -464,7 +464,7 @@ Empty parameter list is required even if function takes no arguments. Good: -``` cpp +```cpp auto lambda = []() { do_that(); @@ -473,7 +473,7 @@ auto lambda = []() Bad: -``` cpp +```cpp auto lambda = [] { do_that(); @@ -484,7 +484,7 @@ Do not use inline lambda expressions inside if-else, for and other conditions. Good: -``` cpp +```cpp auto lambda = []() { do_that(); @@ -497,7 +497,7 @@ if(lambda) Bad: -``` cpp +```cpp if([]() { do_that(); @@ -511,7 +511,7 @@ Do not pass inline lambda expressions as parameter unless it's the last paramete Good: -``` cpp +```cpp auto lambda = []() { do_that(); @@ -521,7 +521,7 @@ obj->someMethod(lambda, true); Bad: -``` cpp +```cpp obj->someMethod([]() { do_that(); @@ -530,7 +530,7 @@ obj->someMethod([]() Good: -``` cpp +```cpp obj->someMethod(true, []() { do_that(); @@ -543,7 +543,7 @@ Serialization of each element must be on it's own line since this make debugging Good: -``` cpp +```cpp template void serialize(Handler & h, const int version) { h & identifier; @@ -555,7 +555,7 @@ template void serialize(Handler & h, const int version) Bad: -``` cpp +```cpp template void serialize(Handler & h, const int version) { h & identifier & description & name & dependencies; @@ -566,7 +566,7 @@ Save backward compatibility code is exception when extra brackets are always use Good: -``` cpp +```cpp template void serialize(Handler & h, const int version) { h & identifier; @@ -586,7 +586,7 @@ template void serialize(Handler & h, const int version) Bad: -``` cpp +```cpp template void serialize(Handler & h, const int version) { h & identifier; @@ -604,7 +604,7 @@ template void serialize(Handler & h, const int version) For any new files, please paste the following info block at the very top of the source file: -``` cpp +```cpp /* * Name_of_File.h, part of VCMI engine * @@ -622,13 +622,13 @@ The above notice have to be included both in header and source files (.h/.cpp). For any header or source file code must be in following order: -1. Licensing information -2. pragma once preprocessor directive -3. include directives -4. Forward declarations -5. All other code +1. Licensing information +2. pragma once preprocessor directive +3. include directives +4. Forward declarations +5. All other code -``` cpp +```cpp /* * Name_of_File.h, part of VCMI engine * @@ -652,7 +652,7 @@ If you comment on the same line with code there must be one single space between Good: -``` cpp +```cpp if(a) { code(); //Do something @@ -665,7 +665,7 @@ else // Do something. Bad: -``` cpp +```cpp if(a) { code();//Do something @@ -680,7 +680,7 @@ If you add single-line comment on own line slashes must have same indent as code Good: -``` cpp +```cpp // Do something if(a) { @@ -692,7 +692,7 @@ if(a) Bad: -``` cpp +```cpp // Do something if(a) { @@ -706,7 +706,7 @@ Avoid comments inside multi-line if-else conditions. If your conditions are too Good: -``` cpp +```cpp bool isMyHeroAlive = a && b || (c + 1 > 15); bool canMyHeroMove = myTurn && hero.movePoints > 0; if(isMyHeroAlive && canMyHeroMove) @@ -717,7 +717,7 @@ if(isMyHeroAlive && canMyHeroMove) Bad: -``` cpp +```cpp if((a && b || (c + 1 > 15)) //Check if hero still alive && myTurn && hero.movePoints > 0) //Check if hero can move { @@ -727,7 +727,7 @@ if((a && b || (c + 1 > 15)) //Check if hero still alive You should write a comment before the class definition which describes shortly the class. 1-2 sentences are enough. Methods and class data members should be commented if they aren't self-describing only. Getters/Setters, simple methods where the purpose is clear or similar methods shouldn't be commented, because vertical space is usually limited. The style of documentation comments should be the three slashes-style: ///. -``` cpp +```cpp /// Returns true if a debug/trace log message will be logged, false if not. /// Useful if performance is important and concatenating the log message is a expensive task. bool isDebugEnabled() const; @@ -738,7 +738,7 @@ The above example doesn't follow a strict scheme on how to comment a method. It If you need a more detailed description for a method you can use such style: -``` cpp +```cpp /// /// /// @@ -749,7 +749,7 @@ If you need a more detailed description for a method you can use such style: /// @return Description of the return value ``` -A good essay about writing comments: http://ardalis.com/when-to-comment-your-code +A good essay about writing comments: ### Casing @@ -775,7 +775,7 @@ Outdated. There is separate entry for [Logging API](Logging_API.md) If you want to trace the control flow of VCMI, then you should use the macro LOG_TRACE or LOG_TRACE_PARAMS. The first one prints a message when the function is entered or leaved. The name of the function will also be logged. In addition to this the second macro, let's you specify parameters which you want to print. You should print traces with parameters like this: -``` cpp +```cpp LOG_TRACE_PARAMS(logGlobal, "hero '%s', spellId '%d', pos '%s'.", hero, spellId, pos); ``` @@ -797,14 +797,14 @@ Do not use uncommon abbreviations for class, method, parameter and global object Bad: -``` cpp +```cpp CArt * getRandomArt(...) class CIntObject ``` Good: -``` cpp +```cpp CArtifact * getRandomArtifact(...) class CInterfaceObject ``` @@ -827,7 +827,7 @@ The header StdInc.h should be included in every compilation unit. It has to be i Do not declare enumerations in global namespace. It is better to use strongly typed enum or to wrap them in class or namespace to avoid polluting global namespace: -``` cpp +```cpp enum class EAlignment { GOOD, @@ -848,7 +848,7 @@ namespace EAlignment If the comment duplicates the name of commented member, it's better if it wouldn't exist at all. It just increases maintenance cost. Bad: -``` cpp +```cpp size_t getHeroesCount(); //gets count of heroes (surprise?) ``` @@ -862,16 +862,16 @@ Don't return const objects or primitive types from functions -- it's pointless. Bad: -``` cpp +```cpp const std::vector guardingCreatures(int3 pos) const; ``` Good: -``` cpp +```cpp std::vector guardingCreatures(int3 pos) const; ``` ## Sources -[Mono project coding guidelines](http://www.mono-project.com/Coding_Guidelines) \ No newline at end of file +[Mono project coding guidelines](http://www.mono-project.com/Coding_Guidelines) diff --git a/docs/developers/Conan.md b/docs/developers/Conan.md index 8c754ce46..0e8a1f162 100644 --- a/docs/developers/Conan.md +++ b/docs/developers/Conan.md @@ -27,7 +27,7 @@ The following platforms are supported and known to work, others might require ch - **Windows**: libraries are built with x86_64-mingw-w64-gcc version 10 (which is available in repositories of Ubuntu 22.04) - **Android**: libraries are built with NDK r25c (25.2.9519653) -2. Download the binaries archive and unpack it to `~/.conan` directory from https://github.com/vcmi/vcmi-dependencies/releases/latest +2. Download the binaries archive and unpack it to `~/.conan` directory from - macOS: pick **dependencies-mac-intel.txz** if you have Intel Mac, otherwise - **dependencies-mac-arm.txz** - iOS: pick ***dependencies-ios.txz*** @@ -65,7 +65,7 @@ If you use `--build=never` and this command fails, then it means that you can't VCMI "recipe" also has some options that you can specify. For example, if you don't care about game videos, you can disable FFmpeg dependency by passing `-o with_ffmpeg=False`. If you only want to make release build, you can use `GENERATE_ONLY_BUILT_CONFIG=1` environment variable to skip generating files for other configurations (our CI does this). -_Note_: you can find full reference of this command [in the official documentation](https://docs.conan.io/1/reference/commands/consumer/install.html) or by executing `conan help install`. +*Note*: you can find full reference of this command [in the official documentation](https://docs.conan.io/1/reference/commands/consumer/install.html) or by executing `conan help install`. ### Using our prebuilt binaries for macOS/iOS @@ -86,7 +86,7 @@ This subsection describes platform specifics to build libraries from source prop #### Building for macOS/iOS -- To build Locale module of Boost in versions >= 1.81, you must use `compiler.cppstd=11` Conan setting (our profiles already contain it). To use it with another profile, either add this setting to your _host_ profile or pass `-s compiler.cppstd=11` on the command line. +- To build Locale module of Boost in versions >= 1.81, you must use `compiler.cppstd=11` Conan setting (our profiles already contain it). To use it with another profile, either add this setting to your *host* profile or pass `-s compiler.cppstd=11` on the command line. - If you wish to build dependencies against system libraries (like our prebuilt ones do), follow [below instructions](#using-recipes-for-system-libraries) executing `conan create` for all directories. Don't forget to pass `-o with_apple_system_libs=True` to `conan install` afterwards. #### Building for Android @@ -105,11 +105,11 @@ After applying patch(es): 2. Run `make` 3. Copy file `qtbase/jar/QtAndroid.jar` from the build directory to the **package directory**, e.g. `~/.conan/data/qt/5.15.14/_/_/package/SOME_HASH/jar`. -_Note_: if you plan to build Qt from source again, then you don't need to perform the above _After applying patch(es)_ steps after building. +*Note*: if you plan to build Qt from source again, then you don't need to perform the above *After applying patch(es)* steps after building. ##### Using recipes for system libraries -1. Clone/download https://github.com/kambala-decapitator/conan-system-libs +1. Clone/download 2. Execute `conan create PACKAGE vcmi/CHANNEL`, where `PACKAGE` is a directory path in that repository and `CHANNEL` is **apple** for macOS/iOS and **android** for Android. Do it for each library you need. 3. Now you can execute `conan install` to build all dependencies. @@ -172,7 +172,7 @@ cmake --preset ios-conan `CMakeUserPresets.json` file: -```json +```json5 { "version": 3, "cmakeMinimumRequired": { diff --git a/docs/developers/Development_with_Qt_Creator.md b/docs/developers/Development_with_Qt_Creator.md index 4e3c96f8e..66c621781 100644 --- a/docs/developers/Development_with_Qt_Creator.md +++ b/docs/developers/Development_with_Qt_Creator.md @@ -6,7 +6,7 @@ Qt Creator is the recommended IDE for VCMI development on Linux distributions, b - Almost no manual configuration when used with CMake. Project configuration is read from CMake text files, - Easy to setup and use with multiple different compiler toolchains: GCC, Visual Studio, Clang -You can install Qt Creator from repository, but better to stick to latest version from Qt website: https://www.qt.io/download-qt-installer-oss +You can install Qt Creator from repository, but better to stick to latest version from Qt website: ## Configuration @@ -21,4 +21,4 @@ The build dir should be set to something like /trunk/build for the debug build a There is a problem with QtCreator when debugging both vcmiclient and vcmiserver. If you debug the vcmiclient, start a game, attach the vcmiserver process to the gdb debugger(Debug \> Start Debugging \> Attach to Running External Application...) then breakpoints which are set for vcmiserver will be ignored. This looks like a bug, in any case it's not intuitively. Two workarounds are available luckily: 1. Run vcmiclient (no debug mode), then attach server process to the debugger -2. Open two instances of QtCreator and debug vcmiserver and vcmiclient separately(it works!) \ No newline at end of file +2. Open two instances of QtCreator and debug vcmiserver and vcmiclient separately (it works!) diff --git a/docs/developers/Logging_API.md b/docs/developers/Logging_API.md index a3ce23b76..dd81fdeb2 100644 --- a/docs/developers/Logging_API.md +++ b/docs/developers/Logging_API.md @@ -2,14 +2,14 @@ ## Features -- A logger belongs to a "domain", this enables us to change log level settings more selectively -- The log format can be customized -- The color of a log entry can be customized based on logger domain and logger level -- Logger settings can be changed in the settings.json file -- No std::endl at the end of a log entry required -- Thread-safe -- Macros for tracing the application flow -- Provides stream-like and function-like logging +- A logger belongs to a "domain", this enables us to change log level settings more selectively +- The log format can be customized +- The color of a log entry can be customized based on logger domain and logger level +- Logger settings can be changed in the settings.json file +- No std::endl at the end of a log entry required +- Thread-safe +- Macros for tracing the application flow +- Provides stream-like and function-like logging ## Class diagram @@ -17,14 +17,14 @@ Some notes: -- There are two methods `configure` and `configureDefault` of the class `CBasicLogConfigurator` to initialize and setup the logging system. The latter one setups default logging and isn't dependent on VCMI's filesystem, whereas the first one setups logging based on the user's settings which can be configured in the settings.json. -- The methods `isDebugEnabled` and `isTraceEnabled` return true if a log record of level debug respectively trace will be logged. This can be useful if composing the log message is a expensive task and performance is important. +- There are two methods `configure` and `configureDefault` of the class `CBasicLogConfigurator` to initialize and setup the logging system. The latter one setups default logging and isn't dependent on VCMI's filesystem, whereas the first one setups logging based on the user's settings which can be configured in the settings.json. +- The methods `isDebugEnabled` and `isTraceEnabled` return true if a log record of level debug respectively trace will be logged. This can be useful if composing the log message is a expensive task and performance is important. ## Usage ### Setup settings.json -``` javascript +```json5 { "logging" : { "console" : { @@ -68,7 +68,7 @@ The following code shows how the logging system can be configured: If `configureDefault` or `configure` won't be called, then logs aren't written either to the console or to the file. The default logging setups a system like this: -**Console** +#### Console Format: %m Threshold: info @@ -76,17 +76,18 @@ coloredOutputEnabled: true colorMapping: trace -\> gray, debug -\> white, info -\> green, warn -\> yellow, error -\> red -**File** +#### File Format: %d %l %n \[%t\] - %m -**Loggers** +#### Loggers global -\> info ### How to get a logger There exist only one logger object per domain. A logger object cannot be copied. You can get access to a logger object by using the globally defined ones like `logGlobal` or `logAi`, etc... or by getting one manually: + ```cpp Logger * logger = CLogger::getLogger(CLoggerDomain("rmg")); ``` @@ -104,22 +105,22 @@ Don't include a '\n' or std::endl at the end of your log message, a new line wil The following list shows several log levels from the highest one to the lowest one: -- error -\> for errors, e.g. if resource is not available, if a initialization fault has occurred, if a exception has been thrown (can result in program termination) -- warn -\> for warnings, e.g. if sth. is wrong, but the program can continue execution "normally" -- info -\> informational messages, e.g. Filesystem initialized, Map loaded, Server started, etc... -- debug -\> for debugging, e.g. hero moved to (12,3,0), direction 3', 'following artifacts influence X: .. or pattern detected at pos (10,15,0), p-nr. 30, flip 1, repl. 'D' -- trace -\> for logging the control flow, the execution progress or fine-grained events, e.g. hero movement completed, entering CMapEditManager::updateTerrainViews: posx '10', posy '5', width '10', height '10', mapLevel '0',... +- error -\> for errors, e.g. if resource is not available, if a initialization fault has occurred, if a exception has been thrown (can result in program termination) +- warn -\> for warnings, e.g. if sth. is wrong, but the program can continue execution "normally" +- info -\> informational messages, e.g. Filesystem initialized, Map loaded, Server started, etc... +- debug -\> for debugging, e.g. hero moved to (12,3,0), direction 3', 'following artifacts influence X: .. or pattern detected at pos (10,15,0), p-nr. 30, flip 1, repl. 'D' +- trace -\> for logging the control flow, the execution progress or fine-grained events, e.g. hero movement completed, entering CMapEditManager::updateTerrainViews: posx '10', posy '5', width '10', height '10', mapLevel '0',... The following colors are available for console output: -- default -- green -- red -- magenta -- yellow -- white -- gray -- teal +- default +- green +- red +- magenta +- yellow +- white +- gray +- teal ### How to trace execution @@ -143,10 +144,10 @@ The program execution can be traced by using the macros TRACE_BEGIN, TRACE_END a A domain is a specific part of the software. In VCMI there exist several domains: -- network -- ai -- bonus -- network +- network +- ai +- bonus +- network In addition to these domains, there exist always a super domain called "global". Sub-domains can be created with "ai.battle" or "ai.adventure" for example. The dot between the "ai" and "battle" is important and notes the parent-child relationship of those two domains. A few examples how the log level will be inherited: diff --git a/docs/developers/Lua_Scripting_System.md b/docs/developers/Lua_Scripting_System.md index 16124c800..8542a3ea3 100644 --- a/docs/developers/Lua_Scripting_System.md +++ b/docs/developers/Lua_Scripting_System.md @@ -2,7 +2,7 @@ ## Configuration -``` javascript +```json5 { //general purpose script, Lua or ERM, runs on server "myScript": @@ -87,75 +87,75 @@ VCMI uses LuaJIT, which is Lua 5.1 API, see [upstream documentation](https://www Following libraries are supported -- base -- table -- string -- math -- bit +- base +- table +- string +- math +- bit ## ERM ### Features -- no strict limit on function/variable numbers (technical limit 32 bit integer except 0)) -- TODO semi compare -- DONE macros +- no strict limit on function/variable numbers (technical limit 32 bit integer except 0)) +- TODO semi compare +- DONE macros ### Bugs -- TODO Broken XOR support (clashes with \`X\` option) +- TODO Broken XOR support (clashes with \`X\` option) ### Triggers -- TODO **!?AE** Equip/Unequip artifact -- WIP **!?BA** when any battle occurs -- WIP **!?BF** when a battlefield is prepared for a battle -- TODO **!?BG** at every action taken by any stack or by the hero -- TODO **!?BR** at every turn of a battle -- *!?CM (client only) click the mouse button.* -- TODO **!?CO** Commander triggers -- TODO **!?DL** Custom dialogs -- DONE **!?FU** function -- TODO **!?GE** "global" event -- TODO **!?GM** Saving/Loading -- TODO **!?HE** when the hero \# is attacked by an enemy hero or +- TODO **!?AE** Equip/Unequip artifact +- WIP **!?BA** when any battle occurs +- WIP **!?BF** when a battlefield is prepared for a battle +- TODO **!?BG** at every action taken by any stack or by the hero +- TODO **!?BR** at every turn of a battle +- *!?CM (client only) click the mouse button.* +- TODO **!?CO** Commander triggers +- TODO **!?DL** Custom dialogs +- DONE **!?FU** function +- TODO **!?GE** "global" event +- TODO **!?GM** Saving/Loading +- TODO **!?HE** when the hero \# is attacked by an enemy hero or visited by an allied hero -- TODO **!?HL** hero gains a level -- TODO **!?HM** every step a hero \# takes -- *!?IP Multiplayer support.* -- TODO **!?LE** (!$LE) An Event on the map -- WIP **!?MF** stack taking physical damage(before an action) -- TODO **!?MG** casting on the adventure map -- *!?MM scroll text during a battle* -- TODO **!?MR** Magic resistance -- TODO **!?MW** Wandering Monsters -- WIP **!?OB** (!$OB) visiting objects -- DONE **!?PI** Post Instruction. -- TODO **!?SN** Sound and ERA extensions -- *!?TH town hall* -- TODO **!?TL** Real-Time Timer -- TODO **!?TM** timed events +- TODO **!?HL** hero gains a level +- TODO **!?HM** every step a hero \# takes +- *!?IP Multiplayer support.* +- TODO **!?LE** (!$LE) An Event on the map +- WIP **!?MF** stack taking physical damage(before an action) +- TODO **!?MG** casting on the adventure map +- *!?MM scroll text during a battle* +- TODO **!?MR** Magic resistance +- TODO **!?MW** Wandering Monsters +- WIP **!?OB** (!$OB) visiting objects +- DONE **!?PI** Post Instruction. +- TODO **!?SN** Sound and ERA extensions +- *!?TH town hall* +- TODO **!?TL** Real-Time Timer +- TODO **!?TM** timed events ### Receivers #### VCMI -- **!!MC:S@varName@** - declare new "normal" variable (technically +- **!!MC:S@varName@** - declare new "normal" variable (technically v-var with string key) -- TODO Identifier resolver -- WIP Bonus system +- TODO Identifier resolver +- WIP Bonus system #### ERA -- DONE !!if !!el !!en -- TODO !!br !!co -- TODO !!SN:X +- DONE !!if !!el !!en +- TODO !!br !!co +- TODO !!SN:X #### WoG - TODO !!AR Артефакт (ресурс) в определенной позиции - TODO !!BA Битва - - !!BA:A$ return 1 for battle evaluation +- !!BA:A$ return 1 for battle evaluation - TODO !!BF Препятствия на поле боя - TODO !!BG Действий монстров в бою - TODO !!BH Действия героя в бою @@ -201,4 +201,4 @@ Following libraries are supported - *!#VC Контроль переменных* - WIP !!VR Установка переменных -### Persistence \ No newline at end of file +### Persistence diff --git a/docs/developers/Networking.md b/docs/developers/Networking.md index 5c3736abe..7d7b1c386 100644 --- a/docs/developers/Networking.md +++ b/docs/developers/Networking.md @@ -5,12 +5,14 @@ For implementation details see files located at `lib/network` directory. VCMI uses connection using TCP to communicate with server, even in single-player games. However, even though TCP is stream-based protocol, VCMI uses atomic messages for communication. Each message is a serialized stream of bytes, preceded by 4-byte message size: + ``` int32_t messageSize; byte messagePayload[messageSize]; ``` Networking can be used by: + - game client (vcmiclient / VCMI_Client.exe). Actual application that player interacts with directly using UI. - match server (vcmiserver / VCMI_Server.exe / part of game client). This app controls game logic and coordinates multiplayer games. - lobby server (vcmilobby). This app provides access to global lobby through which players can play game over Internet. @@ -28,11 +30,13 @@ For gameplay, VCMI serializes data into a binary stream. See [Serialization](Ser ## Global lobby communication For implementation details see: + - game client: `client/globalLobby/GlobalLobbyClient.h - match server: `server/GlobalLobbyProcessor.h - lobby server: `client/globalLobby/GlobalLobbyClient.h In case of global lobby, message payload uses plaintext json format - utf-8 encoded string: + ``` int32_t messageSize; char jsonString[messageSize]; @@ -43,6 +47,7 @@ Every message must be a struct (json object) that contains "type" field. Unlike ### Communication flow Notes: + - invalid message, such as corrupted json format or failure to validate message will result in no reply from server - in addition to specified messages, match server will send `operationFailed` message on failure to apply player request @@ -51,7 +56,8 @@ Notes: - client -> lobby: `clientRegister` - lobby -> client: `accountCreated` -#### Login +#### Login + - client -> lobby: `clientLogin` - lobby -> client: `loginSuccess` - lobby -> client: `chatHistory` @@ -59,10 +65,12 @@ Notes: - lobby -> client: `activeGameRooms` #### Chat Message + - client -> lobby: `sendChatMessage` - lobby -> every client: `chatMessage` #### New Game Room + - client starts match server instance - match -> lobby: `serverLogin` - lobby -> match: `loginSuccess` @@ -73,19 +81,23 @@ Notes: - lobby -> every client: `activeGameRooms` #### Joining a game room + See [#Proxy mode](proxy-mode) #### Leaving a game room + - client closes connection to match server - match -> lobby: `leaveGameRoom` -#### Sending an invite: +#### Sending an invite + - client -> lobby: `sendInvite` - lobby -> target client: `inviteReceived` Note: there is no dedicated procedure to accept an invite. Instead, invited player will use same flow as when joining public game room #### Logout + - client closes connection - lobby -> every client: `activeAccounts` @@ -94,6 +106,7 @@ Note: there is no dedicated procedure to accept an invite. Instead, invited play In order to connect players located behind NAT, VCMI lobby can operate in "proxy" mode. In this mode, connection will be act as proxy and will transmit gameplay data from client to a match server, without any data processing on lobby server. Currently, process to establish connection using proxy mode is: + - Player attempt to join open game room using `joinGameRoom` message - Lobby server validates requests and on success - notifies match server about new player in lobby using control connection - Match server receives request, establishes new connection to game lobby, sends `serverProxyLogin` message to lobby server and immediately transfers this connection to VCMIServer class to use as connection for gameplay communication @@ -101,4 +114,4 @@ Currently, process to establish connection using proxy mode is: - Game client receives message and establishes own side of proxy connection - connects to lobby, sends `clientProxyLogin` message and transfers to ServerHandler class to use as connection for gameplay communication - Lobby server accepts new connection and moves it into a proxy mode - all packages that will be received by one side of this connection will be re-sent to another side without any processing. -Once the game is over (or if one side disconnects) lobby server will close another side of the connection and erase proxy connection \ No newline at end of file +Once the game is over (or if one side disconnects) lobby server will close another side of the connection and erase proxy connection diff --git a/docs/developers/RMG_Description.md b/docs/developers/RMG_Description.md index 59a6b683a..33dca93d7 100644 --- a/docs/developers/RMG_Description.md +++ b/docs/developers/RMG_Description.md @@ -74,4 +74,4 @@ For every zone, a few random obstacle sets are selected. [Details](https://githu ### Filling space -Tiles which need to be `blocked` but are not `used` are filled with obstacles. Largest obstacles which cover the most tiles are picked first, other than that they are chosen randomly. \ No newline at end of file +Tiles which need to be `blocked` but are not `used` are filled with obstacles. Largest obstacles which cover the most tiles are picked first, other than that they are chosen randomly. diff --git a/docs/developers/Serialization.md b/docs/developers/Serialization.md index 143eaf0ad..56b428f1e 100644 --- a/docs/developers/Serialization.md +++ b/docs/developers/Serialization.md @@ -140,7 +140,7 @@ CLoadFile/CSaveFile classes allow to read data to file and store data to file. T #### Networking -See [Networking](Networking.md) +See [Networking](Networking.md) ### Additional features @@ -259,4 +259,4 @@ Foo *loadedA, *loadedB; The feature recognizes pointers by addresses. Therefore it allows mixing pointers to base and derived classes. However, it does not allow serializing classes with multiple inheritance using a "non-first" base (other bases have a certain address offset from the actual object). -Pointer cycles are properly handled. This feature makes sense for savegames and is turned on for them. \ No newline at end of file +Pointer cycles are properly handled. This feature makes sense for savegames and is turned on for them. diff --git a/docs/maintainers/Project_Infrastructure.md b/docs/maintainers/Project_Infrastructure.md index a55e6ad41..41032fc54 100644 --- a/docs/maintainers/Project_Infrastructure.md +++ b/docs/maintainers/Project_Infrastructure.md @@ -9,30 +9,30 @@ So far we using following services: ### Most important - VCMI.eu domain paid until July of 2019. - - Owner: Tow - - Our main domain used by services. + - Owner: Tow + - Our main domain used by services. - VCMI.download paid until November of 2026. - - Owner: SXX - - Intended to be used for all assets downloads. - - Domain registered on GANDI and **can be renewed by anyone without access to account**. + - Owner: SXX + - Intended to be used for all assets downloads. + - Domain registered on GANDI and **can be renewed by anyone without access to account**. - [DigitalOcean](https://cloud.digitalocean.com/) team. - - Our hosting sponsor. - - Administrator access: SXX, Warmonger. - - User access: AVS, Tow. + - Our hosting sponsor. + - Administrator access: SXX, Warmonger. + - User access: AVS, Tow. - [CloudFlare](https://www.cloudflare.com/a/overview) account. - - Access through shared login / password. - - All of our infrastructure is behind CloudFlare and all our web. We manage our DNS there. + - Access through shared login / password. + - All of our infrastructure is behind CloudFlare and all our web. We manage our DNS there. - [Google Apps (G Suite)](https://admin.google.com/) account. - - It's only for vcmi.eu domain and limited to 5 users. Each account has limit of 500 emails / day. - - One administrative email used for other services registration. - - "noreply" email used for outgoing mail on Wiki and Bug Tracker. - - "forum" email used for outgoing mail on Forums. Since we authenticate everyone through forum it's should be separate email. - - Administrator access: Tow, SXX. + - It's only for vcmi.eu domain and limited to 5 users. Each account has limit of 500 emails / day. + - One administrative email used for other services registration. + - "noreply" email used for outgoing mail on Wiki and Bug Tracker. + - "forum" email used for outgoing mail on Forums. Since we authenticate everyone through forum it's should be separate email. + - Administrator access: Tow, SXX. - [Google Play Console](https://play.google.com/apps/publish/) account. - - Hold ownership over VCMI Android App. - - Owner: SXX - - Administrator access: Warmonger, AVS, Ivan. - - Release manager access: Fay. + - Hold ownership over VCMI Android App. + - Owner: SXX + - Administrator access: Warmonger, AVS, Ivan. + - Release manager access: Fay. Not all services let us safely share login credentials, but at least when possible at least two of core developers must have access to them in case of emergency. @@ -41,20 +41,20 @@ Not all services let us safely share login credentials, but at least when possib We want to notify players about updates on as many social services as possible. - Facebook page: - - Administrator access: SXX, Warmonger + - Administrator access: SXX, Warmonger - Twitter account: - - Administrator access: SXX. - - User access via TweetDeck: -- VK / VKontakte page: - - Owner: SXX - - Administrator access: AVS + - Administrator access: SXX. +- User access via TweetDeck: + - VK / VKontakte page: +- Owner: SXX + - Administrator access: AVS - Steam group: - - Administrator access: SXX - - Moderator access: Dydzio -- Reddit: - - Administrator access: SXX -- ModDB entry: - - Administrator access: SXX + - Administrator access: SXX +- Moderator access: Dydzio + - Reddit: +- Administrator access: SXX + - ModDB entry: +- Administrator access: SXX ### Communication channels @@ -70,48 +70,46 @@ We want to notify players about updates on as many social services as possible. ### Other services - Launchpad PPA: - - Member access: AVS - - Administrator access: Ivan, SXX + - Member access: AVS + - Administrator access: Ivan, SXX - Snapcraft Dashboard: - - Administrator access: SXX + - Administrator access: SXX - Coverity Scan page: - - Administrator access: SXX, Warmonger, AVS + - Administrator access: SXX, Warmonger, AVS - OpenHub page: - - Administrator access: Tow + - Administrator access: Tow - Docker Hub organization: - - Administrator access: SXX + - Administrator access: SXX Reserve accounts for other code hosting services: - GitLab organization: - - Administrator access: SXX + - Administrator access: SXX - BitBucket organization: - - Administrator access: SXX + - Administrator access: SXX ## What's to improve -1. Encourage Tow to transfer VCMI.eu to GANDI so it's can be also renewed without access. -2. Use 2FA on CloudFlare and just ask everyone to get FreeOTP and then use shared secret. -3. Centralized way to post news about game updates to all social media. +1. Encourage Tow to transfer VCMI.eu to GANDI so it's can be also renewed without access. +2. Use 2FA on CloudFlare and just ask everyone to get FreeOTP and then use shared secret. +3. Centralized way to post news about game updates to all social media. -# Project Servers Configuration +## Project Servers Configuration This section dedicated to explain specific configurations of our servers for anyone who might need to improve it in future. -## Droplet configuration - -### Droplet and hosted services +### Droplet configuration Currently we using two droplets: - First one serve all of our web services: - - [Forum](https://forum.vcmi.eu/) - - [Bug tracker](https://bugs.vcmi.eu/) - - [Wiki](https://wiki.vcmi.eu/) - - [Slack invite page](https://slack.vcmi.eu/) + - [Forum](https://forum.vcmi.eu/) + - [Bug tracker](https://bugs.vcmi.eu/) + - [Wiki](https://wiki.vcmi.eu/) + - [Slack invite page](https://slack.vcmi.eu/) - Second serve downloads: - - [Legacy download page](http://download.vcmi.eu/) - - [Build download page](https://builds.vcmi.download/) + - [Legacy download page](http://download.vcmi.eu/) + - [Build download page](https://builds.vcmi.download/) To keep everything secure we should always keep binary downloads separate from any web services. @@ -131,4 +129,4 @@ We only expose floating IP that can be detached from droplet in case of emergenc - Address: beholder.vcmi.eu (67.207.75.182) - Port 22 serve SFTP for file uploads as well as CI artifacts uploads. -If new services added firewall rules can be adjusted in [DO control panel](https://cloud.digitalocean.com/networking/firewalls). \ No newline at end of file +If new services added firewall rules can be adjusted in [DO control panel](https://cloud.digitalocean.com/networking/firewalls). diff --git a/docs/maintainers/Release_Process.md b/docs/maintainers/Release_Process.md index 51d7a8e1a..32456c1a4 100644 --- a/docs/maintainers/Release_Process.md +++ b/docs/maintainers/Release_Process.md @@ -1,12 +1,16 @@ # Release Process ## Versioning + For releases VCMI uses version numbering in form "1.X.Y", where: + - 'X' indicates major release. Different major versions are generally not compatible with each other. Save format is different, network protocol is different, mod format likely different. - 'Y' indicates hotfix release. Despite its name this is usually not urgent, but planned release. Different hotfixes for same major version are fully compatible with each other. ## Branches + Our branching strategy is very similar to GitFlow: + - `master` branch has release commits. One commit - one release. Each release commit should be tagged with version `1.X.Y` when corresponding version is released. State of master branch represents state of latest public release. - `beta` branch is for stabilization of ongoing release. Beta branch is created when new major release enters stabilization stage and is used for both major release itself as well as for subsequent hotfixes. Only changes that are safe, have minimal chance of regressions and improve player experience should be targeted into this branch. Breaking changes (e.g. save format changes) are forbidden in beta. - `develop` branch is a main branch for ongoing development. Pull requests with new features should be targeted to this branch, `develop` version is one major release ahead of `beta`. @@ -14,12 +18,14 @@ Our branching strategy is very similar to GitFlow: ## Release process step-by-step ### Initial release setup (major releases only) + Should be done immediately after start of stabilization stage for previous release - Create project named `Release 1.X` - Add all features and bugs that should be fixed as part of this release into this project ### Start of stabilization stage (major releases only) + Should be done 2 weeks before planned release date. All major features should be finished at this point. - Create `beta` branch from `develop` @@ -34,6 +40,7 @@ Should be done 2 weeks before planned release date. All major features should be - Bump version and build ID for Android on `beta` branch ### Release preparation stage + Should be done 1 week before release. Release date should be decided at this point. - Make sure to announce codebase freeze deadline (1 day before release) to all developers @@ -45,21 +52,23 @@ Should be done 1 week before release. Release date should be decided at this poi - - Update downloads counter in `docs/readme.md` ### Release preparation stage + Should be done 1 day before release. At this point beta branch is in full freeze. - Merge release preparation PR into `beta` - Merge `beta` into `master`. This will trigger CI pipeline that will generate release packages - Create draft release page, specify `1.x.y` as tag for `master` after publishing - Check that artifacts for all platforms have been built by CI on `master` branch -- Download and rename all build artifacts to use form "VCMI-1.X.Y-Platform.xxx" +- Download and rename all build artifacts to use form `VCMI-1.X.Y-Platform.xxx` - Attach build artifacts for all platforms to release page - Manually extract Windows installer, remove `$PLUGINSDIR` directory which contains installer files and repackage data as .zip archive - Attach produced zip archive to release page as an alternative Windows installer - Upload built AAB to Google Play and send created release draft for review (usually takes several hours) - Prepare pull request for [vcmi-updates](https://github.com/vcmi/vcmi-updates) -- (major releases only) Prepare pull request with release update for web site https://github.com/vcmi/VCMI.eu +- (major releases only) Prepare pull request with release update for web site ### Release publishing phase + Should be done on release date - Trigger builds for new release on Ubuntu PPA diff --git a/docs/maintainers/Ubuntu_PPA.md b/docs/maintainers/Ubuntu_PPA.md index 8ada53d86..1dbd6a7d0 100644 --- a/docs/maintainers/Ubuntu_PPA.md +++ b/docs/maintainers/Ubuntu_PPA.md @@ -1,6 +1,7 @@ # Ubuntu PPA ## Main links + - [Team](https://launchpad.net/~vcmi) - [Project](https://launchpad.net/vcmi) - [Sources](https://code.launchpad.net/~vcmi/vcmi/+git/vcmi) @@ -14,31 +15,42 @@ ## Automatic daily builds process ### Code import + - Launchpad performs regular (once per few hours) clone of our git repository. - This process can be observed on [Sources](https://code.launchpad.net/~vcmi/vcmi/+git/vcmi) page. - If necessary, it is possible to trigger fresh clone immediately (Import Now button) + ### Build dependencies + - All packages required for building of vcmi are defined in [debian/control](https://github.com/vcmi/vcmi/blob/develop/debian/control) file - Launchpad will automatically install build dependencies during build - Dependencies of output .deb package are defined implicitly as dependencies of packages required for build + ### Recipe building + - Every 24 hours Launchpad triggers daily builds on all recipes that have build schedule enable. For vcmi this is [Daily recipe](https://code.launchpad.net/~vcmi/+recipe/vcmi-daily) - Alternatively, builds can be triggered manually using "request build(s) link on recipe page. VCMI uses this for [Stable recipe](https://code.launchpad.net/~vcmi/+recipe/vcmi-stable) + ### Recipe content (build settings) + - Version of resulting .deb package is set in recipe content, e.g `{debupstream}+git{revtime}` for daily builds - Base version (referred as `debupstream` on Launchpad is taken from source code, [debian/changelog](https://github.com/vcmi/vcmi/blob/develop/debian/changelog) file - CMake configuration settings are taken from source code, [debian/rules](https://github.com/vcmi/vcmi/blob/develop/debian/rules) file - Branch which is used for build is specified in recipe content, e.g. `lp:vcmi master` + ## Workflow for creating a release build + - if necessary, push all required changes including `debian/changelog` update to `vcmi/master` branch - Go to [Sources](https://code.launchpad.net/~vcmi/vcmi/+git/vcmi) and run repository import. - Wait for import to finish, which usually happens within a minute. Press F5 to actually see changes. - Go to [Stable recipe](https://code.launchpad.net/~vcmi/+recipe/vcmi-stable) and request new builds - Wait for builds to finish. This takes quite a while, usually - over a hour, even more for arm builds - Once built, all successfully built packages are automatically copied to PPA linked to the recipe -- If any of builds have failed, open page with build info and check logs. +- If any of builds have failed, open page with build info and check logs. + ## People with access -- [alexvins](https://github.com/alexvins) (https://launchpad.net/~alexvins) -- [ArseniyShestakov](https://github.com/ArseniyShestakov) (https://launchpad.net/~sxx) -- [IvanSavenko](https://github.com/IvanSavenko) (https://launchpad.net/~saven-ivan) -- (Not member of VCMI, creator of PPA) (https://launchpad.net/~mantas) \ No newline at end of file + +- [alexvins](https://github.com/alexvins) () +- [ArseniyShestakov](https://github.com/ArseniyShestakov) () +- [IvanSavenko](https://github.com/IvanSavenko) () +- (Not member of VCMI, creator of PPA) () diff --git a/docs/modders/Animation_Format.md b/docs/modders/Animation_Format.md index 2aece5165..2abb72805 100644 --- a/docs/modders/Animation_Format.md +++ b/docs/modders/Animation_Format.md @@ -2,13 +2,13 @@ VCMI allows overriding HoMM3 .def files with .json replacement. Compared to .def this format allows: -- Overriding individual frames from json file (e.g. icons) -- Modern graphics formats (targa, png - all formats supported by VCMI image loader) -- Does not requires any special tools - all you need is text editor and images. +- Overriding individual frames from json file (e.g. icons) +- Modern graphics formats (targa, png - all formats supported by VCMI image loader) +- Does not requires any special tools - all you need is text editor and images. ## Format description -``` javascript +```json5 { // Base path of all images in animation. Optional. // Can be used to avoid using long path to images @@ -58,12 +58,14 @@ VCMI allows overriding HoMM3 .def files with .json replacement. Compared to .def ### Replacing a button This json file will allow replacing .def file for a button with png images. Buttons require following images: + 1. Active state. Button is active and can be pressed by player 2. Pressed state. Player pressed button but have not released it yet 3. Blocked state. Button is blocked and can not be interacted with. Note that some buttons are never blocked and can be used without this image 4. Highlighted state. This state is used by only some buttons and only in some cases. For example, in main menu buttons will appear highlighted when mouse cursor is on top of the image. Another example is buttons that can be selected, such as settings that can be toggled on or off -```javascript +```json5 +{ "basepath" : "interface/MyButton", // all images are located in this directory "images" : @@ -80,7 +82,8 @@ This json file will allow replacing .def file for a button with png images. Butt This json file allows defining one animation sequence, for example for adventure map objects or for town buildings. -```javascript +```json5 +{ "basepath" : "myTown/myBuilding", // all images are located in this directory "sequences" : diff --git a/docs/modders/Bonus/Bonus_Duration_Types.md b/docs/modders/Bonus/Bonus_Duration_Types.md index 3c20d0e7c..c82448116 100644 --- a/docs/modders/Bonus/Bonus_Duration_Types.md +++ b/docs/modders/Bonus/Bonus_Duration_Types.md @@ -4,14 +4,14 @@ Bonus may have any of these durations. They acts in disjunction. ## List of all bonus duration types -- PERMANENT -- ONE_BATTLE: at the end of battle -- ONE_DAY: at the end of day -- ONE_WEEK: at the end of week (bonus lasts till the end of week, NOT 7 days) -- N_TURNS: used during battles, after battle bonus is always removed -- N_DAYS -- UNTIL_BEING_ATTACKED: removed after any damage-inflicting attack -- UNTIL_ATTACK: removed after attack and counterattacks are performed -- STACK_GETS_TURN: removed when stack gets its turn - used for defensive stance -- COMMANDER_KILLED -- UNTIL_OWN_ATTACK: removed after attack (not counterattack) is performed \ No newline at end of file +- PERMANENT +- ONE_BATTLE: at the end of battle +- ONE_DAY: at the end of day +- ONE_WEEK: at the end of week (bonus lasts till the end of week, NOT 7 days) +- N_TURNS: used during battles, after battle bonus is always removed +- N_DAYS +- UNTIL_BEING_ATTACKED: removed after any damage-inflicting attack +- UNTIL_ATTACK: removed after attack and counterattacks are performed +- STACK_GETS_TURN: removed when stack gets its turn - used for defensive stance +- COMMANDER_KILLED +- UNTIL_OWN_ATTACK: removed after attack (not counterattack) is performed diff --git a/docs/modders/Bonus/Bonus_Limiters.md b/docs/modders/Bonus/Bonus_Limiters.md index 5dc9ee46a..daf33f3f1 100644 --- a/docs/modders/Bonus/Bonus_Limiters.md +++ b/docs/modders/Bonus/Bonus_Limiters.md @@ -15,7 +15,7 @@ The limiters take no parameters: Example: -``` javascript +```json5 "limiters" : [ "SHOOTER_ONLY" ] ``` @@ -25,12 +25,12 @@ Example: Parameters: -- Bonus type -- (optional) bonus subtype -- (optional) bonus sourceType and sourceId in struct -- example: (from Adele's bless): +- Bonus type +- (optional) bonus subtype +- (optional) bonus sourceType and sourceId in struct +- example: (from Adele's bless): -``` javascript +```json5 "limiters" : [ { "type" : "HAS_ANOTHER_BONUS_LIMITER", @@ -50,20 +50,21 @@ Parameters: Parameters: -- Creature id (string) -- (optional) include upgrades - default is false +- Creature id (string) +- (optional) include upgrades - default is false ### CREATURE_ALIGNMENT_LIMITER Parameters: -- Alignment identifier +- Alignment identifier ### CREATURE_LEVEL_LIMITER If parameters is empty, level limiter works as CREATURES_ONLY limiter Parameters: + - Minimal level - Maximal level @@ -71,24 +72,24 @@ Parameters: Parameters: -- Faction identifier +- Faction identifier ### CREATURE_TERRAIN_LIMITER Parameters: -- Terrain identifier +- Terrain identifier Example: -``` javascript +```json5 "limiters": [ { "type":"CREATURE_TYPE_LIMITER", "parameters": [ "angel", true ] } ], ``` -``` javascript +```json5 "limiters" : [ { "type" : "CREATURE_TERRAIN_LIMITER", "parameters" : ["sand"] @@ -106,13 +107,13 @@ Parameters: The following limiters must be specified as the first element of a list, and operate on the remaining limiters in that list: -- allOf (default when no aggregate limiter is specified) -- anyOf -- noneOf +- allOf (default when no aggregate limiter is specified) +- anyOf +- noneOf Example: -``` javascript +```json5 "limiters" : [ "noneOf", "IS_UNDEAD", @@ -121,4 +122,4 @@ Example: "parameters" : [ "SIEGE_WEAPON" ] } ] -``` \ No newline at end of file +``` diff --git a/docs/modders/Bonus/Bonus_Propagators.md b/docs/modders/Bonus/Bonus_Propagators.md index 90f09cf84..2a3e93e20 100644 --- a/docs/modders/Bonus/Bonus_Propagators.md +++ b/docs/modders/Bonus/Bonus_Propagators.md @@ -2,9 +2,9 @@ ## Available propagators -- BATTLE_WIDE: Affects both sides during battle -- VISITED_TOWN_AND_VISITOR: Used with Legion artifacts (town visited by hero) -- PLAYER_PROPAGATOR: Bonus will affect all objects owned by player. Used by Statue of Legion. -- HERO: Bonus will be transferred to hero (for example from stacks in his army). -- TEAM_PROPAGATOR: Bonus will affect all objects owned by player and his allies. -- GLOBAL_EFFECT: This effect will influence all creatures, heroes and towns on the map. \ No newline at end of file +- BATTLE_WIDE: Affects both sides during battle +- VISITED_TOWN_AND_VISITOR: Used with Legion artifacts (town visited by hero) +- PLAYER_PROPAGATOR: Bonus will affect all objects owned by player. Used by Statue of Legion. +- HERO: Bonus will be transferred to hero (for example from stacks in his army). +- TEAM_PROPAGATOR: Bonus will affect all objects owned by player and his allies. +- GLOBAL_EFFECT: This effect will influence all creatures, heroes and towns on the map. diff --git a/docs/modders/Bonus/Bonus_Range_Types.md b/docs/modders/Bonus/Bonus_Range_Types.md index 5be67c077..c1aeef280 100644 --- a/docs/modders/Bonus/Bonus_Range_Types.md +++ b/docs/modders/Bonus/Bonus_Range_Types.md @@ -16,4 +16,4 @@ For replacing ONLY_ENEMY_ARMY alias, you should use the following parameters of "limiters" : [ "OPPOSITE_SIDE" ] ``` -If some propagators was set before, it was actually ignored and should be replaced to code above. And OPPOSITE_SIDE limiter should be first, if any other limiters exists. \ No newline at end of file +If some propagators was set before, it was actually ignored and should be replaced to code above. And OPPOSITE_SIDE limiter should be first, if any other limiters exists. diff --git a/docs/modders/Bonus/Bonus_Sources.md b/docs/modders/Bonus/Bonus_Sources.md index 86754e407..c23140132 100644 --- a/docs/modders/Bonus/Bonus_Sources.md +++ b/docs/modders/Bonus/Bonus_Sources.md @@ -19,4 +19,4 @@ - STACK_EXPERIENCE - COMMANDER - GLOBAL -- OTHER \ No newline at end of file +- OTHER diff --git a/docs/modders/Bonus/Bonus_Types.md b/docs/modders/Bonus/Bonus_Types.md index 34e4d0b57..4370de7ba 100644 --- a/docs/modders/Bonus/Bonus_Types.md +++ b/docs/modders/Bonus/Bonus_Types.md @@ -57,9 +57,9 @@ Bonus that does not account for propagation and gives extra resources per day wi Increases amount of movement points available to affected hero on new turn -- subtype: - - heroMovementLand: only land movement will be affected - - heroMovementSea: only sea movement will be affected +- subtype: + - heroMovementLand: only land movement will be affected + - heroMovementSea: only sea movement will be affected - val: number of movement points (100 points for a tile) ### WATER_WALKING @@ -128,7 +128,7 @@ Allows to raise different creatures than Skeletons after battle. - addInfo: Level of Necromancy secondary skill (1 - Basic, 3 - Expert) - Example (from Cloak Of The Undead King): -```jsonc +```json5 { "type" : "IMPROVED_NECROMANCY", "subtype" : "creature.walkingDead", @@ -158,7 +158,7 @@ Allows affected heroes to learn spells from each other during hero exchange Reduces movement points penalty when moving on terrains with movement cost over 100 points. Can not reduce movement cost below 100 points -- val: penalty reduction, in movement points per tile. +- val: penalty reduction, in movement points per tile. ### WANDERING_CREATURES_JOIN_BONUS @@ -256,7 +256,7 @@ Gives creature under effect of this spell additional bonus, which is hardcoded a Modifies 'val' parameter of spell effects that give bonuses by specified value. For example, Aenain makes Disrupting Ray decrease target's defense by additional 2 points: -```jsonc +```json5 "disruptingRay" : { "addInfo" : -2, "subtype" : "spell.disruptingRay", @@ -271,7 +271,7 @@ Modifies 'val' parameter of spell effects that give bonuses by specified value. Changes 'val' parameter of spell effects that give bonuses to a specified value. For example, Fortune cast by Melody always modifies luck by +3: -```jsonc +```json5 "fortune" : { "addInfo" : 3, "subtype" : "spell.fortune", @@ -346,8 +346,8 @@ Heroes affected by this bonus can not retreat or surrender in battle (Shackles o Negates all natural immunities for affected stacks. (Orb of Vulnerability) - subtype: - - immunityBattleWide: Entire battle will be affected by bonus - - immunityEnemyHero: Only enemy hero will be affected by bonus + - immunityBattleWide: Entire battle will be affected by bonus + - immunityEnemyHero: Only enemy hero will be affected by bonus ### OPENING_BATTLE_SPELL @@ -383,9 +383,9 @@ Increases movement speed of units in battle Increases base damage of creature in battle - subtype: - - creatureDamageMin: increases only minimal damage - - creatureDamageMax: increases only maximal damage - - creatureDamageBoth: increases both minimal and maximal damage + - creatureDamageMin: increases only minimal damage + - creatureDamageMax: increases only maximal damage + - creatureDamageBoth: increases both minimal and maximal damage - val: additional damage points ### SHOTS @@ -402,6 +402,10 @@ Increases starting amount of shots that unit has in battle Affected unit is considered to not be alive and not affected by morale and certain spells +### MECHANICAL + +Affected unit is considered to not be alive and not affected by morale and certain spells but should be repairable from engineers (factory). + ### GARGOYLE Affected unit is considered to be a gargoyle and not affected by certain spells @@ -443,8 +447,8 @@ Affected units can not receive good or bad morale Affected unit can fly on the battlefield - subtype: - - movementFlying: creature will fly (slowly move across battlefield) - - movementTeleporting: creature will instantly teleport to destination, skipping movement animation. + - movementFlying: creature will fly (slowly move across battlefield) + - movementTeleporting: creature will instantly teleport to destination, skipping movement animation. ### SHOOTER @@ -502,6 +506,10 @@ Affected unit attacks all adjacent creatures (Hydra). Only directly targeted cre Affected unit attacks creature located directly behind targeted tile (Dragons). Only directly targeted creature will attempt to retaliate +### PRISM_HEX_ATTACK_BREATH + +Like `TWO_HEX_ATTACK_BREATH` but affects also two additional cratures (in triangle form from target tile) + ### RETURN_AFTER_STRIKE Affected unit can return to his starting location after attack (Harpies) @@ -517,19 +525,19 @@ Affected unit will ignore specified percentage of attacked unit defense (Behemot Affected units will receive reduced damage from attacks by other units - val: damage reduction, percentage -- subtype: - - damageTypeMelee: only melee damage will be reduced - - damageTypeRanged: only ranged damage will be reduced - - damageTypeAll: all damage will be reduced +- subtype: + - damageTypeMelee: only melee damage will be reduced + - damageTypeRanged: only ranged damage will be reduced + - damageTypeAll: all damage will be reduced ### PERCENTAGE_DAMAGE_BOOST Affected units will deal increased damage when attacking other units - val: damage increase, percentage -- subtype: - - damageTypeMelee: only melee damage will increased - - damageTypeRanged: only ranged damage will increased +- subtype: + - damageTypeMelee: only melee damage will increased + - damageTypeRanged: only ranged damage will increased ### GENERAL_ATTACK_REDUCTION @@ -568,18 +576,18 @@ Affected unit will never receive retaliations when attacking Affected unit will gain new creatures for each enemy killed by this unit - val: number of units gained per enemy killed -- subtype: - - soulStealPermanent: creature will stay after the battle - - soulStealBattle: creature will be lost after the battle +- subtype: + - soulStealPermanent: creature will stay after the battle + - soulStealBattle: creature will be lost after the battle ### TRANSMUTATION Affected units have chance to transform attacked unit to other creature type - val: chance for ability to trigger, percentage -- subtype: - - transmutationPerHealth: transformed unit will have same HP pool as original stack, - - transmutationPerUnit: transformed unit will have same number of units as original stack +- subtype: + - transmutationPerHealth: transformed unit will have same HP pool as original stack, + - transmutationPerUnit: transformed unit will have same number of units as original stack - addInfo: creature to transform to. If not set, creature will transform to same unit as attacker ### SUMMON_GUARDIANS @@ -605,10 +613,10 @@ Affected unit will attack units on all hexes that surround attacked hex Affected unit will retaliate before enemy attacks, if able -- subtype: - - damageTypeMelee: only melee attacks affected - - damageTypeRanged: only ranged attacks affected. Note that unit also requires ability to retaliate in ranged, such as RANGED_RETALIATION bonus - - damageTypeAll: any attacks are affected +- subtype: + - damageTypeMelee: only melee attacks affected + - damageTypeRanged: only ranged attacks affected. Note that unit also requires ability to retaliate in ranged, such as RANGED_RETALIATION bonus + - damageTypeAll: any attacks are affected ### SHOOTS_ALL_ADJACENT @@ -619,9 +627,9 @@ Affected unit will attack units on all hexes that surround attacked hex in range Affected unit will kills additional units after attack - val: chance to trigger, percentage -- subtype: - - destructionKillPercentage: kill percentage of units, - - destructionKillAmount: kill amount +- subtype: + - destructionKillPercentage: kill percentage of units, + - destructionKillAmount: kill amount - addInfo: amount or percentage to kill ### LIMITED_SHOOTING_RANGE @@ -661,6 +669,7 @@ Affected unit can attack walls during siege battles (Cyclops) ### CATAPULT_EXTRA_SHOTS Defines spell mastery level for spell used by CATAPULT bonus + - subtype: affected spell - val: spell mastery level to use @@ -752,18 +761,18 @@ Affected unit will deal additional damage after attack Affected unit will kill additional units after attack. Used for Death stare (Mighty Gorgon) ability and for Accurate Shot (Pirates, HotA) -- subtype: - - deathStareGorgon: only melee attack, random amount of killed units - - deathStareNoRangePenalty: only ranged attacks without obstacle (walls) or range penalty - - deathStareRangePenalty: only ranged attacks with range penalty - - deathStareObstaclePenalty: only ranged attacks with obstacle (walls) penalty - - deathStareRangeObstaclePenalty: only ranged attacks with both range and obstacle penalty - - deathStareCommander: fixed amount, both melee and ranged attacks -- val: - - for deathStareCommander: number of creatures to kill, total amount of killed creatures is (attacker level / defender level) \* val - - for all other subtypes: chance to kill, counted separately for each unit in attacking stack, percentage. At most (stack size \* chance) units can be killed at once, rounded up +- subtype: + - deathStareGorgon: only melee attack, random amount of killed units + - deathStareNoRangePenalty: only ranged attacks without obstacle (walls) or range penalty + - deathStareRangePenalty: only ranged attacks with range penalty + - deathStareObstaclePenalty: only ranged attacks with obstacle (walls) penalty + - deathStareRangeObstaclePenalty: only ranged attacks with both range and obstacle penalty + - deathStareCommander: fixed amount, both melee and ranged attacks +- val: + - for deathStareCommander: number of creatures to kill, total amount of killed creatures is (attacker level / defender level) \* val + - for all other subtypes: chance to kill, counted separately for each unit in attacking stack, percentage. At most (stack size \* chance) units can be killed at once, rounded up - addInfo: - - SpellID to be used as hit effect. If not set - 'deathStare' spell will be used. If set to "accurateShot" battle log messages will use alternative description + - SpellID to be used as hit effect. If not set - 'deathStare' spell will be used. If set to "accurateShot" battle log messages will use alternative description ### SPECIAL_CRYSTAL_GENERATION @@ -808,9 +817,9 @@ Determines how many times per combat affected creature can cast its targeted spe - subtype - spell id, eg. spell.iceBolt - value - chance (percent) - additional info - \[X, Y, Z\] - - X - spell mastery level (1 - Basic, 3 - Expert) - - Y = 0 - all attacks, 1 - shot only, 2 - melee only - - Z (optional) - layer for multiple SPELL_AFTER_ATTACK bonuses and multi-turn casting. Empty or value less than 0 = not participating in layering. + - X - spell mastery level (1 - Basic, 3 - Expert) + - Y = 0 - all attacks, 1 - shot only, 2 - melee only + - Z (optional) - layer for multiple SPELL_AFTER_ATTACK bonuses and multi-turn casting. Empty or value less than 0 = not participating in layering. When enabled - spells from specific layer will not be cast until target has all spells from previous layer on him. Spell from last layer is on repeat if none of spells on lower layers expired. ### SPELL_BEFORE_ATTACK @@ -818,12 +827,12 @@ Determines how many times per combat affected creature can cast its targeted spe - subtype - spell id - value - chance % - additional info - \[X, Y, Z\] - - X - spell mastery level (1 - Basic, 3 - Expert) - - Y = 0 - all attacks, 1 - shot only, 2 - melee only - - Z (optional) - layer for multiple SPELL_BEFORE_ATTACK bonuses and multi-turn casting. Empty or value less than 0 = not participating in layering. + - X - spell mastery level (1 - Basic, 3 - Expert) + - Y = 0 - all attacks, 1 - shot only, 2 - melee only + - Z (optional) - layer for multiple SPELL_BEFORE_ATTACK bonuses and multi-turn casting. Empty or value less than 0 = not participating in layering. When enabled - spells from specific layer will not be cast until target has all spells from previous layer on him. Spell from last layer is on repeat if none of spells on lower layers expired. -### SPECIFIC_SPELL_POWER +### SPECIFIC_SPELL_POWER - value: Used for Thunderbolt and Resurrection cast by units (multiplied by stack size). Also used for Healing secondary skill (for core:spell.firstAid used by First Aid tent) - subtype - spell id @@ -834,16 +843,16 @@ Determines how many times per combat affected creature can cast its targeted spe ### CREATURE_ENCHANT_POWER - - val: Total duration of spells cast by creature, in turns +- val: Total duration of spells cast by creature, in turns ### REBIRTH Affected stack will resurrect after death -- val - percent of total stack HP restored, not rounded. For instance, when 4 Phoenixes with Rebirth chance of 20% die, there is 80% chance than one Phoenix will rise. +- val - percent of total stack HP restored, not rounded. For instance, when 4 Phoenixes with Rebirth chance of 20% die, there is 80% chance than one Phoenix will rise. - subtype: - - rebirthRegular: Phoenix, as described above. - - rebirthSpecial: At least one unit will always rise (Sacred Phoenix) + - rebirthRegular: Phoenix, as described above. + - rebirthSpecial: At least one unit will always rise (Sacred Phoenix) ### ENCHANTED @@ -992,9 +1001,9 @@ Affected heroes will be under effect of Visions spell, revealing information of - val: multiplier to effect range. Information is revealed within (val \* hero spell power) range - subtype: - - visionsMonsters: reveal information on monsters, - - visionsHeroes: reveal information on heroes, - - visionsTowns: reveal information on towns + - visionsMonsters: reveal information on monsters, + - visionsHeroes: reveal information on heroes, + - visionsTowns: reveal information on towns ### BLOCK_MAGIC_BELOW diff --git a/docs/modders/Bonus/Bonus_Updaters.md b/docs/modders/Bonus/Bonus_Updaters.md index f0d95f940..a1c993b40 100644 --- a/docs/modders/Bonus/Bonus_Updaters.md +++ b/docs/modders/Bonus/Bonus_Updaters.md @@ -10,45 +10,43 @@ Check the files in *config/heroes/* for additional usage examples. ## GROWS_WITH_LEVEL -- Type: Complex -- Parameters: valPer20, stepSize=1 -- Effect: Updates val to - -` ceil(valPer20 * floor(heroLevel / stepSize) / 20)` +- Type: Complex +- Parameters: valPer20, stepSize=1 +- Effect: Updates val to `ceil(valPer20 * floor(heroLevel / stepSize) / 20)` Example: The following updater will cause a bonus to grow by 6 for every 40 levels. At first level, rounding will cause the bonus to be 0. -` "updater" : {` -` "parameters" : [ 6, 2 ],` -` "type" : "GROWS_WITH_LEVEL"` -` }` +```json5 +"updater" : { + "parameters" : [ 6, 2 ], + "type" : "GROWS_WITH_LEVEL" +} +``` Example: The following updater will cause a bonus to grow by 3 for every 20 levels. At first level, rounding will cause the bonus to be 1. -` "updater" : {` -` "parameters" : [ 3 ],` -` "type" : "GROWS_WITH_LEVEL"` -` }` +```json5 +"updater" : { + "parameters" : [ 3 ], + "type" : "GROWS_WITH_LEVEL" +} +``` Remarks: -- The rounding rules are designed to match the attack/defense bonus +- The rounding rules are designed to match the attack/defense bonus progression for heroes with creature specialties in HMM3. -- There is no point in specifying val for a bonus with a +- There is no point in specifying val for a bonus with a GROWS_WITH_LEVEL updater. ## TIMES_HERO_LEVEL -- Type: Simple -- Effect: Updates val to +- Type: Simple +- Effect: Updates val to `val * heroLevel` -` val * heroLevel` - -Usage: - -` "updater" : "TIMES_HERO_LEVEL"` +Usage: `"updater" : "TIMES_HERO_LEVEL"` Remark: This updater is redundant, in the sense that GROWS_WITH_LEVEL can also express the desired scaling by setting valPer20 to 20\*val. It @@ -56,34 +54,30 @@ has been added for convenience. ## TIMES_STACK_LEVEL -- Type: Simple -- Effect: Updates val to - -` val * stackLevel` +- Type: Simple +- Effect: Updates val to `val * stackLevel` Usage: -` "updater" : "TIMES_STACK_LEVEL"` +`"updater" : "TIMES_STACK_LEVEL"` Remark: The stack level for war machines is 0. ## ARMY_MOVEMENT -- Type: Complex -- Parameters: basePerSpeed, dividePerSpeed, additionalMultiplier, - maxValue -- Effect: Updates val to val+= max((floor(basePerSpeed / - dividePerSpeed)\* additionalMultiplier), maxValue) -- Remark: this updater is designed for MOVEMENT bonus to match H3 army - movement rules (in the example - actual movement updater, which - produces values same as in default movement.txt). -- Example: +- Type: Complex +- Parameters: basePerSpeed, dividePerSpeed, additionalMultiplier, maxValue +- Effect: Updates val to `val+= max((floor(basePerSpeed / dividePerSpeed) * additionalMultiplier), maxValue)` +- Remark: this updater is designed for MOVEMENT bonus to match H3 army movement rules (in the example - actual movement updater, which produces values same as in default movement.txt). +- Example: -` "updater" : {` -` "parameters" : [ 20, 3, 10, 700 ],` -` "type" : "ARMY_MOVEMENT"` -` }` +```json5 +"updater" : { + "parameters" : [ 20, 3, 10, 700 ], + "type" : "ARMY_MOVEMENT" +} +``` ## BONUS_OWNER_UPDATER -TODO: document me \ No newline at end of file +TODO: document me diff --git a/docs/modders/Bonus/Bonus_Value_Types.md b/docs/modders/Bonus/Bonus_Value_Types.md index aca624516..d9950a122 100644 --- a/docs/modders/Bonus/Bonus_Value_Types.md +++ b/docs/modders/Bonus/Bonus_Value_Types.md @@ -2,24 +2,29 @@ Total value of Bonus is calculated using the following: -- For each bonus source type we calculate new source value (for all bonus value types except PERCENT_TO_SOURCE and PERCENT_TO_TARGET_TYPE) using the following: -` newVal = (val * (100 + PERCENT_TO_SOURCE) / 100))` +- For each bonus source type we calculate new source value (for all bonus value types except PERCENT_TO_SOURCE and PERCENT_TO_TARGET_TYPE) using the following: + +``` +newVal = (val * (100 + PERCENT_TO_SOURCE) / 100)) +``` - PERCENT_TO_TARGET_TYPE applies as PERCENT_TO_SOURCE to targetSourceType of bonus. -- All bonus value types summarized and then used as subject of the following formula: +- All bonus value types summarized and then used as subject of the following formula: -` clamp(((BASE_NUMBER * (100 + PERCENT_TO_BASE) / 100) + ADDITIVE_VALUE) * (100 + PERCENT_TO_ALL) / 100), INDEPENDENT_MAX, INDEPENDENT_MIN)` +``` +clamp(((BASE_NUMBER * (100 + PERCENT_TO_BASE) / 100) + ADDITIVE_VALUE) * (100 + PERCENT_TO_ALL) / 100), INDEPENDENT_MAX, INDEPENDENT_MIN) +``` Semantics of INDEPENDENT_MAX and INDEPENDENT_MIN are wrapped, and first means than bonus total value will be at least INDEPENDENT_MAX, and second means than bonus value will be at most INDEPENDENT_MIN. ## List of all bonus value types -- ADDITIVE_VALUE -- BASE_NUMBER -- PERCENT_TO_ALL -- PERCENT_TO_BASE -- INDEPENDENT_MAX -- INDEPENDENT_MIN -- PERCENT_TO_SOURCE -- PERCENT_TO_TARGET_TYPE \ No newline at end of file +- ADDITIVE_VALUE +- BASE_NUMBER +- PERCENT_TO_ALL +- PERCENT_TO_BASE +- INDEPENDENT_MAX +- INDEPENDENT_MIN +- PERCENT_TO_SOURCE +- PERCENT_TO_TARGET_TYPE diff --git a/docs/modders/Bonus_Format.md b/docs/modders/Bonus_Format.md index 5dce1c1d0..88c901f38 100644 --- a/docs/modders/Bonus_Format.md +++ b/docs/modders/Bonus_Format.md @@ -4,7 +4,7 @@ All parameters but type are optional. -``` javascript +```json5 { // Type of the bonus. See Bonus Types for full list "type": "BONUS_TYPE", @@ -78,10 +78,10 @@ All parameters but type are optional. All string identifiers of items can be used in "subtype" field. This allows cross-referencing between the mods and make config file more readable. See [Game Identifiers](Game_Identifiers.md) for full list of available identifiers - + ### Example -``` javascript +```json5 "bonus" : { "type" : "HATE", @@ -90,4 +90,4 @@ See [Game Identifiers](Game_Identifiers.md) for full list of available identifie } ``` -This bonus makes creature do 50% more damage to Enchanters. \ No newline at end of file +This bonus makes creature do 50% more damage to Enchanters. diff --git a/docs/modders/Building_Bonuses.md b/docs/modders/Building_Bonuses.md index ac4e0c7d9..f5219e581 100644 --- a/docs/modders/Building_Bonuses.md +++ b/docs/modders/Building_Bonuses.md @@ -12,17 +12,17 @@ should be moved to scripting. Includes: -- mystic pond -- treasury -- god of fire -- castle gates -- cover of darkness -- portal of summoning -- escape tunnel +- mystic pond +- treasury +- god of fire +- castle gates +- cover of darkness +- portal of summoning +- escape tunnel Function of all of these objects can be enabled by this: -``` javascript +```json5 "function" : "castleGates" ``` @@ -31,13 +31,13 @@ Function of all of these objects can be enabled by this: Hardcoded functionality for now due to complexity of these objects. Temporary can be handles as unique buildings. Includes: -- resource - resource -- resource - player -- artifact - resource -- resource - artifact -- creature - resource -- resource - skills -- creature - skeleton +- resource - resource +- resource - player +- artifact - resource +- resource - artifact +- creature - resource +- resource - skills +- creature - skeleton ### hero visitables @@ -46,10 +46,10 @@ handled via configurable objects system. Includes: -- gives mana points -- gives movement points -- give bonus to visitor -- permanent bonus to hero +- gives mana points +- gives movement points +- give bonus to visitor +- permanent bonus to hero ### generic functions @@ -58,31 +58,31 @@ CBuilding class. #### unlock guild level -``` javascript +```json5 "guildLevels" : 1 ``` #### unlock hero recruitment -``` javascript +```json5 "allowsHeroPurchase" : true ``` #### unlock ship purchase -``` javascript +```json5 "allowsShipPurchase" : true ``` #### unlock building purchase -``` javascript +```json5 "allowsBuildingPurchase" : true ``` #### unlocks creatures -``` javascript +```json5 "dwelling" : { "level" : 1, "creature" : "archer" } ``` @@ -92,31 +92,31 @@ Turn into town bonus? What about creature-specific bonuses from hordes? #### gives resources -``` javascript +```json5 "provides" : { "gold" : 500 } ``` #### gives guild spells -``` javascript +```json5 "guildSpells" : [5, 0, 0, 0, 0] ``` #### gives thieves guild -``` javascript +```json5 "thievesGuildLevels" : 1 ``` #### gives fortifications -``` javascript +```json5 "fortificationLevels" : 1 ``` #### gives war machine -``` javascript +```json5 "warMachine" : "ballista" ``` @@ -129,12 +129,12 @@ TODO: how to handle stackable bonuses like Necromancy Amplifier? Includes: -- bonus to defender -- bonus to alliance -- bonus to scouting range -- bonus to player +- bonus to defender +- bonus to alliance +- bonus to scouting range +- bonus to player -``` javascript +```json5 "bonuses" : { "moraleToDefenders" : @@ -162,12 +162,12 @@ Possible issue - with removing of fixed ID's buildings in different town may no longer share same ID. However Capitol must be unique across all town. Should be fixed somehow. -``` javascript +```json5 "onePerPlayer" : true ``` #### chance to be built on start -``` javascript +```json5 "prebuiltChance" : 75 -``` \ No newline at end of file +``` diff --git a/docs/modders/Campaign_Format.md b/docs/modders/Campaign_Format.md index 9fec41be2..1f8b811a4 100644 --- a/docs/modders/Campaign_Format.md +++ b/docs/modders/Campaign_Format.md @@ -3,12 +3,13 @@ ## Introduction Starting from version 1.3, VCMI supports its own campaign format. -Campaigns have *.vcmp file format and it consists from campaign json and set of scenarios (can be both *.vmap and *.h3m) +Campaigns have `*.vcmp` file format and it consists from campaign json and set of scenarios (can be both `*.vmap` and `*.h3m`) To start making campaign, create file named `header.json`. See also [Packing campaign](#packing-campaign) Basic structure of this file is here, each section is described in details below -```js + +```json5 { "version" : 1, @@ -32,7 +33,8 @@ Basic structure of this file is here, each section is described in details below ## Header properties In header are parameters describing campaign properties -```js + +```json5 ... "regions": {...}, "name": "Campaign name", @@ -60,7 +62,8 @@ In header are parameters describing campaign properties ## Scenario description Scenario description looks like follow: -```js + +```json5 { "map": "maps/SomeMap", "preconditions": [], @@ -77,7 +80,7 @@ Scenario description looks like follow: } ``` -- `"map"` map name without extension but with relative path. Both *.h3m and *.vmap maps are supported. If you will pack scenarios inside campaign, numerical map name should be used, see details in [packing campaign](#packing-campaign) +- `"map"` map name without extension but with relative path. Both `*.h3m` and `*.vmap` maps are supported. If you will pack scenarios inside campaign, numerical map name should be used, see details in [packing campaign](#packing-campaign) - `"preconditions"` enumerate scenarios indexes which must be completed to unlock this scenario. For example, if you want to make sequential missions, you should specify `"preconditions": []` for first scenario, but for second scenario it should be `"preconditions": [0]` and for third `"preconditions": [0, 1]`. But you can allow non-linear conquering using this parameter - `"color"` defines color id for the region. Possible values are `0: red, 1: blue, tan: 2, green: 3, orange: 4, purple: 5, teal: 6, pink: 7` - `"difficulty"` sets initial difficulty for this scenario. If `"allowDifficultySelection"`is defined for campaign, difficulty may be changed by player. Possible values are `0: pawn, 1: knight, 2: rook, 3: queen, 4: king` @@ -96,7 +99,8 @@ Scenario description looks like follow: ### Prolog/Epilog Prolog and epilog properties are optional -```js + +```json5 { "video": "NEUTRALA.smk", //video to show "music": "musicFile.ogg", //music to play, should be located in music directory @@ -115,7 +119,7 @@ If `startOptions` is `none`, `bonuses` field will be ignored If `startOptions` is `bonus`, bonus format may vary depending on its type. -```js +```json5 { "what": "", @@ -158,19 +162,21 @@ If `startOptions` is `bonus`, bonus format may vary depending on its type. If `startOptions` is `crossover`, heroes from specific scenario will be moved to this scenario. Bonus format is following -```js +```json5 { "playerColor": 0, "scenario": 0 }, ``` + - `"playerColor"` from what player color heroes shall be taken. Possible values are `0: red, 1: blue, tan: 2, green: 3, orange: 4, purple: 5, teal: 6, pink: 7` - `"scenario"` from which scenario heroes shall be taken. 0 means first scenario #### Hero start option If `startOptions` is `hero`, hero can be chosen as a starting bonus. Bonus format is following -```js + +```json5 { "playerColor": 0, "hero": "random" @@ -184,7 +190,7 @@ If `startOptions` is `hero`, hero can be chosen as a starting bonus. Bonus forma Predefined campaign regions are located in file `campaign_regions.json` -```js +```json5 { "background": "ownRegionBackground.png", "suffix": ["Enabled", "Selected", "Conquered"], @@ -201,7 +207,7 @@ Predefined campaign regions are located in file `campaign_regions.json` - `"background"` optional - use own image name for background instead of adding "_BG" to the prefix as name - `"prefix"` used to identify all images related to campaign. In this example (if background parameter wouldn't exists), background picture will be `G3_BG` - `"suffix"` optional - use other suffixes than the default `En`, `Se` and `Co` for the three different images -- `"infix"` used to identify all images related to region. In this example, it will be pictures whose files names begin with `G3A_..., G3B_..., G3C_..."` +- `"infix"` used to identify all images related to region. In this example, it will be pictures whose files names begin with `G3A_..., G3B_..., G3C_..."` - `"labelPos"` optional - to add scenario name as label on map - `"colorSuffixLength"` identifies suffix length for region colourful frames. 0 is no color suffix (no colorisation), 1 is used for `R, B, N, G, O, V, T, P`, value 2 is used for `Re, Bl, Br, Gr, Or, Vi, Te, Pi` @@ -213,6 +219,7 @@ This file is a zip archive. The scenarios should be named as in `"map"` field from header. Subfolders are allowed. ## Compatibility table + | Version | Min VCMI | Max VCMI | Description | |---------|----------|----------|-------------| -| 1 | 1.3 | | Initial release | \ No newline at end of file +| 1 | 1.3 | | Initial release | diff --git a/docs/modders/Configurable_Widgets.md b/docs/modders/Configurable_Widgets.md index 883b249b1..541e03e76 100644 --- a/docs/modders/Configurable_Widgets.md +++ b/docs/modders/Configurable_Widgets.md @@ -21,6 +21,7 @@ In this tutorial we will recreate options tab to support chess timers UI. ### Creating mod structure To start making mod, create following folders structure; + ``` extendedLobby/ |- content/ @@ -31,7 +32,8 @@ extendedLobby/ ``` File `mod.json` is generic and could look like this: -```json + +```json5 { "name" : "Configurable UI tutorial mod", "description" : "See tutorial here https://github.com/vcmi/vcmi/wiki/Configurable-UI-widgets", @@ -44,7 +46,7 @@ File `mod.json` is generic and could look like this: } ``` -After that you can copy `extendedLobby/ folder to `mods/` folder and your mod will immediately appear in launcher but it does nothing for now. +After that you can copy `extendedLobby/` folder to `mods/` folder and your mod will immediately appear in launcher but it does nothing for now. ### Making layout for timer @@ -64,7 +66,8 @@ So we need to modify turn duration label and add combo box with timer types Open `optionsTab.json` and scroll it until you see comment `timer`. Three elements after this comment are related to timer. Let's find first element, which is label -```json + +```json5 { "items" [ @@ -85,7 +88,8 @@ Let's find first element, which is label ``` And modify it a bit -```json + +```json5 { "name": "labelTimer", //add name, only for convenience "type": "label", @@ -98,7 +102,8 @@ And modify it a bit ``` But we also need proper background image for this label. Add image widget BEFORE labelTimer widget: -```json + +```json5 { "type": "picture", "image": "RmgTTBk", @@ -109,6 +114,7 @@ But we also need proper background image for this label. Add image widget BEFORE ... }, ``` + In order to make it work, add file `RmgTTBk.bmp` to `content/sprites/` Elements named `labelTurnDurationValue` and `sliderTurnDuration` we will keep without change - they are needed to configure classic timer. @@ -121,7 +127,7 @@ Copy image `DrDoCoBk.bmp` to `content/sprites/`. Button objects use animated ima For normal, pressed, blocked and highlighted. Our combo box inherits this behavior, so let's convert image to animation. In order to do it, we need to create file `DrDoCoBk.json` in same folder `content/sprites/` with following content: -```json +```json5 { "sequences" : [ @@ -140,7 +146,7 @@ Thus we created file with animation, containing single frame which can be used f Let's add one more element after `//timer` comment: -```json +```json5 ... //timer { @@ -157,7 +163,7 @@ Let's add one more element after `//timer` comment: We also want to have label on the top of this combo box showing which element is selected. You need to add `items` array, where additional elements can be specified, label in our case: -```json +```json5 ... //timer { @@ -189,7 +195,7 @@ First of all, add images to `content/sprites/` folder: `List2Bk.bmp` for drop-do Now specify items inside `dropDown` field -```json +```json5 "dropDown": { "items": @@ -272,7 +278,8 @@ After view part is done, let's make behavioural part. Let's hide elements, related to classic timer when chess timer is selected and show them back if classic selected. To do that, find `"variables"` part inside `optionsTab.json` and add there `"timers"` array, containing 2 elements: -```json + +```json5 "variables": { "timers": @@ -300,10 +307,10 @@ Now we show and hide elements, but visually you still can some "artifacts": Снимок экрана 2023-08-30 в 15 51 22 -It's because options tab background image we use has those elements drawn. Let's hide them with overlay image `timchebk.bmp`. +It's because options tab background image we use has those elements drawn. Let's hide them with overlay image `timchebk.bmp`. It should be drawn before all other timer elements: -```json +```json5 ... // timer { @@ -328,9 +335,10 @@ It works and can switch elements, the only missing part is chess timer configura We should add text input fields, to specify different timers. We will use background for them `timerField.bmp`, copy it to `content/sprites/` folder of your mod. -There are 4 different timers: base, turn, battle and creature. Read about them here: https://github.com/vcmi/vcmi/issues/1364 +There are 4 different timers: base, turn, battle and creature. Read about them here: We can add editors for them into items list, their format will be following: -```json + +```json5 { "name": "chessFieldBase", "type": "textInput", @@ -345,6 +353,7 @@ We can add editors for them into items list, their format will be following: ``` Add three remaining elements for different timers by yourself. You can play with all settings, except callback. There are 4 predefined callbacks to setup timers: + - `parseAndSetTimer_base` - `parseAndSetTimer_turn` - `parseAndSetTimer_battle` @@ -363,7 +372,7 @@ There are different basic types, which can be used as value. #### Primitive types -Read JSON documentation for primitive types description: https://www.json.org/json-en.html +Read JSON documentation for primitive types description: #### Text @@ -430,7 +439,8 @@ One of predefined values: ### Configurable objects Configurable object has following structure: -```json + +```json5 { "items": [], "variables": {}, //optional @@ -621,7 +631,7 @@ Filling area with texture `"color"`: [color](#color), -`"text": string` optional, default text. Translations are not supported +`"text": string` optional, default text. Translations are not supported `"position"`: [position](#position) @@ -748,11 +758,13 @@ Used only as special object for [combo box](#combo-box) `"position"`: [position](#position) `"items": []` array of overlay widgets with certain types and names: - - `"name": "hoverImage"`, `"type": ` [picture](#picture) - image to be shown when cursor hovers elements - - `"name": "labelName"`, `"type": ` [label](#label) - element caption + +- `"name": "hoverImage"`, `"type":` [picture](#picture) - image to be shown when cursor hovers elements +- `"name": "labelName"`, `"type":` [label](#label) - element caption **Callbacks** - - `sliderMove` connect to slider callback to correctly navigate over elements + +- `sliderMove` connect to slider callback to correctly navigate over elements #### Layout @@ -769,7 +781,8 @@ Used only as special object for [combo box](#combo-box) While designing a new element, you can make it configurable to reuse all functionality described above. It will provide flexibility to further changes as well as modding capabilities. Class should inherit `InterfaceObjectConfigurable`. -```C++ + +```cpp #include "gui/InterfaceObjectConfigurable.h" //assuming we are in client folder class MyYesNoDialog: public InterfaceObjectConfigurable @@ -781,7 +794,7 @@ class MyYesNoDialog: public InterfaceObjectConfigurable To make new object work, it's sufficient to define constructor, which receives const reference to `JsonNode`. -```C++ +```cpp MyYesNoDialog::MyYesNoDialog(const JsonNode & config): InterfaceObjectConfigurable(), //you can pass arguments same as for CIntObject { @@ -808,13 +821,13 @@ MyYesNoDialog::MyYesNoDialog(const JsonNode & config): You can build custom widgets, related to your UI element specifically. Like in example above, there is Item widget, which can be also used on JSON config. -```C++ +```cpp REGISTER_BUILDER("myItem", &MyYesNoDialog::buildMyItem); ``` You have to define function, which takes JsonNode as an argument and return pointer to built widget -```C++ +```cpp std::shared_ptr MyYesNoDialog::buildMyItem(const JsonNode & config) { auto position = readPosition(config["position"]); @@ -824,7 +837,7 @@ std::shared_ptr MyYesNoDialog::buildMyItem(const JsonNode & After that, if your JSON file has items with type "MyItem", the new Item element will be constructed. -```json +```json5 { "items": [ @@ -840,7 +853,7 @@ After that, if your JSON file has items with type "MyItem", the new Item element After calling `build(config)` variables defined in config JSON file become available. You can interpret them and use in callbacks or in element code -```C++ +```cpp build(config); if(variables["colorfulText"].Bool()) diff --git a/docs/modders/Difficulty.md b/docs/modders/Difficulty.md index 91f78af9c..52e265807 100644 --- a/docs/modders/Difficulty.md +++ b/docs/modders/Difficulty.md @@ -7,7 +7,7 @@ Difficulty configuration is located in [config/difficulty.json](../config/diffic ## Format summary -``` javascript +```json5 { "human": //parameters impacting human players only { @@ -50,7 +50,7 @@ For both types of bonuses, `source` should be specified as `OTHER`. ## Example -```js +```json5 { //will give 150% extra health to all players' creatures if specified in "battleBonuses" array "type" : "STACK_HEALTH", "val" : 150, @@ -63,4 +63,4 @@ For both types of bonuses, `source` should be specified as `OTHER`. ## Compatibility Starting from VCMI 1.4 `startres.json` is not available anymore and will be ignored if present in any mod. -Thus, `Resourceful AI` mod of version 1.2 won't work anymore. \ No newline at end of file +Thus, `Resourceful AI` mod of version 1.2 won't work anymore. diff --git a/docs/modders/Entities_Format/Artifact_Format.md b/docs/modders/Entities_Format/Artifact_Format.md index 928496dc7..e308e64e7 100644 --- a/docs/modders/Entities_Format/Artifact_Format.md +++ b/docs/modders/Entities_Format/Artifact_Format.md @@ -6,13 +6,13 @@ Artifact bonuses use [Bonus Format](../Bonus_Format.md) In order to make functional artifact you also need: -- Icon for hero inventory (1 image) -- Icon for popup windows (1 image, optional) -- Animation for adventure map (1 animation) +- Icon for hero inventory (1 image) +- Icon for popup windows (1 image, optional) +- Animation for adventure map (1 animation) ## Format -``` jsonc +```json5 { // Type of this artifact - creature, hero or commander "type": ["HERO", "CREATURE", "COMMANDER"] @@ -67,6 +67,9 @@ In order to make functional artifact you also need: "artifact2", "artifact3" ], + + // Optional, by default is false. Set to true if components are supposed to be fused. + "fusedComponents" : true, // Creature id to use on battle field. If set, this artifact is war machine "warMachine" : "some.creature" diff --git a/docs/modders/Entities_Format/Battle_Obstacle_Format.md b/docs/modders/Entities_Format/Battle_Obstacle_Format.md index e9b00d3d6..fb5319db0 100644 --- a/docs/modders/Entities_Format/Battle_Obstacle_Format.md +++ b/docs/modders/Entities_Format/Battle_Obstacle_Format.md @@ -1,6 +1,6 @@ # Battle Obstacle Format -```jsonc +```json5 // List of terrains on which this obstacle can be used "allowedTerrains" : [] @@ -24,4 +24,4 @@ // If set to true, obstacle will appear in front of units or other battlefield objects "foreground" : false -``` \ No newline at end of file +``` diff --git a/docs/modders/Entities_Format/Battlefield_Format.md b/docs/modders/Entities_Format/Battlefield_Format.md index 21c219273..7435400ad 100644 --- a/docs/modders/Entities_Format/Battlefield_Format.md +++ b/docs/modders/Entities_Format/Battlefield_Format.md @@ -1,6 +1,6 @@ # Battlefield Format -```jsonc +```json5 // Human-readable name of the battlefield "name" : "", @@ -22,4 +22,4 @@ // List of battle hexes that will be always blocked on this battlefield (e.g. ship to ship battles) "impassableHexes" : [ 10, 20, 50 ], -``` \ No newline at end of file +``` diff --git a/docs/modders/Entities_Format/Biome_Format.md b/docs/modders/Entities_Format/Biome_Format.md index a0d2a3689..d750a9dcd 100644 --- a/docs/modders/Entities_Format/Biome_Format.md +++ b/docs/modders/Entities_Format/Biome_Format.md @@ -8,7 +8,7 @@ The purpose is to create visually attractive and consistent maps, which will als If not enough biomes are defined for [terrain type](Terrain_Format.md), map generator will fall back to using all available templates that match this terrain, which was original behavior before 1.5.0. -``` json +```json5 "obstacleSetId" : { "biome" : { "terrain" : "grass", // Id or vector of Ids this obstacle set can spawn at @@ -38,5 +38,3 @@ Currently algorithm picks randomly: - One or two sets of **rocks** (small objects) - One of each remaining types of object (**structure**, **animal**, **other**), until enough number of sets is picked. - Obstacles marked as **other** are picked last, and are generally rare. - - diff --git a/docs/modders/Entities_Format/Creature_Format.md b/docs/modders/Entities_Format/Creature_Format.md index 252a5e140..4dc65f157 100644 --- a/docs/modders/Entities_Format/Creature_Format.md +++ b/docs/modders/Entities_Format/Creature_Format.md @@ -8,22 +8,22 @@ In order to make functional creature you also need: ### Animation -- Battle animation (1 def file) -- Set of rendered projectiles (1 def files, shooters only) -- Adventure map animation (1 def file) +- Battle animation (1 def file) +- Set of rendered projectiles (1 def files, shooters only) +- Adventure map animation (1 def file) ### Images -- Small portrait for hero exchange window (1 image) -- Large portrait for hero window (1 image) +- Small portrait for hero exchange window (1 image) +- Large portrait for hero window (1 image) ### Sounds -- Set of sounds (up to 8 sounds) +- Set of sounds (up to 8 sounds) ## Format -``` javascript +```json5 // camelCase unique creature identifier "creatureName" : { @@ -217,4 +217,4 @@ In order to make functional creature you also need: ... ] } -``` \ No newline at end of file +``` diff --git a/docs/modders/Entities_Format/Creature_Help.md b/docs/modders/Entities_Format/Creature_Help.md index d5ca1a590..a2234e40b 100644 --- a/docs/modders/Entities_Format/Creature_Help.md +++ b/docs/modders/Entities_Format/Creature_Help.md @@ -5,6 +5,7 @@ This page helps you to create a creature (i.e. a unit that fights in a battle) f ## Utilities You need to download the two utilities [`DefPreview`](https://sourceforge.net/projects/grayface-misc/files/DefPreview-1.2.1/) and [`H3DefTool`](https://sourceforge.net/projects/grayface-misc/files/H3DefTool-3.4.2/) from the internet: + - `DefPreview` converts a `.def` file to `.bmp` images - `H3DefTool` converts `.bmp` images to a `.def` file @@ -21,7 +22,9 @@ The sun is always at zenith, so the shadow is always behind. The reason is that We don't know the right elevation angle for the view. ### 3D render -You can render your creature using a 3D software like _Blender_. You can start with those free-licenced rigged 3D models: + +You can render your creature using a 3D software like *Blender*. You can start with those free-licenced rigged 3D models: + - [Fantasy-bandit](https://www.cgtrader.com/free-3d-models/character/man/fantasy-bandit) - [Monster-4](https://www.cgtrader.com/free-3d-models/character/fantasy-character/monster-4-f5757b92-dc9c-4f5e-ad0d-593203d14fe2) - [Crypt-fiend-modular-character](https://www.cgtrader.com/free-3d-models/character/fantasy-character/crypt-fiend-modular-character-demo-scene) @@ -33,57 +36,60 @@ You can render your creature using a 3D software like _Blender_. You can start w - [Shani](https://www.cgtrader.com/free-3d-models/character/woman/shani-3d-character) You can also create your 3D model from a single image: -- _Stable Fast 3D_: https://huggingface.co/spaces/stabilityai/stable-fast-3d -- _Unique3D_: https://huggingface.co/spaces/abreza/Unique3D -To use it in _Blender_, create a `.blend` project and import the file. To render the texture: -1. Add a _Principled BSDF_ material to the object -1. Create a _Color Attribute_ in the _Shader Editor_ view -1. Link the Color output of the _Color Attribute_ to the _Base color_ input of the _Principled BSDF_ +- *Stable Fast 3D*: +- *Unique3D*: -You can improve details by cropping the source image on a detail and generate a model for this detail. Once both imported in _Blender_, melt them together. +To use it in *Blender*, create a `.blend` project and import the file. To render the texture: -Render the images without background by selecting png RVBA and disabling background (_Film_ -> _Filter_ -> _Transparent_). It avoids the creatures to have an ugly dark border. Then, to correctly separate the creature from the cyan area, in _GIMP_, apply the threeshold on the transparency by clicking on _Layer_ -> _Transparency_ -> _Alpha threeshold_. +1. Add a *Principled BSDF* material to the object +1. Create a *Color Attribute* in the *Shader Editor* view +1. Link the Color output of the *Color Attribute* to the *Base color* input of the *Principled BSDF* + +You can improve details by cropping the source image on a detail and generate a model for this detail. Once both imported in *Blender*, melt them together. + +Render the images without background by selecting png RVBA and disabling background (*Film* -> *Filter* -> *Transparent*). It avoids the creatures to have an ugly dark border. Then, to correctly separate the creature from the cyan area, in *GIMP*, apply the threeshold on the transparency by clicking on *Layer* -> *Transparency* -> *Alpha threeshold*. The global FPS of the game is 10 f/s but you can render at a higher level and configure it in the `.json` files. We are not in the 1990's. ### IA render -You can also use an AI like _Flux_ to generate the main creature representation: https://huggingface.co/spaces/multimodalart/FLUX.1-merged +You can also use an AI like *Flux* to generate the main creature representation: -Then you can add random animations for idle states with _SVD_: https://huggingface.co/spaces/xi0v/Stable-Video-Diffusion-Img2Vid +Then you can add random animations for idle states with *SVD*: -Most of the time, the creatures do not move more than one pixel in an idle animation. The reason may be to avoid too much animation on screen and make the transition with the other animations always seamless. Use poses with _ControlNet_ or _OpenPose_. For specific animations, I recommend to use _Cinemo_ because it adds a description prompt but the resolution is smaller: https://huggingface.co/spaces/maxin-cn/Cinemo +Most of the time, the creatures do not move more than one pixel in an idle animation. The reason may be to avoid too much animation on screen and make the transition with the other animations always seamless. Use poses with *ControlNet* or *OpenPose*. For specific animations, I recommend to use *Cinemo* because it adds a description prompt but the resolution is smaller: -Make animations seamless from one to another. To do this, you can draw the first and the last images with a prompt with _ToonCrafter_: https://huggingface.co/spaces/ChristianHappy/tooncrafter +Make animations seamless from one to another. To do this, you can draw the first and the last images with a prompt with *ToonCrafter*: -Most of the time, you need to increase the resolution or the quality of your template image, so use _SUPIR_: https://huggingface.co/spaces/Fabrice-TIERCELIN/SUPIR +Most of the time, you need to increase the resolution or the quality of your template image, so use *SUPIR*: ## Battle sound effect -To create the audio effects, I recommend to use _Tango 2_: https://huggingface.co/spaces/declare-lab/tango2 +To create the audio effects, I recommend to use *Tango 2*: -The quality is better than _Stable Audio_. +The quality is better than *Stable Audio*. ## Map render We don't know the right elevation angle for the view but 45° elevation seems to be a good choice. For the sunlight direction, I would say 45° elevation and 45° azimut. -The map creatures are not rendered on the map with vanishing points but in isometric. You can [get an orthogonal render in Blender](https://blender.stackexchange.com/a/135384/2768). If you are creating a creature and its updated version, most of the time, the both creatures are not oriented to the same side on the map. I think that the animation on the map is usually the _Mouse Over_ animation on battle. +The map creatures are not rendered on the map with vanishing points but in isometric. You can [get an orthogonal render in Blender](https://blender.stackexchange.com/a/135384/2768). If you are creating a creature and its updated version, most of the time, the both creatures are not oriented to the same side on the map. I think that the animation on the map is usually the *Mouse Over* animation on battle. -You can see that the view angle is higher than on a battle. To change the angle from a battle sprite, you can use _Zero 1-to-3_: https://huggingface.co/spaces/cvlab/zero123-live +You can see that the view angle is higher than on a battle. To change the angle from a battle sprite, you can use *Zero 1-to-3*: -You can get higher resolution using this Video AI that can control the motion of the camera: https://huggingface.co/spaces/TencentARC/MotionCtrl_SVD +You can get higher resolution using this Video AI that can control the motion of the camera: -If you have a 3D software, you can get better quality by converting your image into 3D model and then render it from another angle using _Stable Fast 3D_: https://huggingface.co/spaces/stabilityai/stable-fast-3d +If you have a 3D software, you can get better quality by converting your image into 3D model and then render it from another angle using *Stable Fast 3D*: -Follow this comment to retrieve the color: https://huggingface.co/stabilityai/TripoSR/discussions/1#65e8a8e5e214f37d85dad366 +Follow this comment to retrieve the color: ### Shadow render There are no strong rules in the original game about the angle of the shadows on the map. Different buildings have inconsistent shadows. To draw the shadow, I recommend the following technique: Let's consider that the object is a vertical cone: + | | | | | | | | | | | |---|---|---|---|---|---|---|---|---|---| | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | @@ -98,6 +104,7 @@ Let's consider that the object is a vertical cone: | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | Locate the top and its projection to the ground: + | | | | | | | | | | | |---|---|---|---|---|---|---|---|---|---| | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | @@ -112,6 +119,7 @@ Locate the top and its projection to the ground: | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | Then draw a rectangle triangle on the left: + | | | | | | | | | | | |---|---|---|---|---|---|---|---|---|---| | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | @@ -126,6 +134,7 @@ Then draw a rectangle triangle on the left: | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | The square top is the projection of the shadow of the top of the cone: + | | | | | | | | | | | |---|---|---|---|---|---|---|---|---|---| | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | @@ -140,6 +149,7 @@ The square top is the projection of the shadow of the top of the cone: | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | Then you can draw the rest of the shadow: + | | | | | | | | | | | |---|---|---|---|---|---|---|---|---|---| | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | diff --git a/docs/modders/Entities_Format/Faction_Format.md b/docs/modders/Entities_Format/Faction_Format.md index 739086331..7cb1e95e0 100644 --- a/docs/modders/Entities_Format/Faction_Format.md +++ b/docs/modders/Entities_Format/Faction_Format.md @@ -8,46 +8,46 @@ In order to make functional town, you also need: ### Images -- Creature backgrounds images, 120x100 and 130x100 versions (2 images) -- Set of puzzle map pieces (48 images) -- Background scenery (1 image) -- Mage guild window view (1 image) -- Town hall background (1 image) +- Creature backgrounds images, 120x100 and 130x100 versions (2 images) +- Set of puzzle map pieces (48 images) +- Background scenery (1 image) +- Mage guild window view (1 image) +- Town hall background (1 image) -- Set of town icons, consists from all possible combinations of: (8 +- Set of town icons, consists from all possible combinations of: (8 images total) - - small and big icons - - village and fort icons - - built and normal icons + - small and big icons + - village and fort icons + - built and normal icons -- Set for castle siege screen, consists from: - - Background (1 image) - - Destructible towers (3 parts, 3 images each) - - Destructible walls (4 parts, 3 images each) - - Static walls (3 images) - - Town gates (5 images) - - Moat (2 images) +- Set for castle siege screen, consists from: + - Background (1 image) + - Destructible towers (3 parts, 3 images each) + - Destructible walls (4 parts, 3 images each) + - Static walls (3 images) + - Town gates (5 images) + - Moat (2 images) ### Animation -- Adventure map images for village, town and capitol (3 def files) +- Adventure map images for village, town and capitol (3 def files) ### Music -- Town theme music track (at least 1 music file) +- Town theme music track (at least 1 music file) ### Buildings Each town requires a set of buildings (Around 30-45 buildings) -- Town animation file (1 animation file) -- Selection highlight (1 image) -- Selection area (1 image) -- Town hall icon (1 image) +- Town animation file (1 animation file) +- Selection highlight (1 image) +- Selection area (1 image) +- Town hall icon (1 image) ## Faction node (root entry for town configuration) -```jsonc +```json5 // Unique faction identifier. "myFaction" : { @@ -108,7 +108,7 @@ Each town requires a set of buildings (Around 30-45 buildings) ## Town node -```jsonc +```json5 { // Field that describes behavior of map object part of town. Town-specific part of object format "mapObject" : @@ -256,7 +256,7 @@ Each town requires a set of buildings (Around 30-45 buildings) ## Siege node -```jsonc +```json5 // Describes town siege screen // Comments in the end of each graphic position indicate specify required suffix for image // Note: one not included image is battlefield background with suffix "BACK" @@ -340,7 +340,9 @@ Each town requires a set of buildings (Around 30-45 buildings) ``` ## Building node + See [Town Building Format](Town_Building_Format.md) ## Structure node -See [Town Building Format](Town_Building_Format.md) \ No newline at end of file + +See [Town Building Format](Town_Building_Format.md) diff --git a/docs/modders/Entities_Format/Faction_Help.md b/docs/modders/Entities_Format/Faction_Help.md index c141a8673..ebbcd38fa 100644 --- a/docs/modders/Entities_Format/Faction_Help.md +++ b/docs/modders/Entities_Format/Faction_Help.md @@ -3,90 +3,109 @@ This page helps you to create from scratch a VCMI mod that adds a new faction. The faction mod structure is described [here](Faction_Format.md). ## Questioning the faction creation + Before creating a faction, be aware that creating a faction mod is lots of work. You can start [creating creatures](Creature_Help.md) in a creature mod that can be converted into a faction mod after. This way, you are sure to release something. The smallest contribution is a hero portrait that you can suggest on an existing mod. You can also restore the former version of the [Ruins faction](https://github.com/vcmi-mods/ruins-town/tree/1bea30a1d915770e2fd0f95d158030815ff462cd). You would only have to remake the similar parts to the new version. ## Make a playable faction mod -Before creating your content, retrieve the content of an existing faction mod like [Highlands town](https://github.com/vcmi-mods/highlands-town). To download the project, click on the _Code_ button and click on _Download ZIP_. The first thing to do is to change all the faction identifiers in the files following the [faction format](Faction_Format.md) and manage to play with the faction and the original without any conflict. To play to a faction, you have to add all the files in your _Mods_ folder. When it works, you will be able to modify the content step by step. + +Before creating your content, retrieve the content of an existing faction mod like [Highlands town](https://github.com/vcmi-mods/highlands-town). To download the project, click on the *Code* button and click on *Download ZIP*. The first thing to do is to change all the faction identifiers in the files following the [faction format](Faction_Format.md) and manage to play with the faction and the original without any conflict. To play to a faction, you have to add all the files in your *Mods* folder. When it works, you will be able to modify the content step by step. Keep in mind that the most important part of a faction mod, above the animations, the graphisms and the musics, is the concept because if you have to change it, you have to change everything else. All the remaining content can be improved by the community. ## Town screen -### Background -Beware to direct all the shadows to the same direction. The easiest way to create the background is to use a text-to-image AI. The free most powerful AI at the moment is _Flux_ available here: https://huggingface.co/spaces/multimodalart/FLUX.1-merged -In the _Advanced Options_, set the width to 800px and set the height to 374px. +### Background + +Beware to direct all the shadows to the same direction. The easiest way to create the background is to use a text-to-image AI. The free most powerful AI at the moment is *Flux* available here: + +In the *Advanced Options*, set the width to 800px and set the height to 374px. ### Buildings -To render a building upon the background, I recommend to use an inpainting AI like _BRIA Inpaint_: https://huggingface.co/spaces/briaai/BRIA-2.3-Inpainting + +To render a building upon the background, I recommend to use an inpainting AI like *BRIA Inpaint*: The idea is to select the area where you want to add the building. As a prompt, describe the new building. The advantage is a perfect match between the background and the building. Keep in mind that to correctly integrate a building image, it must contain the image of the background on its edges. It simulates the semi-transparency. -You can also animate the building or the background using _Stable Video Diffusion_: https://huggingface.co/spaces/multimodalart/stable-video-diffusion +You can also animate the building or the background using *Stable Video Diffusion*: ## Map dwellings -You may want to get the same render as in the town, so you have to change the angle and the shadows. If you handle a 3D model software, you can start with _Stable Fast 3D_: https://huggingface.co/spaces/stabilityai/stable-fast-3d + +You may want to get the same render as in the town, so you have to change the angle and the shadows. If you handle a 3D model software, you can start with *Stable Fast 3D*: The map dwellings are not rendered on the map with vanishing points but in isometric. You can [get an orthogonal render in Blender](https://blender.stackexchange.com/a/135384/2768). -Without 3D, you can use _Zero 1-to-3_: https://huggingface.co/spaces/cvlab/zero123-live +Without 3D, you can use *Zero 1-to-3*: -You can get higher resolution using this Video AI that can control the motion of the camera: https://huggingface.co/spaces/TencentARC/MotionCtrl_SVD +You can get higher resolution using this Video AI that can control the motion of the camera: -The buildings on the map are more satured than on town screen. If you have to reduce the size of an image, do not use interpolation (LANCZOS, Bilinear...) to get more details, not a blurred image. If you need to increase the resolution or the quality of your template image, use _SUPIR_: https://huggingface.co/spaces/Fabrice-TIERCELIN/SUPIR +The buildings on the map are more satured than on town screen. If you have to reduce the size of an image, do not use interpolation (LANCZOS, Bilinear...) to get more details, not a blurred image. If you need to increase the resolution or the quality of your template image, use *SUPIR*: ## Map buildings + The AIs badly understand the sun direction and the perspective angles. To generate the buildings on the adventure map: 1. Open the HOMM3 map editor 1. Put items all around a big empty area 1. Make a screenshot -1. Go on an AI like _BRIA Inpaint_: https://huggingface.co/spaces/briaai/BRIA-2.3-Inpainting +1. Go on an AI like *BRIA Inpaint*: 1. Inpaint the (big) empty middle with the brush 1. Use a prompt like: `A dark house, at the center of the image, map, isometric, parallel perspective, sunlight from the bottom right` ## Music + Here are unused available themes: * [Synthetic Horizon](https://github.com/Fabrice-TIERCELIN/forge/raw/theme/content/music/factions/theme.ogg) + 1. Prompt: `Dystopy, Cinematic classical, Science fiction, 160 bpm, Best quality, Futuristic` -1. Initially created for: _Forge town_ +1. Initially created for: *Forge town* * [Quantum Overture](https://github.com/Fabrice-TIERCELIN/asylum-town/raw/theme/asylum-town/content/Music/factions/AsylumTown.ogg) + 1. Prompt: `Clef shifting, Fantasy, Mystical, Overworldly, Cinematic classical` -1. Initially created for: _Asylum town_ +1. Initially created for: *Asylum town* * [Warrior s March](https://github.com/vcmi-mods/ruins-town/assets/20668759/964f27de-6feb-4ef6-9d25-455f52938cef) + 1. Prompt: `Powerful percussions, Drums, Battle Anthem, Rythm, Warrior, 160 bpm, Celtic, New age, Instrumental, Accoustic, Medieval` -1. Initially created for: _Ruins town_ +1. Initially created for: *Ruins town* * [Clan of Echoes](https://github.com/Fabrice-TIERCELIN/ruins-town/raw/theme/ruins-town/content/music/ruins.ogg) + 1. Prompt: `new age, medieval, celtic, warrior, battle, soundtrack, accoustic, drums, rythm` -1. Initially created for: _Ruins town_ +1. Initially created for: *Ruins town* * [Enchanted Reverie](https://github.com/Fabrice-TIERCELIN/grove/raw/theme/Grove/content/Music/factions/GroveTown.ogg) + 1. Prompt: `Classical music, Soundtrack, Score, Instrumental, 160 bpm, ((((fantasy)))), mystic` -1. Initially created for: _Grove town_ +1. Initially created for: *Grove town* * [World Discovery](https://github.com/vcmi-mods/asylum-town/assets/20668759/34438523-8a44-44ca-b493-127501b474a6) + 1. Prompt: `Clef shifting, fantasy, mystical, overworldly, Cinematic classical` -1. Initially created for: _Asylum town_ +1. Initially created for: *Asylum town* * [Enchanted Ballad](https://github.com/vcmi-mods/fairy-town/assets/20668759/619e6e33-d940-4899-8c76-9c1e8d3d20aa) + 1. Prompt: `Females vocalize, Cinematic classical, Harp, Fairy tale, Princess, 160 bpm` -1. Initially created for: _Fairy town_ +1. Initially created for: *Fairy town* * [Baroque Resurgence](https://github.com/Fabrice-TIERCELIN/courtyard_proposal/raw/theme/Courtyard/Content/music/factions/courtyard/CourtTown.ogg) + 1. Prompt: `Baroque, Instrumental, 160 bpm, Cinematic classical, Best quality` -1. Initially created for: _Courtyard town_ +1. Initially created for: *Courtyard town* * [Harvest Parade](https://github.com/Fabrice-TIERCELIN/greenhouse-town/raw/theme/Greenhouse/content/Music/town.ogg) -1. Prompt: `Marching band, Best quality, Happy, Vegetables` -1. Initially created for: _Green town_ -Those themes have been generated using _[Udio](https://udio.com)_. +1. Prompt: `Marching band, Best quality, Happy, Vegetables` +1. Initially created for: *Green town* + +Those themes have been generated using *[Udio](https://udio.com)*. ## Screenshots + Most of the time, the first screenshot is the townscreen because it's the most specific content. ## Recycle -Some mods contain neutral heroes or creatures. You can integrate them in your faction mod. Don't forget to remove the content from the original mod. \ No newline at end of file + +Some mods contain neutral heroes or creatures. You can integrate them in your faction mod. Don't forget to remove the content from the original mod. diff --git a/docs/modders/Entities_Format/Hero_Class_Format.md b/docs/modders/Entities_Format/Hero_Class_Format.md index 68833fa5d..78e743ac1 100644 --- a/docs/modders/Entities_Format/Hero_Class_Format.md +++ b/docs/modders/Entities_Format/Hero_Class_Format.md @@ -4,12 +4,12 @@ In order to make functional hero class you also need: -- Adventure animation (1 def file) -- Battle animation, male and female version (2 def files) +- Adventure animation (1 def file) +- Battle animation, male and female version (2 def files) ## Format -``` javascript +```json5 // Unique identifier of hero class, camelCase "myClassName" : { @@ -106,4 +106,4 @@ In order to make functional hero class you also need: "conflux" : 6 } } -``` \ No newline at end of file +``` diff --git a/docs/modders/Entities_Format/Hero_Type_Format.md b/docs/modders/Entities_Format/Hero_Type_Format.md index b646c0e89..e0755d23f 100644 --- a/docs/modders/Entities_Format/Hero_Type_Format.md +++ b/docs/modders/Entities_Format/Hero_Type_Format.md @@ -4,12 +4,12 @@ In order to make functional hero you also need: -- Portraits, small and big versions (2 images) -- Specialty icons, small and big versions (2 images) +- Portraits, small and big versions (2 images) +- Specialty icons, small and big versions (2 images) ## Format -``` javascript +```json5 "myHeroName" : { // Identifier of class this hero belongs to. Such as knight or battleMage @@ -133,4 +133,4 @@ In order to make functional hero you also need: "creature" : "griffin" } } -``` \ No newline at end of file +``` diff --git a/docs/modders/Entities_Format/River_Format.md b/docs/modders/Entities_Format/River_Format.md index 35635768d..64e1e085f 100644 --- a/docs/modders/Entities_Format/River_Format.md +++ b/docs/modders/Entities_Format/River_Format.md @@ -2,7 +2,7 @@ ## Format -```jsonc +```json5 "newRiver" : { // Two-letters unique identifier for this river. Used in map format @@ -28,4 +28,4 @@ ... ] } -``` \ No newline at end of file +``` diff --git a/docs/modders/Entities_Format/Road_Format.md b/docs/modders/Entities_Format/Road_Format.md index d78de9b10..59d384881 100644 --- a/docs/modders/Entities_Format/Road_Format.md +++ b/docs/modders/Entities_Format/Road_Format.md @@ -2,7 +2,7 @@ ## Format -```jsonc +```json5 "newRoad" : { // Two-letters unique identifier for this road. Used in map format @@ -17,4 +17,4 @@ // How many movement points needed to move hero "moveCost" : 66 } -``` \ No newline at end of file +``` diff --git a/docs/modders/Entities_Format/Secondary_Skill_Format.md b/docs/modders/Entities_Format/Secondary_Skill_Format.md index 367ca775f..09cbaf79f 100644 --- a/docs/modders/Entities_Format/Secondary_Skill_Format.md +++ b/docs/modders/Entities_Format/Secondary_Skill_Format.md @@ -2,7 +2,16 @@ ## Main format -```jsonc +```json5 +{ + // Skill be only be available on maps with water + "onlyOnWaterMap" : false, + // Skill is not available on maps at random + "special" : true +} +``` + +```json5 { "skillName": { @@ -46,7 +55,7 @@ level fields become optional if they equal "base" configuration. ## Skill level format -```jsonc +```json5 { // Localizable description // Use {xxx} for formatting @@ -78,7 +87,7 @@ level fields become optional if they equal "base" configuration. The following modifies the tactics skill to grant an additional speed boost at advanced and expert levels. -```jsonc +```json5 "core:tactics" : { "base" : { "effects" : { @@ -114,4 +123,4 @@ boost at advanced and expert levels. } } } -``` \ No newline at end of file +``` diff --git a/docs/modders/Entities_Format/Spell_Format.md b/docs/modders/Entities_Format/Spell_Format.md index 41461b606..67a3a73ed 100644 --- a/docs/modders/Entities_Format/Spell_Format.md +++ b/docs/modders/Entities_Format/Spell_Format.md @@ -2,7 +2,7 @@ ## Main format -``` javascript +```json5 { "spellName": { @@ -64,6 +64,9 @@ // If true, then creature capable of casting this spell can cast this spell on itself // If false, then creature can only cast this spell on other units "canCastOnSelf" : false, + + // If true, then creature capable of casting this spell can cast this spell only on itself + "canCastOnlyOnSelf" : false, // If true the creature will not skip the turn after casting a spell "canCastWithoutSkip": false, @@ -153,7 +156,7 @@ TODO -``` javascript +```json5 { "projectile": [ {"minimumAngle": 0 ,"defName":"C20SPX4"}, @@ -164,7 +167,7 @@ TODO ], "cast" : [] "hit":["C20SPX"], - "affect":[{"defName":"C03SPA0", "verticalPosition":"bottom"}, "C11SPA1"] + "affect":[{"defName":"C03SPA0", "verticalPosition":"bottom", "transparency" : 0.5}, "C11SPA1"] } ``` @@ -176,7 +179,7 @@ Json object with data common for all levels can be put here. These configuration This will make spell affect single target on all levels except expert, where it is massive spell. -``` javascript +```json5 "base":{ "range": 0 }, @@ -189,7 +192,7 @@ This will make spell affect single target on all levels except expert, where it TODO -``` javascript +```json5 { //Mandatory, localizable description. Use {xxx} for formatting @@ -259,7 +262,7 @@ Configurable spells ignore *offensive* flag, *effects* and *cumulativeEffects*. TODO -``` javascript +```json5 "mod:effectId":{ @@ -280,7 +283,7 @@ TODO TODO -``` javascript +```json5 "mod:effectId":{ @@ -301,7 +304,7 @@ TODO Configurable version of Clone spell. -``` javascript +```json5 "mod:effectId":{ @@ -317,7 +320,7 @@ TODO If effect is automatic, spell behave like offensive spell (uses power, levelPower etc) -``` javascript +```json5 "mod:effectId":{ @@ -365,7 +368,7 @@ TODO If effect is automatic, spell behave like \[de\]buff spell (effect and cumulativeEffects ignored) -``` javascript +```json5 "mod:effectId":{ @@ -386,21 +389,21 @@ cumulativeEffects ignored) TODO - CREATURE target (only battle spells) - - range 0: smart assumed single creature target - - range "X" + smart modifier = enchanter casting, expert massive spells - - range "X" + no smart modifier = armageddon, death ripple, destroy undead - - any other range (including chain effect) - - smart modifier: smth like cloud of confusion in H4 (if I remember correctly :) ) - - no smart modifier: like inferno, fireball etc. but target only creature +- range 0: smart assumed single creature target +- range "X" + smart modifier = enchanter casting, expert massive spells +- range "X" + no smart modifier = armageddon, death ripple, destroy undead +- any other range (including chain effect) +- smart modifier: smth like cloud of confusion in H4 (if I remember correctly :) ) +- no smart modifier: like inferno, fireball etc. but target only creature - NO_TARGET - - no target selection,(abilities, most adventure spells) +- no target selection,(abilities, most adventure spells) - LOCATION - - any tile on map/battlefield (inferno, fireball etc.), DD also here but with special handling - - clearTarget - destination hex must be clear (unused so far) - - clearAfffected - all affected hexes must be clear (forceField, fireWall) +- any tile on map/battlefield (inferno, fireball etc.), DD also here but with special handling +- clearTarget - destination hex must be clear (unused so far) +- clearAfffected - all affected hexes must be clear (forceField, fireWall) - OBSTACLE target - - range 0: any single obstacle - - range X: all obstacles \ No newline at end of file +- range 0: any single obstacle +- range X: all obstacles diff --git a/docs/modders/Entities_Format/Terrain_Format.md b/docs/modders/Entities_Format/Terrain_Format.md index d12a7f7cf..1562d78c8 100644 --- a/docs/modders/Entities_Format/Terrain_Format.md +++ b/docs/modders/Entities_Format/Terrain_Format.md @@ -2,7 +2,7 @@ ## Format -```jsonc +```json5 "newTerrain" : { // Two-letters unique identifier for this terrain. Used in map format @@ -75,4 +75,4 @@ "terrainViewPatterns" : "", } -``` \ No newline at end of file +``` diff --git a/docs/modders/Entities_Format/Town_Building_Format.md b/docs/modders/Entities_Format/Town_Building_Format.md index 67079fda4..e0586c6e8 100644 --- a/docs/modders/Entities_Format/Town_Building_Format.md +++ b/docs/modders/Entities_Format/Town_Building_Format.md @@ -4,17 +4,20 @@ Each building requires following assets: -- Town animation file (1 animation file) -- Selection highlight (1 image) -- Selection area (1 image) -- Town hall icon (1 image) +- Town animation file (1 animation file) +- Selection highlight (1 image) +- Selection area (1 image) +- Town hall icon (1 image) ## Examples + These are just a couple of examples of what can be done in VCMI. See vcmi configuration files to check how buildings from Heroes III are implemented or other mods for more examples + #### -##### Order of Fire from Inferno: -```jsonc +##### Order of Fire from Inferno + +```json5 "special4": { "requires" : [ "mageGuild1" ], "name" : "Order of Fire", @@ -34,10 +37,11 @@ These are just a couple of examples of what can be done in VCMI. See vcmi config ] } } -``` +``` ##### Mana Vortex from Dungeon -```jsonc + +```json5 "special2": { "requires" : [ "mageGuild1" ], "name" : "Mana Vortex", @@ -65,7 +69,8 @@ These are just a couple of examples of what can be done in VCMI. See vcmi config ``` #### Resource Silo with custom production -```jsonc + +```json5 "resourceSilo": { "name" : "Wood Resource Silo", "description" : "Produces 2 wood every day", @@ -80,7 +85,8 @@ These are just a couple of examples of what can be done in VCMI. See vcmi config ``` #### Brotherhood of Sword - bonuses in siege -```jsonc + +```json5 "special3": { // replaces +1 Morale bonus from Tavern "upgradeReplacesBonuses" : true, @@ -96,7 +102,8 @@ These are just a couple of examples of what can be done in VCMI. See vcmi config ``` #### Lighthouse - bonus to all heroes under player control -```jsonc + +```json5 "special1": { "bonuses": [ { @@ -112,7 +119,7 @@ These are just a couple of examples of what can be done in VCMI. See vcmi config ## Town Building node -```jsonc +```json5 { // Numeric identifier of this building "id" : 0, @@ -211,7 +218,7 @@ These are just a couple of examples of what can be done in VCMI. See vcmi config Building requirements can be described using logical expressions: -```jsonc +```json5 "requires" : [ "allOf", // Normal H3 "build all" mode @@ -228,10 +235,13 @@ Building requirements can be described using logical expressions: ] ] ``` + ### List of unique town buildings #### Buildings from Heroes III + Following Heroes III buildings can be used as unique buildings for a town. Their functionality should be identical to a corresponding H3 building. H3 buildings that are not present in this list contain no hardcoded functionality. See vcmi json configuration to see how such buildings can be implemented in a mod. + - `mysticPond` - `artifactMerchant` - `freelancersGuild` @@ -244,16 +254,18 @@ Following Heroes III buildings can be used as unique buildings for a town. Their - `treasury` #### Buildings from other Heroes III mods + Following HotA buildings can be used as unique building for a town. Functionality should match corresponding HotA building: + - `bank` #### Custom buildings -In addition to above, it is possible to use same format as [Rewardable](../Map_Objects/Rewardable.md) map objects for town buildings. In order to do that, configuration of a rewardable object must be placed into `configuration` json node in building config. +In addition to above, it is possible to use same format as [Rewardable](../Map_Objects/Rewardable.md) map objects for town buildings. In order to do that, configuration of a rewardable object must be placed into `configuration` json node in building config. ### Town Structure node -```jsonc +```json5 { // Main animation file for this building "animation" : "", @@ -281,16 +293,18 @@ In addition to above, it is possible to use same format as [Rewardable](../Map_O } ``` - #### Markets in towns + Market buildings require list of available [modes](../Map_Objects/Market.md) ##### Marketplace -```jsonc + +```json5 "marketplace": { "marketModes" : ["resource-resource", "resource-player"] }, ``` ##### Artifact merchant -```jsonc + +```json5 "special1": { "type" : "artifactMerchant", "requires" : [ "marketplace" ], "marketModes" : ["resource-artifact", "artifact-resource"] }, -``` \ No newline at end of file +``` diff --git a/docs/modders/File_Formats.md b/docs/modders/File_Formats.md index d1466e4cf..900c90a25 100644 --- a/docs/modders/File_Formats.md +++ b/docs/modders/File_Formats.md @@ -1,12 +1,13 @@ # File Formats -This page describes which file formats are supported by vcmi. +This page describes which file formats are supported by vcmi. In most cases, VCMI supports formats that were supported by Heroes III, with addition of new formats that are more convenient to use without specialized tools. See categories below for more details on specific formats ### Images For images VCMI supports: + - png. Recommended for usage in mods - bmp. While this format is supported, bmp images have no compressions leading to large file sizes - pcx (h3 version). Note that this is format that is specific to Heroes III and has nothing in common with widely known .pcx format. Files in this format generally can only be found inside of .lod archive of Heroes III and are usually extracted as .bmp files @@ -25,13 +26,23 @@ For animations VCMI supports .def format from Heroes III as well as alternative ### Sounds -For sounds VCMI currently requires .wav format. Generally, VCMI will support any .wav parameters, however you might want to use high-bitrate versions, such as 44100 Hz or 48000 Hz, 32 bit, 1 or 2 channels +For sounds VCMI currently supports: -Support for additional formats, such as ogg/vorbis and ogg/opus is likely to be added in future +- .ogg/vorbis format - preferred for mods. Unlike wav, vorbis uses compression which may cause some data loss, however even 128kbit is generally undistinguishable from lossless formats +- .wav format. This is format used by H3. It is supported by vcmi, but it may result in large file sizes (and as result - large mods) + +Generally, VCMI will support any audio parameters, however you might want to use high-bitrate versions, such as 44100 Hz or 48000 Hz, 32 bit, 1 or 2 channels + +Support for additional formats, such as ogg/opus or flac may be added in future ### Music -For sounds VCMI currently requires .mp3 format. Support for additional formats, such as ogg/vorbis and ogg/opus is likely to be added in future +For music VCMI currently supports: + +- .ogg/vorbis format - preferred for mods. Generally offers better quality and lower sizes compared to mp3 +- .mp3 format. This is format used by H3 + +Support for additional formats, such as ogg/opus may be added in future ### Video @@ -43,6 +54,7 @@ Starting from VCMI 1.6, following video container formats are supported by VCMI: - .webm - modern, free format that is recommended for modding. Supported video codecs: + - bink and smacker - formats used by Heroes III, should be used only to avoid re-encoding - theora - used by Heroes III: HD Edition - vp8 - modern format with way better compression compared to formats used by Heroes III @@ -51,6 +63,7 @@ Supported video codecs: Support for av1 video codec is likely to be added in future. Supported audio codecs: + - binkaudio and smackaud - formats used by Heroes III - vorbis - modern format with good compression level - opus - recommended, improvement over vorbis. Any bitrate is supported, with 128 kbit probably being the best option diff --git a/docs/modders/Game_Identifiers.md b/docs/modders/Game_Identifiers.md index 4e212fe01..24e95b530 100644 --- a/docs/modders/Game_Identifiers.md +++ b/docs/modders/Game_Identifiers.md @@ -493,7 +493,7 @@ This is a list of all game identifiers available to modders. Note that only iden - hero.thorgrim - hero.thunar - hero.tiva -- hero.torosar +- hero.torosar - hero.tyraxor - hero.tyris - hero.ufretin diff --git a/docs/modders/HD_Graphics.md b/docs/modders/HD_Graphics.md new file mode 100644 index 000000000..c5a8d3d2a --- /dev/null +++ b/docs/modders/HD_Graphics.md @@ -0,0 +1,38 @@ +# HD Graphics + +It's possible to provide alternative high-definition graphics within mods. They will be used if any upscaling filter is activated. + +## Preconditions + +It's still necessary to add 1x standard definition graphics as before. HD graphics are seperate from usual graphics. This allows to partitially use HD for a few graphics in mod. And avoid handling huge graphics if upscaling isn't enabled. + +Currently following scaling factors are possible to use: 2x, 3x, 4x. You can also provide multiple of them (increases size of mod, but improves loading performance for player). It's recommend to provide 2x and 3x images. + +If user for example selects 3x resolution and only 2x exists in mod then the 2x images are upscaled to 3x (same for other combinations > 1x). + +## Mod + +For upscaled images you have to use following folders (next to `sprites`, `data` and `video` folders): + +- `sprites2x`, `sprites3x`, `sprites4x` for sprites +- `data2x`, `data3x`, `data4x` for images +- `video2x`, `video3x`, `video4x` for videos + +The sprites should have the same name and folder structure as in `sprites`, `data` and `video` folder. All images that are missing in the upscaled folders are scaled with the selected upscaling filter instead of using prescaled images. + +### Shadows / Overlays + +It's also possible (but not necessary) to add high-definition shadows: Just place a image next to the normal upscaled image with the suffix `-shadow`. E.g. `TestImage.png` and `TestImage-shadow.png`. +In future, such shadows will likely become required to correctly exclude shadow from effects such as Clone spell. + +Shadow images are used only for animations of following objects: + +- All adventure map objects +- All creature animations in combat + +Same for overlays with `-overlay`. But overlays are **necessary** for some animation graphics. They will be colorized by VCMI. + +Currently needed for: + +- Flaggable adventure map objects. Overlay must contain a transparent image with white flags on it and will be used to colorize flags to owning player +- Creature battle animations, idle and mouse hover group. Overlay must contain a transparent image with white outline of creature for highlighting on mouse hover diff --git a/docs/modders/Map_Editor.md b/docs/modders/Map_Editor.md index 3c472d8a3..ceb6853a4 100644 --- a/docs/modders/Map_Editor.md +++ b/docs/modders/Map_Editor.md @@ -38,7 +38,7 @@ Templates are dynamically filtered depending on parameters you choose. To load the map, press open and select map file from the browser. -You can load both *.h3m and *.vmap formats but for saving *.vmap is allowed only. +You can load both *.h3m and*.vmap formats but for saving *.vmap is allowed only. ## Views @@ -60,7 +60,7 @@ There are 3 buttons switching views Снимок экра
 
 <img width= -# Setup terrain +## Setup terrain 1. Select brush you want @@ -77,7 +77,7 @@ There are 3 buttons switching views Снимок экра
 
 #### Drawing roads and rivers
 
-Actually, the process to draw rivers or roads is exactly the same as for terrains. You need to select tiles and then choose road/river type from the panel. 
+Actually, the process to draw rivers or roads is exactly the same as for terrains. You need to select tiles and then choose road/river type from the panel.
 
 <img width= @@ -85,9 +85,10 @@ To erase roads or rivers, you need to select tiles to be cleaned and press empty -_Erasing works either for roads or for rivers, e.g. empty button from the roads tab erases roads only, but not rivers. You also can safely select bigger area, because it won't erase anything on tiles without roads/rivers accordingly_ +*Erasing works either for roads or for rivers, e.g. empty button from the roads tab erases roads only, but not rivers. You also can safely select bigger area, because it won't erase anything on tiles without roads/rivers accordingly* ### About brushes + * Buttons "1", "2", "4" - 1x1, 2x2, 4x4 brush sizes accordingly * Button "[]" - non-additive rectangle selection * Button "O" - lasso brush (not implemented yet) @@ -171,7 +172,7 @@ You can modify general properties of the map ## Player settings -Open **Map** menu on the top and select **Player settings" +Open **Map** menu on the top and select **Player settings** @@ -208,6 +209,7 @@ vcmieditor loads set of mods using exactly same mechanism as game uses and mod m The mods mechanism used in map editor is the same as in game. To enable or disable mods + * Start launcher, activate or deactivate mods you want * Close launcher * Run map editor @@ -233,4 +235,4 @@ You also may have other mods being activated in addition to what was used during #### Mod versions -In the future, the will be support of mods versioning so map will contain information about mods used and game can automatically search and activate required mods or let user know which are required. However, it's not implemented yet \ No newline at end of file +In the future, the will be support of mods versioning so map will contain information about mods used and game can automatically search and activate required mods or let user know which are required. However, it's not implemented yet diff --git a/docs/modders/Map_Object_Format.md b/docs/modders/Map_Object_Format.md index 22785ac55..93a8e3fbe 100644 --- a/docs/modders/Map_Object_Format.md +++ b/docs/modders/Map_Object_Format.md @@ -4,11 +4,11 @@ Full object consists from 3 parts: -- Object group - set of objects that have similar behavior and share +- Object group - set of objects that have similar behavior and share same identifier in H3 (towns, heroes, mines, etc) -- Object type - object with fixed behavior but without fixed +- Object type - object with fixed behavior but without fixed appearance. Multiple objects types may share same group -- Object template - defines appearance of an object - image used to +- Object template - defines appearance of an object - image used to display it, its size & blockmap. These entries only describe templates that will be used when object is placed via map editor or generated by the game. When new object is created its starting @@ -16,7 +16,7 @@ Full object consists from 3 parts: ## Object group format -``` javascript +```json5 { "myCoolObjectGroup": @@ -42,6 +42,7 @@ Full object consists from 3 parts: ## Object types ### Moddable types + These are object types that are available for modding and have configurable properties - `configurable` - see [Rewardable](Map_Objects/Rewardable.md). Visitable object which grants all kinds of rewards (gold, experience, Bonuses etc...) @@ -49,18 +50,19 @@ These are object types that are available for modding and have configurable prop - `dwelling` - see [Dwelling](Map_Objects/Dwelling.md). Object that allows recruitments of units outside of towns - `market` - see [Market](Map_Objects/Market.md). Trading resources, artifacts, creatures and such - `boat` - see [Boat](Map_Objects/Boat.md). Object to move across different terrains, such as water +- `flaggable` - see [Flaggable](Map_Objects/Flaggable.md). Object that can be flagged by a player to provide [Bonus](Bonus_Format.md) or resources - `hillFort` - TODO: documentation. See config files in vcmi installation for reference - `shipyard` - TODO: documentation. See config files in vcmi installation for reference - `terrain` - Defines terrain overlays such as magic grounds. TODO: documentation. See config files in vcmi installation for reference ### Common types + These are types that don't have configurable properties, however it is possible to add additional map templates for this objects, for use in editor or in random maps generator - `static` - Defines unpassable static map obstacles that can be used by RMG - `generic` - Defines empty object type that provides no functionality. Note that unlike `static`, objects of this type are never used by RMG - `borderGate` - `borderGuard` -- `lighthouse` - `magi` - `mine` - `obelisk` @@ -79,6 +81,7 @@ These are types that don't have configurable properties, however it is possible - `monolith` ### Internal types + These are internal types that are generally not available for modding and are handled by vcmi internally. - `hero` @@ -96,7 +99,7 @@ These are internal types that are generally not available for modding and are ha ## Object type format -``` javascript +```json5 { "myCoolObject": { @@ -150,7 +153,7 @@ These are internal types that are generally not available for modding and are ha ## Object template format -``` javascript +```json5 { "myCoolObjectTemplate" : { diff --git a/docs/modders/Map_Objects/Boat.md b/docs/modders/Map_Objects/Boat.md index 8d67581a5..cb827bad1 100644 --- a/docs/modders/Map_Objects/Boat.md +++ b/docs/modders/Map_Objects/Boat.md @@ -1,6 +1,6 @@ # Boat -``` javascript +```json5 { // Layer on which this boat moves. Possible values: // "land" - same rules as movement of hero on land @@ -27,4 +27,4 @@ // List of bonuses that will be granted to hero located in the boat "bonuses" : { BONUS_FORMAT } } -``` \ No newline at end of file +``` diff --git a/docs/modders/Map_Objects/Creature_Bank.md b/docs/modders/Map_Objects/Creature_Bank.md index e5389846b..e75613266 100644 --- a/docs/modders/Map_Objects/Creature_Bank.md +++ b/docs/modders/Map_Objects/Creature_Bank.md @@ -6,9 +6,11 @@ Format of rewards is same as in [Rewardable Objects](Rewardable.md) Deprecated in 1.6. Please use [Rewardable Objects](Rewardable.md) instead. See Conversion from 1.5 format section below for help with migration ### Example + This example defines a rewardable object with functionality similar of H3 creature bank. See [Rewardable Objects](Rewardable.md) for detailed documentation of these properties. -```jsonc + +```json5 { "name" : "Cyclops Stockpile", @@ -93,6 +95,7 @@ See [Rewardable Objects](Rewardable.md) for detailed documentation of these prop ``` ### Conversion from 1.5 format + This is a list of changes that needs to be done to bank config to migrate it to 1.6 system. See [Rewardable Objects](Rewardable.md) documentation for description of new fields - If your object type has defined `handler`, change its value from `bank` to `configurable` @@ -112,7 +115,7 @@ This is a list of changes that needs to be done to bank config to migrate it to ### Old format (1.5 or earlier) -``` jsonc +```json5 { /// If true, battle setup will be like normal - Attacking player on the left, enemy on the right "regularUnitPlacement" : true, diff --git a/docs/modders/Map_Objects/Dwelling.md b/docs/modders/Map_Objects/Dwelling.md index df369c502..562f8f239 100644 --- a/docs/modders/Map_Objects/Dwelling.md +++ b/docs/modders/Map_Objects/Dwelling.md @@ -1,6 +1,6 @@ # Dwelling -``` javascript +```json5 { /// List of creatures in this bank. Each list represents one "level" of bank /// Creatures on the same level will have shared growth and available number (similar to towns) @@ -19,4 +19,4 @@ { "amount" : 12, "type" : "earthElemental" } ] } -``` \ No newline at end of file +``` diff --git a/docs/modders/Map_Objects/Flaggable.md b/docs/modders/Map_Objects/Flaggable.md new file mode 100644 index 000000000..69063baaf --- /dev/null +++ b/docs/modders/Map_Objects/Flaggable.md @@ -0,0 +1,40 @@ +# Flaggable objects + +Flaggable object are those that can be captured by a visiting hero. H3 examples are mines, dwellings, or lighthouse. + +Currently, it is possible to make flaggable objects that provide player with: + +- Any [Bonus](Bonus_Format.md) supported by bonus system +- Daily resources income (wood, ore, gold, etc) + +## Format description + +```json5 +{ + "baseObjectName" : { + "name" : "Object name", + "handler" : "flaggable", + "types" : { + "objectName" : { + + // Text for message that player will get on capturing this object with a hero + // Alternatively, it is possible to reuse existing string from H3 using form '@core.advevent.69' + "onVisit" : "{Object Name}\r\n\r\nText of messages that player will see on visit.", + + // List of bonuses that will be granted to player that owns this object + "bonuses" : { + "firstBonus" : { BONUS FORMAT }, + "secondBonus" : { BONUS FORMAT }, + }, + + // Resources that will be given to owner on every day + "dailyIncome" : { + "wood" : 2, + "ore" : 2, + "gold" : 1000 + } + } + } + } +} +``` diff --git a/docs/modders/Map_Objects/Market.md b/docs/modders/Map_Objects/Market.md index 2b411dfb8..18c07d6f6 100644 --- a/docs/modders/Map_Objects/Market.md +++ b/docs/modders/Map_Objects/Market.md @@ -7,7 +7,7 @@ Markets can be added as any other object with special handler called "market". Here is schema describing such object -```js +```json5 "seafaringAcademy" : //object name { "handler" : "market", //market handler @@ -34,6 +34,7 @@ Here is schema describing such object Mode parameter defines a way to exchange different entities. Multiple modes can be specified to support several types of exchange. Following options are supported: + * `"resource-resource"` - regular resource exchange, like trading post * `"resource-player"` - allows to send resources to another player * `"creature-resource"` - acts like freelance guild @@ -49,19 +50,20 @@ Following options are supported: ### Trading post Trading post allows to exchange resources and send resources to another player, so it shall be configured this way: -```json + +```json5 "modes" : ["resource-resource", "resource-player"] ``` ### Black market -```json +```json5 "modes" : ["resource-artifact"] ``` ### Freelance guild -```json +```json5 "modes" : ["creature-resource"] ``` @@ -71,7 +73,7 @@ Altar of sacrifice allows exchange creatures for experience for evil factions an So both modes shall be available in the market. Game logic prohibits using modes unavailable for faction -```json +```json5 "modes" : ["creature-experience", "artifact-experience"] ``` @@ -83,14 +85,14 @@ See [Secondary skills](Rewardable.md#secondary-skills) description for more deta ### Example for University of magic (e.g conflux building) -```js +```json5 "modes" : ["resource-skill"], "offer" : ["airMagic", "waterMagic", "earthMagic", "fireMagic"] ``` ### Example for regular University -```js +```json5 "modes" : ["resource-skill"], "offer" : [ //4 random skills except necromancy { "noneOf" : ["necromancy"] }, @@ -98,4 +100,4 @@ See [Secondary skills](Rewardable.md#secondary-skills) description for more deta { "noneOf" : ["necromancy"] }, { "noneOf" : ["necromancy"] } ] -``` \ No newline at end of file +``` diff --git a/docs/modders/Map_Objects/Rewardable.md b/docs/modders/Map_Objects/Rewardable.md index d7b1761d3..60c3ebcb2 100644 --- a/docs/modders/Map_Objects/Rewardable.md +++ b/docs/modders/Map_Objects/Rewardable.md @@ -1,8 +1,10 @@ # Rewardable ## Base object definition + Rewardable object is defined similarly to other objects, with key difference being `handler`. This field must be set to `"handler" : "configurable"` in order for vcmi to use this mode. -```jsonc + +```json5 { "baseObjectName" : { "name" : "Object name", @@ -34,7 +36,8 @@ Rewardable object is defined similarly to other objects, with key difference bei ``` ## Configurable object definition -```jsonc + +```json5 // List of potential rewards "rewards" : [ { @@ -173,7 +176,8 @@ This property allows defining "variables" that are shared between all rewards an Variables are randomized only once, so you can use them multiple times for example, to give skill only if hero does not have this skill (e.g. Witch Hut). Example of creation of a variable named "gainedSkill" of type "secondarySkill": -```json + +```json5 "variables" : { "secondarySkill" : { "gainedSkill" : { @@ -187,6 +191,7 @@ Example of creation of a variable named "gainedSkill" of type "secondarySkill": ``` Possible variable types: + - number: can be used in any place that expects a number - artifact - spell @@ -194,19 +199,22 @@ Possible variable types: - secondarySkill To reference variable in limiter prepend variable name with '@' symbol: -```json + +```json5 "secondary" : { "@gainedSkill" : 1 }, ``` ## Reset Parameters definition + This property describes how object state should be reset. Objects without this field will never reset its state. + - Period describes interval between object resets in day. Periods are counted from game start and not from hero visit, so reset duration of 7 will always reset object on new week & duration of 28 will always reset on new month. - If `visitors` is set to true, game will reset list of visitors (heroes and players) on start of new period, allowing revisits of objects with `visitMode` set to `once`, `hero`, or `player`. Objects with visit mode set to `bonus` are not affected. In order to allow revisit such objects use appropriate bonus duration (e.g. `ONE_DAY` or `ONE_WEEK`) instead. - If `rewards` is set to true, object will re-randomize its provided rewards, similar to such H3 objects as "Fountain of Fortune" or "Windmill" -```jsonc +```json5 "resetParameters" : { "period" : 7, "visitors" : true, @@ -215,15 +223,17 @@ This property describes how object state should be reset. Objects without this f ``` ## Appear Chance definition + This property describes chance for reward to be selected. When object is initialized on map load, game will roll a "dice" - random number in range 0-99, and pick all awards that have appear chance within selected number. Note that object that uses appearChance MUST have continuous range for every value in 0-99 range. For example, object with 3 different rewards may want to define them as + - `"min" : 0, "max" : 33` - `"min" : 33, "max" : 66` - `"min" : 66, "max" : 100` In other words, min chance of second reward must be equal to max chance of previous reward -```jsonc +```json5 "appearChance": { // (Advanced) rewards with different dice number will get different dice number @@ -240,42 +250,47 @@ In other words, min chance of second reward must be equal to max chance of previ ``` ## Configurable Properties + Unless stated othervice, all numbers in this section can be replaced with random values, e.g. -```jsonc + +```json5 "minLevel" : { "min" : 5, "max" : 10 } // select random number between 5-10, including both 5 & 10 "minLevel" : [ 2, 4, 6, 8, 10] // (VCMI 1.2) select random number out of provided list, with equal chance for each ``` -In this case, actual value for minLevel will be picked randomly. +In this case, actual value for minLevel will be picked randomly. Keep in mind, that all randomization is performed on map load and on object reset (if `rewards` field in `resetParameter` was set). ### Current Day + - Can only be used as limiter. To pass, current day of week should be equal to this value. 1 = first day of the week, 7 = last day -```jsonc +```json5 "dayOfWeek" : 0 ``` - Can only be used as limiter. To pass, number of days since game started must be at equal or greater than this value -```jsonc +```json5 "daysPassed" : 8 ``` ### Resource + - Can be used as limiter. To pass, player needs to have specified resources. Note that limiter will NOT take resources. - Can be used as reward to grant resources to player - If negative value is used as reward, it will be used as cost and take resources from player -```jsonc +```json5 "resources": { "crystal" : 6, "gold" : -1000, }, ``` + - Alternative format that allows random selection of a resource type -```jsonc +```json5 "resources": [ { "anyOf" : [ "wood", "ore" ], @@ -289,68 +304,75 @@ Keep in mind, that all randomization is performed on map load and on object rese ``` ### Experience + - Can be used as limiter - Can be used as reward to grant experience to hero -```jsonc +```json5 "heroExperience" : 1000, ``` ### Hero Level + - Can be used as limiter. Hero requires to have at least specified level - Can be used as reward, will grant hero experience amount equal to the difference between the hero's next level and current level (Tree of Knowledge) -```jsonc +```json5 "heroLevel" : 1, ``` ### Mana Points + - Can be used as limiter. Hero must have at least specific mana amount - Can be used as reward, to give mana points to hero. Mana points may go above mana pool limit. - If negative value is used as reward, it will be used as cost and take mana from player -```jsonc +```json5 "manaPoints": -10, ``` - If giving mana points puts hero above mana pool limit, any overflow will be multiplied by specified percentage. If set to 0, mana will not go above mana pool limit. -```jsonc +```json5 "manaOverflowFactor" : 50, ``` ### Mana Percentage + - Can be used as limiter. Hero must have at least specific mana percentage - Can be used to set hero mana level to specified percentage value, not restricted to mana pool limit (Magic Well, Mana Spring) -```jsonc +```json5 "manaPercentage": 200, ``` ### Movement Points + - Can NOT be used as limiter - Can be used as reward, to give movement points to hero. Movement points may go above mana pool limit. -```jsonc +```json5 "movePoints": 200, ``` - + ### Movement Percentage + - Can NOT be used as limiter - Can be used to set hero movement points level to specified percentage value. Value of 0 will take away any remaining movement points -```jsonc +```json5 "movePercentage": 50, ``` ### Primary Skills + - Can be used as limiter, hero must have primary skill at least at specified level - Can be used as reward, to increase hero primary skills by selected value - If reward value is negative, value will be used as cost, decreasing primary skill - Each primary skill can be explicitly specified or randomly selected - Possible values: `"attack", "defence", "spellpower", "knowledge"` -```jsonc +```json5 "primary": [ { // Specific primary skill @@ -376,13 +398,15 @@ Keep in mind, that all randomization is performed on map load and on object rese ``` ### Secondary Skills + - Can be used as limiter, hero must have secondary skill at least at specified level - Can be used as reward, to grant secondary skills to hero - If hero already has specified skill, the skills will be leveled up specified number of times - If hero does not have selected skill and have free skill slots, he will receive skill at specified level - Possible values: 1 (basic), 2 (advanced), 3 (expert) - Each secondary skill can be explicitly specified or randomly selected -```jsonc + +```json5 "secondary": [ { // Specific skill @@ -411,17 +435,18 @@ Keep in mind, that all randomization is performed on map load and on object rese - Can be used as limiter. Hero must have free skill slot to pass limiter -```json +```json5 "canLearnSkills" : true ``` ### Bonus System + - Can be used as reward, to grant bonus to player - if present, MORALE and LUCK bonus will add corresponding image component to UI. - Note that unlike most values, parameter of bonuses can NOT be randomized - Description can be string or number of corresponding string from `arraytxt.txt` -```json +```json5 "bonuses" : [ { "type" : "MORALE", @@ -433,11 +458,12 @@ Keep in mind, that all randomization is performed on map load and on object rese ``` ### Artifacts + - Can be used as limiter, hero must have artifact either equipped or in backpack - Can be used as reward, to give new artifact to a hero - Artifacts added as reward will be used for text substitution. First `%s` in text string will be replaced with name of an artifact -```jsonc +```json5 "artifacts": [ "ribCage" ], @@ -447,7 +473,7 @@ Keep in mind, that all randomization is performed on map load and on object rese - For artifact class possible values are "TREASURE", "MINOR", "MAJOR", "RELIC" - Artifact value range can be specified with min value and max value -```jsonc +```json5 "artifacts": [ { "class" : "TREASURE", @@ -458,11 +484,12 @@ Keep in mind, that all randomization is performed on map load and on object rese ``` ### Spells + - Can be used as limiter - Can be used as reward, to give new spell to a hero - Spells added as reward will be used for text substitution. First `%s` in text string will be replaced with spell name -```jsonc +```json5 "spells": [ "magicArrow" ], @@ -471,7 +498,7 @@ Keep in mind, that all randomization is performed on map load and on object rese - Alternative format, random spell selection - Spell can be selected from specifically selected school -```jsonc +```json5 "spells": [ { "level" : 1, @@ -485,21 +512,23 @@ Keep in mind, that all randomization is performed on map load and on object rese - Can be used as limiter. Hero must be able to learn spell to pass the limiter - Hero is considered to not able to learn the spell if: - - he already has specified spell -- - he does not have a spellbook +- - he does not have a spellbook - - he does not have sufficient Wisdom level for this spell -```json +```json5 "canLearnSpells" : [ "magicArrow" ], ``` ### Creatures + - Can be used as limiter - Can be used as reward, to give new creatures to a hero - If hero does not have enough free slots, game will show selection dialog to pick troops to keep - It is possible to specify probability to receive upgraded creature -```jsonc + +```json5 "creatures" : [ { "type" : "archer", @@ -510,13 +539,15 @@ Keep in mind, that all randomization is performed on map load and on object rese ``` ### Guards + - When used in a reward, these creatures will be added to guards of the objects - Hero must defeat all guards before being able to receive rewards - Guards are only reset when object rewards are reset - Requires `guardsLayout` property to be set in main part of object configuration - It is possible to add up to 7 slots of creatures - Guards of the same creature type will never merge or rearrange their stacks -```jsonc + +```json5 "guards" : [ { "type" : "archer", "amount" : 20 }, { "type" : "archer", "amount" : 20, "upgradeChance" : 30 }, @@ -525,22 +556,24 @@ Keep in mind, that all randomization is performed on map load and on object rese ``` ### Creatures Change + - Can NOT be used as limiter - Can be used as reward, to replace creatures in hero army. It is possible to use this parameter both for upgrades of creatures as well as for changing them into completely unrelated creature, e.g. similar to Skeleton Transformer - This parameter will not change creatures given by `creatures` parameter on the same visit -```jsonc +```json5 "changeCreatures" : { "cavalier" : "champion" } ``` ### Spell cast + - Can NOT be used as limiter - As reward, instantly casts adventure map spell for visiting hero. All checks for spell book, wisdom or presence of mana will be ignored. It's possible to specify school level at which spell will be casted. If it's necessary to reduce player's mana or do some checks, they shall be introduced as limiters and other rewards - School level possible values: 1 (basic), 2 (advanced), 3 (expert) -```json +```json5 "spellCast" : { "spell" : "townPortal", "schoolLevel": 3 @@ -555,7 +588,7 @@ Keep in mind, that all randomization is performed on map load and on object rese - It is possible to specify which terrain classes should be affected. Tile will be affected if sum of values its classes is positive. For example, `"water" : 1` will affect all water tiles, while `"surface" : 1, "subterra" : -1` will include terrains that have "surface" flag but do not have "subterra" flag - If 'hide' is set to true, then instead of revealing terrain, game will hide affected tiles for all other players -```json +```json5 "revealTiles" : { "radius" : 20, "surface" : 1, @@ -567,28 +600,31 @@ Keep in mind, that all randomization is performed on map load and on object rese ``` ### Player color + - Can be used as limiter - Can NOT be used as reward - Only players with specific color can pass the limiter -```jsonc +```json5 "colors" : [ "red", "blue", "tan", "green", "orange", "purple", "teal", "pink" ] ``` ### Hero types + - Can be used as limiter - Can NOT be used as reward - Only specific heroes can pass the limiter -```jsonc +```json5 "heroes" : [ "orrin" ] ``` ### Hero classes + - Can be used as limiter - Can NOT be used as reward - Only heroes belonging to specific classes can pass the limiter -```jsonc +```json5 "heroClasses" : [ "battlemage" ] -``` \ No newline at end of file +``` diff --git a/docs/modders/Mod_File_Format.md b/docs/modders/Mod_File_Format.md index f0ac58070..514b1a231 100644 --- a/docs/modders/Mod_File_Format.md +++ b/docs/modders/Mod_File_Format.md @@ -2,7 +2,7 @@ ## Fields with description of mod -``` javascript +```json5 { // Name of your mod. While it does not have hard length limit // it should not be longer than ~30 symbols to fit into allowed space @@ -48,6 +48,12 @@ [ "baseMod" ], + + // List of mods if they are enabled, should be loaded before this one. This mod will overwrite any conflicting items from its soft dependency mods. + "softDepends" : + [ + "baseMod" + ], // List of mods that can't be enabled in the same time as this one "conflicts" : @@ -85,30 +91,31 @@ These are fields that are present only in local mod.json file -``` javascript +```json5 { // Following section describes configuration files with content added by mod // It can be split into several files in any way you want but recommended organization is - // to keep one file per object (creature/hero/etc) and, if applicable, add separate file - // with translatable strings for each type of content + // to keep one file per object (creature/hero/etc) + // Alternatively, for small changes you can embed changes to content directly in here, e.g. + // "creatures" : { "core:imp" : { "health" : 5 }} // list of factions/towns configuration files "factions" : [ - "config/myMod/faction.json" + "config/faction.json" ] // List of hero classes configuration files "heroClasses" : [ - "config/myMod/heroClasses.json" + "config/heroClasses.json" ], // List of heroes configuration files "heroes" : [ - "config/myMod/heroes.json" + "config/heroes.json" ], // List of configuration files for skills @@ -117,68 +124,68 @@ These are fields that are present only in local mod.json file // list of creature configuration files "creatures" : [ - "config/myMod/creatures.json" + "config/creatures.json" ], // List of artifacts configuration files "artifacts" : [ - "config/myMod/artifacts.json" + "config/artifacts.json" ], // List of objects defined in this mod "objects" : [ - "config/myMod/objects.json" + "config/objects.json" ], // List of spells defined in this mod "spells" : [ - "config/myMod/spells.json" + "config/spells.json" ], // List of configuration files for terrains "terrains" : [ - "config/myMod/terrains.json" + "config/terrains.json" ], // List of configuration files for roads "roads" : [ - "config/myMod/roads.json" + "config/roads.json" ], // List of configuration files for rivers "rivers" : [ - "config/myMod/rivers.json" + "config/rivers.json" ], // List of configuration files for battlefields "battlefields" : [ - "config/myMod/battlefields.json" + "config/battlefields.json" ], // List of configuration files for obstacles "obstacles" : [ - "config/myMod/obstacles.json" + "config/obstacles.json" ], // List of RMG templates defined in this mod "templates" : [ - "config/myMod/templates.json" + "config/templates.json" ], // Optional, primaly used by translation mods // Defines strings that are translated by mod into base language specified in "language" field "translations" : [ - "config/myMod/englishStrings.json + "config/englishStrings.json ] } ``` @@ -194,7 +201,7 @@ See [Translations](Translations.md) for more information "description" : "", "author" : "", "translations" : [ - "config//.json" + "config/.json" ] }, ``` @@ -203,7 +210,7 @@ See [Translations](Translations.md) for more information These are fields that are present only in remote repository and are generally not used in mod.json -```jsonc +```json5 { // URL to mod.json that describes this mod "mod" : "https://raw.githubusercontent.com/vcmi-mods/vcmi-extras/vcmi-1.4/mod.json", @@ -221,4 +228,4 @@ These are fields that are present only in remote repository and are generally no For mod description it is possible to use certain subset of HTML as described here: - \ No newline at end of file + diff --git a/docs/modders/Random_Map_Template.md b/docs/modders/Random_Map_Template.md index 65bfb3724..319c134e9 100644 --- a/docs/modders/Random_Map_Template.md +++ b/docs/modders/Random_Map_Template.md @@ -2,7 +2,7 @@ ## Template format -``` javascript +```json5 /// Unique template name "Triangle" : { @@ -28,10 +28,11 @@ /// List of game settings that were overriden by this template. See config/gameConfig.json in vcmi install directory for possible values /// Settings defined here will always override any settings from vcmi or from mods - "settings" : { - "heroes" : { - "perPlayerOnMapCap" : 1 - } + "settings" : + { + "heroes" : + { + "perPlayerOnMapCap" : 1 } }, @@ -56,10 +57,14 @@ ## Zone format -``` javascript +```json5 { // Type of this zone. Possible values are: - // "playerStart", "cpuStart", "treasure", "junction" + // "playerStart" - Starting zone for a "human or CPU" players + // "cpuStart" - Starting zone for "CPU only" players + // "treasure" - Generic neutral zone + // "junction" - Neutral zone with narrow passages only. The rest of area is filled with obstacles. + // "sealed" - Decorative impassable zone completely filled with obstacles "type" : "playerStart", // relative size of zone @@ -99,10 +104,13 @@ "minesLikeZone" : 1, // Treasures will have same configuration as in linked zone - "treasureLikeZone" : 1 + "treasureLikeZone" : 1, // Terrain type will have same configuration as in linked zone - "terrainTypeLikeZone" : 3 + "terrainTypeLikeZone" : 3, + + // Custom objects will have same configuration as in linked zone + "customObjectsLikeZone" : 1, // factions of monsters allowed on this zone "allowedMonsters" : ["inferno", "necropolis"] @@ -130,6 +138,28 @@ "density" : 5 } ... - ] + ], + + // Objects with different configuration than default / set by mods + "customObjects" : + { + // All of objects of this kind will be removed from zone + // Possible values: "all", "none", "creatureBank", "bonus", "dwelling", "resource", "resourceGenerator", "spellScroll", "randomArtifact", "pandorasBox", "questArtifact", "seerHut", "other + "bannedCategories" : ["all", "dwelling", "creatureBank", "other"], + // Specify object types and subtypes + "bannedObjects" :["core:object.randomArtifactRelic"], + // Configure individual common objects - overrides banned objects + "commonObjects": + [ + { + "id" : "core:object.creatureBank.dragonFlyHive", + "rmg" : { + "value" : 9000, + "rarity" : 500, + "zoneLimit" : 2 + } + } + ] + } } -``` \ No newline at end of file +``` diff --git a/docs/modders/Readme.md b/docs/modders/Readme.md index 5fba25113..6bc87cd6e 100644 --- a/docs/modders/Readme.md +++ b/docs/modders/Readme.md @@ -21,8 +21,8 @@ Example of how directory structure of your mod may look like: sprites/ - animation, image sets (H3 .def files or VCMI .json files) video/ - video files, .bik, .smk, .ogv .webm ``` -See [File Formats](File_Formats.md) page for more information on which formats are supported or recommended for vcmi +See [File Formats](File_Formats.md) page for more information on which formats are supported or recommended for vcmi ## Creating mod file @@ -30,7 +30,7 @@ All VCMI configuration files use [JSON format](http://en.wikipedia.org/wiki/Json Mod.json is main file in your mod and must be present in any mod. This file contains basic description of your mod, dependencies or conflicting mods (if present), list of new content and so on. Minimalistic version of this file: -``` javascript +```json5 { "name" : "My test mod", "description" : "My test mod that add a lot of useless stuff into the game", @@ -45,6 +45,7 @@ See [Mod file Format](Mod_File_Format.md) for its full description. ## Creation of new objects In order to create new object use following steps: + 1. Create json file with definition of new object. See list of supported object types below. 2. Add any resources needed for this object, such as images, animations or sounds. 2. Add reference to new object in corresponding section of mod.json file @@ -52,9 +53,11 @@ In order to create new object use following steps: ### List of supported new object types Random Map Generator: + - [Random Map Template](Random_Map_Template.md) Game Entities: + - [Artifact](Entities_Format/Artifact_Format.md) - [Creature Requirement](Entities_Format/Creature_Format.md) - [Creature Help](Entities_Format/Creature_Help.md) @@ -66,6 +69,7 @@ Game Entities: - [Secondary Skill](Entities_Format/Secondary_Skill_Format.md) Map objects: + - [Map Objects](Map_Object_Format.md) - - [Rewardable](Map_Objects/Rewardable.md) - - [Creature Bank](Map_Objects/Creature_Bank.md) @@ -74,6 +78,7 @@ Map objects: - - [Boat](Map_Objects/Boat.md) Other: + - [Terrain](Entities_Format/Terrain_Format.md) - [River](Entities_Format/River_Format.md) - [Road](Entities_Format/Road_Format.md) @@ -96,7 +101,8 @@ VCMI uses strings to reference objects. Examples: ### Modifying existing objects Alternatively to creating new objects, you can edit existing objects. Normally, when creating new objects you specify object name as: -``` javascript + +```json5 "newCreature" : { // creature parameters } @@ -104,7 +110,7 @@ Alternatively to creating new objects, you can edit existing objects. Normally, In order to access and modify existing object you need to specify mod that you wish to edit: -``` javascript +```json5 /// "core" specifier refers to objects that exist in H3 "core:archer" : { /// This will set health of Archer to 10 @@ -123,6 +129,7 @@ In order to access and modify existing object you need to specify mod that you w "speed" : 10 }, ``` + Note that modification of existing objects does not requires a dependency on edited mod. Such definitions will only be used by game if corresponding mod is installed and active. This allows using objects editing not just for rebalancing mods but also to provide compatibility between two different mods or to add interaction between two mods. @@ -132,6 +139,7 @@ This allows using objects editing not just for rebalancing mods but also to prov Any graphical replacer mods fall under this category. In VCMI directory **/Content** acts as mod-specific game root directory. So for example file **/Content/Data/AISHIELD.PNG** will replace file with same name from **H3Bitmap.lod** game archive. Any other files can be replaced in exactly same way. Note that replacing files from archives requires placing them into specific location: + - H3Bitmap.lod -> Data - H3Sprite.lod -> Sprites - Heroes3.snd -> Sounds @@ -145,12 +153,13 @@ This includes archives added by expansions (e.g. **H3ab_bmp.lod** uses same rule Heroes III uses custom format for storing animation: def files. These files are used to store all in-game animations as well as for some GUI elements like buttons and for icon sets. These files can be replaced by another def file but in some cases original format can't be used. This includes but not limited to: -- Replacing one (or several) icons in set -- Replacing animation with fully-colored 32-bit images + +- Replacing one (or several) icons in set +- Replacing animation with fully-colored 32-bit images In VCMI these animation files can also be replaced by json description of their content. See [Animation Format](Animation_Format.md) for full description of this format. Example: replacing single icon -``` javascript +```json5 { // List of replaced images "images" : @@ -191,7 +200,7 @@ Same way we can also create special stable branch for every mod under "vcmi-mods ### Getting into vcmi-mods organization Before your mod can be accepted into official mod list you need to get it into repository under "vcmi-mods" organization umbrella. To do this contact one of mod repository maintainers. If needed you can get own team within "vcmi-mods" organization. -Link to our mod will looks like that: https://github.com/vcmi-mods/adventure-ai-trace +Link to our mod will looks like that: ## Rules of repository @@ -199,8 +208,10 @@ Link to our mod will looks like that: https://github.com/vcmi-mods/adventure-ai- For sanity reasons mod identifier must only contain lower-case English characters, numbers and hyphens. - my-mod-name - 2000-new-maps +``` +my-mod-name +2000-new-maps +``` Sub-mods can be named as you like, but we strongly encourage everyone to use proper identifiers for them as well. diff --git a/docs/players/Bug_Reporting_Guidelines.md b/docs/players/Bug_Reporting_Guidelines.md index eabe6b6af..067a59e84 100644 --- a/docs/players/Bug_Reporting_Guidelines.md +++ b/docs/players/Bug_Reporting_Guidelines.md @@ -23,9 +23,9 @@ First of all, if you encounter a crash, don't re-run VCMI immediately to see if By default, log files are written to: -- Windows: Documents\My Games\vcmi\\ -- Linux: ~/.cache/vcmi/ -- Android: Android/data/is.xyz.vcmi/files/vcmi-data/cache/ +- Windows: Documents\My Games\vcmi\\ +- Linux: ~/.cache/vcmi/ +- Android: Android/data/is.xyz.vcmi/files/vcmi-data/cache/ Now you should try to reproduce encountered issue. It's best when you write how to reproduce the issue by starting a new game and taking some steps (e.g. start Arrogance map as red player and attack monster Y with hero X). If you have troubles with reproducing it this way but you can do it from a savegame - that's good too. Finally, when you are not able to reproduce the issue at all, just upload the files mentioned above. To sum up, this is a list of what's the most desired for a developer: diff --git a/docs/players/Cheat_Codes.md b/docs/players/Cheat_Codes.md index f5a915562..413c60c33 100644 --- a/docs/players/Cheat_Codes.md +++ b/docs/players/Cheat_Codes.md @@ -70,13 +70,15 @@ Alternative usage: `vcmiexp ` - gives selected hero specified amount of `nwcbluepill` or `vcmimelkor` or `vcmilose` - player loses ### Misc + `nwctheone` or `vcmigod` - reveals the whole map, gives 5 archangels in each empty slot, unlimited movement points and permanent flight ## Using cheat codes on other players + By default, all cheat codes apply to current player. Alternatively, it is possible to specify player that you want to target: - Specific players: `red`/`blue`/`green`... -- Only AI players: `ai` +- Only AI players: `ai` - All players: `all` ### Examples @@ -89,12 +91,14 @@ By default, all cheat codes apply to current player. Alternatively, it is possib ## Multiplayer chat commands Following commands can be used in multiplayer only by host player to control the session: + - `!exit` - finish the game - `!save ` - save the game into the specified file - `!kick red/blue/tan/green/orange/purple/teal/pink` - kick player of specified color from the game -- `!kick 0/1/2/3/4/5/6/7/8` - kick player of specified ID from the game (_zero indexed!_) (`0: red, 1: blue, tan: 2, green: 3, orange: 4, purple: 5, teal: 6, pink: 7`) +- `!kick 0/1/2/3/4/5/6/7/8` - kick player of specified ID from the game (*zero indexed!*) (`0: red, 1: blue, tan: 2, green: 3, orange: 4, purple: 5, teal: 6, pink: 7`) Following commands can be used by any player in multiplayer: + - `!help` - displays in-game list of available commands - `!cheaters` - lists players that have entered cheat at any point of the game - `!vote` - initiates voting to change one of the possible options: @@ -114,13 +118,16 @@ Windows builds of VCMI run separate console window by default, on other platform Below a list of supported commands, with their arguments wrapped in `<>` #### Game Commands + `die, fool` - quits game `save ` - saves game in given file (at the moment doesn't work) `mp` - on adventure map with a hero selected, shows heroes current movement points, max movement points on land and on water `bonuses` - shows bonuses of currently selected adventure map object #### Extract commands + `translate` - save game texts into json files +`translate missing` - save untranslated game texts into json files `translate maps` - save map and campaign texts into json files `get config` - save game objects data into json files `get scripts` - dumps lua script stuff into files (currently inactive due to scripting disabled for default builds) @@ -130,15 +137,17 @@ Below a list of supported commands, with their arguments wrapped in `<>` `generate assets` - generate all assets at once #### AI commands + `setBattleAI ` - change battle AI used by neutral creatures to the one specified, persists through game quit `gosolo` - AI takes over until the end of turn (unlike original H3 currently causes AI to take over until typed again) `controlai <[red][blue][tan][green][orange][purple][teal][pink]>` - gives you control over specified AI player. If none is specified gives you control over all AI players `autoskip` - Toggles autoskip mode on and off. In this mode, player turns are automatically skipped and only AI moves. However, GUI is still present and allows to observe AI moves. After this option is activated, you need to end first turn manually. Press `[Shift]` before your turn starts to not skip it #### Settings + `set ` - sets special temporary settings that reset on game quit. Below some of the most notable commands: -`autoskip` - identical to `autoskip` option --`onlyAI` - run without human player, all players will be _default AI_ +-`onlyAI` - run without human player, all players will be *default AI* -`headless` - run without GUI, implies `onlyAI` is set -`showGrid` - display a square grid overlay on top of adventure map -`showBlocked` - show blocked tiles on map @@ -146,6 +155,7 @@ Below a list of supported commands, with their arguments wrapped in `<>` -`hideSystemMessages` - suppress server messages in chat #### Developer Commands + `crash` - force a game crash. It is sometimes useful to generate memory dump file in certain situations, for example game freeze `gui` - displays tree view of currently present VCMI common GUI elements `activate <0/1/2>` - activate game windows (no current use, apparently broken long ago) diff --git a/docs/players/Game_Mechanics.md b/docs/players/Game_Mechanics.md index e64f7e94e..54df67881 100644 --- a/docs/players/Game_Mechanics.md +++ b/docs/players/Game_Mechanics.md @@ -58,7 +58,7 @@ These bugs were present in original Shadow of Death game, however the team decid Some of H3 mechanics can't be straight considered as bug, but default VCMI behaviour is different: - Pathfinding. Hero can't grab artifact while flying when all tiles around it are guarded without triggering attack from guard. -- Battles. Hero that won battle, but only have temporary summoned creatures alive going to appear in tavern like if he retreated. +- Battles. Hero that won battle, but only have temporary summoned creatures alive going to appear in tavern like if he retreated. - Battles. Spells from artifacts like AOTD are autocasted on beginning of the battle, not beginning of turn. ## Adventure map features @@ -106,7 +106,7 @@ In combat, some creatures, such as Dragon or Cerberi, may attack enemies on mult - [LCtrl] + LClick – splits a single unit from the selected stack into an empty slot. - [LCtrl] + [LShift] + LClick – split single units from the selected stack into all empty hero/garrison slots - [Alt] + LClick – merge all split single units into one stack -- [Alt] + [LCtrl] + LClick - move all units of selected stack to the city's garrison or to the met hero +- [Alt] + [LCtrl] + LClick - move all units of selected stack to the city's garrison or to the met hero - [Alt] + [LShift] + LClick - dismiss selected stack` - Directly type numbers in the Split Stack window to split them in any way you wish @@ -174,6 +174,7 @@ TODO Simultaneous turns allow multiple players to act at the same time, speeding up early game phase in multiplayer games. During this phase if different players (allies or not) attempt to interact with each other, such as capture objects owned by other players (mines, dwellings, towns) or attack their heroes, game will block such actions. Interaction with same map objects at the same time, such as attacking same wandering monster is also blocked. Following options can be used to configure simultaneous turns: + - Minimal duration (at least for): this is duration during which simultaneous turns will run unconditionally. Until specified number of days have passed, simultaneous turns will never break and game will not attempt to detect contacts. - Maximal duration (at most for): this is duration after which simultaneous turns will end unconditionally, even if players still have not contacted each other. However if contact detection discovers contact between two players, simultaneous turns between them might end before specified duration. - Simultaneous turns for AI: If this option is on, AI can act at the same time as human players. Note that AI shares settings for simultaneous turns with human players - if no simultaneous turns have been set up this option has no effect. @@ -185,6 +186,7 @@ Players are considered to be "in contact" if movement range of their heroes at t Once detected, contact can never be "lost". If game detected contact between two players, this contact will remain active till the end of the game, even if their heroes move far enough from each other. Game performs contact detection once per turn, at the very start of each in-game day. Once contact detection has been performed, players that are not in contact with each other can start making turn. For example, in game with 4 players: red, blue, brown and green. If game detected contact between red and blue following will happen: + - red, brown and green will all instantly start turn - once red ends his turn, blue will be able to start his own turn (even if brown or green are still making turn) @@ -197,4 +199,4 @@ Differences compared to HD Mod version: ## Manuals and guides -- https://heroes.thelazy.net/index.php/Main_Page Wiki that aims to be a complete reference to Heroes of Might and Magic III. +- Wiki that aims to be a complete reference to Heroes of Might and Magic III. diff --git a/docs/players/Heroes_Chronicles.md b/docs/players/Heroes_Chronicles.md new file mode 100644 index 000000000..d08987618 --- /dev/null +++ b/docs/players/Heroes_Chronicles.md @@ -0,0 +1,9 @@ +# Heroes Chronicles + +It also possible to play the Heroes Chronicles with VCMI. You still need a completly installed VCMI (with heroes 3 sod / complete files). + +You also need Heroes Chronicles from [gog.com](https://www.gog.com/en/game/heroes_chronicles_all_chapters). You need to download the offline installer. CD installations are not supported yet. + +You can use the "Install file" button in the launcher to select the downloaded exe files. This process can take a while (especially on mobile platforms) and need some temporary free space. + +After that you can select Heroes Chronicles from Campaign selection menu (button or custom campaign). diff --git a/docs/players/Installation_Linux.md b/docs/players/Installation_Linux.md index 9440246b3..c15df8c93 100644 --- a/docs/players/Installation_Linux.md +++ b/docs/players/Installation_Linux.md @@ -9,6 +9,7 @@ VCMI requires data from original Heroes 3: Shadow of Death or Complete editions. Up-to-date releases can be found in our PPA here: To install VCMI from PPA use: + ``` sudo apt-add-repository ppa:vcmi/ppa sudo apt update @@ -20,25 +21,30 @@ To install VCMI from PPA use: We also provide latest, unstable builds mostly suitable for testing here: In order to install from this PPA use: + ``` sudo add-apt-repository ppa:vcmi/vcmi-latest sudo apt update sudo apt install vcmi ``` + ### Ubuntu - From Ubuntu repository VCMI stable builds available in "multiverse" repository. Learn how to enable it in [Ubuntu wiki](https://help.ubuntu.com/community/Repositories/Ubuntu). Once enabled, you can install VCMI using Ubuntu Store or in terminal using following commands: + ``` sudo apt update sudo apt install vcmi ``` + Note that version available in Ubuntu is outdated. Install via PPA is preferred. ### Debian Stable VCMI version is available in "contrib" repository. Learn how to enable it in [Debian wiki](https://wiki.debian.org/SourcesList). To install VCMI from repository: + ``` sudo apt-get update sudo apt-get install vcmi @@ -52,6 +58,7 @@ Stable VCMI version is available in RPM Fusion repository. Learn how to enable i sudo dnf update sudo dnf install vcmi ``` + ### Flatpak (distribution-agnostic) Latest public release build can be installed via Flatpak. @@ -63,8 +70,8 @@ Once you have flatpak, you can install VCMI package which can be found here: -- Daily builds (unstable): -- Please report about problems on GitHub: [Bug Tracker](https://github.com/vcmi/vcmi/issues) +- Latest release (recommended): +- Daily builds (unstable): +- Please report about problems on GitHub: [Bug Tracker](https://github.com/vcmi/vcmi/issues) ## Step 2: Installing Heroes III data files **Since VCMI 1.2 you can skip this step, just run VCMI launcher and it will help you with importing H3 data. For older releases you can follow this step.** -- Install Heroes III from disk or using GOG installer. -- Place "Data", "Maps" and "Mp3" from Heroes III to: `Documents\My Games\vcmi\` +- Install Heroes III from disk or using GOG installer. +- Place "Data", "Maps" and "Mp3" from Heroes III to: `Documents\My Games\vcmi\` Create this folder if it doesnt exist yet ## Step 3: connect to the mod repository - If that's your first installation, connection to the mod repository will be configured automatically, you'll see mods available to install from VCMI launcher - -- We recommend you to install VCMI extras to support various helpful UI tweaks +- We recommend you to install VCMI extras to support various helpful UI tweaks diff --git a/docs/players/Installation_iOS.md b/docs/players/Installation_iOS.md index fc9f2cef4..775b077c4 100644 --- a/docs/players/Installation_iOS.md +++ b/docs/players/Installation_iOS.md @@ -4,41 +4,74 @@ You can run VCMI on iOS 12.0 and later, all devices are supported. If you wish t ## Step 1: Download and install VCMI +The easiest and recommended way to install on a non-jailbroken device is to install the [AltStore Classic](https://altstore.io/) or [Sideloadly](https://sideloadly.io/). We will use AltStore as an example below. Using this method means the VCMI certificate is auto-signed automatically. + +i) Use [AltStore Windows](https://faq.altstore.io/altstore-classic/how-to-install-altstore-windows) or [AltStore macOS](https://faq.altstore.io/altstore-classic/how-to-install-altstore-macos) instructions to install the store depending on the operating system you are using. + +If you're having trouble enabling "sync with this iOS device over Wi-Fi" press on the rectangular shape below "Account". Windows example from iTunes shown below: + +![iTunes](images/itunes.jpg) + +ii) Download the VCMI-iOS.ipa file on your iOS device directly from the [latest releases](https://github.com/vcmi/vcmi/releases/latest). + +iii) To install the .ipa file on your device do one of the following: + +- In AltStore go to >My Apps > press + in the top left corner. Select VCMI-iOS.ipa to install, +- or drag and drop the .ipa file into your iOS device in iTunes + +## Step 2: Installing Heroes III data files + +If you bought HoMM3 on [GOG](https://www.gog.com/de/game/heroes_of_might_and_magic_3_complete_edition), you can download the files directly from the browser in the device. + +Launch VCMI app on the device and the launcher will prompt two files to complete the installation. Select the **.bin** file first, then the **.exe** file. This may take a few seconds. Please be patient. + +## Step 3: Configuration settings + +Once you have installed VCMI and have the launcher opened, select Settings on the left bar. The following Video settings are recommended: + +- Lower reserved screen area to zero. +- Increase interface Scaling to maximum. This number will depend on your device. For 11" iPad Air it was at 273% as an example + +Together, the two options should eliminate black bars and enable full screen VCMI experience. Enjoy! + +## Alternative Step 1: Download and install VCMI + - The latest release (recommended): - Daily builds: -To run on a non-jailbroken device you need to sign the IPA file, you -have the following options: +To run on a non-jailbroken device you need to sign the IPA file, you have the following aternative options: -- (Easiest way) [AltStore](https://altstore.io/) or [Sideloadly](https://sideloadly.io/) - can be installed on Windows or macOS, don't require dealing with signing on your own -- if you're on iOS 14.0-15.4.1, you can try +- if you're on iOS 14.0-15.4.1, you can try . - Get signer tool [here](https://dantheman827.github.io/ios-app-signer/) and a guide [here](https://forum.kodi.tv/showthread.php?tid=245978) (it's for Kodi, but the logic is the same). Signing with this app can only be done on macOS. - [Create signing assets on macOS from terminal](https://github.com/kambala-decapitator/xcode-auto-signing-assets). In the command replace `your.bundle.id` with something like `com.MY-NAME.vcmi`. After that use the above signer tool. - [Sign from any OS (Rust)](https://github.com/indygreg/PyOxidizer/tree/main/tugger-code-signing) / [alternative project (C++)](https://github.com/zhlynn/zsign). You'd still need to find a way to create signing assets (private key and provisioning profile) though. -To install the signed ipa on your device, you can use Xcode or Apple Configurator (available on the Mac App Store for free). The latter also allows installing ipa from the command line, here's an example that assumes you have only 1 device connected to your Mac and the signed ipa is on your desktop: +The easiest way to install the ipa on your device is to do one of the following: - /Applications/Apple\ Configurator.app/Contents/MacOS/cfgutil install-app ~/Desktop/vcmi.ipa +- In AltStore go to >My Apps > press + in the top left corner. Select VCMI-iOS.ipa to install or +- Drag and drop the .ipa file into your iOS device in iTunes -## Step 2: Installing Heroes III data files +Alternatively, to install the signed ipa on your device, you can use Xcode or Apple Configurator (available on the Mac App Store for free). The latter also allows installing ipa from the command line, here's an example that assumes you have only 1 device connected to your Mac and the signed ipa is on your desktop: -Note: if you don't need in-game videos, you can omit downloading/copying file VIDEO.VID from data folder - it will save your time and space. The same applies to the Mp3 directory. +``` +/Applications/Apple\ Configurator.app/Contents/MacOS/cfgutil install-app ~/Desktop/vcmi.ipa +``` -### Step 2.a: Installing data files with GOG offline installer +## Alternative Step 2: Installing Heroes III data files -If you bought HoMM3 on [GOG](https://www.gog.com/de/game/heroes_of_might_and_magic_3_complete_edition), you can download the files directly from the browser and install them in the launcher. Select the .exe file first, then the .bin file. This may take a few seconds. Please be patient. +Note: if you don't need in-game videos, you can omit downloading/copying file VIDEO.VID from the Data folder - it will save your time and space. The same applies to the Mp3 directory. -### Step 2.b: Installing data files with Finder or Windows explorer +### Step 2.a: Installing data files with Finder or Windows explorer To play the game, you need to upload HoMM3 data files - **Data**, **Maps** and **Mp3** directories - to the device. Use Finder (or iTunes, if you're on Windows or your macOS is 10.14 or earlier) for that. You can also add various mods by uploading **Mods** directory. Follow [official Apple guide](https://support.apple.com/en-us/HT210598) and place files into VCMI app. Unfortunately, Finder doesn't display copy progress, give it about 10 minutes to finish. -### Step 2.c: Installing data files using iOS device only +### Step 2.b: Installing data files using iOS device only If you have data somewhere on device or in shared folder or you have downloaded it, you can copy it directly on your iPhone/iPad using Files application. Place **Data**, **Maps** and **Mp3** folders into vcmi application - it will be visible in Files along with other applications' folders. -### Step 2.d: Installing data files with Xcode on macOS +### Step 2.c: Installing data files with Xcode on macOS You can also upload files with Xcode. You need to prepare "container" for that. @@ -47,7 +80,7 @@ You can also upload files with Xcode. You need to prepare "container" for that. 3. Open Devices and Simulators window: Cmd+Shift+2 or Menu - Window - Devices and Simulators 4. Select your device 5. Select VCMI -6. In the bottom find "three dots" or "cogwheel" button (it should be next to + - buttons) - click it - select Download Container... +6. In the bottom find "three dots" or "cogwheel" button (it should be next to + - buttons) - click it - select Download Container... 7. Place the game directories inside the downloaded container - AppData - Documents 8. Click the "three dots" / "cogwheel" button in Xcode again - Replace Container... - select the downloaded container 9. Wait until Xcode finishes copying, progress is visible (although it might be "indefinite") diff --git a/docs/players/Installation_macOS.md b/docs/players/Installation_macOS.md index 1ce876d59..d327dc62e 100644 --- a/docs/players/Installation_macOS.md +++ b/docs/players/Installation_macOS.md @@ -1,15 +1,15 @@ # Installation macOS -For iOS installation look here: (Installation on iOS)[Installation_iOS.md] - ## Step 1: Download and install VCMI -- The latest release (recommended): -- Daily builds (might be unstable) - - Intel (x86_64) builds: - - Apple Silicon (arm64) builds: +- The latest release (recommended): + - manually: + - via Homebrew: `brew install --cask --no-quarantine vcmi/vcmi/vcmi` +- Daily builds (might be unstable) + - Intel (x86_64) builds: + - Apple Silicon (arm64) builds: -If the app doesn't open, right-click the app bundle - select *Open* menu item - press *Open* button. +If the app doesn't open, right-click the app bundle - select *Open* menu item - press *Open* button. You may also need to allow running it in System Settings - Privacy & Security. Please report about gameplay problem on forums: [Help & Bugs](https://forum.vcmi.eu/c/international-board/help-bugs) Make sure to specify what hardware and macOS version you use. @@ -17,9 +17,9 @@ Please report about gameplay problem on forums: [Help & Bugs](https://forum.vcmi ### Step 2.a: Installing data files with GOG offline installer -If you bought HoMM3 on [GOG](https://www.gog.com/de/game/heroes_of_might_and_magic_3_complete_edition), you can download the files directly from the browser and install them in the launcher. Select the .exe file first, then the .bin file. This may take a few seconds. Please be patient. +If you bought HoMM3 on [GOG](https://www.gog.com/de/game/heroes_of_might_and_magic_3_complete_edition), you can download the files directly from the browser and install them in the launcher. Select the .bin file first, then the .exe file. This may take a few seconds. Please be patient. ### Step 2.b: Installing by the classic way -1. Find a way to unpack Windows Heroes III or GOG installer. For example, use `vcmibuilder` script inside app bundle or install the game with [CrossOver](https://www.codeweavers.com/crossover) or [Wineskin](https://github.com/Gcenx/WineskinServer). -2. Place or symlink **Data**, **Maps** and **Mp3** directories from Heroes III to:`~/Library/Application\ Support/vcmi/` +1. Find a way to unpack Windows Heroes III or GOG installer. For example, use `vcmibuilder` script inside app bundle or install the game with [CrossOver](https://www.codeweavers.com/crossover) or [Kegworks](https://github.com/Kegworks-App/Kegworks). +2. Place or symlink **Data**, **Maps** and **Mp3** directories from Heroes III to:`~/Library/Application\ Support/vcmi/` diff --git a/docs/players/Privacy_Policy.md b/docs/players/Privacy_Policy.md index 5bd841505..b35074107 100644 --- a/docs/players/Privacy_Policy.md +++ b/docs/players/Privacy_Policy.md @@ -13,4 +13,4 @@ VCMI team does not collect any data produced by VCMI app. All game files, logs, ## Multiplayer -If you decide to play with other users via Internet there are two roles. The host is the one who provides the game server. The clients are the other players who connect to the host. The host provides to the client its IP address in order to establish connections. The clients and the host during the gameplay exchange their usernames, messages and other game activity. All this data is collected and stored by the host. VCMI team does not collect and store any multiplayer data. \ No newline at end of file +If you decide to play with other users via Internet there are two roles. The host is the one who provides the game server. The clients are the other players who connect to the host. The host provides to the client its IP address in order to establish connections. The clients and the host during the gameplay exchange their usernames, messages and other game activity. All this data is collected and stored by the host. VCMI team does not collect and store any multiplayer data. diff --git a/docs/players/images/itunes.jpg b/docs/players/images/itunes.jpg new file mode 100644 index 000000000..1301fbf6d Binary files /dev/null and b/docs/players/images/itunes.jpg differ diff --git a/docs/translators/Translations.md b/docs/translators/Translations.md index 89e6cbba8..3bcc6397c 100644 --- a/docs/translators/Translations.md +++ b/docs/translators/Translations.md @@ -23,6 +23,7 @@ This is list of all languages that are currently supported by VCMI. If your lang - Vietnamese ## Progress of the translations + You can see the current progress of the different translations here: [Translation progress](https://github.com/vcmi/vcmi-translation-status) @@ -32,17 +33,18 @@ The page will be automatically updated once a week. VCMI allows translating game data into languages other than English. In order to translate Heroes III in your language easiest approach is to: -- Copy existing translation, such as English translation from here: https://github.com/vcmi-mods/h3-for-vcmi-englisation (delete sound and video folders) -- Copy text-free images from here: https://github.com/vcmi-mods/empty-translation +- Copy existing translation, such as English translation from here: (delete sound and video folders) +- Copy text-free images from here: - Rename mod to indicate your language, preferred form is "(language)-translation" - Update mod.json to match your mod - Translate all texts strings from `game.json`, `campaigns.json` and `maps.json` - Replace images in data and sprites with translated ones (or delete it if you don't want to translate them) -- If unicode characters needed for language: Create a submod with a free font like here: https://github.com/vcmi-mods/vietnamese-translation/tree/vcmi-1.4/vietnamese-translation/mods/VietnameseTrueTypeFonts +- If unicode characters needed for language: Create a submod with a free font like here: If you can't produce some content on your own (like the images or the sounds): + - Create a `README.md` file at the root of the mod -- Write into the file the translations and the detailled location +- Write into the file the translations and the **detailled** location This way, a contributor that is not a native speaker can do it for you in the future. @@ -56,6 +58,21 @@ This will export all strings from game into `Documents/My Games/VCMI/extracted/t To export maps and campaigns, use '/translate maps' command instead. +### Video subtitles + +It's possible to add video subtitles. Create a JSON file in `video` folder of translation mod with the name of the video (e.g. `H3Intro.json`): + +```json5 +[ + { + "timeStart" : 5.640, // start time, seconds + "timeEnd" : 8.120, // end time, seconds + "text" : " ... " // text to show during this period + }, + ... +] +``` + ## Translating VCMI data VCMI contains several new strings, to cover functionality not existing in Heroes III. It can be roughly split into following parts: @@ -71,6 +88,7 @@ Before you start, make sure that you have copy of VCMI source code. If you are n ### Translation of in-game data In order to translate in-game data you need: + - Add section with your language to `/Mods/VCMI/mod.json`, similar to other languages - Copy English translation file in `/Mods/VCMI/config/vcmi/english.json` and rename it to name of your language. Note that while you can copy any language other than English, other files might not be up to date and may have missing strings. - Translate copied file to your language. @@ -81,7 +99,7 @@ After this, you can set language in Launcher to your language and start game. Al VCMI Launcher and Map Editor use translation system provided by Qt framework so it requires slightly different approach than in-game translations: -- Install Qt Linguist. You can find find standalone version here: https://download.qt.io/linguist_releases/ +- Install Qt Linguist. You can find find standalone version here: - Open `/launcher/translation/` directory, copy `english.ts` file and rename it to your language - Open `/launcher/CMakeLists.txt` file with a text editor. In there you need to find list of existing translation files and add new file to the list. - Launch Qt Linguist, select Open and navigate to your copied file @@ -99,23 +117,27 @@ TODO: how to test translation locally The [AppStream](https://freedesktop.org/software/appstream/docs/chap-Metadata.html) [metainfo file](https://github.com/vcmi/vcmi/blob/develop/launcher/eu.vcmi.VCMI.metainfo.xml) is used for Linux software centers. It can be translated using a text editor or using [jdAppStreamEdit](https://flathub.org/apps/page.codeberg.JakobDev.jdAppStreamEdit): + - Install jdAppStreamEdit - Open `/launcher/eu.vcmi.VCMI.metainfo.xml` - Translate and save the file ##### Desktop file + - Edit `/launcher/vcmilauncher.desktop` and `/launcher/vcmieditor.desktop` - Add `GenericName[xyz]` and `Comment[xyz]` with your language code and translation ##### Translation of Android Launcher + - Copy `/android/vcmi-app/src/main/res/values/strings.xml` to `/android/vcmi-app/src/main/res/values-xyz/strings.xml` (`xyz` is your language code) - Translate this file -See also here: https://developer.android.com/guide/topics/resources/localization +See also here: ### Submitting changes Once you have finished with translation you need to submit these changes to vcmi team using git or Github Desktop + - Commit all your changed files - Push changes to your forked repository - Create pull request in VCMI repository with your changes @@ -136,9 +158,13 @@ After that, start Launcher, switch to Help tab and open "log files directory". Y If your mod also contains maps or campaigns that you want to translate, then use '/translate maps' command instead. +If you want to update existing translation, you can use '/translate missing' command that will export only strings that were not translated + ### Translating mod information + In order to display information in Launcher in language selected by user add following block into your `mod.json`: -``` + +```json5 "" : { "name" : "", "description" : "", @@ -148,6 +174,7 @@ In order to display information in Launcher in language selected by user add fol ] }, ``` + However, normally you don't need to use block for English. Instead, English text should remain in root section of your `mod.json` file, to be used when game can not find translated version. ### Translating in-game strings @@ -159,7 +186,9 @@ Use any text editor (Notepad++ is recommended for Windows) and translate all str ## Developers documentation ### Adding new languages + In order to add new language it needs to be added in multiple locations in source code: + - Generate new .ts files for launcher and map editor, either by running `lupdate` with name of new .ts or by copying `english.ts` and editing language tag in the header. - Add new language into `lib/Languages.h` entry. This will trigger static_assert's in places that needs an update in code - Add new language into json schemas validation list - settings schema and mod schema @@ -172,7 +201,8 @@ Also, make full search for a name of an existing language to ensure that there a At the moment, build system will generate binary translation files (`.qs`) that can be opened by Qt. However, any new or changed lines will not be added into existing .ts files. In order to update `.ts` files manually, open command line shell in `mapeditor` or `launcher` source directories and execute command -``` + +```sh lupdate -no-obsolete * -ts translation/*.ts ``` @@ -182,5 +212,6 @@ There *may* be a way to do the same via QtCreator UI or via CMake, if you find o ### Updating translation of Launcher and Map Editor using new .ts file from translators Generally, this should be as simple as overwriting old files. Things that may be necessary if translation update is not visible in executable: + - Rebuild subproject (map editor/launcher). - Regenerate translations via `lupdate -no-obsolete * -ts translation/*.ts` diff --git a/include/vcmi/Entity.h b/include/vcmi/Entity.h index 406a879b5..b00a61525 100644 --- a/include/vcmi/Entity.h +++ b/include/vcmi/Entity.h @@ -26,7 +26,7 @@ class DLL_LINKAGE INativeTerrainProvider { public: virtual TerrainId getNativeTerrain() const = 0; - virtual FactionID getFaction() const = 0; + virtual FactionID getFactionID() const = 0; virtual bool isNativeTerrain(TerrainId terrain) const; }; diff --git a/include/vcmi/ServerCallback.h b/include/vcmi/ServerCallback.h index d69257009..1267e885e 100644 --- a/include/vcmi/ServerCallback.h +++ b/include/vcmi/ServerCallback.h @@ -36,15 +36,15 @@ public: virtual vstd::RNG * getRNG() = 0; - virtual void apply(CPackForClient * pack) = 0; + virtual void apply(CPackForClient & pack) = 0; - virtual void apply(BattleLogMessage * pack) = 0; - virtual void apply(BattleStackMoved * pack) = 0; - virtual void apply(BattleUnitsChanged * pack) = 0; - virtual void apply(SetStackEffect * pack) = 0; - virtual void apply(StacksInjured * pack) = 0; - virtual void apply(BattleObstaclesChanged * pack) = 0; - virtual void apply(CatapultAttack * pack) = 0; + virtual void apply(BattleLogMessage & pack) = 0; + virtual void apply(BattleStackMoved & pack) = 0; + virtual void apply(BattleUnitsChanged & pack) = 0; + virtual void apply(SetStackEffect & pack) = 0; + virtual void apply(StacksInjured & pack) = 0; + virtual void apply(BattleObstaclesChanged & pack) = 0; + virtual void apply(CatapultAttack & pack) = 0; }; VCMI_LIB_NAMESPACE_END diff --git a/include/vcmi/spells/Spell.h b/include/vcmi/spells/Spell.h index 6b40bf258..7a3852a5b 100644 --- a/include/vcmi/spells/Spell.h +++ b/include/vcmi/spells/Spell.h @@ -45,6 +45,7 @@ public: virtual bool hasSchool(SpellSchool school) const = 0; virtual bool canCastOnSelf() const = 0; + virtual bool canCastOnlyOnSelf() const = 0; virtual bool canCastWithoutSkip() const = 0; virtual void forEachSchool(const SchoolCallback & cb) const = 0; virtual int32_t getCost(const int32_t skillLevel) const = 0; diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index 1432b1b90..1dad5bc7f 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -7,10 +7,11 @@ set(launcher_SRCS StdInc.cpp aboutProject/aboutproject_moc.cpp modManager/cdownloadmanager_moc.cpp - modManager/cmodlist.cpp - modManager/cmodlistmodel_moc.cpp + modManager/modstateitemmodel_moc.cpp modManager/cmodlistview_moc.cpp - modManager/cmodmanager.cpp + modManager/modstatecontroller.cpp + modManager/modstatemodel.cpp + modManager/modstate.cpp modManager/imageviewer_moc.cpp modManager/chroniclesextractor.cpp settingsView/csettingsview_moc.cpp @@ -20,8 +21,6 @@ set(launcher_SRCS innoextract.cpp mainwindow_moc.cpp languages.cpp - launcherdirs.cpp - jsonutils.cpp updatedialog_moc.cpp prepare.cpp ) @@ -39,18 +38,17 @@ set(launcher_HEADERS StdInc.h aboutProject/aboutproject_moc.h modManager/cdownloadmanager_moc.h - modManager/cmodlist.h - modManager/cmodlistmodel_moc.h + modManager/modstateitemmodel_moc.h modManager/cmodlistview_moc.h - modManager/cmodmanager.h + modManager/modstatecontroller.h + modManager/modstatemodel.h + modManager/modstate.h modManager/imageviewer_moc.h modManager/chroniclesextractor.h settingsView/csettingsview_moc.h firstLaunch/firstlaunch_moc.h mainwindow_moc.h languages.h - launcherdirs.h - jsonutils.h updatedialog_moc.h main.h helper.h @@ -206,7 +204,11 @@ elseif(NOT APPLE_IOS) target_link_libraries(vcmilauncher SDL2::SDL2) endif() -target_link_libraries(vcmilauncher vcmi Qt${QT_VERSION_MAJOR}::Widgets Qt${QT_VERSION_MAJOR}::Network) +if(ENABLE_STATIC_LIBS OR NOT (ENABLE_EDITOR AND ENABLE_LAUNCHER)) + target_compile_definitions(vcmilauncher PRIVATE VCMIQT_STATIC) +endif() + +target_link_libraries(vcmilauncher vcmi vcmiqt Qt${QT_VERSION_MAJOR}::Widgets Qt${QT_VERSION_MAJOR}::Network) target_include_directories(vcmilauncher PUBLIC ${CMAKE_CURRENT_SOURCE_DIR} ) diff --git a/launcher/StdInc.h b/launcher/StdInc.h index 506773a9a..52e742b43 100644 --- a/launcher/StdInc.h +++ b/launcher/StdInc.h @@ -18,24 +18,8 @@ #include #include #include -#include +#include + +#include "../vcmiqt/convpathqstring.h" VCMI_LIB_USING_NAMESPACE - -inline QString pathToQString(const boost::filesystem::path & path) -{ -#ifdef VCMI_WINDOWS - return QString::fromStdWString(path.wstring()); -#else - return QString::fromStdString(path.string()); -#endif -} - -inline boost::filesystem::path qstringToPath(const QString & path) -{ -#ifdef VCMI_WINDOWS - return boost::filesystem::path(path.toStdWString()); -#else - return boost::filesystem::path(path.toUtf8().data()); -#endif -} diff --git a/launcher/icons/submod-disabled.png b/launcher/icons/submod-disabled.png new file mode 100644 index 000000000..7e7c1a142 Binary files /dev/null and b/launcher/icons/submod-disabled.png differ diff --git a/launcher/icons/submod-enabled.png b/launcher/icons/submod-enabled.png new file mode 100644 index 000000000..3eee77480 Binary files /dev/null and b/launcher/icons/submod-enabled.png differ diff --git a/launcher/mainwindow_moc.cpp b/launcher/mainwindow_moc.cpp index 488d6d73f..6b0b245e5 100644 --- a/launcher/mainwindow_moc.cpp +++ b/launcher/mainwindow_moc.cpp @@ -206,11 +206,6 @@ void MainWindow::on_startEditorButton_clicked() startEditor({}); } -const CModList & MainWindow::getModList() const -{ - return ui->modlistView->getModList(); -} - CModListView * MainWindow::getModView() { return ui->modlistView; diff --git a/launcher/mainwindow_moc.h b/launcher/mainwindow_moc.h index 6581086ff..9c0dc8d7d 100644 --- a/launcher/mainwindow_moc.h +++ b/launcher/mainwindow_moc.h @@ -46,7 +46,6 @@ public: explicit MainWindow(QWidget * parent = nullptr); ~MainWindow() override; - const CModList & getModList() const; CModListView * getModView(); void updateTranslation(); diff --git a/launcher/modManager/cdownloadmanager_moc.cpp b/launcher/modManager/cdownloadmanager_moc.cpp index c1621dd75..c08e6cb4c 100644 --- a/launcher/modManager/cdownloadmanager_moc.cpp +++ b/launcher/modManager/cdownloadmanager_moc.cpp @@ -10,7 +10,7 @@ #include "StdInc.h" #include "cdownloadmanager_moc.h" -#include "../launcherdirs.h" +#include "../vcmiqt/launcherdirs.h" #include "../../lib/CConfigHandler.h" diff --git a/launcher/modManager/chroniclesextractor.cpp b/launcher/modManager/chroniclesextractor.cpp index 57759e2df..6f58bdd06 100644 --- a/launcher/modManager/chroniclesextractor.cpp +++ b/launcher/modManager/chroniclesextractor.cpp @@ -106,7 +106,7 @@ void ChroniclesExtractor::createBaseMod() const { "author", "3DO" }, { "version", "1.0" }, { "contact", "vcmi.eu" }, - { "heroes", QJsonArray({"config/heroes/portraitsChronicles.json"}) }, + { "heroes", QJsonArray({"config/portraitsChronicles.json"}) }, { "settings", QJsonObject({{"mapFormat", QJsonObject({{"chronicles", QJsonObject({{ {"supported", true}, {"portraits", QJsonObject({ @@ -123,6 +123,21 @@ void ChroniclesExtractor::createBaseMod() const QFile jsonFile(dir.filePath("mod.json")); jsonFile.open(QFile::WriteOnly); jsonFile.write(QJsonDocument(mod).toJson()); + + for(auto & dataPath : VCMIDirs::get().dataPaths()) + { + auto file = dataPath / "config" / "heroes" / "portraitsChronicles.json"; + auto destFolder = VCMIDirs::get().userDataPath() / "Mods" / "chronicles" / "content" / "config"; + if(boost::filesystem::exists(file)) + { + boost::filesystem::create_directories(destFolder); +#if BOOST_VERSION >= 107400 + boost::filesystem::copy_file(file, destFolder / "portraitsChronicles.json", boost::filesystem::copy_options::overwrite_existing); +#else + boost::filesystem::copy_file(file, destFolder / "portraitsChronicles.json", boost::filesystem::copy_option::overwrite_if_exists); +#endif + } + } } void ChroniclesExtractor::createChronicleMod(int no) diff --git a/launcher/modManager/cmodlist.cpp b/launcher/modManager/cmodlist.cpp deleted file mode 100644 index fad621601..000000000 --- a/launcher/modManager/cmodlist.cpp +++ /dev/null @@ -1,434 +0,0 @@ -/* - * cmodlist.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 "cmodlist.h" - -#include "../lib/CConfigHandler.h" -#include "../../lib/filesystem/CFileInputStream.h" -#include "../../lib/GameConstants.h" -#include "../../lib/modding/CModVersion.h" - -QString CModEntry::sizeToString(double size) -{ - static const std::array sizes { - QT_TRANSLATE_NOOP("File size", "%1 B"), - QT_TRANSLATE_NOOP("File size", "%1 KiB"), - QT_TRANSLATE_NOOP("File size", "%1 MiB"), - QT_TRANSLATE_NOOP("File size", "%1 GiB"), - QT_TRANSLATE_NOOP("File size", "%1 TiB") - }; - size_t index = 0; - while(size > 1024 && index < sizes.size()) - { - size /= 1024; - index++; - } - return QCoreApplication::translate("File size", sizes[index]).arg(QString::number(size, 'f', 1)); -} - -CModEntry::CModEntry(QVariantMap repository, QVariantMap localData, QVariantMap modSettings, QString modname) - : repository(repository), localData(localData), modSettings(modSettings), modname(modname) -{ -} - -bool CModEntry::isEnabled() const -{ - if(!isInstalled()) - return false; - - if (!isVisible()) - return false; - - return modSettings["active"].toBool(); -} - -bool CModEntry::isDisabled() const -{ - if(!isInstalled()) - return false; - return !isEnabled(); -} - -bool CModEntry::isAvailable() const -{ - if(isInstalled()) - return false; - return !repository.isEmpty(); -} - -bool CModEntry::isUpdateable() const -{ - if(!isInstalled()) - return false; - - auto installedVer = localData["installedVersion"].toString().toStdString(); - auto availableVer = repository["latestVersion"].toString().toStdString(); - - return (CModVersion::fromString(installedVer) < CModVersion::fromString(availableVer)); -} - -bool isCompatible(const QVariantMap & compatibility) -{ - auto compatibleMin = CModVersion::fromString(compatibility["min"].toString().toStdString()); - auto compatibleMax = CModVersion::fromString(compatibility["max"].toString().toStdString()); - - return (compatibleMin.isNull() || CModVersion::GameVersion().compatible(compatibleMin, true, true)) - && (compatibleMax.isNull() || compatibleMax.compatible(CModVersion::GameVersion(), true, true)); -} - -bool CModEntry::isCompatible() const -{ - return ::isCompatible(localData["compatibility"].toMap()); -} - -bool CModEntry::isEssential() const -{ - return getName() == "vcmi"; -} - -bool CModEntry::isInstalled() const -{ - return !localData.isEmpty(); -} - -bool CModEntry::isVisible() const -{ - if (isCompatibilityPatch()) - { - if (isSubmod()) - return false; - } - - if (isTranslation()) - { - // Do not show not installed translation mods to languages other than player language - if (localData.empty() && getBaseValue("language") != QString::fromStdString(settings["general"]["language"].String()) ) - return false; - } - - return !localData.isEmpty() || (!repository.isEmpty() && !repository.contains("mod")); -} - -bool CModEntry::isTranslation() const -{ - return getBaseValue("modType").toString() == "Translation"; -} - -bool CModEntry::isCompatibilityPatch() const -{ - return getBaseValue("modType").toString() == "Compatibility"; -} - -bool CModEntry::isSubmod() const -{ - return getName().contains('.'); -} - -int CModEntry::getModStatus() const -{ - int status = 0; - if(isEnabled()) - status |= ModStatus::ENABLED; - if(isInstalled()) - status |= ModStatus::INSTALLED; - if(isUpdateable()) - status |= ModStatus::UPDATEABLE; - - return status; -} - -QString CModEntry::getName() const -{ - return modname; -} - -QVariant CModEntry::getValue(QString value) const -{ - return getValueImpl(value, true); -} - -QStringList CModEntry::getDependencies() const -{ - QStringList result; - for (auto const & entry : getValue("depends").toStringList()) - result.push_back(entry.toLower()); - return result; -} - -QStringList CModEntry::getConflicts() const -{ - QStringList result; - for (auto const & entry : getValue("conflicts").toStringList()) - result.push_back(entry.toLower()); - return result; -} - -QVariant CModEntry::getBaseValue(QString value) const -{ - return getValueImpl(value, false); -} - -QVariant CModEntry::getValueImpl(QString value, bool localized) const - -{ - QString langValue = QString::fromStdString(settings["general"]["language"].String()); - - // Priorities - // 1) data from newest version - // 2) data from preferred language - - bool useRepositoryData = repository.contains(value); - - if(repository.contains(value) && localData.contains(value)) - { - // value is present in both repo and locally installed. Select one from latest version - auto installedVer = localData["installedVersion"].toString().toStdString(); - auto availableVer = repository["latestVersion"].toString().toStdString(); - - useRepositoryData = CModVersion::fromString(installedVer) < CModVersion::fromString(availableVer); - } - - auto & storage = useRepositoryData ? repository : localData; - - if(localized && storage.contains(langValue)) - { - auto langStorage = storage[langValue].toMap(); - if (langStorage.contains(value)) - return langStorage[value]; - } - - if(storage.contains(value)) - return storage[value]; - - return QVariant(); -} - -QVariantMap CModList::copyField(QVariantMap data, QString from, QString to) const -{ - QVariantMap renamed; - - for(auto it = data.begin(); it != data.end(); it++) - { - QVariantMap modConf = it.value().toMap(); - - modConf.insert(to, modConf.value(from)); - renamed.insert(it.key(), modConf); - } - return renamed; -} - -void CModList::reloadRepositories() -{ - cachedMods.clear(); -} - -void CModList::resetRepositories() -{ - repositories.clear(); - cachedMods.clear(); -} - -void CModList::addRepository(QVariantMap data) -{ - for(auto & key : data.keys()) - data[key.toLower()] = data.take(key); - repositories.push_back(copyField(data, "version", "latestVersion")); - - cachedMods.clear(); -} - -void CModList::setLocalModList(QVariantMap data) -{ - localModList = copyField(data, "version", "installedVersion"); - cachedMods.clear(); -} - -void CModList::setModSettings(QVariant data) -{ - modSettings = data.toMap(); - cachedMods.clear(); -} - -void CModList::modChanged(QString modID) -{ - cachedMods.clear(); -} - -static QVariant getValue(QVariant input, QString path) -{ - if(path.size() > 1) - { - QString entryName = path.section('/', 0, 1); - QString remainder = "/" + path.section('/', 2, -1); - - entryName.remove(0, 1); - return getValue(input.toMap().value(entryName), remainder); - } - else - { - return input; - } -} - -const CModEntry & CModList::getMod(QString modName) const -{ - modName = modName.toLower(); - - auto it = cachedMods.find(modName); - - if (it != cachedMods.end()) - return it.value(); - - auto itNew = cachedMods.insert(modName, getModUncached(modName)); - return *itNew; -} - -CModEntry CModList::getModUncached(QString modname) const -{ - QVariantMap repo; - QVariantMap local = localModList[modname].toMap(); - QVariantMap settings; - - QString path = modname; - path = "/" + path.replace(".", "/mods/"); - QVariant conf = getValue(modSettings, path); - - if(conf.isNull()) - { - settings["active"] = !local.value("keepDisabled").toBool(); - } - else - { - if(!conf.toMap().isEmpty()) - { - settings = conf.toMap(); - if(settings.value("active").isNull()) - settings["active"] = !local.value("keepDisabled").toBool(); - } - else - settings.insert("active", conf); - } - - if(settings["active"].toBool()) - { - QString rootPath = path.section('/', 0, 1); - if(path != rootPath) - { - conf = getValue(modSettings, rootPath); - const auto confMap = conf.toMap(); - if(!conf.isNull() && !confMap["active"].isNull() && !confMap["active"].toBool()) - { - settings = confMap; - } - } - } - - if(settings.value("active").toBool()) - { - if(!::isCompatible(local.value("compatibility").toMap())) - settings["active"] = false; - } - - for(auto entry : repositories) - { - QVariant repoVal = getValue(entry, path); - if(repoVal.isValid()) - { - auto repoValMap = repoVal.toMap(); - if(::isCompatible(repoValMap["compatibility"].toMap())) - { - if(repo.empty() - || CModVersion::fromString(repo["version"].toString().toStdString()) - < CModVersion::fromString(repoValMap["version"].toString().toStdString())) - { - //take valid download link, screenshots and mod size before assignment - auto download = repo.value("download"); - auto screenshots = repo.value("screenshots"); - auto size = repo.value("downloadSize"); - repo = repoValMap; - if(repo.value("download").isNull()) - { - repo["download"] = download; - if(repo.value("screenshots").isNull()) //taking screenshot from the downloadable version - repo["screenshots"] = screenshots; - } - if(repo.value("downloadSize").isNull()) - repo["downloadSize"] = size; - } - } - } - } - - return CModEntry(repo, local, settings, modname); -} - -bool CModList::hasMod(QString modname) const -{ - if(localModList.contains(modname)) - return true; - - for(auto entry : repositories) - if(entry.contains(modname)) - return true; - - return false; -} - -QStringList CModList::getRequirements(QString modname) -{ - QStringList ret; - - if(hasMod(modname)) - { - auto mod = getMod(modname); - - for(auto entry : mod.getDependencies()) - ret += getRequirements(entry.toLower()); - } - ret += modname; - - return ret; -} - -QVector CModList::getModList() const -{ - QSet knownMods; - QVector modList; - for(auto repo : repositories) - { - for(auto it = repo.begin(); it != repo.end(); it++) - { - knownMods.insert(it.key().toLower()); - } - } - for(auto it = localModList.begin(); it != localModList.end(); it++) - { - knownMods.insert(it.key().toLower()); - } - - for(auto entry : knownMods) - { - modList.push_back(entry); - } - return modList; -} - -QVector CModList::getChildren(QString parent) const -{ - QVector children; - - int depth = parent.count('.') + 1; - for(const QString & mod : getModList()) - { - if(mod.count('.') == depth && mod.startsWith(parent)) - children.push_back(mod); - } - return children; -} diff --git a/launcher/modManager/cmodlist.h b/launcher/modManager/cmodlist.h deleted file mode 100644 index a9c595a08..000000000 --- a/launcher/modManager/cmodlist.h +++ /dev/null @@ -1,112 +0,0 @@ -/* - * cmodlist.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 -#include -#include - -namespace ModStatus -{ -enum EModStatus -{ - MASK_NONE = 0, - ENABLED = 1, - INSTALLED = 2, - UPDATEABLE = 4, - MASK_ALL = 255 -}; -} - -class CModEntry -{ - // repository contains newest version only (if multiple are available) - QVariantMap repository; - QVariantMap localData; - QVariantMap modSettings; - - QString modname; - - QVariant getValueImpl(QString value, bool localized) const; -public: - CModEntry(QVariantMap repository, QVariantMap localData, QVariantMap modSettings, QString modname); - - // installed and enabled - bool isEnabled() const; - // installed but disabled - bool isDisabled() const; - // available in any of repositories but not installed - bool isAvailable() const; - // installed and greater version exists in repository - bool isUpdateable() const; - // installed - bool isInstalled() const; - // vcmi essential files - bool isEssential() const; - // checks if version is compatible with vcmi - bool isCompatible() const; - // returns true if mod should be visible in Launcher - bool isVisible() const; - // returns true if mod type is Translation - bool isTranslation() const; - // returns true if mod type is Compatibility - bool isCompatibilityPatch() const; - // returns true if this is a submod - bool isSubmod() const; - - // see ModStatus enum - int getModStatus() const; - - QString getName() const; - - // get value of some field in mod structure. Returns empty optional if value is not present - QVariant getValue(QString value) const; - QVariant getBaseValue(QString value) const; - - QStringList getDependencies() const; - QStringList getConflicts() const; - - static QString sizeToString(double size); -}; - -class CModList -{ - QVector repositories; - QVariantMap localModList; - QVariantMap modSettings; - - mutable QMap cachedMods; - - QVariantMap copyField(QVariantMap data, QString from, QString to) const; - - CModEntry getModUncached(QString modname) const; -public: - virtual void resetRepositories(); - virtual void reloadRepositories(); - virtual void addRepository(QVariantMap data); - virtual void setLocalModList(QVariantMap data); - virtual void setModSettings(QVariant data); - virtual void modChanged(QString modID); - - // returns mod by name. Note: mod MUST exist - const CModEntry & getMod(QString modname) const; - - // returns list of all mods necessary to run selected one, including mod itself - // order is: first mods in list don't have any dependencies, last mod is modname - // note: may include mods not present in list - QStringList getRequirements(QString modname); - - bool hasMod(QString modname) const; - - // returns list of all available mods - QVector getModList() const; - - QVector getChildren(QString parent) const; -}; diff --git a/launcher/modManager/cmodlistview_moc.cpp b/launcher/modManager/cmodlistview_moc.cpp index d8797979a..44f9ffa58 100644 --- a/launcher/modManager/cmodlistview_moc.cpp +++ b/launcher/modManager/cmodlistview_moc.cpp @@ -17,13 +17,14 @@ #include #include -#include "cmodlistmodel_moc.h" -#include "cmodmanager.h" +#include "modstatemodel.h" +#include "modstateitemmodel_moc.h" +#include "modstatecontroller.h" #include "cdownloadmanager_moc.h" #include "chroniclesextractor.h" #include "../settingsView/csettingsview_moc.h" -#include "../launcherdirs.h" -#include "../jsonutils.h" +#include "../vcmiqt/launcherdirs.h" +#include "../vcmiqt/jsonutils.h" #include "../helper.h" #include "../../lib/VCMIDirs.h" @@ -31,18 +32,21 @@ #include "../../lib/texts/Languages.h" #include "../../lib/modding/CModVersion.h" #include "../../lib/filesystem/Filesystem.h" +#include "../../lib/texts/CGeneralTextHandler.h" #include -static double mbToBytes(double mb) -{ - return mb * 1024 * 1024; -} - void CModListView::setupModModel() { - modModel = new CModListModel(this); - manager = std::make_unique(modModel); + static const QString repositoryCachePath = CLauncherDirs::downloadsPath() + "/repositoryCache.json"; + const auto &cachedRepositoryData = JsonUtils::jsonFromFile(repositoryCachePath); + + modStateModel = std::make_shared(); + if (!cachedRepositoryData.isNull()) + modStateModel->appendRepositories(cachedRepositoryData); + + modModel = new ModStateItemModel(modStateModel, this); + manager = std::make_unique(modStateModel); } void CModListView::changeEvent(QEvent *event) @@ -148,13 +152,7 @@ CModListView::CModListView(QWidget * parent) dlManager = nullptr; if(settings["launcher"]["autoCheckRepositories"].Bool()) - { loadRepositories(); - } - else - { - manager->resetRepositories(); - } #ifdef VCMI_MOBILE for(auto * scrollWidget : { @@ -171,8 +169,6 @@ CModListView::CModListView(QWidget * parent) void CModListView::loadRepositories() { - manager->resetRepositories(); - QStringList repositories; if (settings["launcher"]["defaultRepositoryEnabled"].Bool()) @@ -181,7 +177,7 @@ void CModListView::loadRepositories() if (settings["launcher"]["extraRepositoryEnabled"].Bool()) repositories.push_back(QString::fromStdString(settings["launcher"]["extraRepositoryURL"].String())); - for(auto entry : repositories) + for(const auto & entry : repositories) { if (entry.isEmpty()) continue; @@ -204,11 +200,21 @@ CModListView::~CModListView() static QString replaceIfNotEmpty(QVariant value, QString pattern) { - if(value.canConvert()) - return pattern.arg(value.toStringList().join(", ")); - if(value.canConvert()) - return pattern.arg(value.toString()); + { + if (value.toString().isEmpty()) + return ""; + else + return pattern.arg(value.toString()); + } + + if(value.canConvert()) + { + if (value.toStringList().isEmpty()) + return ""; + else + return pattern.arg(value.toStringList().join(", ")); + } // all valid types of data should have been filtered by code above assert(!value.isValid()); @@ -223,7 +229,7 @@ static QString replaceIfNotEmpty(QStringList value, QString pattern) return ""; } -QString CModListView::genChangelogText(CModEntry & mod) +QString CModListView::genChangelogText(const ModState & mod) { QString headerTemplate = "

%1:

"; QString entryBegin = "

    "; @@ -233,7 +239,7 @@ QString CModListView::genChangelogText(CModEntry & mod) QString result; - QVariantMap changelog = mod.getValue("changelog").toMap(); + QMap changelog = mod.getChangelog(); QList versions = changelog.keys(); std::sort(versions.begin(), versions.end(), [](QString lesser, QString greater) @@ -242,37 +248,59 @@ QString CModListView::genChangelogText(CModEntry & mod) }); std::reverse(versions.begin(), versions.end()); - for(auto & version : versions) + for(const auto & version : versions) { result += headerTemplate.arg(version); result += entryBegin; - for(auto & line : changelog.value(version).toStringList()) + for(const auto & line : changelog.value(version)) result += entryLine.arg(line); result += entryEnd; } return result; } -QStringList CModListView::getModNames(QStringList input) +QStringList CModListView::getModNames(QString queryingModID, QStringList input) { QStringList result; + auto queryingMod = modStateModel->getMod(queryingModID); + for(const auto & modID : input) { - auto mod = modModel->getMod(modID.toLower()); + if (modStateModel->isModExists(modID) && modStateModel->getMod(modID).isHidden()) + continue; - QString modName = mod.getValue("name").toString(); + QString parentModID = modStateModel->getTopParent(modID); + QString displayName; - if (modName.isEmpty()) - result += modID.toLower(); + if (modStateModel->isSubmod(modID) && queryingMod.getParentID() != parentModID ) + { + // show in form "parent mod (submod)" + + QString parentDisplayName = parentModID; + QString submodDisplayName = modID; + + if (modStateModel->isModExists(parentModID)) + parentDisplayName = modStateModel->getMod(parentModID).getName(); + + if (modStateModel->isModExists(modID)) + submodDisplayName = modStateModel->getMod(modID).getName(); + + displayName = QString("%1 (%2)").arg(submodDisplayName, parentDisplayName); + } else - result += modName; + { + // show simply as mod name + displayName = modID; + if (modStateModel->isModExists(modID)) + displayName = modStateModel->getMod(modID).getName(); + } + result += displayName; } - return result; } -QString CModListView::genModInfoText(CModEntry & mod) +QString CModListView::genModInfoText(const ModState & mod) { QString prefix = "

    %1: "; // shared prefix QString redPrefix = "

    %1: "; // shared prefix @@ -286,29 +314,40 @@ QString CModListView::genModInfoText(CModEntry & mod) QString result; - result += replaceIfNotEmpty(mod.getValue("name"), lineTemplate.arg(tr("Mod name"))); - result += replaceIfNotEmpty(mod.getValue("installedVersion"), lineTemplate.arg(tr("Installed version"))); - result += replaceIfNotEmpty(mod.getValue("latestVersion"), lineTemplate.arg(tr("Latest version"))); + result += replaceIfNotEmpty(mod.getName(), lineTemplate.arg(tr("Mod name"))); + if (mod.isUpdateAvailable()) + { + result += replaceIfNotEmpty(mod.getInstalledVersion(), lineTemplate.arg(tr("Installed version"))); + result += replaceIfNotEmpty(mod.getRepositoryVersion(), lineTemplate.arg(tr("Latest version"))); + } + else + { + if (mod.isInstalled()) + result += replaceIfNotEmpty(mod.getInstalledVersion(), lineTemplate.arg(tr("Installed version"))); + else + result += replaceIfNotEmpty(mod.getRepositoryVersion(), lineTemplate.arg(tr("Latest version"))); + } - if(mod.getValue("localSizeBytes").isValid()) - result += replaceIfNotEmpty(CModEntry::sizeToString(mod.getValue("localSizeBytes").toDouble()), lineTemplate.arg(tr("Size"))); - if((mod.isAvailable() || mod.isUpdateable()) && mod.getValue("downloadSize").isValid()) - result += replaceIfNotEmpty(CModEntry::sizeToString(mbToBytes(mod.getValue("downloadSize").toDouble())), lineTemplate.arg(tr("Download size"))); + if (mod.isInstalled()) + result += replaceIfNotEmpty(modStateModel->getInstalledModSizeFormatted(mod.getID()), lineTemplate.arg(tr("Size"))); + + if((!mod.isInstalled() || mod.isUpdateAvailable()) && !mod.getDownloadSizeFormatted().isEmpty()) + result += replaceIfNotEmpty(mod.getDownloadSizeFormatted(), lineTemplate.arg(tr("Download size"))); - result += replaceIfNotEmpty(mod.getValue("author"), lineTemplate.arg(tr("Authors"))); + result += replaceIfNotEmpty(mod.getAuthors(), lineTemplate.arg(tr("Authors"))); - if(mod.getValue("licenseURL").isValid()) - result += urlTemplate.arg(tr("License")).arg(mod.getValue("licenseURL").toString()).arg(mod.getValue("licenseName").toString()); + if(!mod.getLicenseName().isEmpty()) + result += urlTemplate.arg(tr("License")).arg(mod.getLicenseUrl()).arg(mod.getLicenseName()); - if(mod.getValue("contact").isValid()) - result += urlTemplate.arg(tr("Contact")).arg(mod.getValue("contact").toString()).arg(mod.getValue("contact").toString()); + if(!mod.getContact().isEmpty()) + result += urlTemplate.arg(tr("Contact")).arg(mod.getContact()).arg(mod.getContact()); //compatibility info if(!mod.isCompatible()) { - auto compatibilityInfo = mod.getValue("compatibility").toMap(); - auto minStr = compatibilityInfo.value("min").toString(); - auto maxStr = compatibilityInfo.value("max").toString(); + auto compatibilityInfo = mod.getCompatibleVersionRange(); + auto minStr = compatibilityInfo.first; + auto maxStr = compatibilityInfo.second; result += incompatibleString.arg(tr("Compatibility")); if(minStr == maxStr) @@ -327,52 +366,57 @@ QString CModListView::genModInfoText(CModEntry & mod) } } - QStringList supportedLanguages; - QVariant baseLanguageVariant = mod.getBaseValue("language"); + QVariant baseLanguageVariant = mod.getBaseLanguage(); QString baseLanguageID = baseLanguageVariant.isValid() ? baseLanguageVariant.toString() : "english"; - bool needToShowSupportedLanguages = false; + QStringList supportedLanguages = mod.getSupportedLanguages(); - for(const auto & language : Languages::getLanguageList()) + if(supportedLanguages.size() > 1) { - QString languageID = QString::fromStdString(language.identifier); + QStringList supportedLanguagesTranslated; - if (languageID != baseLanguageID && !mod.getValue(languageID).isValid()) - continue; + for (const auto & languageID : supportedLanguages) + supportedLanguagesTranslated += QApplication::translate("Language", Languages::getLanguageOptions(languageID.toStdString()).nameEnglish.c_str()); - if (languageID != baseLanguageID) - needToShowSupportedLanguages = true; - - supportedLanguages += QApplication::translate("Language", language.nameEnglish.c_str()); + result += replaceIfNotEmpty(supportedLanguagesTranslated, lineTemplate.arg(tr("Languages"))); } - if(needToShowSupportedLanguages) - result += replaceIfNotEmpty(supportedLanguages, lineTemplate.arg(tr("Languages"))); + QStringList conflicts = mod.getConflicts(); + for (const auto & otherMod : modStateModel->getAllMods()) + { + QStringList otherConflicts = modStateModel->getMod(otherMod).getConflicts(); - result += replaceIfNotEmpty(getModNames(mod.getDependencies()), lineTemplate.arg(tr("Required mods"))); - result += replaceIfNotEmpty(getModNames(mod.getConflicts()), lineTemplate.arg(tr("Conflicting mods"))); - result += replaceIfNotEmpty(mod.getValue("description"), textTemplate.arg(tr("Description"))); + if (otherConflicts.contains(mod.getID()) && !conflicts.contains(otherMod)) + conflicts.push_back(otherMod); + } + + result += replaceIfNotEmpty(getModNames(mod.getID(), mod.getDependencies()), lineTemplate.arg(tr("Required mods"))); + result += replaceIfNotEmpty(getModNames(mod.getID(), conflicts), lineTemplate.arg(tr("Conflicting mods"))); + result += replaceIfNotEmpty(mod.getDescription(), textTemplate.arg(tr("Description"))); result += "

    "; // to get some empty space - QString unknownDeps = tr("This mod can not be installed or enabled because the following dependencies are not present"); - QString blockingMods = tr("This mod can not be enabled because the following mods are incompatible with it"); - QString hasActiveDependentMods = tr("This mod cannot be disabled because it is required by the following mods"); - QString hasDependentMods = tr("This mod cannot be uninstalled or updated because it is required by the following mods"); + QString translationMismatch = tr("This mod cannot be enabled because it translates into a different language."); + QString notInstalledDeps = tr("This mod can not be enabled because the following dependencies are not present"); + QString unavailableDeps = tr("This mod can not be installed because the following dependencies are not present"); QString thisIsSubmod = tr("This is a submod and it cannot be installed or uninstalled separately from its parent mod"); QString notes; - notes += replaceIfNotEmpty(getModNames(findInvalidDependencies(mod.getName())), listTemplate.arg(unknownDeps)); - notes += replaceIfNotEmpty(getModNames(findBlockingMods(mod.getName())), listTemplate.arg(blockingMods)); - if(mod.isEnabled()) - notes += replaceIfNotEmpty(getModNames(findDependentMods(mod.getName(), true)), listTemplate.arg(hasActiveDependentMods)); - if(mod.isInstalled()) - notes += replaceIfNotEmpty(getModNames(findDependentMods(mod.getName(), false)), listTemplate.arg(hasDependentMods)); + QStringList notInstalledDependencies = this->getModsToInstall(mod.getID()); + QStringList unavailableDependencies = this->findUnavailableMods(notInstalledDependencies); + + if (mod.isInstalled()) + notes += replaceIfNotEmpty(getModNames(mod.getID(), notInstalledDependencies), listTemplate.arg(notInstalledDeps)); + else + notes += replaceIfNotEmpty(getModNames(mod.getID(), unavailableDependencies), listTemplate.arg(unavailableDeps)); if(mod.isSubmod()) notes += noteTemplate.arg(thisIsSubmod); + if (mod.isTranslation() && CGeneralTextHandler::getPreferredLanguage() != mod.getBaseLanguage().toStdString()) + notes += noteTemplate.arg(translationMismatch); + if(notes.size()) result += textTemplate.arg(tr("Notes")).arg(notes); @@ -395,6 +439,8 @@ void CModListView::dataChanged(const QModelIndex & topleft, const QModelIndex & void CModListView::selectMod(const QModelIndex & index) { + ui->tabWidget->setCurrentIndex(0); + if(!index.isValid()) { disableModInfo(); @@ -402,7 +448,10 @@ void CModListView::selectMod(const QModelIndex & index) else { const auto modName = index.data(ModRoles::ModNameRole).toString(); - auto mod = modModel->getMod(modName); + auto mod = modStateModel->getMod(modName); + + ui->tabWidget->setTabEnabled(1, !mod.getChangelog().isEmpty()); + ui->tabWidget->setTabEnabled(2, !mod.getScreenshots().isEmpty()); ui->modInfoBrowser->setHtml(genModInfoText(mod)); ui->changelogBrowser->setHtml(genChangelogText(mod)); @@ -410,24 +459,22 @@ void CModListView::selectMod(const QModelIndex & index) Helper::enableScrollBySwiping(ui->modInfoBrowser); Helper::enableScrollBySwiping(ui->changelogBrowser); - bool hasInvalidDeps = !findInvalidDependencies(modName).empty(); - bool hasBlockingMods = !findBlockingMods(modName).empty(); - bool hasDependentMods = !findDependentMods(modName, true).empty(); + QStringList notInstalledDependencies = this->getModsToInstall(modName); + QStringList unavailableDependencies = this->findUnavailableMods(notInstalledDependencies); + bool translationMismatch = mod.isTranslation() && CGeneralTextHandler::getPreferredLanguage() != mod.getBaseLanguage().toStdString(); - ui->disableButton->setVisible(mod.isEnabled()); - ui->enableButton->setVisible(mod.isDisabled()); + ui->disableButton->setVisible(modStateModel->isModInstalled(mod.getID()) && modStateModel->isModEnabled(mod.getID())); + ui->enableButton->setVisible(modStateModel->isModInstalled(mod.getID()) && !modStateModel->isModEnabled(mod.getID())); ui->installButton->setVisible(mod.isAvailable() && !mod.isSubmod()); ui->uninstallButton->setVisible(mod.isInstalled() && !mod.isSubmod()); - ui->updateButton->setVisible(mod.isUpdateable()); + ui->updateButton->setVisible(mod.isUpdateAvailable()); // Block buttons if action is not allowed at this time - // TODO: automate handling of some of these cases instead of forcing player - // to resolve all conflicts manually. - ui->disableButton->setEnabled(!hasDependentMods && !mod.isEssential()); - ui->enableButton->setEnabled(!hasBlockingMods && !hasInvalidDeps); - ui->installButton->setEnabled(!hasInvalidDeps); - ui->uninstallButton->setEnabled(!hasDependentMods && !mod.isEssential()); - ui->updateButton->setEnabled(!hasInvalidDeps && !hasDependentMods); + ui->disableButton->setEnabled(true); + ui->enableButton->setEnabled(notInstalledDependencies.empty() && !translationMismatch); + ui->installButton->setEnabled(unavailableDependencies.empty()); + ui->uninstallButton->setEnabled(true); + ui->updateButton->setEnabled(unavailableDependencies.empty()); loadScreenshots(); } @@ -460,149 +507,118 @@ void CModListView::on_lineEdit_textChanged(const QString & arg1) void CModListView::on_comboBox_currentIndexChanged(int index) { - switch(index) - { - case 0: - filterModel->setTypeFilter(ModStatus::MASK_NONE, ModStatus::MASK_NONE); - break; - case 1: - filterModel->setTypeFilter(ModStatus::MASK_NONE, ModStatus::INSTALLED); - break; - case 2: - filterModel->setTypeFilter(ModStatus::INSTALLED, ModStatus::INSTALLED); - break; - case 3: - filterModel->setTypeFilter(ModStatus::UPDATEABLE, ModStatus::UPDATEABLE); - break; - case 4: - filterModel->setTypeFilter(ModStatus::ENABLED | ModStatus::INSTALLED, ModStatus::ENABLED | ModStatus::INSTALLED); - break; - case 5: - filterModel->setTypeFilter(ModStatus::INSTALLED, ModStatus::ENABLED | ModStatus::INSTALLED); - break; - } + auto enumIndex = static_cast(index); + filterModel->setTypeFilter(enumIndex); } -QStringList CModListView::findInvalidDependencies(QString mod) +QStringList CModListView::findUnavailableMods(QStringList candidates) { - QStringList ret; - for(QString requirement : modModel->getRequirements(mod)) + QStringList invalidMods; + + for(QString modName : candidates) { - if(!modModel->hasMod(requirement) && !modModel->hasMod(requirement.split(QChar('.'))[0])) - ret += requirement; + if(!modStateModel->isModExists(modName)) + invalidMods.push_back(modName); } - return ret; -} - -QStringList CModListView::findBlockingMods(QString modUnderTest) -{ - QStringList ret; - auto required = modModel->getRequirements(modUnderTest); - - for(QString name : modModel->getModList()) - { - auto mod = modModel->getMod(name); - - if(mod.isEnabled()) - { - // one of enabled mods have requirement (or this mod) marked as conflict - for(auto conflict : mod.getConflicts()) - { - if(required.contains(conflict)) - ret.push_back(name); - } - } - } - - return ret; -} - -QStringList CModListView::findDependentMods(QString mod, bool excludeDisabled) -{ - QStringList ret; - for(QString modName : modModel->getModList()) - { - auto current = modModel->getMod(modName); - - if(!current.isInstalled() || !current.isVisible()) - continue; - - if(current.getDependencies().contains(mod, Qt::CaseInsensitive)) - { - if(!(current.isDisabled() && excludeDisabled)) - ret += modName; - } - } - return ret; + return invalidMods; } void CModListView::on_enableButton_clicked() { QString modName = ui->allModsView->currentIndex().data(ModRoles::ModNameRole).toString(); - enableModByName(modName); - checkManagerErrors(); } void CModListView::enableModByName(QString modName) { - assert(findBlockingMods(modName).empty()); - assert(findInvalidDependencies(modName).empty()); - - for(auto & name : modModel->getRequirements(modName)) - { - if(modModel->getMod(name).isDisabled()) - manager->enableMod(name); - } - emit modsChanged(); + manager->enableMods({modName}); + modModel->modChanged(modName); } void CModListView::on_disableButton_clicked() { QString modName = ui->allModsView->currentIndex().data(ModRoles::ModNameRole).toString(); - disableModByName(modName); - checkManagerErrors(); } void CModListView::disableModByName(QString modName) { - if(modModel->hasMod(modName) && modModel->getMod(modName).isEnabled()) - manager->disableMod(modName); + manager->disableMod(modName); + modModel->modChanged(modName); +} - emit modsChanged(); +QStringList CModListView::getModsToInstall(QString mod) +{ + QStringList result; + QStringList candidates; + QStringList processed; + + candidates.push_back(mod); + while (!candidates.empty()) + { + QString potentialToInstall = candidates.back(); + candidates.pop_back(); + processed.push_back(potentialToInstall); + + if (modStateModel->isModExists(potentialToInstall) && modStateModel->isModInstalled(potentialToInstall)) + continue; + + if (modStateModel->isSubmod(potentialToInstall)) + { + QString topParent = modStateModel->getTopParent(potentialToInstall); + if (modStateModel->isModInstalled(topParent)) + { + if (modStateModel->isModUpdateAvailable(topParent)) + potentialToInstall = modStateModel->getTopParent(potentialToInstall); + // else - potentially broken mod that depends on non-existing submod + } + else + potentialToInstall = modStateModel->getTopParent(potentialToInstall); + } + + result.push_back(potentialToInstall); + + if (modStateModel->isModExists(potentialToInstall)) + { + QStringList dependencies = modStateModel->getMod(potentialToInstall).getDependencies(); + for (const auto & dependency : dependencies) + { + if (!processed.contains(dependency) && !candidates.contains(dependency)) + candidates.push_back(dependency); + } + } + } + result.removeDuplicates(); + return result; } void CModListView::on_updateButton_clicked() { QString modName = ui->allModsView->currentIndex().data(ModRoles::ModNameRole).toString(); - assert(findInvalidDependencies(modName).empty()); - - for(auto & name : modModel->getRequirements(modName)) + for(const auto & name : getModsToInstall(modName)) { - auto mod = modModel->getMod(name); + auto mod = modStateModel->getMod(name); // update required mod, install missing (can be new dependency) - if(mod.isUpdateable() || !mod.isInstalled()) - downloadFile(name + ".zip", mod.getValue("download").toString(), name, mbToBytes(mod.getValue("downloadSize").toDouble())); + if(mod.isUpdateAvailable() || !mod.isInstalled()) + downloadFile(name + ".zip", mod.getDownloadUrl(), name, mod.getDownloadSizeBytes()); } } void CModListView::on_uninstallButton_clicked() { QString modName = ui->allModsView->currentIndex().data(ModRoles::ModNameRole).toString(); - // NOTE: perhaps add "manually installed" flag and uninstall those dependencies that don't have it? - if(modModel->hasMod(modName) && modModel->getMod(modName).isInstalled()) + if(modStateModel->isModExists(modName) && modStateModel->getMod(modName).isInstalled()) { - if(modModel->getMod(modName).isEnabled()) + if(modStateModel->isModEnabled(modName)) manager->disableMod(modName); manager->uninstallMod(modName); + modModel->reloadRepositories(); } - emit modsChanged(); checkManagerErrors(); } @@ -610,28 +626,14 @@ void CModListView::on_installButton_clicked() { QString modName = ui->allModsView->currentIndex().data(ModRoles::ModNameRole).toString(); - assert(findInvalidDependencies(modName).empty()); - - for(auto & name : modModel->getRequirements(modName)) + for(const auto & name : getModsToInstall(modName)) { - auto mod = modModel->getMod(name); + auto mod = modStateModel->getMod(name); if(mod.isAvailable()) - downloadFile(name + ".zip", mod.getValue("download").toString(), name, mbToBytes(mod.getValue("downloadSize").toDouble())); - else if(!mod.isEnabled()) + downloadFile(name + ".zip", mod.getDownloadUrl(), name, mod.getDownloadSizeBytes()); + else if(!modStateModel->isModEnabled(name)) enableModByName(name); } - - for(auto & name : modModel->getMod(modName).getConflicts()) - { - auto mod = modModel->getMod(name); - if(mod.isEnabled()) - { - //TODO: consider reverse dependencies disabling - //TODO: consider if it may be possible for subdependencies to block disabling conflicting mod? - //TODO: consider if it may be possible to get subconflicts that will block disabling conflicting mod? - disableModByName(name); - } - } } void CModListView::on_installFromFileButton_clicked() @@ -683,11 +685,12 @@ void CModListView::manualInstallFile(QString filePath) // reload settings Helper::loadSettings(); - for(auto widget : qApp->allWidgets()) + for(const auto widget : qApp->allWidgets()) if(auto settingsView = qobject_cast(widget)) settingsView->loadSettings(); - manager->loadMods(); - manager->loadModSettings(); + + modStateModel->reloadLocalState(); + modModel->reloadRepositories(); } } } @@ -695,12 +698,12 @@ void CModListView::manualInstallFile(QString filePath) downloadFile(fileName, QUrl::fromLocalFile(filePath), fileName); } -void CModListView::downloadFile(QString file, QString url, QString description, qint64 size) +void CModListView::downloadFile(QString file, QString url, QString description, qint64 sizeBytes) { - downloadFile(file, QUrl{url}, description, size); + downloadFile(file, QUrl{url}, description, sizeBytes); } -void CModListView::downloadFile(QString file, QUrl url, QString description, qint64 size) +void CModListView::downloadFile(QString file, QUrl url, QString description, qint64 sizeBytes) { if(!dlManager) { @@ -715,13 +718,13 @@ void CModListView::downloadFile(QString file, QUrl url, QString description, qin connect(manager.get(), SIGNAL(extractionProgress(qint64,qint64)), this, SLOT(extractionProgress(qint64,qint64))); - connect(modModel, &CModListModel::dataChanged, filterModel, &QAbstractItemModel::dataChanged); + connect(modModel, &ModStateItemModel::dataChanged, filterModel, &QAbstractItemModel::dataChanged); const auto progressBarFormat = tr("Downloading %1. %p% (%v MB out of %m MB) finished").arg(description); ui->progressBar->setFormat(progressBarFormat); } - dlManager->downloadFile(url, file, size); + dlManager->downloadFile(url, file, sizeBytes); } void CModListView::downloadProgress(qint64 current, qint64 max) @@ -780,7 +783,6 @@ void CModListView::downloadFinished(QStringList savedFiles, QStringList failedFi installFiles(savedFiles); hideProgressBar(); - emit modsChanged(); } void CModListView::hideProgressBar() @@ -799,7 +801,7 @@ void CModListView::installFiles(QStringList files) QStringList maps; QStringList images; QStringList exe; - QVector repositories; + JsonNode repository; // TODO: some better way to separate zip's with mods and downloaded repository files for(QString filename : files) @@ -813,36 +815,47 @@ void CModListView::installFiles(QStringList files) else if(filename.endsWith(".json", Qt::CaseInsensitive)) { //download and merge additional files - auto repoData = JsonUtils::JsonFromFile(filename).toMap(); - if(repoData.value("name").isNull()) + const auto &repoData = JsonUtils::jsonFromFile(filename); + if(repoData["name"].isNull()) { - for(const auto & key : repoData.keys()) + // This is main repository index. Download all referenced mods + for(const auto & [modName, modJson] : repoData.Struct()) { - auto modjson = repoData[key].toMap().value("mod"); - if(!modjson.isNull()) - { - downloadFile(key + ".json", modjson.toString(), tr("mods repository index")); - } + auto modNameLower = boost::algorithm::to_lower_copy(modName); + auto modJsonUrl = modJson["mod"]; + if(!modJsonUrl.isNull()) + downloadFile(QString::fromStdString(modName + ".json"), QString::fromStdString(modJsonUrl.String()), tr("mods repository index")); + + repository[modNameLower] = modJson; } } else { - auto modn = QFileInfo(filename).baseName(); - QVariantMap temp; - temp[modn] = repoData; - repoData = temp; + // This is json of a single mod. Extract name of mod and add it to repo + auto modName = QFileInfo(filename).baseName().toStdString(); + auto modNameLower = boost::algorithm::to_lower_copy(modName); + repository[modNameLower] = repoData; } - repositories.push_back(repoData); } else if(filename.endsWith(".png", Qt::CaseInsensitive)) images.push_back(filename); } - if (!repositories.empty()) - manager->loadRepositories(repositories); + if (!repository.isNull()) + { + manager->appendRepositories(repository); + modModel->reloadRepositories(); + + static const QString repositoryCachePath = CLauncherDirs::downloadsPath() + "/repositoryCache.json"; + JsonUtils::jsonToFile(repositoryCachePath, modStateModel->getRepositoryData()); + } if(!mods.empty()) + { installMods(mods); + modStateModel->reloadLocalState(); + modModel->reloadRepositories(); + } if(!maps.empty()) installMaps(maps); @@ -869,10 +882,8 @@ void CModListView::installFiles(QStringList files) if(futureExtract.get()) { //update - CResourceHandler::get("initial")->updateFilteredFiles([](const std::string &){ return true; }); - manager->loadMods(); + modStateModel->reloadLocalState(); modModel->reloadRepositories(); - emit modsChanged(); } } @@ -883,6 +894,7 @@ void CModListView::installFiles(QStringList files) void CModListView::installMods(QStringList archives) { QStringList modNames; + QStringList modsToEnable; for(QString archive : archives) { @@ -893,65 +905,30 @@ void CModListView::installMods(QStringList archives) modNames.push_back(modName); } - QStringList modsToEnable; - - // disable mod(s), to properly recalculate dependencies, if changed - for(QString mod : boost::adaptors::reverse(modNames)) + // uninstall old version of mod, if installed + for(QString mod : modNames) { - CModEntry entry = modModel->getMod(mod); - if(entry.isInstalled()) + if(modStateModel->getMod(mod).isInstalled()) { - // enable mod if installed and enabled - if(entry.isEnabled()) + if (modStateModel->isModEnabled(mod)) modsToEnable.push_back(mod); + + manager->uninstallMod(mod); } else { - // enable mod if m - if(settings["launcher"]["enableInstalledMods"].Bool()) - modsToEnable.push_back(mod); + // installation of previously not present mod -> enable it + modsToEnable.push_back(mod); } } - // uninstall old version of mod, if installed - for(QString mod : boost::adaptors::reverse(modNames)) - { - if(modModel->getMod(mod).isInstalled()) - manager->uninstallMod(mod); - } - for(int i = 0; i < modNames.size(); i++) { ui->progressBar->setFormat(tr("Installing mod %1").arg(modNames[i])); manager->installMod(modNames[i], archives[i]); } - std::function enableMod; - - enableMod = [&](QString modName) - { - auto mod = modModel->getMod(modName); - if(mod.isInstalled() && !mod.getValue("keepDisabled").toBool()) - { - for (auto const & dependencyName : mod.getDependencies()) - { - auto dependency = modModel->getMod(dependencyName); - if(dependency.isDisabled()) - manager->enableMod(dependencyName); - } - - if(mod.isDisabled() && manager->enableMod(modName)) - { - for(QString child : modModel->getChildren(modName)) - enableMod(child); - } - } - }; - - for(QString mod : modsToEnable) - { - enableMod(mod); - } + manager->enableMods(modsToEnable); checkManagerErrors(); @@ -1014,9 +991,9 @@ void CModListView::loadScreenshots() ui->screenshotsList->clear(); QString modName = ui->allModsView->currentIndex().data(ModRoles::ModNameRole).toString(); - assert(modModel->hasMod(modName)); //should be filtered out by check above + assert(modStateModel->isModExists(modName)); //should be filtered out by check above - for(QString url : modModel->getMod(modName).getValue("screenshots").toStringList()) + for(QString url : modStateModel->getMod(modName).getScreenshots()) { // URL must be encoded to something else to get rid of symbols illegal in file names const auto hashed = QCryptographicHash::hash(url.toUtf8(), QCryptographicHash::Md5); @@ -1050,52 +1027,42 @@ void CModListView::on_screenshotsList_clicked(const QModelIndex & index) } } -const CModList & CModListView::getModList() const -{ - assert(modModel); - return *modModel; -} - void CModListView::doInstallMod(const QString & modName) { - assert(findInvalidDependencies(modName).empty()); - - for(auto & name : modModel->getRequirements(modName)) + for(const auto & name : modStateModel->getMod(modName).getDependencies()) { - auto mod = modModel->getMod(name); + auto mod = modStateModel->getMod(name); if(!mod.isInstalled()) - downloadFile(name + ".zip", mod.getValue("download").toString(), name, mbToBytes(mod.getValue("downloadSize").toDouble())); + downloadFile(name + ".zip", mod.getDownloadUrl(), name, mod.getDownloadSizeBytes()); } } bool CModListView::isModAvailable(const QString & modName) { - auto mod = modModel->getMod(modName); - return mod.isAvailable(); + return !modStateModel->isModInstalled(modName); } bool CModListView::isModEnabled(const QString & modName) { - auto mod = modModel->getMod(modName); - return mod.isEnabled(); + return modStateModel->isModEnabled(modName); } bool CModListView::isModInstalled(const QString & modName) { - auto mod = modModel->getMod(modName); + auto mod = modStateModel->getMod(modName); return mod.isInstalled(); } QString CModListView::getTranslationModName(const QString & language) { - for(const auto & modName : modModel->getModList()) + for(const auto & modName : modStateModel->getAllMods()) { - auto mod = modModel->getMod(modName); + auto mod = modStateModel->getMod(modName); if (!mod.isTranslation()) continue; - if (mod.getBaseValue("language").toString() != language) + if (mod.getBaseLanguage() != language) continue; return modName; @@ -1110,19 +1077,18 @@ void CModListView::on_allModsView_doubleClicked(const QModelIndex &index) return; auto modName = index.data(ModRoles::ModNameRole).toString(); - auto mod = modModel->getMod(modName); + auto mod = modStateModel->getMod(modName); - bool hasInvalidDeps = !findInvalidDependencies(modName).empty(); - bool hasBlockingMods = !findBlockingMods(modName).empty(); - bool hasDependentMods = !findDependentMods(modName, true).empty(); - - if(!hasInvalidDeps && mod.isAvailable() && !mod.isSubmod()) + QStringList notInstalledDependencies = this->getModsToInstall(mod.getID()); + QStringList unavailableDependencies = this->findUnavailableMods(notInstalledDependencies); + + if(unavailableDependencies.empty() && mod.isAvailable() && !mod.isSubmod()) { on_installButton_clicked(); return; } - if(!hasInvalidDeps && !hasDependentMods && mod.isUpdateable() && index.column() == ModFields::STATUS_UPDATE) + if(unavailableDependencies.empty() && mod.isUpdateAvailable() && index.column() == ModFields::STATUS_UPDATE) { on_updateButton_clicked(); return; @@ -1138,13 +1104,13 @@ void CModListView::on_allModsView_doubleClicked(const QModelIndex &index) return; } - if(!hasBlockingMods && !hasInvalidDeps && mod.isDisabled()) + if(notInstalledDependencies.empty() && !modStateModel->isModEnabled(modName)) { on_enableButton_clicked(); return; } - if(!hasDependentMods && !mod.isEssential() && mod.isEnabled()) + if(modStateModel->isModEnabled(modName)) { on_disableButton_clicked(); return; diff --git a/launcher/modManager/cmodlistview_moc.h b/launcher/modManager/cmodlistview_moc.h index 595680bf5..c958c2aff 100644 --- a/launcher/modManager/cmodlistview_moc.h +++ b/launcher/modManager/cmodlistview_moc.h @@ -17,21 +17,23 @@ namespace Ui class CModListView; } -class CModManager; +class ModStateController; class CModList; -class CModListModel; +class ModStateItemModel; +class ModStateModel; class CModFilterModel; class CDownloadManager; class QTableWidgetItem; -class CModEntry; +class ModState; class CModListView : public QWidget { Q_OBJECT - std::unique_ptr manager; - CModListModel * modModel; + std::shared_ptr modStateModel; + std::unique_ptr manager; + ModStateItemModel * modModel; CModFilterModel * filterModel; CDownloadManager * dlManager; @@ -42,31 +44,28 @@ class CModListView : public QWidget void checkManagerErrors(); /// replace mod ID's with proper human-readable mod names - QStringList getModNames(QStringList input); + QStringList getModNames(QString queryingMod, QStringList input); + + /// returns list of mods that are needed for install of this mod (potentially including this mod itself) + QStringList getModsToInstall(QString mod); // find mods unknown to mod list (not present in repo and not installed) - QStringList findInvalidDependencies(QString mod); - // find mods that block enabling of this mod: conflicting with this mod or one of required mods - QStringList findBlockingMods(QString modUnderTest); - // find mods that depend on this one - QStringList findDependentMods(QString mod, bool excludeDisabled); + QStringList findUnavailableMods(QStringList candidates); void manualInstallFile(QString filePath); - void downloadFile(QString file, QString url, QString description, qint64 size = 0); - void downloadFile(QString file, QUrl url, QString description, qint64 size = 0); + void downloadFile(QString file, QString url, QString description, qint64 sizeBytes = 0); + void downloadFile(QString file, QUrl url, QString description, qint64 sizeBytes = 0); void installMods(QStringList archives); void installMaps(QStringList maps); void installFiles(QStringList mods); - QString genChangelogText(CModEntry & mod); - QString genModInfoText(CModEntry & mod); + QString genChangelogText(const ModState & mod); + QString genModInfoText(const ModState & mod); void changeEvent(QEvent *event) override; void dragEnterEvent(QDragEnterEvent* event) override; void dropEvent(QDropEvent *event) override; -signals: - void modsChanged(); public: explicit CModListView(QWidget * parent = nullptr); @@ -79,8 +78,6 @@ public: void selectMod(const QModelIndex & index); - const CModList & getModList() const; - // First Launch View interface /// install mod by name diff --git a/launcher/modManager/modstate.cpp b/launcher/modManager/modstate.cpp new file mode 100644 index 000000000..61b17b613 --- /dev/null +++ b/launcher/modManager/modstate.cpp @@ -0,0 +1,231 @@ +/* + * modstate.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 "modstate.h" + +#include "../../lib/modding/ModDescription.h" +#include "../../lib/json/JsonNode.h" +#include "../../lib/texts/CGeneralTextHandler.h" +#include "../../lib/texts/Languages.h" + +ModState::ModState(const ModDescription & impl) + : impl(impl) +{ +} + +QString ModState::getName() const +{ + return QString::fromStdString(impl.getName()); +} + +QString ModState::getType() const +{ + return QString::fromStdString(impl.getValue("modType").String()); +} + +QString ModState::getDescription() const +{ + return QString::fromStdString(impl.getLocalizedValue("description").String()); +} + +QString ModState::getID() const +{ + return QString::fromStdString(impl.getID()); +} + +QString ModState::getParentID() const +{ + return QString::fromStdString(impl.getParentID()); +} + +QString ModState::getTopParentID() const +{ + return QString::fromStdString(impl.getTopParentID()); +} + +template +QStringList stringListStdToQt(const Container & container) +{ + QStringList result; + for (const auto & str : container) + result.push_back(QString::fromStdString(str)); + return result; +} + +QStringList ModState::getDependencies() const +{ + return stringListStdToQt(impl.getDependencies()); +} + +QStringList ModState::getConflicts() const +{ + return stringListStdToQt(impl.getConflicts()); +} + +QStringList ModState::getScreenshots() const +{ + return stringListStdToQt(impl.getLocalizedValue("screenshots").convertTo>()); +} + +QString ModState::getBaseLanguage() const +{ + return QString::fromStdString(impl.getBaseLanguage()); +} + +QStringList ModState::getSupportedLanguages() const +{ + QStringList result; + result.push_back(getBaseLanguage()); + + for (const auto & language : Languages::getLanguageList()) + { + QString languageID = QString::fromStdString(language.identifier); + + if (languageID != getBaseLanguage() && !impl.getValue(language.identifier).isNull()) + result.push_back(languageID); + } + return result; +} + +QMap ModState::getChangelog() const +{ + QMap result; + const JsonNode & changelog = impl.getLocalizedValue("changelog"); + + for (const auto & entry : changelog.Struct()) + { + QString version = QString::fromStdString(entry.first); + QStringList changes = stringListStdToQt(entry.second.convertTo>()); + + result[version] = changes; + } + + return result; +} + +QString ModState::getInstalledVersion() const +{ + return QString::fromStdString(impl.getLocalValue("version").String()); +} + +QString ModState::getRepositoryVersion() const +{ + return QString::fromStdString(impl.getRepositoryValue("version").String()); +} + +QString ModState::getVersion() const +{ + return QString::fromStdString(impl.getValue("version").String()); +} + +double ModState::getDownloadSizeMegabytes() const +{ + return impl.getRepositoryValue("downloadSize").Float(); +} + +size_t ModState::getDownloadSizeBytes() const +{ + return getDownloadSizeMegabytes() * 1024 * 1024; +} + +QString ModState::getDownloadSizeFormatted() const +{ + return QCoreApplication::translate("File size", "%1 MiB").arg(QString::number(getDownloadSizeMegabytes(), 'f', 1)); +} + +QString ModState::getAuthors() const +{ + return QString::fromStdString(impl.getLocalizedValue("author").String()); +} + +QString ModState::getContact() const +{ + return QString::fromStdString(impl.getValue("contact").String()); +} + +QString ModState::getLicenseUrl() const +{ + return QString::fromStdString(impl.getValue("licenseURL").String()); +} + +QString ModState::getLicenseName() const +{ + return QString::fromStdString(impl.getValue("licenseName").String()); +} + +QString ModState::getDownloadUrl() const +{ + return QString::fromStdString(impl.getRepositoryValue("download").String()); +} + +QPair ModState::getCompatibleVersionRange() const +{ + const JsonNode & compatibility = impl.getValue("compatibility"); + + if (compatibility.isNull()) + return {}; + + auto min = QString::fromStdString(compatibility["min"].String()); + auto max = QString::fromStdString(compatibility["max"].String()); + return { min, max}; +} + +bool ModState::isSubmod() const +{ + return !getParentID().isEmpty(); +} + +bool ModState::isCompatibility() const +{ + return impl.isCompatibility(); +} + +bool ModState::isTranslation() const +{ + return impl.isTranslation(); +} + +bool ModState::isVisible() const +{ + return !isHidden(); +} + +bool ModState::isHidden() const +{ + if (isTranslation() && !isInstalled()) + return impl.getBaseLanguage() != CGeneralTextHandler::getPreferredLanguage(); + + return isCompatibility() || getID() == "vcmi" || getID() == "core"; +} + +bool ModState::isAvailable() const +{ + return !isInstalled(); +} + +bool ModState::isInstalled() const +{ + return impl.isInstalled(); +} + +bool ModState::isUpdateAvailable() const +{ + return impl.isUpdateAvailable(); +} + +bool ModState::isCompatible() const +{ + return impl.isCompatible(); +} + +bool ModState::isKeptDisabled() const +{ + return impl.keepDisabled(); +} diff --git a/launcher/modManager/modstate.h b/launcher/modManager/modstate.h new file mode 100644 index 000000000..fb438f078 --- /dev/null +++ b/launcher/modManager/modstate.h @@ -0,0 +1,69 @@ +/* + * modstate.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 + +VCMI_LIB_NAMESPACE_BEGIN +class ModDescription; +VCMI_LIB_NAMESPACE_END + +/// Class that represent current state of mod in Launcher +/// Provides Qt-based interface to library class ModDescription +class ModState +{ + const ModDescription & impl; + +public: + explicit ModState(const ModDescription & impl); + + QString getName() const; + QString getType() const; + QString getDescription() const; + + QString getID() const; + QString getParentID() const; + QString getTopParentID() const; + + QStringList getDependencies() const; + QStringList getConflicts() const; + QStringList getScreenshots() const; + + QString getBaseLanguage() const; + QStringList getSupportedLanguages() const; + + QMap getChangelog() const; + + QString getInstalledVersion() const; + QString getRepositoryVersion() const; + QString getVersion() const; + + double getDownloadSizeMegabytes() const; + size_t getDownloadSizeBytes() const; + QString getDownloadSizeFormatted() const; + QString getAuthors() const; + QString getContact() const; + QString getLicenseUrl() const; + QString getLicenseName() const; + + QString getDownloadUrl() const; + + QPair getCompatibleVersionRange() const; + + bool isSubmod() const; + bool isCompatibility() const; + bool isTranslation() const; + + bool isVisible() const; + bool isHidden() const; + bool isAvailable() const; + bool isInstalled() const; + bool isUpdateAvailable() const; + bool isCompatible() const; + bool isKeptDisabled() const; +}; diff --git a/launcher/modManager/cmodmanager.cpp b/launcher/modManager/modstatecontroller.cpp similarity index 52% rename from launcher/modManager/cmodmanager.cpp rename to launcher/modManager/modstatecontroller.cpp index 4012519d1..fa243f0b8 100644 --- a/launcher/modManager/cmodmanager.cpp +++ b/launcher/modManager/modstatecontroller.cpp @@ -1,5 +1,5 @@ /* - * cmodmanager.cpp, part of VCMI engine + * modstatecontroller.cpp, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * @@ -8,17 +8,20 @@ * */ #include "StdInc.h" -#include "cmodmanager.h" +#include "modstatecontroller.h" + +#include "modstatemodel.h" #include "../../lib/VCMIDirs.h" #include "../../lib/filesystem/Filesystem.h" #include "../../lib/filesystem/CZipLoader.h" #include "../../lib/modding/CModHandler.h" -#include "../../lib/modding/CModInfo.h" #include "../../lib/modding/IdentifierStorage.h" +#include "../../lib/json/JsonNode.h" +#include "../../lib/texts/CGeneralTextHandler.h" -#include "../jsonutils.h" -#include "../launcherdirs.h" +#include "../vcmiqt/jsonutils.h" +#include "../vcmiqt/launcherdirs.h" #include @@ -40,7 +43,7 @@ QString detectModArchive(QString path, QString modName, std::vector for(int folderLevel : {0, 1}) //search in subfolder if there is no mod.json in the root { - for(auto file : filesToExtract) + for(const auto & file : filesToExtract) { QString filename = QString::fromUtf8(file.c_str()); modDirName = filename.section('/', 0, folderLevel); @@ -54,7 +57,7 @@ QString detectModArchive(QString path, QString modName, std::vector logGlobal->error("Failed to detect mod path in archive!"); logGlobal->debug("List of file in archive:"); - for(auto file : filesToExtract) + for(const auto & file : filesToExtract) logGlobal->debug("%s", file.c_str()); return ""; @@ -62,105 +65,60 @@ QString detectModArchive(QString path, QString modName, std::vector } -CModManager::CModManager(CModList * modList) +ModStateController::ModStateController(std::shared_ptr modList) : modList(modList) { - loadMods(); - loadModSettings(); } -QString CModManager::settingsPath() +ModStateController::~ModStateController() = default; + +void ModStateController::appendRepositories(const JsonNode & repomap) { - return pathToQString(VCMIDirs::get().userConfigPath() / "modSettings.json"); + modList->appendRepositories(repomap); } -void CModManager::loadModSettings() -{ - modSettings = JsonUtils::JsonFromFile(settingsPath()).toMap(); - modList->setModSettings(modSettings["activeMods"]); -} - -void CModManager::resetRepositories() -{ - modList->resetRepositories(); -} - -void CModManager::loadRepositories(QVector repomap) -{ - for (auto const & entry : repomap) - modList->addRepository(entry); - modList->reloadRepositories(); -} - -void CModManager::loadMods() -{ - CModHandler handler; - handler.loadMods(); - auto installedMods = handler.getAllMods(); - localMods.clear(); - - for(auto modname : installedMods) - { - auto resID = CModInfo::getModFile(modname); - if(CResourceHandler::get()->existsResource(resID)) - { - //calculate mod size - qint64 total = 0; - ResourcePath resDir(CModInfo::getModDir(modname), EResType::DIRECTORY); - if(CResourceHandler::get()->existsResource(resDir)) - { - for(QDirIterator iter(QString::fromStdString(CResourceHandler::get()->getResourceName(resDir)->string()), QDirIterator::Subdirectories); iter.hasNext(); iter.next()) - total += iter.fileInfo().size(); - } - - boost::filesystem::path name = *CResourceHandler::get()->getResourceName(resID); - auto mod = JsonUtils::JsonFromFile(pathToQString(name)); - auto json = JsonUtils::toJson(mod); - json["localSizeBytes"].Float() = total; - if(!name.is_absolute()) - json["storedLocally"].Bool() = true; - - mod = JsonUtils::toVariant(json); - localMods.insert(QString::fromUtf8(modname.c_str()).toLower(), mod); - } - } - modList->setLocalModList(localMods); -} - -bool CModManager::addError(QString modname, QString message) +bool ModStateController::addError(QString modname, QString message) { recentErrors.push_back(QString("%1: %2").arg(modname).arg(message)); return false; } -QStringList CModManager::getErrors() +QStringList ModStateController::getErrors() { QStringList ret = recentErrors; recentErrors.clear(); return ret; } -bool CModManager::installMod(QString modname, QString archivePath) +bool ModStateController::installMod(QString modname, QString archivePath) { return canInstallMod(modname) && doInstallMod(modname, archivePath); } -bool CModManager::uninstallMod(QString modname) +bool ModStateController::uninstallMod(QString modname) { return canUninstallMod(modname) && doUninstallMod(modname); } -bool CModManager::enableMod(QString modname) +bool ModStateController::enableMods(QStringList modlist) { - return canEnableMod(modname) && doEnableMod(modname, true); + for (const auto & modname : modlist) + if (!canEnableMod(modname)) + return false; + + modList->doEnableMods(modlist); + return true; } -bool CModManager::disableMod(QString modname) +bool ModStateController::disableMod(QString modname) { - return canDisableMod(modname) && doEnableMod(modname, false); + if (!canDisableMod(modname)) + return false; + modList->doDisableMod(modname); + return true; } -bool CModManager::canInstallMod(QString modname) +bool ModStateController::canInstallMod(QString modname) { auto mod = modList->getMod(modname); @@ -172,7 +130,7 @@ bool CModManager::canInstallMod(QString modname) return true; } -bool CModManager::canUninstallMod(QString modname) +bool ModStateController::canUninstallMod(QString modname) { auto mod = modList->getMod(modname); @@ -185,11 +143,11 @@ bool CModManager::canUninstallMod(QString modname) return true; } -bool CModManager::canEnableMod(QString modname) +bool ModStateController::canEnableMod(QString modname) { auto mod = modList->getMod(modname); - if(mod.isEnabled()) + if(modList->isModEnabled(modname)) return addError(modname, tr("Mod is already enabled")); if(!mod.isInstalled()) @@ -199,88 +157,32 @@ bool CModManager::canEnableMod(QString modname) if(!mod.isCompatible()) return addError(modname, tr("Mod is not compatible, please update VCMI and checkout latest mod revisions")); - for(auto modEntry : mod.getDependencies()) + if (mod.isTranslation() && CGeneralTextHandler::getPreferredLanguage() != mod.getBaseLanguage().toStdString()) + return addError(modname, tr("Can not enable translation mod for a different language!")); + + for(const auto & modEntry : mod.getDependencies()) { - if(!modList->hasMod(modEntry)) // required mod is not available + if(!modList->isModExists(modEntry)) // required mod is not available return addError(modname, tr("Required mod %1 is missing").arg(modEntry)); - - CModEntry modData = modList->getMod(modEntry); - - if(!modData.isCompatibilityPatch() && !modData.isEnabled()) - return addError(modname, tr("Required mod %1 is not enabled").arg(modEntry)); } - for(QString modEntry : modList->getModList()) - { - auto mod = modList->getMod(modEntry); - - // "reverse conflict" - enabled mod has this one as conflict - if(mod.isEnabled() && mod.getConflicts().contains(modname)) - return addError(modname, tr("This mod conflicts with %1").arg(modEntry)); - } - - for(auto modEntry : mod.getConflicts()) - { - // check if conflicting mod installed and enabled - if(modList->hasMod(modEntry) && modList->getMod(modEntry).isEnabled()) - return addError(modname, tr("This mod conflicts with %1").arg(modEntry)); - } return true; } -bool CModManager::canDisableMod(QString modname) +bool ModStateController::canDisableMod(QString modname) { auto mod = modList->getMod(modname); - if(mod.isDisabled()) + if(!modList->isModEnabled(modname)) return addError(modname, tr("Mod is already disabled")); if(!mod.isInstalled()) return addError(modname, tr("Mod must be installed first")); - for(QString modEntry : modList->getModList()) - { - auto current = modList->getMod(modEntry); - - if(current.getDependencies().contains(modname) && current.isEnabled()) - return addError(modname, tr("This mod is needed to run %1").arg(modEntry)); - } return true; } -static QVariant writeValue(QString path, QVariantMap input, QVariant value) -{ - if(path.size() > 1) - { - - QString entryName = path.section('/', 0, 1); - QString remainder = "/" + path.section('/', 2, -1); - - entryName.remove(0, 1); - input.insert(entryName, writeValue(remainder, input.value(entryName).toMap(), value)); - return input; - } - else - { - return value; - } -} - -bool CModManager::doEnableMod(QString mod, bool on) -{ - QString path = mod; - path = "/activeMods/" + path.replace(".", "/mods/") + "/active"; - - modSettings = writeValue(path, modSettings, QVariant(on)).toMap(); - modList->setModSettings(modSettings["activeMods"]); - modList->modChanged(mod); - - JsonUtils::JsonToFile(settingsPath(), modSettings); - - return true; -} - -bool CModManager::doInstallMod(QString modname, QString archivePath) +bool ModStateController::doInstallMod(QString modname, QString archivePath) { const auto destDir = CLauncherDirs::modsPath() + QChar{'/'}; @@ -301,7 +203,7 @@ bool CModManager::doInstallMod(QString modname, QString archivePath) { const auto destDirFsPath = qstringToPath(destDir); ZipArchive archive(qstringToPath(archivePath)); - for (auto const & file : filesToExtract) + for(const auto & file : filesToExtract) { if (!archive.extract(destDirFsPath, file)) return false; @@ -333,14 +235,12 @@ bool CModManager::doInstallMod(QString modname, QString archivePath) if(upperLevel != modDirName) removeModDir(destDir + upperLevel); - CResourceHandler::get("initial")->updateFilteredFiles([](const std::string &) { return true; }); - loadMods(); - modList->reloadRepositories(); + modList->reloadLocalState(); return true; } -bool CModManager::doUninstallMod(QString modname) +bool ModStateController::doUninstallMod(QString modname) { ResourcePath resID(std::string("Mods/") + modname.toStdString(), EResType::DIRECTORY); // Get location of the mod, in case-insensitive way @@ -353,14 +253,12 @@ bool CModManager::doUninstallMod(QString modname) if(!removeModDir(modDir)) return addError(modname, tr("Mod is located in protected directory, please remove it manually:\n") + modFullDir.absolutePath()); - CResourceHandler::get("initial")->updateFilteredFiles([](const std::string &){ return true; }); - loadMods(); - modList->reloadRepositories(); + modList->reloadLocalState(); return true; } -bool CModManager::removeModDir(QString path) +bool ModStateController::removeModDir(QString path) { // issues 2673 and 2680 its why you do not recursively remove without sanity check QDir checkDir(path); diff --git a/launcher/modManager/cmodmanager.h b/launcher/modManager/modstatecontroller.h similarity index 69% rename from launcher/modManager/cmodmanager.h rename to launcher/modManager/modstatecontroller.h index 987d1a580..57cfc53e6 100644 --- a/launcher/modManager/cmodmanager.h +++ b/launcher/modManager/modstatecontroller.h @@ -1,5 +1,5 @@ /* - * cmodmanager.h, part of VCMI engine + * modstatecontroller.h, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * @@ -9,22 +9,24 @@ */ #pragma once -#include "cmodlist.h" +#include -class CModManager : public QObject +VCMI_LIB_NAMESPACE_BEGIN +class JsonNode; +VCMI_LIB_NAMESPACE_END + +class ModStateModel; + +class ModStateController : public QObject, public boost::noncopyable { Q_OBJECT - CModList * modList; - - QString settingsPath(); + std::shared_ptr modList; // check-free version of public method - bool doEnableMod(QString mod, bool on); bool doInstallMod(QString mod, QString archivePath); bool doUninstallMod(QString mod); - QVariantMap modSettings; QVariantMap localMods; QStringList recentErrors; @@ -32,12 +34,10 @@ class CModManager : public QObject bool removeModDir(QString mod); public: - CModManager(CModList * modList); + ModStateController(std::shared_ptr modList); + ~ModStateController(); - void resetRepositories(); - void loadRepositories(QVector repomap); - void loadModSettings(); - void loadMods(); + void appendRepositories(const JsonNode & repositoriesList); QStringList getErrors(); @@ -46,7 +46,7 @@ public: /// installs mod from zip archive located at archivePath bool installMod(QString mod, QString archivePath); bool uninstallMod(QString mod); - bool enableMod(QString mod); + bool enableMods(QStringList mod); bool disableMod(QString mod); bool canInstallMod(QString mod); diff --git a/launcher/modManager/cmodlistmodel_moc.cpp b/launcher/modManager/modstateitemmodel_moc.cpp similarity index 54% rename from launcher/modManager/cmodlistmodel_moc.cpp rename to launcher/modManager/modstateitemmodel_moc.cpp index 258ffbd83..c81eab38c 100644 --- a/launcher/modManager/cmodlistmodel_moc.cpp +++ b/launcher/modManager/modstateitemmodel_moc.cpp @@ -1,5 +1,5 @@ /* - * cmodlistmodel_moc.cpp, part of VCMI engine + * modstateview_moc.cpp, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * @@ -8,25 +8,19 @@ * */ #include "StdInc.h" -#include "cmodlistmodel_moc.h" +#include "modstateitemmodel_moc.h" + +#include "modstatemodel.h" #include -namespace ModStatus -{ -static const QString iconDelete = ":/icons/mod-delete.png"; -static const QString iconDisabled = ":/icons/mod-disabled.png"; -static const QString iconDownload = ":/icons/mod-download.png"; -static const QString iconEnabled = ":/icons/mod-enabled.png"; -static const QString iconUpdate = ":/icons/mod-update.png"; -} - -CModListModel::CModListModel(QObject * parent) +ModStateItemModel::ModStateItemModel(std::shared_ptr model, QObject * parent) : QAbstractItemModel(parent) + , model(model) { } -QString CModListModel::modIndexToName(const QModelIndex & index) const +QString ModStateItemModel::modIndexToName(const QModelIndex & index) const { if(index.isValid()) { @@ -36,7 +30,7 @@ QString CModListModel::modIndexToName(const QModelIndex & index) const } -QString CModListModel::modTypeName(QString modTypeID) const +QString ModStateItemModel::modTypeName(QString modTypeID) const { static const QMap modTypes = { {"Translation", tr("Translation")}, @@ -69,28 +63,28 @@ QString CModListModel::modTypeName(QString modTypeID) const return tr("Other"); } -QVariant CModListModel::getValue(const CModEntry & mod, int field) const +QVariant ModStateItemModel::getValue(const ModState & mod, int field) const { switch(field) { case ModFields::STATUS_ENABLED: - return mod.getModStatus() & (ModStatus::ENABLED | ModStatus::INSTALLED); + return model->isModEnabled(mod.getID()); case ModFields::STATUS_UPDATE: - return mod.getModStatus() & (ModStatus::UPDATEABLE | ModStatus::INSTALLED); + return model->isModUpdateAvailable(mod.getID()); case ModFields::NAME: - return mod.getValue("name"); + return mod.getName(); case ModFields::TYPE: - return modTypeName(mod.getValue("modType").toString()); + return modTypeName(mod.getType()); default: return QVariant(); } } -QVariant CModListModel::getText(const CModEntry & mod, int field) const +QVariant ModStateItemModel::getText(const ModState & mod, int field) const { switch(field) { @@ -102,31 +96,58 @@ QVariant CModListModel::getText(const CModEntry & mod, int field) const } } -QVariant CModListModel::getIcon(const CModEntry & mod, int field) const +QVariant ModStateItemModel::getIcon(const ModState & mod, int field) const { - if(field == ModFields::STATUS_ENABLED && mod.isEnabled()) - return QIcon(ModStatus::iconEnabled); - if(field == ModFields::STATUS_ENABLED && mod.isDisabled()) - return QIcon(ModStatus::iconDisabled); + static const QString iconDisabled = ":/icons/mod-disabled.png"; + static const QString iconDisabledSubmod = ":/icons/submod-disabled.png"; + static const QString iconDownload = ":/icons/mod-download.png"; + static const QString iconEnabled = ":/icons/mod-enabled.png"; + static const QString iconEnabledSubmod = ":/icons/submod-enabled.png"; + static const QString iconUpdate = ":/icons/mod-update.png"; - if(field == ModFields::STATUS_UPDATE && mod.isUpdateable()) - return QIcon(ModStatus::iconUpdate); - if(field == ModFields::STATUS_UPDATE && !mod.isInstalled()) - return QIcon(ModStatus::iconDownload); + if (field == ModFields::STATUS_ENABLED) + { + if (!model->isModInstalled(mod.getID())) + return QVariant(); + + if(mod.isSubmod() && !model->isModEnabled(mod.getTopParentID())) + { + QString topParentID = mod.getTopParentID(); + QString settingID = mod.getID().section('.', 1); + + if (model->isModSettingEnabled(topParentID, settingID)) + return QIcon(iconEnabledSubmod); + else + return QIcon(iconDisabledSubmod); + } + + if (model->isModEnabled(mod.getID())) + return QIcon(iconEnabled); + else + return QIcon(iconDisabled); + } + + if(field == ModFields::STATUS_UPDATE) + { + if (model->isModUpdateAvailable(mod.getID())) + return QIcon(iconUpdate); + if (!model->isModInstalled(mod.getID())) + return QIcon(iconDownload); + } return QVariant(); } -QVariant CModListModel::getTextAlign(int field) const +QVariant ModStateItemModel::getTextAlign(int field) const { return QVariant(Qt::AlignLeft | Qt::AlignVCenter); } -QVariant CModListModel::data(const QModelIndex & index, int role) const +QVariant ModStateItemModel::data(const QModelIndex & index, int role) const { if(index.isValid()) { - auto mod = getMod(modIndexToName(index)); + auto mod = model->getMod(modIndexToName(index)); switch(role) { @@ -139,32 +160,32 @@ QVariant CModListModel::data(const QModelIndex & index, int role) const case ModRoles::ValueRole: return getValue(mod, index.column()); case ModRoles::ModNameRole: - return mod.getName(); + return mod.getID(); } } return QVariant(); } -int CModListModel::rowCount(const QModelIndex & index) const +int ModStateItemModel::rowCount(const QModelIndex & index) const { if(index.isValid()) return modIndex[modIndexToName(index)].size(); return modIndex[""].size(); } -int CModListModel::columnCount(const QModelIndex &) const +int ModStateItemModel::columnCount(const QModelIndex &) const { return ModFields::COUNT; } -Qt::ItemFlags CModListModel::flags(const QModelIndex &) const +Qt::ItemFlags ModStateItemModel::flags(const QModelIndex &) const { return Qt::ItemIsSelectable | Qt::ItemIsEnabled; } -QVariant CModListModel::headerData(int section, Qt::Orientation orientation, int role) const +QVariant ModStateItemModel::headerData(int section, Qt::Orientation orientation, int role) const { - static const QString header[ModFields::COUNT] = + static const std::array header = { QT_TRANSLATE_NOOP("ModFields", "Name"), QT_TRANSLATE_NOOP("ModFields", ""), // status icon @@ -173,24 +194,17 @@ QVariant CModListModel::headerData(int section, Qt::Orientation orientation, int }; if(role == Qt::DisplayRole && orientation == Qt::Horizontal) - return QCoreApplication::translate("ModFields", header[section].toStdString().c_str()); + return QCoreApplication::translate("ModFields", header[section]); return QVariant(); } -void CModListModel::reloadRepositories() +void ModStateItemModel::reloadRepositories() { beginResetModel(); endResetModel(); } -void CModListModel::resetRepositories() -{ - beginResetModel(); - CModList::resetRepositories(); - endResetModel(); -} - -void CModListModel::modChanged(QString modID) +void ModStateItemModel::modChanged(QString modID) { int index = modNameToID.indexOf(modID); QModelIndex parent = this->parent(createIndex(0, 0, index)); @@ -198,9 +212,9 @@ void CModListModel::modChanged(QString modID) emit dataChanged(createIndex(row, 0, index), createIndex(row, 4, index)); } -void CModListModel::endResetModel() +void ModStateItemModel::endResetModel() { - modNameToID = getModList(); + modNameToID = model->getAllMods(); modIndex.clear(); for(const QString & str : modNameToID) { @@ -216,7 +230,7 @@ void CModListModel::endResetModel() QAbstractItemModel::endResetModel(); } -QModelIndex CModListModel::index(int row, int column, const QModelIndex & parent) const +QModelIndex ModStateItemModel::index(int row, int column, const QModelIndex & parent) const { if(parent.isValid()) { @@ -231,7 +245,7 @@ QModelIndex CModListModel::index(int row, int column, const QModelIndex & parent return QModelIndex(); } -QModelIndex CModListModel::parent(const QModelIndex & child) const +QModelIndex ModStateItemModel::parent(const QModelIndex & child) const { QString modID = modNameToID[child.internalId()]; for(auto entry = modIndex.begin(); entry != modIndex.end(); entry++) // because using range-for entry type is QMap::value_type oO @@ -244,26 +258,46 @@ QModelIndex CModListModel::parent(const QModelIndex & child) const return QModelIndex(); } -void CModFilterModel::setTypeFilter(int filteredType, int filterMask) +void CModFilterModel::setTypeFilter(ModFilterMask newFilterMask) { - this->filterMask = filterMask; - this->filteredType = filteredType; + filterMask = newFilterMask; invalidateFilter(); } +bool CModFilterModel::filterMatchesCategory(const QModelIndex & source) const +{ + QString modID =source.data(ModRoles::ModNameRole).toString(); + ModState mod = base->model->getMod(modID); + + switch (filterMask) + { + case ModFilterMask::ALL: + return true; + case ModFilterMask::AVAILABLE: + return !mod.isInstalled(); + case ModFilterMask::INSTALLED: + return mod.isInstalled(); + case ModFilterMask::UPDATEABLE: + return mod.isUpdateAvailable(); + case ModFilterMask::ENABLED: + return mod.isInstalled() && base->model->isModEnabled(modID); + case ModFilterMask::DISABLED: + return mod.isInstalled() && !base->model->isModEnabled(modID); + } + assert(0); + return false; +} + bool CModFilterModel::filterMatchesThis(const QModelIndex & source) const { - CModEntry mod = base->getMod(source.data(ModRoles::ModNameRole).toString()); - return (mod.getModStatus() & filterMask) == filteredType && - QSortFilterProxyModel::filterAcceptsRow(source.row(), source.parent()); + return filterMatchesCategory(source) && QSortFilterProxyModel::filterAcceptsRow(source.row(), source.parent()); } bool CModFilterModel::filterAcceptsRow(int source_row, const QModelIndex & source_parent) const { QModelIndex index = base->index(source_row, 0, source_parent); - - CModEntry mod = base->getMod(index.data(ModRoles::ModNameRole).toString()); - if (!mod.isVisible()) + QString modID = index.data(ModRoles::ModNameRole).toString(); + if (base->model->getMod(modID).isHidden()) return false; if(filterMatchesThis(index)) @@ -273,7 +307,7 @@ bool CModFilterModel::filterAcceptsRow(int source_row, const QModelIndex & sourc for(size_t i = 0; i < base->rowCount(index); i++) { - if(filterMatchesThis(base->index((int)i, 0, index))) + if(filterMatchesThis(base->index(i, 0, index))) return true; } @@ -287,8 +321,8 @@ bool CModFilterModel::filterAcceptsRow(int source_row, const QModelIndex & sourc return false; } -CModFilterModel::CModFilterModel(CModListModel * model, QObject * parent) - : QSortFilterProxyModel(parent), base(model), filteredType(ModStatus::MASK_NONE), filterMask(ModStatus::MASK_NONE) +CModFilterModel::CModFilterModel(ModStateItemModel * model, QObject * parent) + : QSortFilterProxyModel(parent), base(model), filterMask(ModFilterMask::ALL) { setSourceModel(model); setSortRole(ModRoles::ValueRole); diff --git a/launcher/modManager/cmodlistmodel_moc.h b/launcher/modManager/modstateitemmodel_moc.h similarity index 58% rename from launcher/modManager/cmodlistmodel_moc.h rename to launcher/modManager/modstateitemmodel_moc.h index 783322029..22f1da0f4 100644 --- a/launcher/modManager/cmodlistmodel_moc.h +++ b/launcher/modManager/modstateitemmodel_moc.h @@ -1,5 +1,5 @@ /* - * cmodlistmodel_moc.h, part of VCMI engine + * modstateview_moc.h, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * @@ -9,11 +9,12 @@ */ #pragma once -#include "cmodlist.h" - #include #include +class ModStateModel; +class ModState; + namespace ModFields { enum EModFields @@ -26,6 +27,16 @@ enum EModFields }; } +enum class ModFilterMask : uint8_t +{ + ALL, + AVAILABLE, + INSTALLED, + UPDATEABLE, + ENABLED, + DISABLED +}; + namespace ModRoles { enum EModRoles @@ -35,14 +46,17 @@ enum EModRoles }; } -class CModListModel : public QAbstractItemModel, public CModList +class ModStateItemModel final : public QAbstractItemModel { + friend class CModFilterModel; Q_OBJECT - QVector modNameToID; + std::shared_ptr model; + + QStringList modNameToID; // contains mapping mod -> numbered list of submods // mods that have no parent located under "" key (empty string) - QMap> modIndex; + QMap modIndex; void endResetModel(); @@ -50,17 +64,16 @@ class CModListModel : public QAbstractItemModel, public CModList QString modTypeName(QString modTypeID) const; QVariant getTextAlign(int field) const; - QVariant getValue(const CModEntry & mod, int field) const; - QVariant getText(const CModEntry & mod, int field) const; - QVariant getIcon(const CModEntry & mod, int field) const; + QVariant getValue(const ModState & mod, int field) const; + QVariant getText(const ModState & mod, int field) const; + QVariant getIcon(const ModState & mod, int field) const; public: - explicit CModListModel(QObject * parent = nullptr); + explicit ModStateItemModel(std::shared_ptr model, QObject * parent); /// CModListContainer overrides - void resetRepositories() override; - void reloadRepositories() override; - void modChanged(QString modID) override; + void reloadRepositories(); + void modChanged(QString modID); QVariant data(const QModelIndex & index, int role) const override; QVariant headerData(int section, Qt::Orientation orientation, int role) const override; @@ -68,29 +81,24 @@ public: int rowCount(const QModelIndex & parent) const override; int columnCount(const QModelIndex & parent) const override; - QModelIndex index(int row, int column, const QModelIndex & parent = QModelIndex()) const override; + QModelIndex index(int row, int column, const QModelIndex & parent) const override; QModelIndex parent(const QModelIndex & child) const override; Qt::ItemFlags flags(const QModelIndex & index) const override; - -signals: - -public slots: - }; -class CModFilterModel : public QSortFilterProxyModel +class CModFilterModel final : public QSortFilterProxyModel { - CModListModel * base; - int filteredType; - int filterMask; + ModStateItemModel * base; + ModFilterMask filterMask; bool filterMatchesThis(const QModelIndex & source) const; + bool filterMatchesCategory(const QModelIndex & source) const; bool filterAcceptsRow(int source_row, const QModelIndex & source_parent) const override; public: - void setTypeFilter(int filteredType, int filterMask); + void setTypeFilter(ModFilterMask filterMask); - CModFilterModel(CModListModel * model, QObject * parent = nullptr); + CModFilterModel(ModStateItemModel * model, QObject * parent = nullptr); }; diff --git a/launcher/modManager/modstatemodel.cpp b/launcher/modManager/modstatemodel.cpp new file mode 100644 index 000000000..be4bc5627 --- /dev/null +++ b/launcher/modManager/modstatemodel.cpp @@ -0,0 +1,130 @@ +/* + * modstatemodel.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 "modstatemodel.h" + +#include "../../lib/filesystem/Filesystem.h" +#include "../../lib/json/JsonUtils.h" +#include "../../lib/modding/ModManager.h" + +ModStateModel::ModStateModel() + : repositoryData(std::make_unique()) + , modManager(std::make_unique()) +{ +} + +ModStateModel::~ModStateModel() = default; + +void ModStateModel::appendRepositories(const JsonNode & repositoriesList) +{ + JsonUtils::mergeCopy(*repositoryData, repositoriesList); + + modManager = std::make_unique(*repositoryData); +} + +void ModStateModel::reloadLocalState() +{ + CResourceHandler::get("initial")->updateFilteredFiles([](const std::string &){ return true; }); + modManager = std::make_unique(*repositoryData); +} + +const JsonNode & ModStateModel::getRepositoryData() const +{ + return *repositoryData; +} + +ModState ModStateModel::getMod(QString modName) const +{ + assert(modName.toLower() == modName); + return ModState(modManager->getModDescription(modName.toStdString())); +} + +template +QStringList stringListStdToQt(const Container & container) +{ + QStringList result; + for (const auto & str : container) + result.push_back(QString::fromStdString(str)); + return result; +} + +QStringList ModStateModel::getAllMods() const +{ + return stringListStdToQt(modManager->getAllMods()); +} + +bool ModStateModel::isModExists(QString modName) const +{ + return vstd::contains(modManager->getAllMods(), modName.toStdString()); +} + +bool ModStateModel::isModInstalled(QString modName) const +{ + return getMod(modName).isInstalled(); +} + +bool ModStateModel::isModSettingEnabled(QString rootModName, QString modSettingName) const +{ + return modManager->isModSettingActive(rootModName.toStdString(), modSettingName.toStdString()); +} + +bool ModStateModel::isModEnabled(QString modName) const +{ + return modManager->isModActive(modName.toStdString()); +} + +bool ModStateModel::isModUpdateAvailable(QString modName) const +{ + return getMod(modName).isUpdateAvailable(); +} + +bool ModStateModel::isModVisible(QString modName) const +{ + return getMod(modName).isVisible(); +} + +QString ModStateModel::getInstalledModSizeFormatted(QString modName) const +{ + return QCoreApplication::translate("File size", "%1 MiB").arg(QString::number(getInstalledModSizeMegabytes(modName), 'f', 1)); +} + +double ModStateModel::getInstalledModSizeMegabytes(QString modName) const +{ + return modManager->getInstalledModSizeMegabytes(modName.toStdString()); +} + +void ModStateModel::doEnableMods(QStringList modList) +{ + std::vector stdList; + + for (const auto & entry : modList) + stdList.push_back(entry.toStdString()); + + modManager->tryEnableMods(stdList); +} + +void ModStateModel::doDisableMod(QString modname) +{ + modManager->tryDisableMod(modname.toStdString()); +} + +bool ModStateModel::isSubmod(QString modname) +{ + return modname.contains('.'); +} + +QString ModStateModel::getTopParent(QString modname) const +{ + QStringList components = modname.split('.'); + if (components.size() > 1) + return components.front(); + else + return ""; +} diff --git a/launcher/modManager/modstatemodel.h b/launcher/modManager/modstatemodel.h new file mode 100644 index 000000000..08bf1935f --- /dev/null +++ b/launcher/modManager/modstatemodel.h @@ -0,0 +1,52 @@ +/* + * modstatemodel.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 "modstate.h" + +VCMI_LIB_NAMESPACE_BEGIN +class JsonNode; +class ModManager; +VCMI_LIB_NAMESPACE_END + +/// Class that represent current state of available mods +/// Provides Qt-based interface to library class ModManager +class ModStateModel +{ + std::unique_ptr repositoryData; + std::unique_ptr modManager; + +public: + ModStateModel(); + ~ModStateModel(); + + void appendRepositories(const JsonNode & repositoriesList); + void reloadLocalState(); + const JsonNode & getRepositoryData() const; + + ModState getMod(QString modName) const; + QStringList getAllMods() const; + + QString getInstalledModSizeFormatted(QString modName) const; + double getInstalledModSizeMegabytes(QString modName) const; + + bool isModExists(QString modName) const; + bool isModInstalled(QString modName) const; + bool isModEnabled(QString modName) const; + bool isModSettingEnabled(QString rootModName, QString modSettingName) const; + bool isModUpdateAvailable(QString modName) const; + bool isModVisible(QString modName) const; + + void doEnableMods(QStringList modList); + void doDisableMod(QString modname); + + bool isSubmod(QString modname); + QString getTopParent(QString modname) const; +}; diff --git a/launcher/prepare.cpp b/launcher/prepare.cpp index c57f703eb..7e158d4ac 100644 --- a/launcher/prepare.cpp +++ b/launcher/prepare.cpp @@ -9,7 +9,7 @@ */ #include "StdInc.h" #include "prepare.h" -#include "launcherdirs.h" +#include "../vcmiqt/launcherdirs.h" #include #include diff --git a/launcher/resources.qrc b/launcher/resources.qrc index 139401504..5be9d4f1b 100644 --- a/launcher/resources.qrc +++ b/launcher/resources.qrc @@ -7,8 +7,10 @@ icons/menu-settings.png icons/mod-delete.png icons/mod-disabled.png + icons/submod-disabled.png icons/mod-download.png icons/mod-enabled.png + icons/submod-enabled.png icons/mod-update.png diff --git a/launcher/settingsView/csettingsview_moc.cpp b/launcher/settingsView/csettingsview_moc.cpp index 8cb8c32a0..c6c136fbb 100644 --- a/launcher/settingsView/csettingsview_moc.cpp +++ b/launcher/settingsView/csettingsview_moc.cpp @@ -15,7 +15,7 @@ #include "../modManager/cmodlistview_moc.h" #include "../helper.h" -#include "../jsonutils.h" +#include "../vcmiqt/jsonutils.h" #include "../languages.h" #include @@ -127,7 +127,12 @@ void CSettingsView::loadSettings() #endif fillValidScalingRange(); - ui->spinBoxInterfaceScaling->setValue(settings["video"]["resolution"]["scaling"].Float()); + ui->buttonScalingAuto->setChecked(settings["video"]["resolution"]["scaling"].Integer() == 0); + if (settings["video"]["resolution"]["scaling"].Integer() == 0) + ui->spinBoxInterfaceScaling->setValue(100); + else + ui->spinBoxInterfaceScaling->setValue(settings["video"]["resolution"]["scaling"].Float()); + ui->spinBoxFramerateLimit->setValue(settings["video"]["targetfps"].Float()); ui->spinBoxFramerateLimit->setDisabled(settings["video"]["vsync"].Bool()); ui->sliderReservedArea->setValue(std::round(settings["video"]["reservedWidth"].Float() * 100)); @@ -174,6 +179,7 @@ void CSettingsView::loadSettings() ui->sliderControllerSticksAcceleration->setValue(settings["input"]["controllerAxisScale"].Float() * 100); ui->lineEditGameLobbyHost->setText(QString::fromStdString(settings["lobby"]["hostname"].String())); ui->spinBoxNetworkPortLobby->setValue(settings["lobby"]["port"].Integer()); + ui->buttonVSync->setChecked(settings["video"]["vsync"].Bool()); if (settings["video"]["fontsType"].String() == "auto") ui->buttonFontAuto->setChecked(true); @@ -182,13 +188,19 @@ void CSettingsView::loadSettings() else ui->buttonFontScalable->setChecked(true); + if (settings["mods"]["validation"].String() == "off") + ui->buttonValidationOff->setChecked(true); + else if (settings["mods"]["validation"].String() == "basic") + ui->buttonValidationBasic->setChecked(true); + else + ui->buttonValidationFull->setChecked(true); + loadToggleButtonSettings(); } void CSettingsView::loadToggleButtonSettings() { setCheckbuttonState(ui->buttonShowIntro, settings["video"]["showIntro"].Bool()); - setCheckbuttonState(ui->buttonVSync, settings["video"]["vsync"].Bool()); setCheckbuttonState(ui->buttonAutoCheck, settings["launcher"]["autoCheckRepositories"].Bool()); setCheckbuttonState(ui->buttonRepositoryDefault, settings["launcher"]["defaultRepositoryEnabled"].Bool()); @@ -205,10 +217,15 @@ void CSettingsView::loadToggleButtonSettings() std::string cursorType = settings["video"]["cursor"].String(); int cursorTypeIndex = vstd::find_pos(cursorTypesList, cursorType); setCheckbuttonState(ui->buttonCursorType, cursorTypeIndex); + ui->sliderScalingCursor->setDisabled(cursorType == "software"); // Not supported + ui->labelScalingCursorValue->setDisabled(cursorType == "software"); // Not supported int fontScalingPercentage = settings["video"]["fontScalingFactor"].Float() * 100; ui->sliderScalingFont->setValue(fontScalingPercentage / 5); + int cursorScalingPercentage = settings["video"]["cursorScalingFactor"].Float() * 100; + ui->sliderScalingCursor->setValue(cursorScalingPercentage / 5); + } void CSettingsView::fillValidResolutions() @@ -487,6 +504,8 @@ void CSettingsView::on_buttonCursorType_toggled(bool value) Settings node = settings.write["video"]["cursor"]; node->String() = cursorTypesList[value ? 1 : 0]; updateCheckbuttonText(ui->buttonCursorType); + ui->sliderScalingCursor->setDisabled(value == 1); // Not supported + ui->labelScalingCursorValue->setDisabled(value == 1); // Not supported } void CSettingsView::loadTranslation() @@ -620,7 +639,6 @@ void CSettingsView::on_buttonVSync_toggled(bool value) Settings node = settings.write["video"]["vsync"]; node->Bool() = value; ui->spinBoxFramerateLimit->setDisabled(settings["video"]["vsync"].Bool()); - updateCheckbuttonText(ui->buttonVSync); } void CSettingsView::on_comboBoxEnemyPlayerAI_currentTextChanged(const QString &arg1) @@ -791,3 +809,46 @@ void CSettingsView::on_buttonFontOriginal_clicked(bool checked) Settings node = settings.write["video"]["fontsType"]; node->String() = "original"; } + +void CSettingsView::on_buttonValidationOff_clicked(bool checked) +{ + Settings node = settings.write["mods"]["validation"]; + node->String() = "off"; +} + +void CSettingsView::on_buttonValidationBasic_clicked(bool checked) +{ + Settings node = settings.write["mods"]["validation"]; + node->String() = "basic"; +} + +void CSettingsView::on_buttonValidationFull_clicked(bool checked) +{ + Settings node = settings.write["mods"]["validation"]; + node->String() = "full"; +} + +void CSettingsView::on_sliderScalingCursor_valueChanged(int value) +{ + int actualValuePercentage = value * 5; + ui->labelScalingCursorValue->setText(QString("%1%").arg(actualValuePercentage)); + Settings node = settings.write["video"]["cursorScalingFactor"]; + node->Float() = actualValuePercentage / 100.0; +} + +void CSettingsView::on_buttonScalingAuto_toggled(bool checked) +{ + if (checked) + { + ui->spinBoxInterfaceScaling->hide(); + } + else + { + ui->spinBoxInterfaceScaling->show(); + ui->spinBoxInterfaceScaling->setValue(100); + } + + Settings node = settings.write["video"]["resolution"]["scaling"]; + node->Integer() = checked ? 0 : 100; +} + diff --git a/launcher/settingsView/csettingsview_moc.h b/launcher/settingsView/csettingsview_moc.h index 1e76f6f2e..d05e7eb1e 100644 --- a/launcher/settingsView/csettingsview_moc.h +++ b/launcher/settingsView/csettingsview_moc.h @@ -83,19 +83,24 @@ private slots: void on_sliderToleranceDistanceController_valueChanged(int value); void on_lineEditGameLobbyHost_textChanged(const QString &arg1); void on_spinBoxNetworkPortLobby_valueChanged(int arg1); - void on_sliderControllerSticksAcceleration_valueChanged(int value); - void on_sliderControllerSticksSensitivity_valueChanged(int value); - - //void on_buttonTtfFont_toggled(bool value); - void on_sliderScalingFont_valueChanged(int value); - void on_buttonFontAuto_clicked(bool checked); void on_buttonFontScalable_clicked(bool checked); void on_buttonFontOriginal_clicked(bool checked); + + void on_buttonValidationOff_clicked(bool checked); + + void on_buttonValidationBasic_clicked(bool checked); + + void on_buttonValidationFull_clicked(bool checked); + + void on_sliderScalingCursor_valueChanged(int value); + + void on_buttonScalingAuto_toggled(bool checked); + private: Ui::CSettingsView * ui; diff --git a/launcher/settingsView/csettingsview_moc.ui b/launcher/settingsView/csettingsview_moc.ui index 578fd55b4..10ac9d634 100644 --- a/launcher/settingsView/csettingsview_moc.ui +++ b/launcher/settingsView/csettingsview_moc.ui @@ -47,583 +47,13 @@ 0 - 0 + -797 729 - 1449 + 1503 - - - - - false - - - BattleAI - - - - BattleAI - - - - - StupidAI - - - - - - - - - 0 - 0 - - - - - - - true - - - - - - - Use Relative Pointer Mode - - - - - - - 25 - - - Qt::Horizontal - - - QSlider::TicksAbove - - - 5 - - - - - - - Additional repository - - - - - - - Music Volume - - - - - - - - - - 0 - - - 50 - - - 1 - - - 10 - - - 0 - - - Qt::Horizontal - - - QSlider::TicksAbove - - - 10 - - - - - - - Long Touch Duration - - - - - - - false - - - BattleAI - - - - BattleAI - - - - - StupidAI - - - - - - - - - true - - - - Video - - - 5 - - - - - - - - true - - - - General - - - 5 - - - - - - - Heroes III Translation - - - - - - - Show Tutorial again - - - - - - - Online Lobby port - - - - - - - Relative Pointer Speed - - - - - - - Touch Tap Tolerance - - - - - - - Select display mode for game - -Windowed - game will run inside a window that covers part of your screen - -Borderless Windowed Mode - game will run in a window that covers entirely of your screen, using same resolution as your screen. - -Fullscreen Exclusive Mode - game will cover entirety of your screen and will use selected resolution. - - - 0 - - - - Windowed - - - - - Borderless fullscreen - - - - - Exclusive fullscreen - - - - - - - - 500 - - - 2000 - - - 250 - - - 250 - - - Qt::Horizontal - - - QSlider::TicksAbove - - - 250 - - - - - - - Neutral AI in battles - - - - - - - - true - - - - Input - Mouse - - - 5 - - - - - - - - 0 - 0 - - - - Automatic - - - true - - - true - - - buttonGroup - - - - - - - Adventure Map Enemies - - - - - - - VCAI - - - - VCAI - - - - - Nullkiller - - - - - - - - 0 - - - 50 - - - 1 - - - 10 - - - 0 - - - Qt::Horizontal - - - QSlider::TicksAbove - - - 10 - - - - - - - Autosave - - - - - - - Renderer - - - - - - - Autosave limit (0 = off) - - - - - - - Default repository - - - - - - - 100 - - - Qt::Horizontal - - - QSlider::TicksAbove - - - 10 - - - - - - - Sticks Sensitivity - - - - - - - - true - - - - Audio - - - 5 - - - - - - - - - - Downscaling Filter - - - - - - - 1024 - - - 65535 - - - 3030 - - - - - - - 0 - - - 50 - - - 1 - - - 10 - - - 0 - - - Qt::Horizontal - - - QSlider::TicksAbove - - - 10 - - - - - - - Ignore SSL errors - - - - - - - Upscaling Filter - - - - - - - Refresh now - - - - - - - true - - - - 0 - 0 - - - - - - - true - - - false - - - - - - - VSync - - - - - - - Resolution - - - - - - - VCMI Language - - - - - - - Interface Scaling - - - - - - - - - - - - - - - - - Online Lobby address - - - - - - - VCAI - - - - VCAI - - - - - Nullkiller - - - - - + + @@ -639,112 +69,14 @@ Fullscreen Exclusive Mode - game will cover entirety of your screen and will use - - + + - + Mods Validation - - - - 100 - - - 500 - - - 10 - - - 100 - - - 100 - - - Qt::Horizontal - - - QSlider::TicksAbove - - - 50 - - - - - - - Autosave prefix - - - - - - - Reserved screen area - - - - - - - Sound Volume - - - - - - - Font Scaling (experimental) - - - - - - - Framerate Limit - - - - - - - - Nearest - - - - - Linear - - - - - Automatic (Linear) - - - - - - - - 100 - - - Qt::Horizontal - - - QSlider::TicksAbove - - - 10 - - - - + @@ -773,53 +105,7 @@ Fullscreen Exclusive Mode - game will cover entirety of your screen and will use - - - - Autocombat AI in battles - - - - - - - Use scalable fonts - - - - - - - - - - true - - - - - - - - true - - - - Artificial Intelligence - - - 5 - - - - - - - Fullscreen - - - - + 500 @@ -847,27 +133,33 @@ Fullscreen Exclusive Mode - game will cover entirety of your screen and will use - - + + - Enemy AI in battles + Reset - - + + - Sticks Acceleration + - - + + - 100 + 500 - 300 + 2000 + + + 250 + + + 250 Qt::Horizontal @@ -876,12 +168,15 @@ Fullscreen Exclusive Mode - game will cover entirety of your screen and will use QSlider::TicksAbove - 25 + 250 - - + + + + true + 0 @@ -894,16 +189,57 @@ Fullscreen Exclusive Mode - game will cover entirety of your screen and will use true - - - - - - empty = map name prefix + + false - + + + + Reserved screen area + + + + + + + + true + + + + General + + + 5 + + + + + + + Autosave + + + + + + + 100 + + + Qt::Horizontal + + + QSlider::TicksAbove + + + 10 + + + + true @@ -925,60 +261,201 @@ Fullscreen Exclusive Mode - game will cover entirety of your screen and will use - - - - - 0 - 0 - - + + - - - - true + Sound Volume - - - - - - - - - - - - 0 - 0 - - - - - - - true - - - - - - - Adventure Map Allies - - - - + Haptic Feedback - + + + + Touch Tap Tolerance + + + + + + + + + + + + + 1024 + + + 65535 + + + 3030 + + + + + + + true + + + + 0 + 0 + + + + Full + + + true + + + false + + + buttonGroupValidation + + + + + + + + true + + + + Input - Controller + + + 5 + + + + + + + 10 + + + 30 + + + 1 + + + 2 + + + 20 + + + 20 + + + Qt::Horizontal + + + QSlider::TicksAbove + + + 2 + + + + + + + Software Cursor + + + + + + + VCAI + + + + VCAI + + + + + Nullkiller + + + + + + + + + + + true + + + + + + + VCAI + + + + VCAI + + + + + Nullkiller + + + + + + + + Heroes III Translation + + + + + + + + true + + + + Artificial Intelligence + + + 5 + + + + + + + Default repository + + + + + + + % @@ -994,50 +471,21 @@ Fullscreen Exclusive Mode - game will cover entirety of your screen and will use - - - - - 0 - 0 - - + + - - - - true - - - false + Relative Pointer Speed - - + + - Mouse Click Tolerance + Downscaling Filter - - - - BattleAI - - - - BattleAI - - - - - StupidAI - - - - - + true @@ -1059,133 +507,8 @@ Fullscreen Exclusive Mode - game will cover entirety of your screen and will use - - - - Reset - - - - - - - - - - Network port - - - - - - - - - - - true - - - - Input - Controller - - - 5 - - - - - - - Display index - - - - - - - - 0 - 0 - - - - - - - true - - - - - - - - true - - - - Network - - - 5 - - - - - - - - 0 - 0 - - - - Original - - - true - - - true - - - buttonGroup - - - - - - - - - - - - - - Show intro - - - - - - - - 0 - 0 - - - - - - - true - - - - - + + 1024 @@ -1197,49 +520,92 @@ Fullscreen Exclusive Mode - game will cover entirety of your screen and will use - - - - - true - - + + - Input - Touchscreen - - - 5 + Show intro - - - - 20 + + + + + 0 + 0 + + + Automatic + + + true + + + true + + + buttonGroupFonts + + + + + - 1000 + 100 - + + Qt::Horizontal + + + QSlider::TicksAbove + + 10 - - + + + + + 0 + 0 + + - Controller Click Tolerance + + + + true + + + false - - + + - Check on startup + Refresh now - + + + + Adventure Map Allies + + + + + + + Neutral AI in battles + + + + 10 @@ -1270,14 +636,467 @@ Fullscreen Exclusive Mode - game will cover entirety of your screen and will use - - + + - Software Cursor + - + + + + Display index + + + + + + + Use Relative Pointer Mode + + + + + + + + + + Autocombat AI in battles + + + + + + + true + + + + 0 + 0 + + + + Off + + + true + + + false + + + buttonGroupValidation + + + + + + + + true + + + + Input - Mouse + + + 5 + + + + + + + Network port + + + + + + + + 0 + 0 + + + + + + + true + + + + + + + + true + + + + Audio + + + 5 + + + + + + + Additional repository + + + + + + + + 0 + 0 + + + + VSync + + + true + + + + + + + Adventure Map Enemies + + + + + + + Framerate Limit + + + + + + + Use scalable fonts + + + + + + + Renderer + + + + + + + + true + + + + Input - Touchscreen + + + 5 + + + + + + + 25 + + + Qt::Horizontal + + + QSlider::TicksAbove + + + 5 + + + + + + + + + + + 0 + 0 + + + + + + + true + + + + + + + VCMI Language + + + + + + + Online Lobby address + + + + + + + Online Lobby port + + + + + + + Controller Click Tolerance + + + + + + + BattleAI + + + + BattleAI + + + + + StupidAI + + + + + + + + + Nearest + + + + + Linear + + + + + Automatic (Linear) + + + + + + + + Ignore SSL errors + + + + + + + + + + Show Tutorial again + + + + + + + Sticks Acceleration + + + + + + + + true + + + + Video + + + 5 + + + + + + + false + + + BattleAI + + + + BattleAI + + + + + StupidAI + + + + + + + + + + + + + + + Fullscreen + + + + + + + Mouse Click Tolerance + + + + + + + Enemy AI in battles + + + + + + + Autosave limit (0 = off) + + + + + + + 0 + + + 50 + + + 1 + + + 10 + + + 0 + + + Qt::Horizontal + + + QSlider::TicksAbove + + + 10 + + + + + + + Interface Scaling + + + + + + + Music Volume + + + + + + + false + + + BattleAI + + + + BattleAI + + + + + StupidAI + + + + + + + + + + + + + + + Cursor Scaling + + + + + + + + 0 + 0 + + + + + + + true + + + + @@ -1295,12 +1114,340 @@ Fullscreen Exclusive Mode - game will cover entirety of your screen and will use true - buttonGroup + buttonGroupFonts - - + + + + + true + + + + Network + + + 5 + + + + + + + + true + + + + Miscellaneous + + + 5 + + + + + + + 100 + + + 500 + + + 10 + + + 100 + + + 100 + + + Qt::Horizontal + + + QSlider::TicksAbove + + + 50 + + + + + + + Select display mode for game + +Windowed - game will run inside a window that covers part of your screen + +Borderless Windowed Mode - game will run in a window that covers entirely of your screen, using same resolution as your screen. + +Fullscreen Exclusive Mode - game will cover entirety of your screen and will use selected resolution. + + + 0 + + + + Windowed + + + + + Borderless fullscreen + + + + + Exclusive fullscreen + + + + + + + + 20 + + + 1000 + + + 10 + + + + + + + Long Touch Duration + + + + + + + Check on startup + + + + + + + 0 + + + 50 + + + 1 + + + 10 + + + 0 + + + Qt::Horizontal + + + QSlider::TicksAbove + + + 10 + + + + + + + + 0 + 0 + + + + + + + true + + + + + + + 100 + + + 300 + + + Qt::Horizontal + + + QSlider::TicksAbove + + + 25 + + + + + + + Font Scaling (experimental) + + + + + + + + 0 + 0 + + + + + + + true + + + + + + + 0 + + + 50 + + + 1 + + + 10 + + + 0 + + + Qt::Horizontal + + + QSlider::TicksAbove + + + 10 + + + + + + + empty = map name prefix + + + + + + + + 0 + 0 + + + + Original + + + true + + + true + + + buttonGroupFonts + + + + + + + + 0 + 0 + + + + Automatic + + + true + + + + + + + Sticks Sensitivity + + + + + + + Upscaling Filter + + + + + + + Autosave prefix + + + + + + + Resolution + + + + + + + + + + true + + + + 0 + 0 + + + + Basic + + + true + + + false + + + buttonGroupValidation + + @@ -1311,6 +1458,7 @@ Fullscreen Exclusive Mode - game will cover entirety of your screen and will use - + + diff --git a/launcher/translation/chinese.ts b/launcher/translation/chinese.ts index 11e156178..dc77e03a3 100644 --- a/launcher/translation/chinese.ts +++ b/launcher/translation/chinese.ts @@ -90,109 +90,85 @@ CModListModel - Translation - 本地化 + 本地化 - Town - 城镇 + 城镇 - Test - 测试 + 测试 - Templates - 地图模板 + 地图模板 - Spells - 法术 + 法术 - Music - 音乐 + 音乐 - Maps - 地图 + 地图 - Sounds - 音效 + 音效 - Skills - 技能 + 技能 - - Other - 其他 + 其他 - Objects - 物件 + 物件 - - Mechanics 无法确定是否分类是游戏机制或者是游戏中的战争器械 - 机制 + 机制 - - Interface - 界面 + 界面 - Heroes - 英雄 + 英雄 - - Graphical - 图像 + 图像 - Expansion - 扩展包 + 扩展包 - Creatures - 生物 + 生物 - Compatibility - 兼容性 + 兼容性 - Artifacts - 宝物 + 宝物 - AI - AI + AI @@ -235,7 +211,7 @@ - + Description 详细介绍 @@ -295,191 +271,204 @@ 终止 - + Mod name 模组名称 - + + Installed version 已安装的版本 - + + Latest version 最新版本 - + Size 大小 - + Download size 下载大小 - + Authors 作者 - + License 授权许可 - + Contact 联系方式 - + Compatibility 兼容性 - - + + Required VCMI version 需要VCMI版本 - + Supported VCMI version 支持的VCMI版本 - + please upgrade mod 请更新模组 - - + + mods repository index 模组源索引号 - + or newer 或更新的版本 - + Supported VCMI versions 支持的VCMI版本 - + Languages 语言 - + Required mods Mod统一翻译为模组 前置模组 - + Conflicting mods Mod统一翻译为模组 冲突的模组 - This mod can not be installed or enabled because the following dependencies are not present - 这个模组无法被安装或者激活,因为下列依赖项未满足 + 这个模组无法被安装或者激活,因为下列依赖项未满足 - This mod can not be enabled because the following mods are incompatible with it - 这个模组无法被激活,因为下列模组与其不兼容 + 这个模组无法被激活,因为下列模组与其不兼容 - This mod cannot be disabled because it is required by the following mods - 这个模组无法被禁用,因为它被下列模组所依赖 + 这个模组无法被禁用,因为它被下列模组所依赖 - This mod cannot be uninstalled or updated because it is required by the following mods - 这个模组无法被卸载或者更新,因为它被下列模组所依赖 + 这个模组无法被卸载或者更新,因为它被下列模组所依赖 - + + This mod cannot be enabled because it translates into a different language. + 这个模组无法被启用,因为它被翻译成其他语言。 + + + + This mod can not be enabled because the following dependencies are not present + 这个模组无法被启用,因为下列依赖不满足 + + + + This mod can not be installed because the following dependencies are not present + 这个模组无法被安装,因为下列依赖不满足 + + + This is a submod and it cannot be installed or uninstalled separately from its parent mod 这是一个附属模组它无法在所属模组外被直接被安装或者卸载 - + Notes 笔记注释 - + All supported files 所有支持的文件格式 - + Maps 地图 - + Campaigns 战役 - + Configs 配置 - + Mods 模组 - + Gog files Gog文件 - + All files (*.*) 所有文件 (*.*) - + Select files (configs, mods, maps, campaigns, gog files) to install... 选择需要安装的文件(配置,模组,地图,战役,gog文件)... - + Replace config file? 替换配置文件? - + Do you want to replace %1? 你想要替换%1吗? - + Downloading %1. %p% (%v MB out of %m MB) finished 正在下载 %1. %p% (%v MB 共 %m MB) 已完成 - + Download failed 下载失败 - + Unable to download all files. Encountered errors: @@ -492,7 +481,7 @@ Encountered errors: - + Install successfully downloaded? @@ -501,39 +490,39 @@ Install successfully downloaded? 安装下载成功的部分? - + Installing chronicles 安装历代记 - + Installing mod %1 正在安装模组 %1 - + Operation failed 操作失败 - + Encountered errors: 遇到问题: - + screenshots 截图 - + Screenshot %1 截图 %1 - + Mod is incompatible Mod统一翻译为模组 模组不兼容 @@ -542,364 +531,387 @@ Install successfully downloaded? CModManager - Can not install submod - 无法安装子模组 + 无法安装子模组 - Mod is already installed - 模组已安装 + 模组已安装 - Can not uninstall submod - 无法卸载子模组 + 无法卸载子模组 - Mod is not installed - 模组未安装 + 模组未安装 - Mod is already enabled - 模组已启用 + 模组已启用 - - Mod must be installed first - 需要先安装模组 + 需要先安装模组 - Mod is not compatible, please update VCMI and checkout latest mod revisions - 模组不兼容,请更新VCMI并获取模组最新版本 + 模组不兼容,请更新VCMI并获取模组最新版本 - Required mod %1 is missing - 需要的模组%1未找到 + 需要的模组%1未找到 - Required mod %1 is not enabled - 需要的模组%1未启用 + 需要的模组%1未启用 - - This mod conflicts with %1 - 此模组和%1冲突 + 此模组和%1冲突 - Mod is already disabled - 模组已禁用 + 模组已禁用 - This mod is needed to run %1 - 此模组需要运行%1 + 此模组需要运行%1 - Mod archive is missing - 模组归档文件未找到 + 模组归档文件未找到 - Mod with such name is already installed - 同名模组已安装 + 同名模组已安装 - Mod archive is invalid or corrupted - 模组归档文件无效或损坏 + 模组归档文件无效或损坏 - Failed to extract mod data - 提取模组数据失败 + 提取模组数据失败 - Data with this mod was not found - 此模组的数据未找到 + 此模组的数据未找到 - Mod is located in protected directory, please remove it manually: - 模组位于受保护的目录,请手动删除它: + 模组位于受保护的目录,请手动删除它: CSettingsView + Off 关闭 - + Artificial Intelligence 人工智能 - + Interface Scaling 界面缩放 - + Neutral AI in battles 战场中立生物AI - + Enemy AI in battles 战场敌方玩家AI - + Additional repository 额外仓库 - + Downscaling Filter 图像缩小过滤器 - + Adventure Map Allies 冒险地图友方玩家 - + Online Lobby port 在线大厅端口 - + Autocombat AI in battles 自动战斗AI - + Sticks Sensitivity 摇杆灵敏度 - + Automatic (Linear) 自动(线性) - + Haptic Feedback 触觉反馈 - + Software Cursor 软件指针 - + + + Automatic 自动 - + + Mods Validation + 模组验证 + + + None - + xBRZ x2 xBRZ x2 - + xBRZ x3 xBRZ x3 - + xBRZ x4 xBRZ x4 - + + Full + 完备 + + + Use scalable fonts 使用可缩放字体 - + Online Lobby address 在线大厅地址 - + + Cursor Scaling + 指针缩放 + + + + Scalable + 可缩放字体 + + + + Miscellaneous + 杂项 + + + + Font Scaling (experimental) + 字体缩放(测试中) + + + + Original + 原始字体 + + + Upscaling Filter 图像放大过滤器 - + + Basic + 基本 + + + Use Relative Pointer Mode 使用相对指针模式 - + Nearest 最邻近 - + Linear 线性 - + Input - Touchscreen 输入 - 触屏 - + Adventure Map Enemies 冒险地图敌方玩家 - + Show Tutorial again 重新显示教程 - + Reset 重置 - + Network 网络 - + Audio 音频 - + Relative Pointer Speed 相对指针速度 - + Music Volume 音乐音量 - + Ignore SSL errors 忽略SSL错误 - + Input - Mouse 输入 - 鼠标 - + Long Touch Duration 长按触屏间隔 - + % % - + Controller Click Tolerance 控制器按键灵敏度 - + Touch Tap Tolerance 触屏点击灵敏度 - + Input - Controller 输入 - 控制器 - + Sound Volume 音效音量 - + Windowed 窗口化 - + Borderless fullscreen 无边框全屏 - + Exclusive fullscreen 独占全屏 - + Autosave limit (0 = off) 自动保存限制 (0 = 不限制) - + Framerate Limit 帧率限制 - + Autosave prefix 自动保存文件名前缀 - + Mouse Click Tolerance 鼠标点击灵敏度 - + Sticks Acceleration 摇杆加速度 - + empty = map name prefix 空 = 地图名称前缀 - + Refresh now 立即刷新 - + Default repository 默认仓库 - + Renderer 渲染器 @@ -909,7 +921,7 @@ Install successfully downloaded? 开启 - + Select display mode for game Windowed - game will run inside a window that covers part of your screen @@ -926,93 +938,93 @@ Fullscreen Exclusive Mode - game will cover entirety of your screen and will use 独占全屏模式 - 游戏会运行在一个覆盖全部屏幕的窗口,使用和你选择的分辨率。 - + Reserved screen area 保留屏幕区域 - + Heroes III Translation 发布版本里找不到这个项,不太清楚意义 英雄无敌3翻译 - + Check on startup 启动时检查更新 - + Fullscreen 全屏 - + General 通用设置 - + VCMI Language VCMI语言 - + Resolution 分辨率 - + Autosave 自动存档 - + VSync 垂直同步 - + Display index 显示器序号 - + Network port 网络端口 - + Video 视频设置 - + Show intro 显示开场动画 - + Active 激活 - + Disabled 禁用 - + Enable 启用 - + Not Installed 未安装 - + Install 安装 @@ -1048,7 +1060,7 @@ Fullscreen Exclusive Mode - game will cover entirety of your screen and will use - + Heroes Chronicles 英雄无敌历代记 @@ -1056,29 +1068,26 @@ Fullscreen Exclusive Mode - game will cover entirety of your screen and will use File size - %1 B - %1 B + %1 B - %1 KiB - %1 KiB + %1 KiB - + + %1 MiB %1 MiB - %1 GiB - %1 GiB + %1 GiB - %1 TiB - %1 TiB + %1 TiB @@ -1320,7 +1329,7 @@ Offline installer consists of two parts, .exe and .bin. Make sure you download b GOG data - GOC数据 + GOG数据 @@ -1529,16 +1538,209 @@ error reason: ModFields - + Name 名称 - + Type 类型 + + ModStateController + + + Can not install submod + 无法安装子模组 + + + + Mod is already installed + 模组已安装 + + + + Can not uninstall submod + 无法卸载子模组 + + + + Mod is not installed + 模组未安装 + + + + Mod is already enabled + 模组已启用 + + + + + Mod must be installed first + 需要先安装模组 + + + + Mod is not compatible, please update VCMI and checkout latest mod revisions + 模组不兼容,请更新VCMI并获取模组最新版本 + + + + Can not enable translation mod for a different language! + 无法启用不同语言的模组! + + + + Required mod %1 is missing + 需要的模组%1未找到 + + + + Mod is already disabled + 模组已禁用 + + + + Mod archive is missing + 模组归档文件未找到 + + + + Mod with such name is already installed + 同名模组已安装 + + + + Mod archive is invalid or corrupted + 模组归档文件无效或损坏 + + + + Failed to extract mod data + 提取模组数据失败 + + + + Data with this mod was not found + 此模组的数据未找到 + + + + Mod is located in protected directory, please remove it manually: + + 模组位于受保护的目录,请手动删除它: + + + + + ModStateItemModel + + + Translation + 本地化 + + + + Town + 城镇 + + + + Test + 测试 + + + + Templates + 地图模板 + + + + Spells + 法术 + + + + Music + 音乐 + + + + Maps + 地图 + + + + Sounds + 音效 + + + + Skills + 技能 + + + + + Other + 其他 + + + + Objects + 物件 + + + + + Mechanics + 机制 + + + + + Interface + 界面 + + + + Heroes + 英雄 + + + + + Graphical + 图像 + + + + Expansion + 扩展包 + + + + Creatures + 生物 + + + + Compatibility + 兼容性 + + + + Artifacts + 宝物 + + + + AI + AI + + QObject diff --git a/launcher/translation/czech.ts b/launcher/translation/czech.ts index f3a1225b1..ec9595630 100644 --- a/launcher/translation/czech.ts +++ b/launcher/translation/czech.ts @@ -54,7 +54,7 @@ Log files directory - Složka záznamů hry + Adresář souborů s logy @@ -90,108 +90,84 @@ CModListModel - Translation - Překlad + Překlad - Town - Město + Město - Test - Zkouška + Zkouška - Templates - Šablony + Šablony - Spells - Kouzla + Kouzla - Music - Hudba + Hudba - Maps - Mapy + Mapy - Sounds - Zvuky + Zvuky - Skills - Schopnosti + Schopnosti - - Other - Ostatní + Ostatní - Objects - Objekty + Objekty - - Mechanics - Mechaniky + Mechaniky - - Interface - Rozhraní + Rozhraní - Heroes - Hrdinové + Hrdinové - - Graphical - Grafika + Grafika - Expansion - Rozšíření + Rozšíření - Creatures - Bojovníci + Jednotky - Compatibility - Kompabilita + Kompabilita - Artifacts - Artefakty + Artefakty - AI - AI + AI @@ -209,7 +185,7 @@ Downloadable - Stahovatelné + Ke stažení @@ -219,7 +195,7 @@ Updatable - Aktualizovatelné + K aktualizaci @@ -233,7 +209,7 @@ - + Description Popis @@ -293,189 +269,202 @@ Zrušit - + Mod name Název modifikace - + + Installed version Nainstalovaná verze - + + Latest version Nejnovější verze - + Size Velikost - + Download size Velikost ke stažení - + Authors Autoři - + License Licence - + Contact Kontakt - + Compatibility Kompabilita - - + + Required VCMI version Vyžadovaná verze VCMI - + Supported VCMI version Podporovaná verze VCMI - + please upgrade mod prosíme aktualizujte modifikaci - - + + mods repository index index repozitáře modifikací - + or newer nebo novější - + Supported VCMI versions Podporované verze VCMI - + Languages Jazyky - + Required mods Vyžadované modifikace VCMI - + Conflicting mods Modifikace v kolizi - This mod can not be installed or enabled because the following dependencies are not present - Tato modifikace nemůže být nainstalována nebo povolena, protože následující závislosti nejsou přítomny + Tato modifikace nelze nainstalovat ani povolit, protože nejsou přítomny následující závislosti - This mod can not be enabled because the following mods are incompatible with it - Tato modifikace nemůže být povolena, protože následující modifikace s ní nejsou kompatibilní + Tato modifikace nemůže být povolena, protože není kompatibilní s následujícími modifikacemi - This mod cannot be disabled because it is required by the following mods - Tato modifikace nemůže být zakázána, protože je vyžadována následujícími modifikacemi + Tato modifikace nemůže být zakázána, protože je vyžadována následujícími modifikacemi - This mod cannot be uninstalled or updated because it is required by the following mods - Tato modifikace nemůže být odinstalována nebo aktualizována, protože je vyžadována následujícími modifikacemi + Tato modifikace nemůže být odinstalována nebo aktualizována, protože je vyžadována následujícími modifikacemi - + + This mod cannot be enabled because it translates into a different language. + Tuto modifikaci nelze aktivovat, protože je určena pro jiný jazyk. + + + + This mod can not be enabled because the following dependencies are not present + Tuto modifikaci nelze aktivovat, protože chybí následující závislosti + + + + This mod can not be installed because the following dependencies are not present + Tuto modifikaci nelze nainstalovat, protože chybí následující závislosti + + + This is a submod and it cannot be installed or uninstalled separately from its parent mod - Toto je podmodifikace, která nemůže být nainstalována nebo odinstalována bez její rodičovské modifikace + Toto je podmodifikace a nelze ji nainstalovat ani odinstalovat samostatně bez hlavní modifikace - + Notes Poznámky - + All supported files Všechny podporované soubory - + Maps Mapy - + Campaigns Kampaně - + Configs Nastavení - + Mods Modifikace - - - Gog files - - - All files (*.*) - + Gog files + Soubory GOG - Select files (configs, mods, maps, campaigns, gog files) to install... - + All files (*.*) + Všechny soubory (*.*) - + + Select files (configs, mods, maps, campaigns, gog files) to install... + Vyberte soubory (konfigurace, modifikace, mapy, kampaně, soubory GOG) k instalaci... + + + Replace config file? Nahradit soubor nastavení? - + Do you want to replace %1? Chcete nahradit %1? - + Downloading %1. %p% (%v MB out of %m MB) finished - + Stahování %1. %p% (%v MB z %m MB) dokončeno - + Download failed Stahování selhalo - + Unable to download all files. Encountered errors: @@ -488,7 +477,7 @@ Vyskytly se chyby: - + Install successfully downloaded? @@ -497,39 +486,39 @@ Install successfully downloaded? Nainstalovat úspěšně stažené? - + Installing chronicles - + Instalování kronik - + Installing mod %1 Instalování modifikace %1 - + Operation failed Operace selhala - + Encountered errors: Vyskytly se chyby: - + screenshots snímky obrazovky - + Screenshot %1 Snímek obrazovky %1 - + Mod is incompatible Modifikace není kompatibilní @@ -537,364 +526,387 @@ Nainstalovat úspěšně stažené? CModManager - Can not install submod - Nelze nainstalovat podmodifikaci + Nelze nainstalovat podmodifikaci - Mod is already installed - Modifikace je již nainstalována + Modifikace je již nainstalována - Can not uninstall submod - Nelze odinstalovat podmodifikaci + Nelze odinstalovat podmodifikaci - Mod is not installed - Modifikace není nainstalována + Modifikace není nainstalována - Mod is already enabled - Modifikace je již povolena + Modifikace je již povolena - - Mod must be installed first - Nejprve musí být nainstalována modifikace + Nejprve musí být nainstalována modifikace - Mod is not compatible, please update VCMI and checkout latest mod revisions - Modifikace není kompatibilní, prosíme aktualizujte VCMI a použijte nejnovější verzi modifikace + Modifikace není kompatibilní, prosíme aktualizujte VCMI a použijte nejnovější verzi modifikace - Required mod %1 is missing - Vyžadovaná modifkace %1 chybí + Vyžadovaná modifkace %1 chybí - Required mod %1 is not enabled - Vyžadovaná modifikace %1 není povolena + Vyžadovaná modifikace %1 není povolena - - This mod conflicts with %1 - Tato modifikace koliduje s %1 + Tato modifikace koliduje s %1 - Mod is already disabled - Modifikace je již povolena + Modifikace je již povolena - This mod is needed to run %1 - Modifikace %1 je vyžadována pro běh + Modifikace %1 je vyžadována pro běh - Mod archive is missing - Archiv modifikace chybí + Archiv modifikace chybí - Mod with such name is already installed - Modifikace s tímto názvem je již nainstalována + Modifikace s tímto názvem je již nainstalována - Mod archive is invalid or corrupted - Archiv modifikace je neplatný nebo poškozený + Archiv modifikace je neplatný nebo poškozený - Failed to extract mod data - Extrakce dat modifikace selhala + Extrakce dat modifikace selhala - Data with this mod was not found - Data s touto modifikací nebyla nalezena + Data s touto modifikací nebyla nalezena - Mod is located in protected directory, please remove it manually: - Modifikace se nachází v zabezpečené složce, prosíme odstraňte ji ručně: + Modifikace se nachází v zabezpečené složce, prosíme odstraňte ji ručně: CSettingsView + Off Vypnuto - + Artificial Intelligence Umělá inteligence - + Interface Scaling Škálování rozhraní - + Neutral AI in battles Neutrální AI v bitvách - + Enemy AI in battles Nepřátelská AI v bitvách - + Additional repository Další repozitáře - + Downscaling Filter - + Filtr pro zmenšování - + Adventure Map Allies Spojenci na mapě světa - + Online Lobby port - Port online předsíně + Port online lobby - + Autocombat AI in battles AI automatického boje v bitvách - + Sticks Sensitivity Citlivost páček - + Automatic (Linear) - + Automaticky (Lineárně) - + Haptic Feedback Zpětná odezva - + Software Cursor Softwarový kurzor - + + + Automatic - + Automaticky - + + Mods Validation + Validace modifikací + + + None - + Nic - + xBRZ x2 - + xBRZ x2 - + xBRZ x3 - + xBRZ x3 - + xBRZ x4 - + xBRZ x4 - + + Full + Plné + + + Use scalable fonts - + Použít škálovatelná písma - + Online Lobby address - Adresa online předsíně + Adresa online lobby - + + Cursor Scaling + Škálování kurzoru + + + + Scalable + Škálovatelné + + + + Miscellaneous + Ostatní + + + + Font Scaling (experimental) + Škálování písma (experimentální) + + + + Original + Původní + + + Upscaling Filter Filtr škálování - + + Basic + Základní + + + Use Relative Pointer Mode Použít režim relativního ukazatele - + Nearest Nejbližší - + Linear Lineární - + Input - Touchscreen Vstup - dotyková obrazovka - + Adventure Map Enemies Nepřátelé na mapě světa - + Show Tutorial again - + Znovu zobrazi Tutoriál - + Reset - + Restart - + Network Síť - + Audio Zvuk - + Relative Pointer Speed Relativní rychlost myši - + Music Volume Hlasitost hudby - + Ignore SSL errors Ignorovat chyby SSL - + Input - Mouse Vstup - Myš - + Long Touch Duration Doba dlouhého podržení - + % % - + Controller Click Tolerance Odchylka klepnutí ovladače - + Touch Tap Tolerance Odchylka klepnutí dotykem - + Input - Controller Vstup - ovladač - + Sound Volume Hlasitost zvuků - + Windowed V okně - + Borderless fullscreen Celá obrazovka bez okrajů - + Exclusive fullscreen Exkluzivní celá obrazovka - + Autosave limit (0 = off) Limit aut. uložení (0=vypnuto) - + Framerate Limit Omezení snímků za sekundu - + Autosave prefix Předpona aut. uložení - + Mouse Click Tolerance Odchylka klepnutí myší - + Sticks Acceleration Zrychlení páček - + empty = map name prefix prázná = předpona - název mapy - + Refresh now Obnovit nyní - + Default repository Výchozí repozitář - + Renderer Vykreslovač @@ -904,7 +916,7 @@ Nainstalovat úspěšně stažené? Zapnuto - + Select display mode for game Windowed - game will run inside a window that covers part of your screen @@ -921,92 +933,92 @@ Celá obrazovka bez okrajů- hra poběží v okně, které zakryje vaši celou Exkluzivní celá obrazovka - hra zakryje vaši celou obrazovku a použije vybrané rozlišení. - + Reserved screen area Vyhrazená část obrazovky - + Heroes III Translation Překlad Heroes III - + Check on startup Zkontrolovat při zapnutí - + Fullscreen Celá obrazovka - + General Všeobecné - + VCMI Language Jazyk VCMI - + Resolution Rozlišení - + Autosave Automatické uložení - + VSync VSync - + Display index - + Monitor - + Network port Síťový port - + Video Zobrazení - + Show intro Zobrazit intro - + Active Aktivní - + Disabled Zakázáno - + Enable Povolit - + Not Installed Nenainstalováno - + Install Instalovat @@ -1016,63 +1028,60 @@ Exkluzivní celá obrazovka - hra zakryje vaši celou obrazovku a použije vybra File cannot opened - + Soubor nelze otevřít Invalid file selected - Vybrán neplatný soubor + Vybrán neplatný soubor You have to select an gog installer file! - + Musíte vybrat instalační soubor GOG! You have to select an chronicle installer file! - + Musíte vybrat instalační soubor kronik! Extracting error! - + Chyb při rozbalování! - + Heroes Chronicles - + Heroes Chronicles File size - %1 B - %1 B + %1 B - %1 KiB - %1 KiB + %1 KiB - + + %1 MiB %1 MiB - %1 GiB - %1 GiB + %1 GiB - %1 TiB - %1 TiB + %1 TiB @@ -1111,13 +1120,13 @@ Before you can start playing, there are a few more steps that need to be complet Please keep in mind that in order to use VCMI you must own the original data files for Heroes® of Might and Magic® III: Complete or The Shadow of Death. Heroes® of Might and Magic® III HD is currently not supported! - Děkujeme za instalaci VCMI! + Děkujeme, že jste si nainstalovali VCMI! -Před začátkem hraní musíte ještě dokončit pár kroků. +Než začnete hrát, je třeba dokončit několik kroků. -Prosíme, mějte na paměti, že abyste mohli hrát VCMI, musíte vlastnit originální datové soubory Heroes® of Might and Magic® III: Complete nebo The Shadow of Death. +Pamatujte, že pro používání VCMI musíte vlastnit originální herní soubory pro Heroes® of Might and Magic® III: Complete nebo The Shadow of Death. -Heroes® of Might and Magic® III HD není v současnosti podporovaný! +Heroes® of Might and Magic® III HD momentálně není podporováno! @@ -1132,7 +1141,7 @@ Heroes® of Might and Magic® III HD není v současnosti podporovaný! You can manually copy directories Maps, Data and Mp3 from the original game directory to VCMI data directory that you can see on top of this page - Můžete ručně zkopírovat existující mapy, data a MP3 z originální složky hry do složky dat VCMI, kterou můžete vidět nahoře na této stránce. + Můžete ručně zkopírovat existující mapy, data a MP3 z originální složky hry do složky dat VCMI, kterou můžete vidět nahoře na této stránce @@ -1152,7 +1161,7 @@ Heroes® of Might and Magic® III HD není v současnosti podporovaný! Install a translation of Heroes III in your preferred language - Instalovat překlad Heroes III vašeho upřednostněného jazyka + Nainstalujte si překlad Heroes III dle preferovaného jazyka @@ -1179,7 +1188,7 @@ Offline instalátor obsahuje dvě části, .exe a .bin. Ujistěte se, že stahuj Optionally, you can install additional mods either now, or at any point later, using the VCMI Launcher - Nyní můžete volitelně nainstalovat další modifikace, nebo též kdykoliv potom pomocí spouštěče VCMI + Můžete si nyní, nebo kdykoliv později, nainstalovat další mody pomocí VCMI Launcheru, podle svých preferencí @@ -1189,12 +1198,12 @@ Offline instalátor obsahuje dvě části, .exe a .bin. Ujistěte se, že stahuj Install compatible version of "Horn of the Abyss", a fan-made Heroes III expansion ported by the VCMI team - + Instalovat kompatibilní verzi 'Horn of the Abyss', fanouškovského rozšíření Heroes III portovaného týmem VCMI Install compatible version of "In The Wake of Gods", a fan-made Heroes III expansion - + "Instalovat kompatibilní verzi In The Wake of Gods', fanouškovského rozšíření Heroes III portovaného týmem VCMI" @@ -1246,12 +1255,12 @@ Offline instalátor obsahuje dvě části, .exe a .bin. Ujistěte se, že stahuj Install VCMI Mod Preset - Instalovat předvybrané modifiakce VCMI + Instalovat předvybrané VCMI modifikace Horn of the Abyss - + Horn of the Abyss @@ -1261,7 +1270,7 @@ Offline instalátor obsahuje dvě části, .exe a .bin. Ujistěte se, že stahuj In The Wake of Gods - + In The Wake of Gods @@ -1298,7 +1307,7 @@ Offline instalátor obsahuje dvě části, .exe a .bin. Ujistěte se, že stahuj File cannot be opened - + Soubor nelze otevřít @@ -1318,17 +1327,17 @@ Offline instalátor obsahuje dvě části, .exe a .bin. Ujistěte se, že stahuj You've provided GOG Galaxy installer! This file doesn't contain the game. Please download the offline backup game installer! - + Poskytli jste instalátor GOG Galaxy! Tento soubor neobsahuje hru. Prosím, stáhněte si záložní offline instalátor hry! Extracting error! - + Chyba při rozbalování! No Heroes III data! - Žádná data Heroes III! + Chybí data Heroes III! @@ -1354,15 +1363,15 @@ Prosíme vyberte složku s nainstalovanými daty Heroes III. Heroes III: HD Edition files are not supported by VCMI. Please select directory with Heroes III: Complete Edition or Heroes III: Shadow of Death. - Soubory Heroes III HD Edice nejsou podporována ve VCMI. -Prosíme vyberte složku s Heroes III: Complete Edition nebo Heroes III: Shadow of Death. + Soubory Heroes III HD Edice nejsou podporovány ve VCMI. +Prosím vyberte složku s Heroes III: Complete Edition nebo Heroes III: Shadow of Death. Unknown or unsupported Heroes III version found. Please select directory with Heroes III: Complete Edition or Heroes III: Shadow of Death. Nalezena neznámá nebo nepodporovaná verze Heroes III. -Prosíme vyberte složku s Heroes III: Complete Edition nebo Heroes III: Shadow of Death. +Prosím vyberte složku s Heroes III: Complete Edition nebo Heroes III: Shadow of Death. @@ -1379,17 +1388,18 @@ Prosíme vyberte složku s Heroes III: Complete Edition nebo Heroes III: Shadow Stream error while extracting files! error reason: - + Chyba při extrahování souborů! +Důvod chyby: Not a supported Inno Setup installer! - + Nepodporovaný Inno Setup instalátor! VCMI was compiled without innoextract support, which is needed to extract exe files! - + VCMI bylo zkompilováno bez podpory innoextract, která je potřebná pro extrahování EXE souborů! @@ -1490,7 +1500,7 @@ error reason: VCMI Launcher - Spouštěč VCMI + VCMI Launcher @@ -1521,16 +1531,209 @@ error reason: ModFields - + Name Název - + Type Druh + + ModStateController + + + Can not install submod + Nelze nainstalovat podmodifikaci + + + + Mod is already installed + Modifikace je již nainstalována + + + + Can not uninstall submod + Nelze odinstalovat podmodifikaci + + + + Mod is not installed + Modifikace není nainstalována + + + + Mod is already enabled + Modifikace je již povolena + + + + + Mod must be installed first + Nejprve je třeba nainstalovat modifikaci + + + + Mod is not compatible, please update VCMI and checkout latest mod revisions + Modifikace není kompatibilní, prosím aktualizujte VCMI a použijte nejnovější verzi modifikace + + + + Can not enable translation mod for a different language! + Nelze zapnout modifikaci s překladem pro jiný jazyk! + + + + Required mod %1 is missing + Vyžadovaná modifkace %1 chybí + + + + Mod is already disabled + Modifikace je již deaktivována + + + + Mod archive is missing + Archiv modifikace chybí + + + + Mod with such name is already installed + Modifikace s tímto názvem je již nainstalována + + + + Mod archive is invalid or corrupted + Archiv modifikace je neplatný nebo poškozený + + + + Failed to extract mod data + Extrakce modifikace selhala + + + + Data with this mod was not found + Data s touto modifikací nebyla nalezena + + + + Mod is located in protected directory, please remove it manually: + + Modifikace se nachází v zabezpečené složce, prosím odstraňte ji ručně: + + + + + ModStateItemModel + + + Translation + Překlad + + + + Town + Město + + + + Test + Zkouška + + + + Templates + Šablony + + + + Spells + Kouzla + + + + Music + Hudba + + + + Maps + Mapy + + + + Sounds + Zvuky + + + + Skills + Schopnosti + + + + + Other + Ostatní + + + + Objects + Objekty + + + + + Mechanics + Mechaniky + + + + + Interface + Rozhraní + + + + Heroes + Hrdinové + + + + + Graphical + Grafika + + + + Expansion + Rozšíření + + + + Creatures + Jednotky + + + + Compatibility + Kompatibilita + + + + Artifacts + Artefakty + + + + AI + AI + + QObject diff --git a/launcher/translation/english.ts b/launcher/translation/english.ts index 604274ac0..5c186eaa0 100644 --- a/launcher/translation/english.ts +++ b/launcher/translation/english.ts @@ -87,113 +87,6 @@ - - CModListModel - - - Translation - - - - - Town - - - - - Test - - - - - Templates - - - - - Spells - - - - - Music - - - - - Maps - - - - - Sounds - - - - - Skills - - - - - - Other - - - - - Objects - - - - - - Mechanics - - - - - - Interface - - - - - Heroes - - - - - - Graphical - - - - - Expansion - - - - - Creatures - - - - - Compatibility - - - - - Artifacts - - - - - AI - - - CModListView @@ -233,7 +126,7 @@ - + Description @@ -293,189 +186,186 @@ - + Mod name - + + Installed version - + + Latest version - + Size - + Download size - + Authors - + License - + Contact - + Compatibility - - + + Required VCMI version - + Supported VCMI version - + please upgrade mod - - + + mods repository index - + or newer - + Supported VCMI versions - + Languages - + Required mods - + Conflicting mods - - This mod can not be installed or enabled because the following dependencies are not present + + This mod cannot be enabled because it translates into a different language. - - This mod can not be enabled because the following mods are incompatible with it + + This mod can not be enabled because the following dependencies are not present - - This mod cannot be disabled because it is required by the following mods + + This mod can not be installed because the following dependencies are not present - - This mod cannot be uninstalled or updated because it is required by the following mods - - - - + This is a submod and it cannot be installed or uninstalled separately from its parent mod - + Notes - + All supported files - + Maps - + Campaigns - + Configs - + Mods - + Gog files - + All files (*.*) - + Select files (configs, mods, maps, campaigns, gog files) to install... - + Replace config file? - + Do you want to replace %1? - + Downloading %1. %p% (%v MB out of %m MB) finished - + Download failed - + Unable to download all files. Encountered errors: @@ -484,409 +374,356 @@ Encountered errors: - + Install successfully downloaded? - + Installing chronicles - + Installing mod %1 - + Operation failed - + Encountered errors: - + screenshots - + Screenshot %1 - + Mod is incompatible - - CModManager - - - Can not install submod - - - - - Mod is already installed - - - - - Can not uninstall submod - - - - - Mod is not installed - - - - - Mod is already enabled - - - - - - Mod must be installed first - - - - - Mod is not compatible, please update VCMI and checkout latest mod revisions - - - - - Required mod %1 is missing - - - - - Required mod %1 is not enabled - - - - - - This mod conflicts with %1 - - - - - Mod is already disabled - - - - - This mod is needed to run %1 - - - - - Mod archive is missing - - - - - Mod with such name is already installed - - - - - Mod archive is invalid or corrupted - - - - - Failed to extract mod data - - - - - Data with this mod was not found - - - - - Mod is located in protected directory, please remove it manually: - - - - CSettingsView + Off - + Artificial Intelligence - + Interface Scaling - + Neutral AI in battles - + Enemy AI in battles - + Additional repository - + Adventure Map Allies - + Online Lobby port - + Autocombat AI in battles - + Sticks Sensitivity - + Automatic (Linear) - + Haptic Feedback - + Software Cursor - + + + Automatic - + + Mods Validation + + + + None - + xBRZ x2 - + xBRZ x3 - + xBRZ x4 - + + Full + + + + Use scalable fonts - + Online Lobby address - - Upscaling Filter + + Cursor Scaling - - Use Relative Pointer Mode - - - - - Nearest - - - - - Linear - - - - - Input - Touchscreen - - - - - Adventure Map Enemies + + Scalable + Miscellaneous + + + + + Font Scaling (experimental) + + + + + Original + + + + + Upscaling Filter + + + + + Basic + + + + + Use Relative Pointer Mode + + + + + Nearest + + + + + Linear + + + + + Input - Touchscreen + + + + + Adventure Map Enemies + + + + Show Tutorial again - + Reset - + Network - + Audio - + Relative Pointer Speed - + Music Volume - + Ignore SSL errors - + Input - Mouse - + Long Touch Duration - + % - + Controller Click Tolerance - + Touch Tap Tolerance - + Input - Controller - + Sound Volume - + Windowed - + Borderless fullscreen - + Exclusive fullscreen - + Autosave limit (0 = off) - + Downscaling Filter - + Framerate Limit - + Autosave prefix - + Mouse Click Tolerance - + Sticks Acceleration - + empty = map name prefix - + Refresh now - + Default repository - + Renderer @@ -896,7 +733,7 @@ Install successfully downloaded? - + Select display mode for game Windowed - game will run inside a window that covers part of your screen @@ -907,92 +744,92 @@ Fullscreen Exclusive Mode - game will cover entirety of your screen and will use - + Reserved screen area - + Heroes III Translation - + Check on startup - + Fullscreen - + General - + VCMI Language - + Resolution - + Autosave - + VSync - + Display index - + Network port - + Video - + Show intro - + Active - + Disabled - + Enable - + Not Installed - + Install @@ -1028,7 +865,7 @@ Fullscreen Exclusive Mode - game will cover entirety of your screen and will use - + Heroes Chronicles @@ -1036,30 +873,11 @@ Fullscreen Exclusive Mode - game will cover entirety of your screen and will use File size - - %1 B - - - - - %1 KiB - - - - + + %1 MiB - - - %1 GiB - - - - - %1 TiB - - FirstLaunchView @@ -1497,16 +1315,208 @@ error reason: ModFields - + Name - + Type + + ModStateController + + + Can not install submod + + + + + Mod is already installed + + + + + Can not uninstall submod + + + + + Mod is not installed + + + + + Mod is already enabled + + + + + + Mod must be installed first + + + + + Mod is not compatible, please update VCMI and checkout latest mod revisions + + + + + Can not enable translation mod for a different language! + + + + + Required mod %1 is missing + + + + + Mod is already disabled + + + + + Mod archive is missing + + + + + Mod with such name is already installed + + + + + Mod archive is invalid or corrupted + + + + + Failed to extract mod data + + + + + Data with this mod was not found + + + + + Mod is located in protected directory, please remove it manually: + + + + + + ModStateItemModel + + + Translation + + + + + Town + + + + + Test + + + + + Templates + + + + + Spells + + + + + Music + + + + + Maps + + + + + Sounds + + + + + Skills + + + + + + Other + + + + + Objects + + + + + + Mechanics + + + + + + Interface + + + + + Heroes + + + + + + Graphical + + + + + Expansion + + + + + Creatures + + + + + Compatibility + + + + + Artifacts + + + + + AI + + + QObject diff --git a/launcher/translation/french.ts b/launcher/translation/french.ts index 04ab48984..3c6a3f946 100644 --- a/launcher/translation/french.ts +++ b/launcher/translation/french.ts @@ -90,108 +90,84 @@ CModListModel - Translation - Traduction + Traduction - Town - Ville + Ville - Test - Test + Test - Templates - Modèles + Modèles - Spells - Sorts + Sorts - Music - Musique + Musique - Maps - Cartes + Cartes - Sounds - Sons + Sons - Skills - Compétences + Compétences - - Other - Autre + Autre - Objects - Objets + Objets - - Mechanics - Mécaniques + Mécaniques - - Interface - Interface + Interface - Heroes - Héros + Héros - - Graphical - Graphisme + Graphisme - Expansion - Extension + Extension - Creatures - Créatures + Créatures - Compatibility - Compatibilité + Compatibilité - Artifacts - Artefacts + Artefacts - AI - IA + IA @@ -238,7 +214,7 @@ - + Description Description @@ -293,194 +269,207 @@ Abandonner - + Mod name Nom du mod - + + Installed version Version installée - + + Latest version Dernière version - + Size Taille - + Download size Taille de téléchargement - + Authors Auteur(s) - + License Licence - + Contact Contact - + Compatibility Compatibilité - - + + Required VCMI version Version requise de VCMI - + Supported VCMI version Version supportée de VCMI - + please upgrade mod veuillez mettre à jour le mod - - + + mods repository index Index du dépôt de mods - + or newer ou plus récente - + Supported VCMI versions Versions supportées de VCMI - + Languages Langues - + Required mods Mods requis - + Conflicting mods Mods en conflit - This mod can not be installed or enabled because the following dependencies are not present - Ce mod ne peut pas être installé ou activé car les dépendances suivantes ne sont pas présents + Ce mod ne peut pas être installé ou activé car les dépendances suivantes ne sont pas présents - This mod can not be enabled because the following mods are incompatible with it - Ce mod ne peut pas être installé ou activé, car les dépendances suivantes sont incompatibles avec lui + Ce mod ne peut pas être installé ou activé, car les dépendances suivantes sont incompatibles avec lui - This mod cannot be disabled because it is required by the following mods - Ce mod ne peut pas être désactivé car il est requis pour les dépendances suivantes + Ce mod ne peut pas être désactivé car il est requis pour les dépendances suivantes - This mod cannot be uninstalled or updated because it is required by the following mods - Ce mod ne peut pas être désinstallé ou mis à jour car il est requis pour les dépendances suivantes + Ce mod ne peut pas être désinstallé ou mis à jour car il est requis pour les dépendances suivantes - + + This mod cannot be enabled because it translates into a different language. + + + + + This mod can not be enabled because the following dependencies are not present + + + + + This mod can not be installed because the following dependencies are not present + + + + This is a submod and it cannot be installed or uninstalled separately from its parent mod Ce sous-mod ne peut pas être installé ou mis à jour séparément du mod parent - + Notes Notes - + All supported files Tous les fichiers supportés - + Maps Cartes - + Campaigns Campagnes - + Configs Configurations - + Mods Mods - + Gog files - + All files (*.*) - + Select files (configs, mods, maps, campaigns, gog files) to install... - + Replace config file? Remplacer le fichier de configuration ? - + Do you want to replace %1? Voulez vous remplacer %1 ? - + Downloading %1. %p% (%v MB out of %m MB) finished Téléchargement %1. %p% (%v Mo sur %m Mo) terminé - + Download failed Téléchargement échoué - + Unable to download all files. Encountered errors: @@ -493,7 +482,7 @@ Erreur rencontrées: - + Install successfully downloaded? @@ -502,39 +491,39 @@ Install successfully downloaded? Installer les téchargements réussis? - + Installing chronicles - + Installing mod %1 Installer le mod %1 - + Operation failed Opération échouée - + Encountered errors: Erreurs rencontrées: - + screenshots captures d'écran - + Screenshot %1 Impression écran %1 - + Mod is incompatible Ce mod est incompatible @@ -542,109 +531,90 @@ Installer les téchargements réussis? CModManager - Can not install submod - Impossible d'installer le sous-mod + Impossible d'installer le sous-mod - Mod is already installed - Le mod est déjà installé + Le mod est déjà installé - Can not uninstall submod - Impossible de désinstaller le sousmod + Impossible de désinstaller le sousmod - Mod is not installed - Le mod n'est pas installé + Le mod n'est pas installé - Mod is already enabled - Mod déjà activé + Mod déjà activé - - Mod must be installed first - Le mode doit d'abord être installé + Le mode doit d'abord être installé - Mod is not compatible, please update VCMI and checkout latest mod revisions - Mod non compatible, veuillez mettre à jour VCMI et vérifier la dernière revision du mod + Mod non compatible, veuillez mettre à jour VCMI et vérifier la dernière revision du mod - Required mod %1 is missing - Le mod requis %1 est manquant + Le mod requis %1 est manquant - Required mod %1 is not enabled - Le mod requis %1 n'est pas activé + Le mod requis %1 n'est pas activé - - This mod conflicts with %1 - Ce mod rentre en conflit avec %1 + Ce mod rentre en conflit avec %1 - Mod is already disabled - Mod déjà désactivé + Mod déjà désactivé - This mod is needed to run %1 - Le mod est requis pour lancer %1 + Le mod est requis pour lancer %1 - Mod archive is missing - Archive du mod manquante + Archive du mod manquante - Mod with such name is already installed - Un mod avec le même nom est déjà installé + Un mod avec le même nom est déjà installé - Mod archive is invalid or corrupted - L'archive du mod est invalide ou corrompue + L'archive du mod est invalide ou corrompue - Failed to extract mod data - Echec de l'extraction des données du mod + Echec de l'extraction des données du mod - Data with this mod was not found - Les données de ce mod n'ont pas étés trouvées + Les données de ce mod n'ont pas étés trouvées - Mod is located in protected directory, please remove it manually: - Le mod est placé dans un dossier protégé, veuillez le supprimer manuellement: + Le mod est placé dans un dossier protégé, veuillez le supprimer manuellement: CSettingsView + Off Désactivé - + Artificial Intelligence Intelligence Artificielle @@ -654,187 +624,229 @@ Installer les téchargements réussis? Activé - + Enemy AI in battles IA ennemie dans les batailles - + Default repository Dépôt par défaut - + VSync Synchronisation verticalle - + Online Lobby port Port de la salle d'attente en ligne - + Autocombat AI in battles IA de combat automatique dans les batailles - + Sticks Sensitivity Sensibilité au batons - + Automatic (Linear) Automatique (Linéaire) - + Haptic Feedback Retour Tactile - + Software Cursor Curseur Logiciel - + + + Automatic Automatique - + + Mods Validation + + + + None Aucun - + xBRZ x2 xBRZ x2 - + xBRZ x3 xBRZ x3 - + xBRZ x4 xBRZ x4 - + + Full + + + + Use scalable fonts - + Online Lobby address Adresse de la salle d'attente en ligne - + + Cursor Scaling + + + + + Scalable + + + + + Miscellaneous + + + + + Font Scaling (experimental) + + + + + Original + + + + Upscaling Filter Filtre d'Agrandissement - + + Basic + + + + Use Relative Pointer Mode Utiliser le Mode de Pointeur Relatif - + Nearest Le plus Proche - + Linear Linéaire - + Input - Touchscreen Entrée - Écran tactile - + Network Réseau - + Downscaling Filter Filtre de Rétrécissement - + Show Tutorial again Remontrer le Didacticiel - + Reset Réinitialiser - + Audio Audio - + Relative Pointer Speed Vitesse de Pointeur Relatif - + Music Volume Volume de la Musique - + Ignore SSL errors Ignorer les erreurs SSL - + Input - Mouse Entrée - Sourie - + Long Touch Duration Durée de Touche Prolongée - + % % - + Controller Click Tolerance Tolérance au Clic de Contrôleur - + Touch Tap Tolerance Tolérance à la Frappe de Touche - + Input - Controller Entrée - Contrôleur - + Sound Volume Volume du Son - + Select display mode for game Windowed - game will run inside a window that covers part of your screen @@ -851,167 +863,167 @@ Mode fenêtré sans bord - le jeu s"exécutera dans une fenêtre qui couvre Mode exclusif plein écran - le jeu couvrira l"intégralité de votre écran et utilisera la résolution sélectionnée. - + Windowed Fenêtré - + Borderless fullscreen Fenêtré sans bord - + Exclusive fullscreen Plein écran exclusif - + Reserved screen area Zone d'écran réservée - + Neutral AI in battles IA neutre dans les batailles - + Autosave limit (0 = off) Limite de sauvegarde auto (0 = désactivé) - + Adventure Map Enemies Ennemis de la carte d"aventure - + Autosave prefix Préfix de sauvegarde auto. - + empty = map name prefix vide = prefix du nom de carte - + Interface Scaling Mise à l"échelle de l"interface - + Framerate Limit Limite de fréquence d"images - + Renderer Moteur de rendu - + Heroes III Translation Traduction de Heroes III - + Adventure Map Allies Alliés de la carte d"aventure - + Additional repository Dépôt supplémentaire - + Check on startup Vérifier au démarrage - + Mouse Click Tolerance Tolérance au Clic de Sourie - + Sticks Acceleration Accelération de Bâton - + Refresh now Actualiser maintenant - + Fullscreen Plein écran - + General Général - + VCMI Language Langue de VCMI - + Resolution Résolution - + Autosave Sauvegarde automatique - + Display index Index d'affichage - + Network port Port de réseau - + Video Vidéo - + Show intro Montrer l'intro - + Active Actif - + Disabled Désactivé - + Enable Activé - + Not Installed Pas Installé - + Install Installer @@ -1047,7 +1059,7 @@ Mode exclusif plein écran - le jeu couvrira l"intégralité de votre écra - + Heroes Chronicles @@ -1055,29 +1067,26 @@ Mode exclusif plein écran - le jeu couvrira l"intégralité de votre écra File size - %1 B - %1 B + %1 B - %1 KiB - %1 KiB + %1 KiB - + + %1 MiB %1 MiB - %1 GiB - %1 GiB + %1 GiB - %1 TiB - %1 TiB + %1 TiB @@ -1527,16 +1536,209 @@ Raison de l'erreur : ModFields - + Name Nom - + Type Type + + ModStateController + + + Can not install submod + Impossible d'installer le sous-mod + + + + Mod is already installed + Le mod est déjà installé + + + + Can not uninstall submod + Impossible de désinstaller le sousmod + + + + Mod is not installed + Le mod n'est pas installé + + + + Mod is already enabled + Mod déjà activé + + + + + Mod must be installed first + Le mode doit d'abord être installé + + + + Mod is not compatible, please update VCMI and checkout latest mod revisions + Mod non compatible, veuillez mettre à jour VCMI et vérifier la dernière revision du mod + + + + Can not enable translation mod for a different language! + + + + + Required mod %1 is missing + Le mod requis %1 est manquant + + + + Mod is already disabled + Mod déjà désactivé + + + + Mod archive is missing + Archive du mod manquante + + + + Mod with such name is already installed + Un mod avec le même nom est déjà installé + + + + Mod archive is invalid or corrupted + L'archive du mod est invalide ou corrompue + + + + Failed to extract mod data + Echec de l'extraction des données du mod + + + + Data with this mod was not found + Les données de ce mod n'ont pas étés trouvées + + + + Mod is located in protected directory, please remove it manually: + + Le mod est placé dans un dossier protégé, veuillez le supprimer manuellement: + + + + + ModStateItemModel + + + Translation + Traduction + + + + Town + Ville + + + + Test + Test + + + + Templates + Modèles + + + + Spells + Sorts + + + + Music + Musique + + + + Maps + Cartes + + + + Sounds + Sons + + + + Skills + Compétences + + + + + Other + Autre + + + + Objects + Objets + + + + + Mechanics + Mécaniques + + + + + Interface + Interface + + + + Heroes + Héros + + + + + Graphical + Graphisme + + + + Expansion + Extension + + + + Creatures + Créatures + + + + Compatibility + Compatibilité + + + + Artifacts + Artefacts + + + + AI + IA + + QObject diff --git a/launcher/translation/german.ts b/launcher/translation/german.ts index 659788841..e1c0f8068 100644 --- a/launcher/translation/german.ts +++ b/launcher/translation/german.ts @@ -90,108 +90,84 @@ CModListModel - Translation - Übersetzung + Übersetzung - Town - Stadt + Stadt - Test - Test + Test - Templates - Templates + Templates - Spells - Zaubersprüche + Zaubersprüche - Music - Musik + Musik - Maps - Karten + Karten - Sounds - Sounds + Sounds - Skills - Fertigkeiten + Fertigkeiten - - Other - Andere + Andere - Objects - Objekte + Objekte - - Mechanics - Mechaniken + Mechaniken - - Interface - Schnittstelle + Schnittstelle - Heroes - Helden + Helden - - Graphical - Grafisches + Grafisches - Expansion - Erweiterung + Erweiterung - Creatures - Kreaturen + Kreaturen - Compatibility - Kompatibilität + Kompatibilität - Artifacts - Artefakte + Artefakte - AI - KI + KI @@ -233,7 +209,7 @@ - + Description Beschreibung @@ -293,189 +269,202 @@ Abbrechen - + Mod name Mod-Name - + + Installed version Installierte Version - + + Latest version Letzte Version - + Size Größe - + Download size Downloadgröße - + Authors Autoren - + License Lizenz - + Contact Kontakt - + Compatibility Kompatibilität - - + + Required VCMI version Benötigte VCMI Version - + Supported VCMI version Unterstützte VCMI Version - + please upgrade mod bitte Mod upgraden - - + + mods repository index Mod Verzeichnis Index - + or newer oder neuer - + Supported VCMI versions Unterstützte VCMI Versionen - + Languages Sprachen - + Required mods Benötigte Mods - + Conflicting mods Mods mit Konflikt - This mod can not be installed or enabled because the following dependencies are not present - Diese Mod kann nicht installiert oder aktiviert werden, da die folgenden Abhängigkeiten nicht vorhanden sind + Diese Mod kann nicht installiert oder aktiviert werden, da die folgenden Abhängigkeiten nicht vorhanden sind - This mod can not be enabled because the following mods are incompatible with it - Diese Mod kann nicht aktiviert werden, da folgende Mods nicht mit dieser Mod kompatibel sind + Diese Mod kann nicht aktiviert werden, da folgende Mods nicht mit dieser Mod kompatibel sind - This mod cannot be disabled because it is required by the following mods - Diese Mod kann nicht deaktiviert werden, da sie zum Ausführen der folgenden Mods erforderlich ist + Diese Mod kann nicht deaktiviert werden, da sie zum Ausführen der folgenden Mods erforderlich ist - This mod cannot be uninstalled or updated because it is required by the following mods - Diese Mod kann nicht deinstalliert oder aktualisiert werden, da sie für die folgenden Mods erforderlich ist + Diese Mod kann nicht deinstalliert oder aktualisiert werden, da sie für die folgenden Mods erforderlich ist - + + This mod cannot be enabled because it translates into a different language. + + + + + This mod can not be enabled because the following dependencies are not present + + + + + This mod can not be installed because the following dependencies are not present + + + + This is a submod and it cannot be installed or uninstalled separately from its parent mod Dies ist eine Submod und kann nicht separat von der Hauptmod installiert oder deinstalliert werden - + Notes Anmerkungen - + All supported files Alle unterstützten Dateien - + Maps Karten - + Campaigns Kampagnen - + Configs Konfigurationen - + Mods Mods - - - Gog files - - - All files (*.*) - + Gog files + Gog-Dateien - Select files (configs, mods, maps, campaigns, gog files) to install... - + All files (*.*) + Alle Dateien (*.*) - + + Select files (configs, mods, maps, campaigns, gog files) to install... + Wähle zu installierenden Dateien aus (Konfigs, Mods, Karten, Kampagnen, Gog-Dateien)... + + + Replace config file? Konfigurationsdatei ersetzen? - + Do you want to replace %1? Soll %1 ersetzt werden? - + Downloading %1. %p% (%v MB out of %m MB) finished Downloade %1. %p% (%v MB von %m MB) abgeschlossen - + Download failed Download fehlgeschlagen - + Unable to download all files. Encountered errors: @@ -488,7 +477,7 @@ Es sind Fehler aufgetreten: - + Install successfully downloaded? @@ -497,39 +486,39 @@ Install successfully downloaded? Installation erfolgreich heruntergeladen? - + Installing chronicles - + Installation der Chronicles - + Installing mod %1 Installation von Mod %1 - + Operation failed Operation fehlgeschlagen - + Encountered errors: Aufgetretene Fehler: - + screenshots Screenshots - + Screenshot %1 Screenshot %1 - + Mod is incompatible Mod ist inkompatibel @@ -537,364 +526,387 @@ Installation erfolgreich heruntergeladen? CModManager - Can not install submod - Submod kann nicht installiert werden + Submod kann nicht installiert werden - Mod is already installed - Mod ist bereits installiert + Mod ist bereits installiert - Can not uninstall submod - Submod kann nicht deinstalliert werden + Submod kann nicht deinstalliert werden - Mod is not installed - Mod ist nicht installiert + Mod ist nicht installiert - Mod is already enabled - Mod ist bereits aktiviert + Mod ist bereits aktiviert - - Mod must be installed first - Mod muss zuerst installiert werden + Mod muss zuerst installiert werden - Mod is not compatible, please update VCMI and checkout latest mod revisions - Mod ist nicht kompatibel, bitte aktualisieren Sie VCMI und überprüfen Sie die neuesten Mod-Versionen + Mod ist nicht kompatibel, bitte aktualisieren Sie VCMI und überprüfen Sie die neuesten Mod-Versionen - Required mod %1 is missing - Der erforderliche Mod %1 fehlt + Der erforderliche Mod %1 fehlt - Required mod %1 is not enabled - Erforderliche Mod %1 ist nicht aktiviert + Erforderliche Mod %1 ist nicht aktiviert - - This mod conflicts with %1 - Diese Mod steht im Konflikt mit %1 + Diese Mod steht im Konflikt mit %1 - Mod is already disabled - Mod ist bereits deaktiviert + Mod ist bereits deaktiviert - This mod is needed to run %1 - Diese Mod wird benötigt, um %1 auszuführen + Diese Mod wird benötigt, um %1 auszuführen - Mod archive is missing - Mod-Archiv fehlt + Mod-Archiv fehlt - Mod with such name is already installed - Mod mit diesem Namen ist bereits installiert + Mod mit diesem Namen ist bereits installiert - Mod archive is invalid or corrupted - Mod-Archiv ist ungültig oder beschädigt + Mod-Archiv ist ungültig oder beschädigt - Failed to extract mod data - Mod-Daten konnten nicht extrahiert werden + Mod-Daten konnten nicht extrahiert werden - Data with this mod was not found - Daten mit dieser Mod wurden nicht gefunden + Daten mit dieser Mod wurden nicht gefunden - Mod is located in protected directory, please remove it manually: - Mod befindet sich im geschützten Verzeichnis, bitte entfernen Sie sie manuell: + Mod befindet sich im geschützten Verzeichnis, bitte entfernen Sie sie manuell: CSettingsView + Off Aus - + Artificial Intelligence Künstliche Intelligenz - + Interface Scaling Skalierung der Benutzeroberfläche - + Neutral AI in battles Neutrale KI in Kämpfen - + Enemy AI in battles Gegnerische KI in Kämpfen - + Additional repository Zusätzliches Repository - + Downscaling Filter - + Herunterskalierungsfilter - + Adventure Map Allies Abenteuerkarte Verbündete - + Online Lobby port Online-Lobby-Port - + Autocombat AI in battles Autokampf-KI in Kämpfen - + Sticks Sensitivity Sticks Empfindlichkeit - + Automatic (Linear) - + Automatisch (linear) - + Haptic Feedback Haptisches Feedback - + Software Cursor Software-Cursor - + + + Automatic - + Automatisch - + + Mods Validation + Mod-Validierung + + + None - + Keiner - + xBRZ x2 - + xBRZ x2 - + xBRZ x3 - + xBRZ x3 - + xBRZ x4 - + xBRZ x4 - + + Full + Voll + + + Use scalable fonts - + Skalierbare Schriftarten verwenden - + Online Lobby address Adresse der Online-Lobby - + + Cursor Scaling + Cursor-Skalierung + + + + Scalable + Skalierbar + + + + Miscellaneous + Sonstiges + + + + Font Scaling (experimental) + Schriftskalierung (experimentell) + + + + Original + Original + + + Upscaling Filter Hochskalierungsfilter - + + Basic + Grundlegend + + + Use Relative Pointer Mode Relativen Zeigermodus verwenden - + Nearest Nearest - + Linear Linear - + Input - Touchscreen Eingabe - Touchscreen - + Adventure Map Enemies Abenteuerkarte Feinde - + Show Tutorial again Zeige Tutorial erneut - + Reset Zurücksetzen - + Network Netzwerk - + Audio Audio - + Relative Pointer Speed Relative Zeigergeschwindigkeit - + Music Volume Musik Lautstärke - + Ignore SSL errors SSL-Fehler ignorieren - + Input - Mouse Eingabe - Maus - + Long Touch Duration Dauer der Berührung für "lange Berührung" - + % % - + Controller Click Tolerance Toleranz bei Controller Klick - + Touch Tap Tolerance Toleranz bei Berührungen - + Input - Controller Eingabe - Controller - + Sound Volume Sound-Lautstärke - + Windowed Fenstermodus - + Borderless fullscreen Randloser Vollbildmodus - + Exclusive fullscreen Exklusiver Vollbildmodus - + Autosave limit (0 = off) Limit für Autospeicherung (0 = aus) - + Framerate Limit Limit der Bildrate - + Autosave prefix Präfix für Autospeicherung - + Mouse Click Tolerance Toleranz bei Mausklick - + Sticks Acceleration Sticks Beschleunigung - + empty = map name prefix leer = Kartenname als Präfix - + Refresh now Jetzt aktualisieren - + Default repository Standard Repository - + Renderer Renderer @@ -904,7 +916,7 @@ Installation erfolgreich heruntergeladen? An - + Select display mode for game Windowed - game will run inside a window that covers part of your screen @@ -921,92 +933,92 @@ Randloser Fenstermodus - das Spiel läuft in einem Fenster, das den gesamten Bil Exklusiver Vollbildmodus - das Spiel bedeckt den gesamten Bildschirm und verwendet die gewählte Auflösung. - + Reserved screen area Reservierter Bildschirmbereich - + Heroes III Translation Heroes III Übersetzung - + Check on startup Beim Start prüfen - + Fullscreen Vollbild - + General Allgemein - + VCMI Language VCMI-Sprache - + Resolution Auflösung - + Autosave Autospeichern - + VSync VSync - + Display index Anzeige-Index - + Network port Netzwerk-Port - + Video Video - + Show intro Intro anzeigen - + Active Aktiv - + Disabled Deaktiviert - + Enable Aktivieren - + Not Installed Nicht installiert - + Install Installieren @@ -1016,63 +1028,60 @@ Exklusiver Vollbildmodus - das Spiel bedeckt den gesamten Bildschirm und verwend File cannot opened - + Datei kann nicht geöffnet werden Invalid file selected - Ungültige Datei ausgewählt + Ungültige Datei ausgewählt You have to select an gog installer file! - + Sie müssen eine Gog-Installer-Datei auswählen! You have to select an chronicle installer file! - + Sie müssen eine Chronicle-Installationsdatei auswählen! Extracting error! - Fehler beim Extrahieren! + Fehler beim Extrahieren! - + Heroes Chronicles - + Heroes Chronicles File size - %1 B - %1 B + %1 B - %1 KiB - %1 KiB + %1 KiB - + + %1 MiB %1 MiB - %1 GiB - %1 GiB + %1 GiB - %1 TiB - %1 TiB + %1 TiB @@ -1379,18 +1388,18 @@ Bitte wählen Sie ein Verzeichnis mit Heroes III: Complete Edition oder Heroes I Stream error while extracting files! error reason: - Stream-Fehler beim Extrahieren von Dateien! + Stream-Fehler beim Extrahieren von Dateien! Fehlerursache: Not a supported Inno Setup installer! - Kein unterstütztes Inno Setup Installationsprogramm! + Kein unterstütztes Inno Setup Installationsprogramm! VCMI was compiled without innoextract support, which is needed to extract exe files! - + VCMI wurde ohne innoextract-Unterstützung kompiliert, die zum Extrahieren von exe-Dateien benötigt wird! @@ -1522,16 +1531,209 @@ Fehlerursache: ModFields - + Name Name - + Type Typ + + ModStateController + + + Can not install submod + Submod kann nicht installiert werden + + + + Mod is already installed + Mod ist bereits installiert + + + + Can not uninstall submod + Submod kann nicht deinstalliert werden + + + + Mod is not installed + Mod ist nicht installiert + + + + Mod is already enabled + Mod ist bereits aktiviert + + + + + Mod must be installed first + Mod muss zuerst installiert werden + + + + Mod is not compatible, please update VCMI and checkout latest mod revisions + Mod ist nicht kompatibel, bitte aktualisieren Sie VCMI und überprüfen Sie die neuesten Mod-Versionen + + + + Can not enable translation mod for a different language! + + + + + Required mod %1 is missing + Der erforderliche Mod %1 fehlt + + + + Mod is already disabled + Mod ist bereits deaktiviert + + + + Mod archive is missing + Mod-Archiv fehlt + + + + Mod with such name is already installed + Mod mit diesem Namen ist bereits installiert + + + + Mod archive is invalid or corrupted + Mod-Archiv ist ungültig oder beschädigt + + + + Failed to extract mod data + Mod-Daten konnten nicht extrahiert werden + + + + Data with this mod was not found + Daten mit dieser Mod wurden nicht gefunden + + + + Mod is located in protected directory, please remove it manually: + + Mod befindet sich im geschützten Verzeichnis, bitte entfernen Sie sie manuell: + + + + + ModStateItemModel + + + Translation + Übersetzung + + + + Town + Stadt + + + + Test + Test + + + + Templates + Templates + + + + Spells + Zaubersprüche + + + + Music + Musik + + + + Maps + Karten + + + + Sounds + Sounds + + + + Skills + Fertigkeiten + + + + + Other + Andere + + + + Objects + Objekte + + + + + Mechanics + Mechaniken + + + + + Interface + Schnittstelle + + + + Heroes + Helden + + + + + Graphical + Grafisches + + + + Expansion + Erweiterung + + + + Creatures + Kreaturen + + + + Compatibility + Kompatibilität + + + + Artifacts + Artefakte + + + + AI + KI + + QObject diff --git a/launcher/translation/polish.ts b/launcher/translation/polish.ts index c30a428c1..154c2bf1d 100644 --- a/launcher/translation/polish.ts +++ b/launcher/translation/polish.ts @@ -90,108 +90,84 @@ CModListModel - Translation - Tłumaczenie + Tłumaczenie - Town - Miasto + Miasto - Test - Test + Test - Templates - Szablony + Szablony - Spells - Zaklęcia + Zaklęcia - Music - Muzyczny + Muzyczny - Maps - Mapy + Mapy - Sounds - Dźwięki + Dźwięki - Skills - Umiejętności + Umiejętności - - Other - Inne + Inne - Objects - Obiekty + Obiekty - - Mechanics - Mechaniki + Mechaniki - - Interface - Interfejs + Interfejs - Heroes - Bohaterowie + Bohaterowie - - Graphical - Graficzny + Graficzny - Expansion - Dodatek + Dodatek - Creatures - Stworzenia + Stworzenia - Compatibility - Kompatybilność + Kompatybilność - Artifacts - Artefakty + Artefakty - AI - AI + AI @@ -233,7 +209,7 @@ - + Description Opis @@ -293,189 +269,202 @@ Przerwij - + Mod name Nazwa moda - + + Installed version Zainstalowana wersja - + + Latest version Najnowsza wersja - + Size Rozmiar - + Download size Rozmiar pobierania - + Authors Autorzy - + License Licencja - + Contact Kontakt - + Compatibility Kompatybilność - - + + Required VCMI version Wymagana wersja VCMI - + Supported VCMI version Wspierana wersja VCMI - + please upgrade mod proszę zaktualizować moda - - + + mods repository index indeks repozytorium modów - + or newer lub nowsze - + Supported VCMI versions Wspierane wersje VCMI - + Languages Języki - + Required mods Wymagane mody - + Conflicting mods Konfliktujące mody - This mod can not be installed or enabled because the following dependencies are not present - Ten mod nie może zostać zainstalowany lub włączony ponieważ następujące zależności nie zostały spełnione + Ten mod nie może zostać zainstalowany lub włączony ponieważ następujące zależności nie zostały spełnione - This mod can not be enabled because the following mods are incompatible with it - Ten mod nie może zostać włączony ponieważ następujące mody są z nim niekompatybilne + Ten mod nie może zostać włączony ponieważ następujące mody są z nim niekompatybilne - This mod cannot be disabled because it is required by the following mods - Ten mod nie może zostać wyłączony ponieważ jest wymagany do uruchomienia następujących modów + Ten mod nie może zostać wyłączony ponieważ jest wymagany do uruchomienia następujących modów - This mod cannot be uninstalled or updated because it is required by the following mods - Ten mod nie może zostać odinstalowany lub zaktualizowany ponieważ jest wymagany do uruchomienia następujących modów + Ten mod nie może zostać odinstalowany lub zaktualizowany ponieważ jest wymagany do uruchomienia następujących modów - + + This mod cannot be enabled because it translates into a different language. + + + + + This mod can not be enabled because the following dependencies are not present + + + + + This mod can not be installed because the following dependencies are not present + + + + This is a submod and it cannot be installed or uninstalled separately from its parent mod To jest moduł składowy innego moda i nie może być zainstalowany lub odinstalowany oddzielnie od moda nadrzędnego - + Notes Uwagi - + All supported files Wszystkie wspierane pliki - + Maps Mapy - + Campaigns Kampanie - + Configs Konfiguracje - + Mods Mody - + Gog files - + All files (*.*) - + Select files (configs, mods, maps, campaigns, gog files) to install... - + Replace config file? Zastąpić plik konfiguracji? - + Do you want to replace %1? Czy chcesz zastąpić %1? - + Downloading %1. %p% (%v MB out of %m MB) finished Pobieranie %1. %p% (%v MB z %m MB) ukończono - + Download failed Pobieranie nieudane - + Unable to download all files. Encountered errors: @@ -488,7 +477,7 @@ Napotkane błędy: - + Install successfully downloaded? @@ -497,39 +486,39 @@ Install successfully downloaded? Zainstalować pomyślnie pobrane? - + Installing chronicles - + Installing mod %1 Instalowanie modyfikacji %1 - + Operation failed Operacja nieudana - + Encountered errors: Napotkane błędy: - + screenshots zrzuty ekranu - + Screenshot %1 Zrzut ekranu %1 - + Mod is incompatible Mod jest niekompatybilny @@ -537,364 +526,387 @@ Zainstalować pomyślnie pobrane? CModManager - Can not install submod - Nie można zainstalować submoda + Nie można zainstalować submoda - Mod is already installed - Mod jest już zainstalowany + Mod jest już zainstalowany - Can not uninstall submod - Nie można odinstalować submoda + Nie można odinstalować submoda - Mod is not installed - Mod nie jest zainstalowany + Mod nie jest zainstalowany - Mod is already enabled - Mod jest już włączony + Mod jest już włączony - - Mod must be installed first - Mod musi zostać najpierw zainstalowany + Mod musi zostać najpierw zainstalowany - Mod is not compatible, please update VCMI and checkout latest mod revisions - Mod nie jest kompatybilny, proszę zaktualizować VCMI i odświeżyć listę modów + Mod nie jest kompatybilny, proszę zaktualizować VCMI i odświeżyć listę modów - Required mod %1 is missing - Brakuje wymaganego moda %1 + Brakuje wymaganego moda %1 - Required mod %1 is not enabled - Wymagany mod %1 jest wyłączony + Wymagany mod %1 jest wyłączony - - This mod conflicts with %1 - Ten mod konfliktuje z %1 + Ten mod konfliktuje z %1 - Mod is already disabled - Mod jest już wyłączony + Mod jest już wyłączony - This mod is needed to run %1 - Ten mod jest potrzebny do uruchomienia %1 + Ten mod jest potrzebny do uruchomienia %1 - Mod archive is missing - Brakuje archiwum modyfikacji + Brakuje archiwum modyfikacji - Mod with such name is already installed - Mod z taką nazwą jest już zainstalowany + Mod z taką nazwą jest już zainstalowany - Mod archive is invalid or corrupted - Archiwum moda jest niepoprawne lub uszkodzone + Archiwum moda jest niepoprawne lub uszkodzone - Failed to extract mod data - Nieudane wyodrębnienie danych moda + Nieudane wyodrębnienie danych moda - Data with this mod was not found - Dane z tym modem nie zostały znalezione + Dane z tym modem nie zostały znalezione - Mod is located in protected directory, please remove it manually: - Mod jest umiejscowiony w chronionym folderze, proszę go usunąć ręcznie: + Mod jest umiejscowiony w chronionym folderze, proszę go usunąć ręcznie: CSettingsView + Off Wyłączony - + Artificial Intelligence Sztuczna Inteligencja - + Interface Scaling Skala interfejsu - + Neutral AI in battles AI bitewne jednostek neutralnych - + Enemy AI in battles AI bitewne wrogów - + Additional repository Dodatkowe repozytorium - + Downscaling Filter - + Adventure Map Allies AI sojuszników mapy przygody - + Online Lobby port Port lobby online - + Autocombat AI in battles AI szybkiej walki - + Sticks Sensitivity Czułość gałek - + Automatic (Linear) - + Haptic Feedback Wibracje - + Software Cursor Kursor programowy - + + + Automatic - + + Mods Validation + + + + None - + xBRZ x2 - + xBRZ x3 - + xBRZ x4 - + + Full + + + + Use scalable fonts - + Online Lobby address Adres lobby online - + + Cursor Scaling + + + + + Scalable + + + + + Miscellaneous + + + + + Font Scaling (experimental) + + + + + Original + + + + Upscaling Filter Filtr wyostrzający - + + Basic + + + + Use Relative Pointer Mode Użyj relatywnego trybu kursora - + Nearest Najbliższych - + Linear Liniowy - + Input - Touchscreen Sterowanie - Ekran dotykowy - + Adventure Map Enemies AI wrogów mapy przygody - + Show Tutorial again Pokaż ponownie samouczek - + Reset Zresetuj - + Network Sieć - + Audio Dźwięk i muzyka - + Relative Pointer Speed Prędkość kursora w trybie relatywnym - + Music Volume Głośność muzyki - + Ignore SSL errors Ignoruj błędy SSL - + Input - Mouse Sterowanie - Mysz - + Long Touch Duration Czas do długiego dotyku - + % % - + Controller Click Tolerance Tolerancja na kliknięcia poza elementami (kontroler) - + Touch Tap Tolerance Tolerancja na nietrafianie dotykiem w elementy - + Input - Controller Sterowanie - Kontroler - + Sound Volume Głośność dźwięku - + Windowed Okno - + Borderless fullscreen Pełny ekran (tryb okna) - + Exclusive fullscreen Pełny ekran klasyczny - + Autosave limit (0 = off) Limit autozapisów (0 = brak) - + Framerate Limit Limit FPS - + Autosave prefix Przedrostek autozapisu - + Mouse Click Tolerance Tolerancja na kliknięcia poza elementami (mysz) - + Sticks Acceleration Przyspieszenie gałek - + empty = map name prefix puste = przedrostek z nazwy mapy - + Refresh now Odśwież - + Default repository Domyślne repozytorium - + Renderer Renderer @@ -904,7 +916,7 @@ Zainstalować pomyślnie pobrane? Włączony - + Select display mode for game Windowed - game will run inside a window that covers part of your screen @@ -921,92 +933,92 @@ Pełny ekran w trybie okna - gra uruchomi się w oknie przysłaniającym cały e Pełny ekran klasyczny - gra przysłoni cały ekran uruchamiając się w wybranej przez ciebie rozdzielczości ekranu. - + Reserved screen area Zarezerwowany obszar ekranu - + Heroes III Translation Tłumaczenie Heroes III - + Check on startup Sprawdzaj przy uruchomieniu - + Fullscreen Pełny ekran - + General Ogólne - + VCMI Language Język VCMI - + Resolution Rozdzielczość - + Autosave Autozapis - + VSync Synchronizacja pionowa (VSync) - + Display index Numer wyświetlacza - + Network port Port sieciowy - + Video Obraz - + Show intro Pokaż intro - + Active Aktywny - + Disabled Wyłączone - + Enable Włącz - + Not Installed Nie zainstalowano - + Install Zainstaluj @@ -1042,7 +1054,7 @@ Pełny ekran klasyczny - gra przysłoni cały ekran uruchamiając się w wybrane - + Heroes Chronicles @@ -1050,29 +1062,26 @@ Pełny ekran klasyczny - gra przysłoni cały ekran uruchamiając się w wybrane File size - %1 B - %1 B + %1 B - %1 KiB - %1 KiB + %1 KiB - + + %1 MiB %1 MiB - %1 GiB - %1 GiB + %1 GiB - %1 TiB - %1 TiB + %1 TiB @@ -1522,16 +1531,209 @@ powód błędu: ModFields - + Name Nazwa - + Type Typ + + ModStateController + + + Can not install submod + Nie można zainstalować submoda + + + + Mod is already installed + Mod jest już zainstalowany + + + + Can not uninstall submod + Nie można odinstalować submoda + + + + Mod is not installed + Mod nie jest zainstalowany + + + + Mod is already enabled + Mod jest już włączony + + + + + Mod must be installed first + Mod musi zostać najpierw zainstalowany + + + + Mod is not compatible, please update VCMI and checkout latest mod revisions + Mod nie jest kompatybilny, proszę zaktualizować VCMI i odświeżyć listę modów + + + + Can not enable translation mod for a different language! + + + + + Required mod %1 is missing + Brakuje wymaganego moda %1 + + + + Mod is already disabled + Mod jest już wyłączony + + + + Mod archive is missing + Brakuje archiwum modyfikacji + + + + Mod with such name is already installed + Mod z taką nazwą jest już zainstalowany + + + + Mod archive is invalid or corrupted + Archiwum moda jest niepoprawne lub uszkodzone + + + + Failed to extract mod data + Nieudane wyodrębnienie danych moda + + + + Data with this mod was not found + Dane z tym modem nie zostały znalezione + + + + Mod is located in protected directory, please remove it manually: + + Mod jest umiejscowiony w chronionym folderze, proszę go usunąć ręcznie: + + + + + ModStateItemModel + + + Translation + Tłumaczenie + + + + Town + Miasto + + + + Test + Test + + + + Templates + Szablony + + + + Spells + Zaklęcia + + + + Music + Muzyczny + + + + Maps + Mapy + + + + Sounds + Dźwięki + + + + Skills + Umiejętności + + + + + Other + Inne + + + + Objects + Obiekty + + + + + Mechanics + Mechaniki + + + + + Interface + Interfejs + + + + Heroes + Bohaterowie + + + + + Graphical + Graficzny + + + + Expansion + Dodatek + + + + Creatures + Stworzenia + + + + Compatibility + Kompatybilność + + + + Artifacts + Artefakty + + + + AI + AI + + QObject diff --git a/launcher/translation/portuguese.ts b/launcher/translation/portuguese.ts index dfe5d75c8..a01983547 100644 --- a/launcher/translation/portuguese.ts +++ b/launcher/translation/portuguese.ts @@ -44,7 +44,7 @@ Check for updates - Verificar por atualizações + Verificar atualizações @@ -54,7 +54,7 @@ Log files directory - Diretório do arquivo de registro + Diretório de arquivos de registro @@ -79,7 +79,7 @@ Project homepage - Página da web do projeto + Página do projeto @@ -87,113 +87,6 @@ Relatar um erro - - CModListModel - - - Translation - Tradução - - - - Town - Cidade - - - - Test - Teste - - - - Templates - Modelos - - - - Spells - Feitiços - - - - Music - Música - - - - Maps - Mapas - - - - Sounds - Sons - - - - Skills - Habilidades - - - - - Other - Outros - - - - Objects - Objetos - - - - - Mechanics - Mecânicas - - - - - Interface - Interface - - - - Heroes - Heróis - - - - - Graphical - Gráficos - - - - Expansion - Expansão - - - - Creatures - Criaturas - - - - Compatibility - Compatibilidade - - - - Artifacts - Artefatos - - - - AI - IA - - CModListView @@ -233,7 +126,7 @@ - + Description Descrição @@ -293,189 +186,186 @@ Cancelar - + Mod name Nome do mod - + + Installed version Versão instalada - + + Latest version Última versão - + Size Tamanho - + Download size Tamanho do download - + Authors Autores - + License Licença - + Contact Contato - + Compatibility Compatibilidade - - + + Required VCMI version Versão do VCMI necessária - + Supported VCMI version Versão do VCMI suportada - + please upgrade mod por favor, atualize o mod - - + + mods repository index índice do repositório de mods - + or newer ou mais recente - + Supported VCMI versions Versões do VCMI suportadas - + Languages Idiomas - + Required mods Mods requeridos - + Conflicting mods Mods conflitantes - - This mod can not be installed or enabled because the following dependencies are not present - Este mod não pode ser instalado ou ativado porque as seguintes dependências não estão presentes + + This mod cannot be enabled because it translates into a different language. + Este mod não pode ser ativado porque traduz para um idioma diferente. - - This mod can not be enabled because the following mods are incompatible with it - Este mod não pode ser ativado porque os seguintes mods são incompatíveis com ele + + This mod can not be enabled because the following dependencies are not present + Este mod não pode ser ativado porque as seguintes dependências estão ausentes - - This mod cannot be disabled because it is required by the following mods - Este mod não pode ser desativado porque é necessário pelos seguintes mods + + This mod can not be installed because the following dependencies are not present + Este mod não pode ser instalado porque as seguintes dependências estão ausentes - - This mod cannot be uninstalled or updated because it is required by the following mods - Este mod não pode ser desinstalado ou atualizado porque é necessário pelos seguintes mods - - - + This is a submod and it cannot be installed or uninstalled separately from its parent mod Este é um submod e não pode ser instalado ou desinstalado separadamente do seu mod principal - + Notes Notas - + All supported files Todos os arquivos suportados - + Maps Mapas - + Campaigns Campanhas - + Configs Configurações - + Mods Mods - - - Gog files - - - All files (*.*) - + Gog files + Arquivos GOG - Select files (configs, mods, maps, campaigns, gog files) to install... - + All files (*.*) + Todos os arquivos (*.*) - + + Select files (configs, mods, maps, campaigns, gog files) to install... + Selecione arquivos (configurações, mods, mapas, campanhas, arquivos GOG) para instalar... + + + Replace config file? Substituir arquivo de configuração? - + Do you want to replace %1? Você deseja substituir %1? - + Downloading %1. %p% (%v MB out of %m MB) finished Baixando %1. %p% (%v MB de %m MB) concluído - + Download failed Falha no download - + Unable to download all files. Encountered errors: @@ -483,418 +373,364 @@ Encountered errors: Não foi possível baixar todos os arquivos. -Encontrados os seguintes erros: +Erros encontrados: - + Install successfully downloaded? -Instalar o download realizado com sucesso? +O download da instalação foi bem-sucedido? - + Installing chronicles - + Instalando crônicas - + Installing mod %1 Instalando mod %1 - + Operation failed Falha na operação - + Encountered errors: Erros encontrados: - + screenshots capturas de tela - + Screenshot %1 Captura de tela %1 - + Mod is incompatible O mod é incompatível - - CModManager - - - Can not install submod - Não é possível instalar o submod - - - - Mod is already installed - O mod já está instalado - - - - Can not uninstall submod - Não é possível desinstalar o submod - - - - Mod is not installed - O mod não está instalado - - - - Mod is already enabled - O mod já está ativado - - - - - Mod must be installed first - O mod deve ser instalado primeiro - - - - Mod is not compatible, please update VCMI and checkout latest mod revisions - O mod não é compatível, por favor, atualize o VCMI e verifique as últimas revisões do mod - - - - Required mod %1 is missing - O mod necessário %1 está faltando - - - - Required mod %1 is not enabled - O mod necessário %1 não está ativado - - - - - This mod conflicts with %1 - Este mod entra em conflito com %1 - - - - Mod is already disabled - O mod já está desativado - - - - This mod is needed to run %1 - Este mod é necessário para executar %1 - - - - Mod archive is missing - O arquivo do mod está faltando - - - - Mod with such name is already installed - Um mod com esse nome já está instalado - - - - Mod archive is invalid or corrupted - O arquivo do mod é inválido ou está corrompido - - - - Failed to extract mod data - Falha ao extrair os dados do mod - - - - Data with this mod was not found - Não foram encontrados dados com este mod - - - - Mod is located in protected directory, please remove it manually: - - O mod está localizado em um diretório protegido, por favor, remova-o manualmente: - - - CSettingsView + Off Desativado - + Artificial Intelligence - Inteligência Artificial + Inteligência artificial - + Interface Scaling - Escala da Interface + Escala da interface - + Neutral AI in battles IA neutra nas batalhas - + Enemy AI in battles IA inimiga em batalhas - + Additional repository Repositório adicional - + Adventure Map Allies - Aliados do Mapa de Aventura + Aliados do mapa de aventura - + Online Lobby port - Porta da Sala de Espera On-line + Porta da sala de espera on-line - + Autocombat AI in battles IA de combate automático nas batalhas - + Sticks Sensitivity - Sensibilidade dos Analógicos + Sensibilidade dos analógicos - + Automatic (Linear) - Automático (Linear) + Automático (linear) - + Haptic Feedback - Resposta Tátil + Resposta tátil - + Software Cursor - Cursor por Software + Cursor por software - + + + Automatic Automático - + + Mods Validation + Validação de mods + + + None Nenhum - + xBRZ x2 xBRZ x2 - + xBRZ x3 xBRZ x3 - + xBRZ x4 xBRZ x4 - + + Full + Completo + + + + Use scalable fonts + Usar fontes escaláveis + + + Online Lobby address - Endereço da Sala de Espera On-line + Endereço da sala de espera on-line - + + Cursor Scaling + Escala do cursor + + + + Scalable + Escalável + + + + Miscellaneous + Diversos + + + + Font Scaling (experimental) + Escala da fonte (experimental) + + + + Original + Original + + + Upscaling Filter - Filtro de Aumento de Escala + Filtro de aumento de escala - + + Basic + Básico + + + Use Relative Pointer Mode - Usar Modo de Ponteiro Relativo + Usar modo de ponteiro relativo - + Nearest - Mais Próximo + Mais próximo - + Linear Linear - + Input - Touchscreen - Entrada - Tela de Toque + Entrada - tela de toque - + Adventure Map Enemies - Inimigos do Mapa de Aventura + Inimigos do mapa de aventura - + Show Tutorial again - Mostrar o Tutorial novamente + Mostrar o tutorial novamente - + Reset Redefinir - - Use scalable fonts - Usar Fontes Escaláveis - - - + Network Linear - + Audio Áudio - + Relative Pointer Speed - Velocidade do Ponteiro Relativo + Velocidade do ponteiro relativo - + Music Volume - Volume da Música + Volume da música - + Ignore SSL errors Ignorar erros SSL - + Input - Mouse - Entrada - Mouse + Entrada - mouse - + Long Touch Duration - Duração do Toque Longo + Duração do toque longo - + % % - + Controller Click Tolerance - Tolerância de Clique do Controle + Tolerância de clique do controle - + Touch Tap Tolerance - Tolerância de Toque Tátil + Tolerância de toque tátil - + Input - Controller - Entrada - Controle + Entrada - controle - + Sound Volume - Volume do Som + Volume do som - + Windowed Janela - + Borderless fullscreen Tela cheia sem bordas - + Exclusive fullscreen Tela cheia exclusiva - + Autosave limit (0 = off) Limite de salvamento automático (0 = sem limite) - + Downscaling Filter - Filtro de Redução de Escala + Filtro de redução de escala - + Framerate Limit - Limite de Taxa de Quadros + Limite de taxa de quadros - + Autosave prefix Prefixo do salvamento automático - + Mouse Click Tolerance - Tolerância de Clique do Mouse + Tolerância de clique do mouse - + Sticks Acceleration - Aceleração dos Analógicos + Aceleração dos analógicos - + empty = map name prefix vazio = prefixo do mapa - + Refresh now Atualizar - + Default repository Repositório padrão - + Renderer Renderizador @@ -904,7 +740,7 @@ Instalar o download realizado com sucesso? Ativado - + Select display mode for game Windowed - game will run inside a window that covers part of your screen @@ -921,92 +757,92 @@ Modo de janela sem bordas - o jogo será executado em uma janela que cobre toda Modo de tela cheia exclusivo - o jogo cobrirá toda a sua tela e usará a resolução selecionada. - + Reserved screen area Área de tela reservada - + Heroes III Translation Tradução do Heroes III - + Check on startup Verificar na inicialização - + Fullscreen Tela cheia - + General Geral - + VCMI Language Idioma do VCMI - + Resolution Resolução - + Autosave Salvamento automático - + VSync - VSync + Sincronização vertical (VSync) - + Display index Índice de exibição - + Network port Porta de rede - + Video Vídeo - + Show intro Mostrar introdução - + Active Ativo - + Disabled Desativado - + Enable Ativar - + Not Installed - Não Instalado + Não instalado - + Install Instalar @@ -1016,64 +852,45 @@ Modo de tela cheia exclusivo - o jogo cobrirá toda a sua tela e usará a resolu File cannot opened - + Não foi possível abrir o arquivo Invalid file selected - Arquivo selecionado inválido + Arquivo selecionado inválido You have to select an gog installer file! - + Você precisa selecionar um arquivo de instalação do GOG! You have to select an chronicle installer file! - + Você precisa selecionar um arquivo de instalação do Heroes Chronicles! Extracting error! - Erro ao extrair! + Erro ao extrair! - + Heroes Chronicles - + Heroes Chronicles File size - - %1 B - %1 B - - - - %1 KiB - %1 KiB - - - + + %1 MiB %1 MiB - - - %1 GiB - %1 GiB - - - - %1 TiB - %1 TiB - FirstLaunchView @@ -1090,7 +907,7 @@ Modo de tela cheia exclusivo - o jogo cobrirá toda a sua tela e usará a resolu Mods Preset - Predefinição de Mod + Predefinição de mod @@ -1220,7 +1037,7 @@ O instalador offline consiste em duas partes, .exe e .bin. Certifique-se de baix Manual Installation - Instalação Manual + Instalação manual @@ -1246,7 +1063,7 @@ O instalador offline consiste em duas partes, .exe e .bin. Certifique-se de baix Install VCMI Mod Preset - Instalar Predefinição de Mod do VCMI + Instalar predefinição de mod do VCMI @@ -1370,7 +1187,7 @@ Por favor, selecione o diretório com Heroes III: Complete Edition ou Heroes III Image Viewer - Visualizador de Imagens + Visualizador de imagens @@ -1379,18 +1196,18 @@ Por favor, selecione o diretório com Heroes III: Complete Edition ou Heroes III Stream error while extracting files! error reason: - Erro de fluxo ao extrair arquivos! + Erro de fluxo ao extrair arquivos! Motivo do erro: Not a supported Inno Setup installer! - Instalador Inno Setup não suportado! + Instalador Inno Setup não suportado! VCMI was compiled without innoextract support, which is needed to extract exe files! - + O VCMI foi compilado sem suporte ao innoextract, que é necessário para extrair arquivos EXE! @@ -1506,7 +1323,7 @@ Motivo do erro: Map Editor - Editor de Mapas + Editor de mapas @@ -1522,16 +1339,209 @@ Motivo do erro: ModFields - + Name Nome - + Type Tipo + + ModStateController + + + Can not install submod + Não é possível instalar o submod + + + + Mod is already installed + O mod já está instalado + + + + Can not uninstall submod + Não é possível desinstalar o submod + + + + Mod is not installed + O mod não está instalado + + + + Mod is already enabled + O mod já está ativado + + + + + Mod must be installed first + O mod deve ser instalado primeiro + + + + Mod is not compatible, please update VCMI and checkout latest mod revisions + O mod não é compatível, por favor, atualize o VCMI e verifique as últimas revisões do mod + + + + Can not enable translation mod for a different language! + Não é possível ativar o mod de tradução para um idioma diferente! + + + + Required mod %1 is missing + O mod necessário %1 está faltando + + + + Mod is already disabled + O mod já está desativado + + + + Mod archive is missing + O arquivo do mod está faltando + + + + Mod with such name is already installed + Um mod com esse nome já está instalado + + + + Mod archive is invalid or corrupted + O arquivo do mod é inválido ou está corrompido + + + + Failed to extract mod data + Falha ao extrair os dados do mod + + + + Data with this mod was not found + Não foram encontrados dados com este mod + + + + Mod is located in protected directory, please remove it manually: + + O mod está localizado em um diretório protegido, por favor, remova-o manualmente: + + + + + ModStateItemModel + + + Translation + Tradução + + + + Town + Cidade + + + + Test + Teste + + + + Templates + Modelos + + + + Spells + Feitiços + + + + Music + Música + + + + Maps + Mapas + + + + Sounds + Sons + + + + Skills + Habilidades + + + + + Other + Outros + + + + Objects + Objetos + + + + + Mechanics + Mecânicas + + + + + Interface + Interface + + + + Heroes + Heróis + + + + + Graphical + Gráficos + + + + Expansion + Expansão + + + + Creatures + Criaturas + + + + Compatibility + Compatibilidade + + + + Artifacts + Artefatos + + + + AI + IA + + QObject diff --git a/launcher/translation/russian.ts b/launcher/translation/russian.ts index b63ae0386..c30057e7c 100644 --- a/launcher/translation/russian.ts +++ b/launcher/translation/russian.ts @@ -90,108 +90,80 @@ CModListModel - Translation - Перевод + Перевод - Town - Город + Город - Test - Тест + Тест - Templates - Шаблоны карт + Шаблоны карт - Spells - Заклинания + Заклинания - Music - Музыка + Музыка - - Maps - - - - Sounds - Звуки + Звуки - Skills - Навыки + Навыки - - Other - Иное + Иное - Objects - Объекты + Объекты - - Mechanics - Механика + Механика - - Interface - Интерфейс + Интерфейс - Heroes - Герои + Герои - - Graphical - Графика + Графика - Expansion - Дополнение + Дополнение - Creatures - Существа + Существа - Compatibility - Совместимость + Совместимость - Artifacts - Артефакт + Артефакт - AI - ИИ + ИИ @@ -233,7 +205,7 @@ - + Description Описание @@ -293,189 +265,202 @@ Отмена - + Mod name Название мода - + + Installed version Установленная версия - + + Latest version Последняя версия - + Size - + Download size Размер загрузки - + Authors Авторы - + License Лицензия - + Contact Контакты - + Compatibility Совместимость - - + + Required VCMI version Требуемая версия VCMI - + Supported VCMI version Поддерживаемая версия VCMI - + please upgrade mod - - + + mods repository index - + or newer - + Supported VCMI versions Поддерживаемые версии VCMI - + Languages Языки - + Required mods Зависимости - + Conflicting mods Конфликтующие моды - This mod can not be installed or enabled because the following dependencies are not present - Этот мод не может быть установлен или активирован, так как отсутствуют следующие зависимости + Этот мод не может быть установлен или активирован, так как отсутствуют следующие зависимости - This mod can not be enabled because the following mods are incompatible with it - Этот мод не может быть установлен или активирован, так как следующие моды несовместимы с этим + Этот мод не может быть установлен или активирован, так как следующие моды несовместимы с этим - This mod cannot be disabled because it is required by the following mods - Этот мод не может быть выключен, так как он является зависимостью для следующих + Этот мод не может быть выключен, так как он является зависимостью для следующих - This mod cannot be uninstalled or updated because it is required by the following mods - Этот мод не может быть удален или обновлен, так как является зависимостью для следующих модов + Этот мод не может быть удален или обновлен, так как является зависимостью для следующих модов - + + This mod cannot be enabled because it translates into a different language. + + + + + This mod can not be enabled because the following dependencies are not present + + + + + This mod can not be installed because the following dependencies are not present + + + + This is a submod and it cannot be installed or uninstalled separately from its parent mod Это вложенный мод, он не может быть установлен или удален отдельно от родительского - + Notes Замечания - + All supported files - + Maps - + Campaigns - + Configs - + Mods Моды - + Gog files - + All files (*.*) - + Select files (configs, mods, maps, campaigns, gog files) to install... - + Replace config file? - + Do you want to replace %1? - + Downloading %1. %p% (%v MB out of %m MB) finished - + Download failed - + Unable to download all files. Encountered errors: @@ -484,153 +469,58 @@ Encountered errors: - + Install successfully downloaded? - + Installing chronicles - + Installing mod %1 - + Operation failed - + Encountered errors: - + screenshots - + Screenshot %1 Скриншот %1 - + Mod is incompatible Мод несовместим - - CModManager - - - Can not install submod - - - - - Mod is already installed - - - - - Can not uninstall submod - - - - - Mod is not installed - - - - - Mod is already enabled - - - - - - Mod must be installed first - - - - - Mod is not compatible, please update VCMI and checkout latest mod revisions - - - - - Required mod %1 is missing - - - - - Required mod %1 is not enabled - - - - - - This mod conflicts with %1 - - - - - Mod is already disabled - - - - - This mod is needed to run %1 - - - - - Mod archive is missing - - - - - Mod with such name is already installed - - - - - Mod archive is invalid or corrupted - - - - - Failed to extract mod data - - - - - Data with this mod was not found - - - - - Mod is located in protected directory, please remove it manually: - - - - CSettingsView - + Interface Scaling + Off Отключено @@ -641,292 +531,334 @@ Install successfully downloaded? Включено - + Neutral AI in battles - + Enemy AI in battles - + Additional repository - + Check on startup Проверять при запуске - + Fullscreen Полноэкранный режим - + General Общее - + VCMI Language Язык VCMI - + Artificial Intelligence Искусственный интеллект - + Adventure Map Allies - + Refresh now - + Adventure Map Enemies - + Online Lobby port - + Autocombat AI in battles - + Sticks Sensitivity - + Automatic (Linear) - + Haptic Feedback - + Software Cursor - + + + Automatic - + + Mods Validation + + + + None - + xBRZ x2 - + xBRZ x3 - + xBRZ x4 - + + Full + + + + Use scalable fonts - + Online Lobby address - - Upscaling Filter + + Cursor Scaling - - Use Relative Pointer Mode - - - - - VSync - - - - - Nearest - - - - - Linear - - - - - Input - Touchscreen - - - - - Network - - - - - Downscaling Filter + + Scalable + Miscellaneous + + + + + Font Scaling (experimental) + + + + + Original + + + + + Upscaling Filter + + + + + Basic + + + + + Use Relative Pointer Mode + + + + + VSync + + + + + Nearest + + + + + Linear + + + + + Input - Touchscreen + + + + + Network + + + + + Downscaling Filter + + + + Show Tutorial again - + Reset - + Audio - + Relative Pointer Speed - + Music Volume - + Ignore SSL errors - + Input - Mouse - + Long Touch Duration - + % - + Controller Click Tolerance - + Touch Tap Tolerance - + Input - Controller - + Sound Volume - + Windowed - + Borderless fullscreen - + Exclusive fullscreen - + Reserved screen area - + Autosave limit (0 = off) - + Framerate Limit - + Autosave prefix - + Mouse Click Tolerance - + Sticks Acceleration - + empty = map name prefix - + Default repository - + Renderer - + Select display mode for game Windowed - game will run inside a window that covers part of your screen @@ -937,62 +869,62 @@ Fullscreen Exclusive Mode - game will cover entirety of your screen and will use - + Heroes III Translation Перевод Героев III - + Resolution Разрешение экрана - + Autosave Автосохранение - + Display index Дисплей - + Network port Сетевой порт - + Video Графика - + Show intro Вступление - + Active Активен - + Disabled Отключен - + Enable Включить - + Not Installed Не установлен - + Install Установить @@ -1028,7 +960,7 @@ Fullscreen Exclusive Mode - game will cover entirety of your screen and will use - + Heroes Chronicles @@ -1036,30 +968,11 @@ Fullscreen Exclusive Mode - game will cover entirety of your screen and will use File size - - %1 B - - - - - %1 KiB - - - - + + %1 MiB - - - %1 GiB - - - - - %1 TiB - - FirstLaunchView @@ -1503,16 +1416,208 @@ error reason: ModFields - + Name Название - + Type Тип + + ModStateController + + + Can not install submod + + + + + Mod is already installed + + + + + Can not uninstall submod + + + + + Mod is not installed + + + + + Mod is already enabled + + + + + + Mod must be installed first + + + + + Mod is not compatible, please update VCMI and checkout latest mod revisions + + + + + Can not enable translation mod for a different language! + + + + + Required mod %1 is missing + + + + + Mod is already disabled + + + + + Mod archive is missing + + + + + Mod with such name is already installed + + + + + Mod archive is invalid or corrupted + + + + + Failed to extract mod data + + + + + Data with this mod was not found + + + + + Mod is located in protected directory, please remove it manually: + + + + + + ModStateItemModel + + + Translation + Перевод + + + + Town + Город + + + + Test + Тест + + + + Templates + Шаблоны карт + + + + Spells + Заклинания + + + + Music + Музыка + + + + Maps + + + + + Sounds + Звуки + + + + Skills + Навыки + + + + + Other + Иное + + + + Objects + Объекты + + + + + Mechanics + Механика + + + + + Interface + Интерфейс + + + + Heroes + Герои + + + + + Graphical + Графика + + + + Expansion + Дополнение + + + + Creatures + Существа + + + + Compatibility + Совместимость + + + + Artifacts + Артефакт + + + + AI + ИИ + + QObject diff --git a/launcher/translation/spanish.ts b/launcher/translation/spanish.ts index 0b853743b..26821adc9 100644 --- a/launcher/translation/spanish.ts +++ b/launcher/translation/spanish.ts @@ -90,108 +90,84 @@ CModListModel - Translation - Traducción + Traducción - Town - Ciudad + Ciudad - Test - Test + Test - Templates - Plantillas + Plantillas - Spells - Hechizos + Hechizos - Music - Música + Música - Maps - Mapas + Mapas - Sounds - Sonidos + Sonidos - Skills - Habilidades + Habilidades - - Other - Otro + Otro - Objects - Objetos + Objetos - - Mechanics - Mecánicas + Mecánicas - - Interface - Interfaz + Interfaz - Heroes - Heroes + Heroes - - Graphical - Gráficos + Gráficos - Expansion - Expansión + Expansión - Creatures - Criaturas + Criaturas - Compatibility - Compatibilidad + Compatibilidad - Artifacts - Artefactos + Artefactos - AI - IA + IA @@ -233,7 +209,7 @@ - + Description Descripción @@ -293,189 +269,202 @@ Cancelar - + Mod name Nombre del mod - + + Installed version Versión instalada - + + Latest version Última versión - + Size Tamaño - + Download size Tamaño de descarga - + Authors Autores - + License Licencia - + Contact Contacto - + Compatibility Compatibilidad - - + + Required VCMI version Versión de VCMI requerida - + Supported VCMI version Versión de VCMI compatible - + please upgrade mod - - + + mods repository index - + or newer - + Supported VCMI versions Versiones de VCMI compatibles - + Languages Idiomas - + Required mods Mods requeridos - + Conflicting mods Mods conflictivos - This mod can not be installed or enabled because the following dependencies are not present - Este mod no se puede instalar o habilitar porque no están presentes las siguientes dependencias + Este mod no se puede instalar o habilitar porque no están presentes las siguientes dependencias - This mod can not be enabled because the following mods are incompatible with it - Este mod no se puede habilitar porque los siguientes mods son incompatibles con él + Este mod no se puede habilitar porque los siguientes mods son incompatibles con él - This mod cannot be disabled because it is required by the following mods - No se puede desactivar este mod porque es necesario para ejecutar los siguientes mods + No se puede desactivar este mod porque es necesario para ejecutar los siguientes mods - This mod cannot be uninstalled or updated because it is required by the following mods - No se puede desinstalar o actualizar este mod porque es necesario para ejecutar los siguientes mods + No se puede desinstalar o actualizar este mod porque es necesario para ejecutar los siguientes mods - + + This mod cannot be enabled because it translates into a different language. + + + + + This mod can not be enabled because the following dependencies are not present + + + + + This mod can not be installed because the following dependencies are not present + + + + This is a submod and it cannot be installed or uninstalled separately from its parent mod Este es un submod y no se puede instalar o desinstalar por separado del mod principal - + Notes Notas - + All supported files - + Maps Mapas - + Campaigns - + Configs - + Mods Mods - + Gog files - + All files (*.*) - + Select files (configs, mods, maps, campaigns, gog files) to install... - + Replace config file? - + Do you want to replace %1? - + Downloading %1. %p% (%v MB out of %m MB) finished - + Download failed Descarga fallida - + Unable to download all files. Encountered errors: @@ -488,7 +477,7 @@ Errores encontrados: - + Install successfully downloaded? @@ -497,403 +486,350 @@ Install successfully downloaded? Instalar lo correctamente descargado? - + Installing chronicles - + Installing mod %1 Instalando mod %1 - + Operation failed Operación fallida - + Encountered errors: Errores encontrados: - + screenshots - + Screenshot %1 Captura de pantalla %1 - + Mod is incompatible El mod es incompatible - - CModManager - - - Can not install submod - - - - - Mod is already installed - - - - - Can not uninstall submod - - - - - Mod is not installed - - - - - Mod is already enabled - - - - - - Mod must be installed first - - - - - Mod is not compatible, please update VCMI and checkout latest mod revisions - - - - - Required mod %1 is missing - - - - - Required mod %1 is not enabled - - - - - - This mod conflicts with %1 - - - - - Mod is already disabled - - - - - This mod is needed to run %1 - - - - - Mod archive is missing - - - - - Mod with such name is already installed - - - - - Mod archive is invalid or corrupted - - - - - Failed to extract mod data - - - - - Data with this mod was not found - - - - - Mod is located in protected directory, please remove it manually: - - - - CSettingsView + Off Desactivado - + Artificial Intelligence Inteligencia Artificial - + Interface Scaling Escala de la interfaz - + Neutral AI in battles IA neutral en batallas - + Enemy AI in battles IA enemiga en batallas - + Additional repository Repositorio adicional - + Downscaling Filter - + Adventure Map Allies Aliados en el Mapa de aventuras - + Online Lobby port - + Autocombat AI in battles - + Sticks Sensitivity - + Automatic (Linear) - + Haptic Feedback - + Software Cursor - + + + Automatic - + + Mods Validation + + + + None - + xBRZ x2 - + xBRZ x3 - + xBRZ x4 - + + Full + + + + Use scalable fonts - + Online Lobby address - + + Cursor Scaling + + + + + Scalable + + + + + Miscellaneous + + + + + Font Scaling (experimental) + + + + + Original + + + + Upscaling Filter - + + Basic + + + + Use Relative Pointer Mode - + Nearest - + Linear - + Input - Touchscreen - + Adventure Map Enemies Enemigos en el Mapa de aventuras - + Show Tutorial again - + Reset - + Network - + Audio - + Relative Pointer Speed - + Music Volume - + Ignore SSL errors - + Input - Mouse - + Long Touch Duration - + % - + Controller Click Tolerance - + Touch Tap Tolerance - + Input - Controller - + Sound Volume - + Windowed Ventana - + Borderless fullscreen Ventana completa sin bordes - + Exclusive fullscreen Pantalla completa - + Autosave limit (0 = off) Límite de autosaves (0 = sin límite) - + Framerate Limit Límite de fotogramas - + Autosave prefix Prefijo autoguardado - + Mouse Click Tolerance - + Sticks Acceleration - + empty = map name prefix Vacio = prefijo del mapa - + Refresh now Actualizar - + Default repository Repositorio por defecto - + Renderer Render @@ -903,62 +839,62 @@ Instalar lo correctamente descargado? Activado - + Heroes III Translation Traducción de Heroes III - + Reserved screen area Área de pantalla reservada - + Fullscreen Pantalla completa - + General General - + VCMI Language Idioma de VCMI - + Resolution Resolución - + Autosave Autoguardado - + VSync Sincronización vertical - + Display index Mostrar índice - + Network port Puerto de red - + Video Vídeo - + Select display mode for game Windowed - game will run inside a window that covers part of your screen @@ -975,37 +911,37 @@ Ventana sin bordes - el juego se ejecutará en una ventana que cubre completamen Pantalla completa - el juego cubrirá la totalidad de la pantalla y utilizará la resolución seleccionada. - + Show intro Mostrar introducción - + Check on startup Comprovar al inicio - + Active Activado - + Disabled Desactivado - + Enable Activar - + Not Installed No Instalado - + Install Instalar @@ -1041,7 +977,7 @@ Pantalla completa - el juego cubrirá la totalidad de la pantalla y utilizará l - + Heroes Chronicles @@ -1049,30 +985,11 @@ Pantalla completa - el juego cubrirá la totalidad de la pantalla y utilizará l File size - - %1 B - - - - - %1 KiB - - - - + + %1 MiB - - - %1 GiB - - - - - %1 TiB - - FirstLaunchView @@ -1516,16 +1433,208 @@ error reason: ModFields - + Name Nombre - + Type Tipo + + ModStateController + + + Can not install submod + + + + + Mod is already installed + + + + + Can not uninstall submod + + + + + Mod is not installed + + + + + Mod is already enabled + + + + + + Mod must be installed first + + + + + Mod is not compatible, please update VCMI and checkout latest mod revisions + + + + + Can not enable translation mod for a different language! + + + + + Required mod %1 is missing + + + + + Mod is already disabled + + + + + Mod archive is missing + + + + + Mod with such name is already installed + + + + + Mod archive is invalid or corrupted + + + + + Failed to extract mod data + + + + + Data with this mod was not found + + + + + Mod is located in protected directory, please remove it manually: + + + + + + ModStateItemModel + + + Translation + Traducción + + + + Town + Ciudad + + + + Test + Test + + + + Templates + Plantillas + + + + Spells + Hechizos + + + + Music + Música + + + + Maps + Mapas + + + + Sounds + Sonidos + + + + Skills + Habilidades + + + + + Other + Otro + + + + Objects + Objetos + + + + + Mechanics + Mecánicas + + + + + Interface + Interfaz + + + + Heroes + Heroes + + + + + Graphical + Gráficos + + + + Expansion + Expansión + + + + Creatures + Criaturas + + + + Compatibility + Compatibilidad + + + + Artifacts + Artefactos + + + + AI + IA + + QObject diff --git a/launcher/translation/swedish.ts b/launcher/translation/swedish.ts index deb1fd13b..dc27a26ff 100644 --- a/launcher/translation/swedish.ts +++ b/launcher/translation/swedish.ts @@ -90,108 +90,84 @@ CModListModel - Translation - Översättning + Översättning - Town - Stad + Stad - Test - Test + Test - Templates - Modeller + Modeller - Spells - Trollformler + Trollformler - Music - Musik + Musik - Maps - Kartor + Kartor - Sounds - Ljud + Ljud - Skills - Färdigheter + Färdigheter - - Other - Övrigt + Annan - Objects - Objekt + Objekt - - Mechanics - Mekanik + Mekanik - - Interface - Gränssnitt + Gränssnitt - Heroes - Hjälte + Hjälte - - Graphical - Grafik + Grafik - Expansion - Expansion/Tillägg + Expansion/Tillägg - Creatures - Varelser + Varelser - Compatibility - Kompatibilitet + Kompatibilitet - Artifacts - Artefakter + Artefakter - AI - AI + AI @@ -238,7 +214,7 @@ - + Description Beskrivning @@ -293,189 +269,202 @@ Avbryt - + Mod name Modd-namn - + + Installed version Installerad version - + + Latest version Senaste version - + Size Storlek - + Download size Nedladdnings-storlek - + Authors Författare - + License Licens - + Contact Kontakt - + Compatibility Kompatibilitet - - + + Required VCMI version VCMI-version som krävs - + Supported VCMI version VCMI-version som stöds - + please upgrade mod vänligen uppdatera modd - - + + mods repository index Modd-repositorie-index - + or newer eller nyare - + Supported VCMI versions VCMI-versioner som stöds - + Languages Språk - + Required mods Moddar som krävs - + Conflicting mods Modd-konflikter - This mod can not be installed or enabled because the following dependencies are not present - Denna modd kan inte installeras eller aktiveras eftersom följande beroenden inte finns + Denna modd kan inte installeras eller aktiveras eftersom följande beroenden inte finns - This mod can not be enabled because the following mods are incompatible with it - Denna modd kan inte aktiveras eftersom följande moddar är inkompatibla med den + Denna modd kan inte aktiveras eftersom följande moddar är inkompatibla med den - This mod cannot be disabled because it is required by the following mods - Denna modd kan inte inaktiveras eftersom den krävs för följande modd + Denna modd kan inte inaktiveras eftersom den krävs för följande modd - This mod cannot be uninstalled or updated because it is required by the following mods - Denna modd kan inte avinstalleras eller uppdateras eftersom den krävs för följande modd + Denna modd kan inte avinstalleras eller uppdateras eftersom den krävs för följande modd - + + This mod cannot be enabled because it translates into a different language. + + + + + This mod can not be enabled because the following dependencies are not present + + + + + This mod can not be installed because the following dependencies are not present + + + + This is a submod and it cannot be installed or uninstalled separately from its parent mod Detta är en undermodd/submodd och den kan inte installeras eller avinstalleras separat från huvud-modden - + Notes Anteckningar - + All supported files Alla filer som stöds - + Maps Kartor - + Campaigns Kampanjer - + Configs Konfigurationer - + Mods Moddar - - - Gog files - - - All files (*.*) - + Gog files + GOG-filer - Select files (configs, mods, maps, campaigns, gog files) to install... - + All files (*.*) + Alla filer (*.*) - + + Select files (configs, mods, maps, campaigns, gog files) to install... + Välj filer (konfigurationsfiler, moddar, kartor, kampanjer och GOG-filer) som ska installeras... + + + Replace config file? Byt ut konfigurationsfilen? - + Do you want to replace %1? Vill du ersätta %1? - + Downloading %1. %p% (%v MB out of %m MB) finished Laddar ner %1. %p% (%v MB av %m MB) slutfört - + Download failed Nedladdning misslyckades - + Unable to download all files. Encountered errors: @@ -488,7 +477,7 @@ Fel påträffat: - + Install successfully downloaded? @@ -497,39 +486,39 @@ Install successfully downloaded? Installation framgångsrikt nedladdad? - + Installing chronicles - + Installera Chronicles - + Installing mod %1 Installera modd %1 - + Operation failed Åtgärden misslyckades - + Encountered errors: Fel påträffades: - + screenshots skärmdumpar - + Screenshot %1 Skärmbild %1 - + Mod is incompatible Denna modd är inkompatibel @@ -537,109 +526,90 @@ Installation framgångsrikt nedladdad? CModManager - Can not install submod - Det går inte att installera undermodd/submodd + Det går inte att installera undermodd/submodd - Mod is already installed - Modden är redan installerad + Modden är redan installerad - Can not uninstall submod - Det går inte att avinstallera undermodd/submodd + Det går inte att avinstallera undermodd/submodd - Mod is not installed - Modden är inte installerad + Modden är inte installerad - Mod is already enabled - Modden är redan aktiverad + Modden är redan aktiverad - - Mod must be installed first - Modden måste installeras först + Modden måste installeras först - Mod is not compatible, please update VCMI and checkout latest mod revisions - Modden är inte kompatibel. Vänligen uppdatera VCMI och kontrollera att du har den senaste kompatibla versionen av modden + Modden är inte kompatibel. Vänligen uppdatera VCMI och kontrollera att du har den senaste kompatibla versionen av modden - Required mod %1 is missing - Den obligatorisk modden %1 saknas + Den obligatorisk modden %1 saknas - Required mod %1 is not enabled - Den obligatoriska modden %1 är inte aktiverad + Den obligatoriska modden %1 är inte aktiverad - - This mod conflicts with %1 - Denna modd är i konflikt med %1 + Denna modd är i konflikt med %1 - Mod is already disabled - Modden är redan inaktiverad + Modden är redan inaktiverad - This mod is needed to run %1 - Denna modden krävs för att köra %1 + Denna modden krävs för att köra %1 - Mod archive is missing - Modd-arkiv saknas + Modd-arkiv saknas - Mod with such name is already installed - En modd med samma namn är redan installerad + En modd med samma namn är redan installerad - Mod archive is invalid or corrupted - Modd-arkivet är ogiltigt eller korrupt + Modd-arkivet är ogiltigt eller korrupt - Failed to extract mod data - Misslyckades att extrahera data från modd + Misslyckades att extrahera data från modd - Data with this mod was not found - Modd-data för denna modd hittades inte + Modd-data för denna modd hittades inte - Mod is located in protected directory, please remove it manually: - Modden är placerad i en skyddad mapp. Vänligen radera den manuellt: + Modden är placerad i en skyddad mapp. Vänligen radera den manuellt: CSettingsView + Off Inaktiverad - + Artificial Intelligence Artificiell intelligens @@ -649,187 +619,229 @@ Installation framgångsrikt nedladdad? Aktiverad - + Enemy AI in battles Fiendens AI i strider - + Default repository Standard-repositorie - + VSync Vertikal-synkronisering (VSync) - + Online Lobby port Port-numret till online-väntrummet - + Autocombat AI in battles Automatiska AI-strider - + Sticks Sensitivity Spak-känslighet - + Automatic (Linear) Automatisk (linjär) - + Haptic Feedback Haptisk återkoppling (vibrationer i kontrollen) - + Software Cursor Programvarustyrd muspekare - + + + Automatic Automatisk - + + Mods Validation + Validering av moddar + + + None Inget - + xBRZ x2 xBRZ x2 - + xBRZ x3 xBRZ x3 - + xBRZ x4 xBRZ x4 - - Use scalable fonts - + + Full + Hela - + + Use scalable fonts + Använd skalbara teckensnitt + + + Online Lobby address Adressen till online-väntrummet - + + Cursor Scaling + Skalning av markör + + + + Scalable + Skalbar + + + + Miscellaneous + Övrigt + + + + Font Scaling (experimental) + Skalning av teckensnitt (experimentell) + + + + Original + Original + + + Upscaling Filter Uppskalnings-filter - + + Basic + Grundläggande + + + Use Relative Pointer Mode Använd läge för relativ muspekare - + Nearest Närmast - + Linear Linjär - + Input - Touchscreen Ingång/indata - Pekskärm - + Network Nätverk - + Downscaling Filter Nerskalnings-filter - + Show Tutorial again Visa handledningen/övningsgenomgången igen - + Reset Återställ - + Audio Ljud - + Relative Pointer Speed Relativ pekarhastighet - + Music Volume Musikvolym - + Ignore SSL errors Ignorera SSL-fel - + Input - Mouse Ingång/indata - Mus - + Long Touch Duration Utökad beröringslängd - + % % - + Controller Click Tolerance Tolerans för klick på styrenhet - + Touch Tap Tolerance Tolerans för pektryck - + Input - Controller Ingång/indata - Kontroll - + Sound Volume Ljudvolym - + Select display mode for game Windowed - game will run inside a window that covers part of your screen @@ -846,167 +858,167 @@ Kantlöst fönsterläge - spelet körs i ett fönster som täcker hela din skär Exklusivt helskärmsläge - spelet kommer att täcka hela skärmen och använda den valda upplösningen. - + Windowed Fönsterläge - + Borderless fullscreen Kantlös helskärm - + Exclusive fullscreen Exklusiv helskärm - + Reserved screen area Reserverat skärmområde - + Neutral AI in battles Neutralt AI i strider - + Autosave limit (0 = off) Antal platser för automatisk-sparning (0 = inaktiverad) - + Adventure Map Enemies Fiender på äventyskartan - + Autosave prefix Prefix för automatisk-sparning - + empty = map name prefix tomt = kartnamnsprefix - + Interface Scaling Gränssnittsskalning - + Framerate Limit Gräns ​​för bildhastighet - + Renderer Renderingsmotor - + Heroes III Translation Heroes III - Översättning - + Adventure Map Allies Allierade på äventyrskartan - + Additional repository Ytterligare repositorier - + Check on startup Kontrollera vid uppstart - + Mouse Click Tolerance Musklickstolerans - + Sticks Acceleration Styrspaks-acceleration - + Refresh now Uppdatera nu - + Fullscreen Helskärm - + General Allmänt - + VCMI Language VCMI-språk - + Resolution Upplösning - + Autosave Auto-spara - + Display index Visa index - + Network port Nätverksport - + Video Video - + Show intro Visa intro - + Active Aktiv - + Disabled Inaktiverad - + Enable Aktivera - + Not Installed Inte installerad - + Install Installera @@ -1016,63 +1028,60 @@ Exklusivt helskärmsläge - spelet kommer att täcka hela skärmen och använda File cannot opened - + Filen kan inte öppnas Invalid file selected - Ogiltig fil vald + Ogiltig fil vald You have to select an gog installer file! - + Du måste välja en GOG-installationsfil! You have to select an chronicle installer file! - + Du måste välja en Chronicles-installationsfil! Extracting error! - Extraktionsfel! + Extraheringsfel! - + Heroes Chronicles - + Heroes Chronicles File size - %1 B - %1 B + %1 B - %1 KiB - %1 KiB + %1 KiB - + + %1 MiB %1 MiB - %1 GiB - %1 GiB + %1 GiB - %1 TiB - %1 TiB + %1 TiB @@ -1151,7 +1160,7 @@ Exklusivt helskärmsläge - spelet kommer att täcka hela skärmen och använda If you own Heroes III on gog.com you can download backup offline installer from gog.com, and VCMI will import Heroes III data using offline installer. Offline installer consists of two parts, .exe and .bin. Make sure you download both of them. - Om du äger Heroes III från GOG.com kan du ladda ner backup offline-installationsprogrammet från 'GOG.com'. VCMI kommer att importera Heroes III-data med hjälp av offline-installationsprogrammet. Offline-installationsprogrammet består av två delar, en '.exe'- och en '.bin'fil. Se till att ladda ner båda. + Om du äger Heroes III från GOG.com kan du ladda ner backup offline-installationsprogrammet från 'GOG.com'. VCMI kommer att importera Heroes III-data med hjälp av offline-installationsprogrammet. Offline-installationsprogrammet består av två delar, en '.exe'- och en '.bin'fil. Se till att ladda ner båda. @@ -1166,12 +1175,12 @@ Offline installer consists of two parts, .exe and .bin. Make sure you download b VCMI on Github - VCMI på 'Github' + VCMI på 'Github' VCMI on Discord - VCMI på 'Discord' + VCMI på 'Discord' @@ -1245,7 +1254,7 @@ Heroes® of Might and Magic® III HD stöds för närvarande inte! Optionally, you can install additional mods either now, or at any point later, using the VCMI Launcher - Du kan välja att installera ytterligare moddar, antingen nu eller vid ett senare tillfälle med hjälp av 'VCMI Launchern' + Du kan välja att installera ytterligare moddar, antingen nu eller vid ett senare tillfälle med hjälp av 'VCMI Launchern' @@ -1377,18 +1386,18 @@ Vänligen välj en mapp som innehåller data från Heroes III: Complete Edition Stream error while extracting files! error reason: - Strömningsfel vid extrahering av filer! + Strömningsfel vid extrahering av filer! Orsak till fel: Not a supported Inno Setup installer! - Inno Setup-installationsprogrammet stöds inte! + Inno Setup-installationsprogrammet stöds inte! VCMI was compiled without innoextract support, which is needed to extract exe files! - + VCMI kompilerades utan stöd för innoextract, vilket behövs för att extrahera exe-filer! @@ -1520,16 +1529,209 @@ Orsak till fel: ModFields - + Name Namn - + Type Typ + + ModStateController + + + Can not install submod + Det går inte att installera undermodd/submodd + + + + Mod is already installed + Modden är redan installerad + + + + Can not uninstall submod + Det går inte att avinstallera undermodd/submodd + + + + Mod is not installed + Modden är inte installerad + + + + Mod is already enabled + Modden är redan aktiverad + + + + + Mod must be installed first + Modden måste installeras först + + + + Mod is not compatible, please update VCMI and checkout latest mod revisions + Modden är inte kompatibel. Vänligen uppdatera VCMI och kontrollera att du har den senaste kompatibla versionen av modden + + + + Can not enable translation mod for a different language! + + + + + Required mod %1 is missing + Den obligatorisk modden %1 saknas + + + + Mod is already disabled + Modden är redan inaktiverad + + + + Mod archive is missing + Modd-arkiv saknas + + + + Mod with such name is already installed + En modd med samma namn är redan installerad + + + + Mod archive is invalid or corrupted + Modd-arkivet är ogiltigt eller korrupt + + + + Failed to extract mod data + Misslyckades att extrahera data från modd + + + + Data with this mod was not found + Modd-data för denna modd hittades inte + + + + Mod is located in protected directory, please remove it manually: + + Modden är placerad i en skyddad mapp. Vänligen radera den manuellt: + + + + + ModStateItemModel + + + Translation + Översättning + + + + Town + Stad + + + + Test + Test + + + + Templates + Modeller + + + + Spells + Trollformler + + + + Music + Musik + + + + Maps + Kartor + + + + Sounds + Ljud + + + + Skills + Färdigheter + + + + + Other + Övrigt + + + + Objects + Objekt + + + + + Mechanics + Mekanik + + + + + Interface + Gränssnitt + + + + Heroes + Hjälte + + + + + Graphical + Grafik + + + + Expansion + Expansion/Tillägg + + + + Creatures + Varelser + + + + Compatibility + Kompatibilitet + + + + Artifacts + Artefakter + + + + AI + AI + + QObject diff --git a/launcher/translation/ukrainian.ts b/launcher/translation/ukrainian.ts index ddfe4a510..1dcaa034b 100644 --- a/launcher/translation/ukrainian.ts +++ b/launcher/translation/ukrainian.ts @@ -90,108 +90,84 @@ CModListModel - Translation - Переклад + Переклад - Town - Місто + Місто - Test - Тестування + Тестування - Templates - Шаблони + Шаблони - Spells - Закляття + Закляття - Music - Музика + Музика - Maps - Мапи + Мапи - Sounds - Звуки + Звуки - Skills - Вміння + Вміння - - Other - Інше + Інше - Objects - Об'єкти + Об'єкти - - Mechanics - Механіки + Механіки - - Interface - Інтерфейс + Інтерфейс - Heroes - Герої + Герої - - Graphical - Графічний + Графічний - Expansion - Розширення + Розширення - Creatures - Істоти + Істоти - Compatibility - Сумісність + Сумісність - Artifacts - Артефакти + Артефакти - AI - ШІ + ШІ @@ -233,7 +209,7 @@ - + Description Опис @@ -293,189 +269,202 @@ Відмінити - + Mod name Назва модифікації - + + Installed version Встановлена версія - + + Latest version Найновіша версія - + Size Розмір - + Download size Розмір для завантаження - + Authors Автори - + License Ліцензія - + Contact Контакти - + Compatibility Сумісність - - + + Required VCMI version Необхідна версія VCMI - + Supported VCMI version Підтримувана версія VCMI - + please upgrade mod будь ласка, оновіть модифікацію - - + + mods repository index каталог модифікацій - + or newer або новіше - + Supported VCMI versions Підтримувані версії VCMI - + Languages Мови - + Required mods Необхідні модифікації - + Conflicting mods Конфліктуючі модифікації - This mod can not be installed or enabled because the following dependencies are not present - Цю модифікацію не можна встановити чи активувати, оскільки відсутні наступні залежності + Цю модифікацію не можна встановити чи активувати, оскільки відсутні наступні залежності - This mod can not be enabled because the following mods are incompatible with it - Цю модифікацію не можна ввімкнути, оскільки наступні модифікації несумісні з цією модифікацією + Цю модифікацію не можна ввімкнути, оскільки наступні модифікації несумісні з цією модифікацією - This mod cannot be disabled because it is required by the following mods - Цю модифікацію не можна відключити, оскільки вона необхідна для запуску наступних модифікацій + Цю модифікацію не можна відключити, оскільки вона необхідна для запуску наступних модифікацій - This mod cannot be uninstalled or updated because it is required by the following mods - Цю модифікацію не можна видалити або оновити, оскільки вона необхідна для запуску наступних модифікацій + Цю модифікацію не можна видалити або оновити, оскільки вона необхідна для запуску наступних модифікацій - + + This mod cannot be enabled because it translates into a different language. + + + + + This mod can not be enabled because the following dependencies are not present + + + + + This mod can not be installed because the following dependencies are not present + + + + This is a submod and it cannot be installed or uninstalled separately from its parent mod Це вкладена модифікація, і її не можна встановити або видалити окремо від батьківської модифікації - + Notes Примітки - + All supported files Усі підтримувані файли - + Maps Мапи - + Campaigns Кампанії - + Configs Налаштування - + Mods Модифікації - + Gog files - + All files (*.*) - + Select files (configs, mods, maps, campaigns, gog files) to install... - + Replace config file? Замінити файл налаштувань? - + Do you want to replace %1? Ви дійсно хочете замінити %1? - + Downloading %1. %p% (%v MB out of %m MB) finished Завантажується %1. %p% (%v MB з %m MB) завершено - + Download failed Помилка завантаження - + Unable to download all files. Encountered errors: @@ -488,7 +477,7 @@ Encountered errors: - + Install successfully downloaded? @@ -497,39 +486,39 @@ Install successfully downloaded? Встановити успішно завантажені? - + Installing chronicles - + Installing mod %1 Встановлення модифікації %1 - + Operation failed Операція завершилася невдало - + Encountered errors: Виникли помилки: - + screenshots знімки екрану - + Screenshot %1 Знімок екрану %1 - + Mod is incompatible Модифікація несумісна @@ -537,364 +526,387 @@ Install successfully downloaded? CModManager - Can not install submod - Неможливо встановити вкладену модифікацію + Неможливо встановити вкладену модифікацію - Mod is already installed - Модифікація вже встановлена + Модифікація вже встановлена - Can not uninstall submod - Неможливо видалити вкладену модифікацію + Неможливо видалити вкладену модифікацію - Mod is not installed - Модифікація не встановлена + Модифікація не встановлена - Mod is already enabled - Модифікація вже увімкнена + Модифікація вже увімкнена - - Mod must be installed first - Спочатку потрібно встановити модифікацію + Спочатку потрібно встановити модифікацію - Mod is not compatible, please update VCMI and checkout latest mod revisions - Модифікація несумісна, будь ласка, оновіть VCMI та перевірте останні версії модифікацій + Модифікація несумісна, будь ласка, оновіть VCMI та перевірте останні версії модифікацій - Required mod %1 is missing - Необхідна модифікація %1 відсутня + Необхідна модифікація %1 відсутня - Required mod %1 is not enabled - Необхідну модифікацію %1 не ввімкнено + Необхідну модифікацію %1 не ввімкнено - - This mod conflicts with %1 - Ця модифікація несумісна з %1 + Ця модифікація несумісна з %1 - Mod is already disabled - Модифікацію вже вимкнено + Модифікацію вже вимкнено - This mod is needed to run %1 - Ця модифікація необхідна для запуску %1 + Ця модифікація необхідна для запуску %1 - Mod archive is missing - Архів з модифікацією відсутній + Архів з модифікацією відсутній - Mod with such name is already installed - Модифікацію з такою назвою вже встановлено + Модифікацію з такою назвою вже встановлено - Mod archive is invalid or corrupted - Архів модифікації непридатний або пошкоджений + Архів модифікації непридатний або пошкоджений - Failed to extract mod data - Не вдалося видобути дані модифікації + Не вдалося видобути дані модифікації - Data with this mod was not found - Дані з цією модифікацією не знайдено + Дані з цією модифікацією не знайдено - Mod is located in protected directory, please remove it manually: - Модифікація знаходиться в захищеному каталозі, будь ласка, видаліть її вручну: + Модифікація знаходиться в захищеному каталозі, будь ласка, видаліть її вручну: CSettingsView + Off Ні - + Artificial Intelligence Штучний інтелект - + Interface Scaling Масштабування інтерфейсу - + Neutral AI in battles Нейтральний ШІ в боях - + Enemy AI in battles Ворожий ШІ в боях - + Additional repository Додатковий репозиторій - + Downscaling Filter - + Adventure Map Allies Союзники на мапі пригод - + Online Lobby port Порт онлайн лобі - + Autocombat AI in battles ШІ автобою - + Sticks Sensitivity Чутливість стиків - + Automatic (Linear) - + Haptic Feedback - + Software Cursor Програмний курсор - + + + Automatic - + + Mods Validation + + + + None - + xBRZ x2 - + xBRZ x3 - + xBRZ x4 - + + Full + + + + Use scalable fonts - + Online Lobby address Адреса онлайн-лобі - + + Cursor Scaling + + + + + Scalable + + + + + Miscellaneous + + + + + Font Scaling (experimental) + + + + + Original + + + + Upscaling Filter Фільтр масштабування - + + Basic + + + + Use Relative Pointer Mode Режим відносного вказівника - + Nearest Найближчий - + Linear Лінійний - + Input - Touchscreen Введення - Сенсорний екран - + Adventure Map Enemies Вороги на мапі пригод - + Show Tutorial again Повторно показати навчання - + Reset Скинути - + Network Мережа - + Audio Аудіо - + Relative Pointer Speed Швидкість відносного вказівника - + Music Volume Гучність музики - + Ignore SSL errors Ігнорувати помилки SSL - + Input - Mouse Введення - Миша - + Long Touch Duration Тривалість довгого дотику - + % % - + Controller Click Tolerance Допуск на натискання контролера - + Touch Tap Tolerance Допуск на натискання дотиком - + Input - Controller Введення - Контролер - + Sound Volume Гучність звуку - + Windowed У вікні - + Borderless fullscreen Повноекранне вікно - + Exclusive fullscreen Повноекранний (ексклюзивно) - + Autosave limit (0 = off) Кількість автозбережень - + Framerate Limit Обмеження частоти кадрів - + Autosave prefix Префікс назв автозбережень - + Mouse Click Tolerance Допуск кліків миші - + Sticks Acceleration Прискорення стиків - + empty = map name prefix (використовувати назву карти) - + Refresh now Оновити зараз - + Default repository Стандартний репозиторій - + Renderer Рендерер @@ -904,7 +916,7 @@ Install successfully downloaded? Так - + Select display mode for game Windowed - game will run inside a window that covers part of your screen @@ -921,92 +933,92 @@ Fullscreen Exclusive Mode - game will cover entirety of your screen and will use Повноекранний ексклюзивний режим - гра займатиме весь екран і використовуватиме вибрану роздільну здатність. - + Reserved screen area Зарезервована зона екрану - + Heroes III Translation Переклад Heroes III - + Check on startup Перевіряти на старті - + Fullscreen Повноекранний режим - + General Загальні налаштування - + VCMI Language Мова VCMI - + Resolution Роздільна здатність - + Autosave Автозбереження - + VSync Вертикальна синхронізація - + Display index Дісплей - + Network port Мережевий порт - + Video Графіка - + Show intro Вступні відео - + Active Активні - + Disabled Деактивований - + Enable Активувати - + Not Installed Не встановлено - + Install Встановити @@ -1042,7 +1054,7 @@ Fullscreen Exclusive Mode - game will cover entirety of your screen and will use - + Heroes Chronicles @@ -1050,29 +1062,26 @@ Fullscreen Exclusive Mode - game will cover entirety of your screen and will use File size - %1 B - %1 Б + %1 Б - %1 KiB - %1 КіБ + %1 КіБ - + + %1 MiB %1 МіБ - %1 GiB - %1 ГіБ + %1 ГіБ - %1 TiB - %1 ТіБ + %1 ТіБ @@ -1522,16 +1531,209 @@ error reason: ModFields - + Name Назва - + Type Тип + + ModStateController + + + Can not install submod + Неможливо встановити вкладену модифікацію + + + + Mod is already installed + Модифікація вже встановлена + + + + Can not uninstall submod + Неможливо видалити вкладену модифікацію + + + + Mod is not installed + Модифікація не встановлена + + + + Mod is already enabled + Модифікація вже увімкнена + + + + + Mod must be installed first + Спочатку потрібно встановити модифікацію + + + + Mod is not compatible, please update VCMI and checkout latest mod revisions + Модифікація несумісна, будь ласка, оновіть VCMI та перевірте останні версії модифікацій + + + + Can not enable translation mod for a different language! + + + + + Required mod %1 is missing + Необхідна модифікація %1 відсутня + + + + Mod is already disabled + Модифікацію вже вимкнено + + + + Mod archive is missing + Архів з модифікацією відсутній + + + + Mod with such name is already installed + Модифікацію з такою назвою вже встановлено + + + + Mod archive is invalid or corrupted + Архів модифікації непридатний або пошкоджений + + + + Failed to extract mod data + Не вдалося видобути дані модифікації + + + + Data with this mod was not found + Дані з цією модифікацією не знайдено + + + + Mod is located in protected directory, please remove it manually: + + Модифікація знаходиться в захищеному каталозі, будь ласка, видаліть її вручну: + + + + + ModStateItemModel + + + Translation + Переклад + + + + Town + Місто + + + + Test + Тестування + + + + Templates + Шаблони + + + + Spells + Закляття + + + + Music + Музика + + + + Maps + Мапи + + + + Sounds + Звуки + + + + Skills + Вміння + + + + + Other + Інше + + + + Objects + Об'єкти + + + + + Mechanics + Механіки + + + + + Interface + Інтерфейс + + + + Heroes + Герої + + + + + Graphical + Графічний + + + + Expansion + Розширення + + + + Creatures + Істоти + + + + Compatibility + Сумісність + + + + Artifacts + Артефакти + + + + AI + ШІ + + QObject diff --git a/launcher/translation/vietnamese.ts b/launcher/translation/vietnamese.ts index c5263d4e9..8a12ddb56 100644 --- a/launcher/translation/vietnamese.ts +++ b/launcher/translation/vietnamese.ts @@ -90,108 +90,80 @@ CModListModel - Translation - Bản dịch + Bản dịch - Town - Thành phố + Thành phố - Test - Kiểm tra + Kiểm tra - Templates - Mẫu + Mẫu - Spells - Phép + Phép - Music - Nhạc + Nhạc - - Maps - - - - Sounds - Âm thanh + Âm thanh - Skills - Kĩ năng + Kĩ năng - - Other - Khác + Khác - Objects - Đối tượng + Đối tượng - - Mechanics - Cơ chế + Cơ chế - - Interface - Giao diện + Giao diện - Heroes - Tướng + Tướng - - Graphical - Đồ họa + Đồ họa - Expansion - Bản mở rộng + Bản mở rộng - Creatures - Quái + Quái - Compatibility - Tương thích + Tương thích - Artifacts - Vật phẩm + Vật phẩm - AI - Trí tuệ nhân tạo + Trí tuệ nhân tạo @@ -233,7 +205,7 @@ - + Description Mô tả @@ -293,189 +265,202 @@ Hủy - + Mod name Tên bản sửa đổi - + + Installed version Phiên bản cài đặt - + + Latest version Phiên bản mới nhất - + Size - + Download size Kích thước tải về - + Authors Tác giả - + License Giấy phép - + Contact Liên hệ - + Compatibility Tương thích - - + + Required VCMI version Cần phiên bản VCMI - + Supported VCMI version Hỗ trợ phiên bản VCMI - + please upgrade mod - - + + mods repository index - + or newer - + Supported VCMI versions Phiên bản VCMI hỗ trợ - + Languages Ngôn ngữ - + Required mods Cần các bản sửa đổi - + Conflicting mods Bản sửa đổi không tương thích - This mod can not be installed or enabled because the following dependencies are not present - Bản sửa đổi này không thể cài đặt hoặc kích hoạt do thiếu các bản sửa đổi sau + Bản sửa đổi này không thể cài đặt hoặc kích hoạt do thiếu các bản sửa đổi sau - This mod can not be enabled because the following mods are incompatible with it - Bản sửa đổi này không thể kích hoạt do không tương thích các bản sửa đổi sau + Bản sửa đổi này không thể kích hoạt do không tương thích các bản sửa đổi sau - This mod cannot be disabled because it is required by the following mods - Bản sửa đổi này không thể tắt do cần thiết cho các bản sửa đổi sau + Bản sửa đổi này không thể tắt do cần thiết cho các bản sửa đổi sau - This mod cannot be uninstalled or updated because it is required by the following mods - Bản sửa đổi này không thể gỡ bỏ hoặc nâng cấp do cần thiết cho các bản sửa đổi sau + Bản sửa đổi này không thể gỡ bỏ hoặc nâng cấp do cần thiết cho các bản sửa đổi sau - + + This mod cannot be enabled because it translates into a different language. + + + + + This mod can not be enabled because the following dependencies are not present + + + + + This mod can not be installed because the following dependencies are not present + + + + This is a submod and it cannot be installed or uninstalled separately from its parent mod Đây là bản con, không thể cài đặt hoặc gỡ bỏ tách biệt với bản cha - + Notes Ghi chú - + All supported files - + Maps - + Campaigns - + Configs - + Mods Bản sửa đổi - + Gog files - + All files (*.*) - + Select files (configs, mods, maps, campaigns, gog files) to install... - + Replace config file? - + Do you want to replace %1? - + Downloading %1. %p% (%v MB out of %m MB) finished - + Download failed - + Unable to download all files. Encountered errors: @@ -484,409 +469,356 @@ Encountered errors: - + Install successfully downloaded? - + Installing chronicles - + Installing mod %1 - + Operation failed - + Encountered errors: - + screenshots - + Screenshot %1 Hình ảnh %1 - + Mod is incompatible Bản sửa đổi này không tương thích - - CModManager - - - Can not install submod - - - - - Mod is already installed - - - - - Can not uninstall submod - - - - - Mod is not installed - - - - - Mod is already enabled - - - - - - Mod must be installed first - - - - - Mod is not compatible, please update VCMI and checkout latest mod revisions - - - - - Required mod %1 is missing - - - - - Required mod %1 is not enabled - - - - - - This mod conflicts with %1 - - - - - Mod is already disabled - - - - - This mod is needed to run %1 - - - - - Mod archive is missing - - - - - Mod with such name is already installed - - - - - Mod archive is invalid or corrupted - - - - - Failed to extract mod data - - - - - Data with this mod was not found - - - - - Mod is located in protected directory, please remove it manually: - - - - CSettingsView + Off Tắt - + Artificial Intelligence Trí tuệ nhân tạo - + Interface Scaling Phóng đại giao diện - + Neutral AI in battles Máy hoang dã trong trận đánh - + Enemy AI in battles Máy đối thủ trong trận đánh - + Additional repository Nguồn bổ sung - + Downscaling Filter - + Adventure Map Allies Máy liên minh ở bản đồ phiêu lưu - + Online Lobby port - + Autocombat AI in battles - + Sticks Sensitivity - + Automatic (Linear) - + Haptic Feedback - + Software Cursor - + + + Automatic - + + Mods Validation + + + + None - + xBRZ x2 - + xBRZ x3 - + xBRZ x4 - + + Full + + + + Use scalable fonts - + Online Lobby address - + + Cursor Scaling + + + + + Scalable + + + + + Miscellaneous + + + + + Font Scaling (experimental) + + + + + Original + + + + Upscaling Filter - + + Basic + + + + Use Relative Pointer Mode - + Nearest - + Linear - + Input - Touchscreen - + Adventure Map Enemies Máy đối thủ ở bản đồ phiêu lưu - + Show Tutorial again - + Reset - + Network - + Audio - + Relative Pointer Speed - + Music Volume - + Ignore SSL errors - + Input - Mouse - + Long Touch Duration - + % - + Controller Click Tolerance - + Touch Tap Tolerance - + Input - Controller - + Sound Volume - + Windowed Cửa sổ - + Borderless fullscreen Toàn màn hình không viền - + Exclusive fullscreen Toàn màn hình riêng biệt - + Autosave limit (0 = off) Giới hạn lưu tự động (0 = không giới hạn) - + Framerate Limit Giới hạn khung hình - + Autosave prefix Thêm tiền tố vào lưu tự động - + Mouse Click Tolerance - + Sticks Acceleration - + empty = map name prefix Rỗng = tên bản đồ - + Refresh now Làm mới - + Default repository Nguồn mặc định - + Renderer @@ -896,7 +828,7 @@ Install successfully downloaded? Bật - + Select display mode for game Windowed - game will run inside a window that covers part of your screen @@ -913,92 +845,92 @@ Toàn màn hình không viền - Trò chơi chạy toàn màn hình, dùng chung Toàn màn hình riêng biệt - Trò chơi chạy toàn màn hình và dùng độ phân giải được chọn. - + Reserved screen area Diện tích màn hình dành riêng - + Heroes III Translation Bản dịch Heroes III - + Check on startup Kiểm tra khi khởi động - + Fullscreen Toàn màn hình - + General Chung - + VCMI Language Ngôn ngữ VCMI - + Resolution Độ phân giải - + Autosave Tự động lưu - + VSync - + Display index Mục hiện thị - + Network port Cổng mạng - + Video Phim ảnh - + Show intro Hiện thị giới thiệu - + Active Bật - + Disabled Tắt - + Enable Bật - + Not Installed Chưa cài đặt - + Install Cài đặt @@ -1034,7 +966,7 @@ Toàn màn hình riêng biệt - Trò chơi chạy toàn màn hình và dùng đ - + Heroes Chronicles @@ -1042,30 +974,11 @@ Toàn màn hình riêng biệt - Trò chơi chạy toàn màn hình và dùng đ File size - - %1 B - - - - - %1 KiB - - - - + + %1 MiB - - - %1 GiB - - - - - %1 TiB - - FirstLaunchView @@ -1509,16 +1422,208 @@ error reason: ModFields - + Name Tên - + Type Loại + + ModStateController + + + Can not install submod + + + + + Mod is already installed + + + + + Can not uninstall submod + + + + + Mod is not installed + + + + + Mod is already enabled + + + + + + Mod must be installed first + + + + + Mod is not compatible, please update VCMI and checkout latest mod revisions + + + + + Can not enable translation mod for a different language! + + + + + Required mod %1 is missing + + + + + Mod is already disabled + + + + + Mod archive is missing + + + + + Mod with such name is already installed + + + + + Mod archive is invalid or corrupted + + + + + Failed to extract mod data + + + + + Data with this mod was not found + + + + + Mod is located in protected directory, please remove it manually: + + + + + + ModStateItemModel + + + Translation + Bản dịch + + + + Town + Thành phố + + + + Test + Kiểm tra + + + + Templates + Mẫu + + + + Spells + Phép + + + + Music + Nhạc + + + + Maps + + + + + Sounds + Âm thanh + + + + Skills + Kĩ năng + + + + + Other + Khác + + + + Objects + Đối tượng + + + + + Mechanics + Cơ chế + + + + + Interface + Giao diện + + + + Heroes + Tướng + + + + + Graphical + Đồ họa + + + + Expansion + Bản mở rộng + + + + Creatures + Quái + + + + Compatibility + Tương thích + + + + Artifacts + Vật phẩm + + + + AI + Trí tuệ nhân tạo + + QObject diff --git a/launcher/vcmilauncher.desktop b/launcher/vcmilauncher.desktop index ad792e8bb..bb25584bf 100644 --- a/launcher/vcmilauncher.desktop +++ b/launcher/vcmilauncher.desktop @@ -5,7 +5,7 @@ GenericName=Strategy Game GenericName[cs]=Strategická hra GenericName[de]=Strategiespiel Comment=Open-source recreation of Heroes of Might & Magic III -Comment[cs]=Spouštěč enginu s otevřeným kódem pro Heroes of Might and Magic III +Comment[cs]=Open-source engine pro Heroes of Might and Magic III Comment[de]=Open-Source-Nachbau von Heroes of Might and Magic III Icon=vcmiclient Exec=vcmilauncher diff --git a/lib/ArtifactUtils.cpp b/lib/ArtifactUtils.cpp index dfc9fca20..3f6964c2d 100644 --- a/lib/ArtifactUtils.cpp +++ b/lib/ArtifactUtils.cpp @@ -202,21 +202,23 @@ DLL_LINKAGE std::vector ArtifactUtils::assemblyPossibilities( if(art->isCombined()) return arts; - for(const auto artifact : art->getPartOf()) + for(const auto combinedArt : art->getPartOf()) { - assert(artifact->isCombined()); + assert(combinedArt->isCombined()); bool possible = true; - - for(const auto constituent : artifact->getConstituents()) //check if all constituents are available + CArtifactFittingSet fittingSet(*artSet); + for(const auto part : combinedArt->getConstituents()) // check if all constituents are available { - if(!artSet->hasArt(constituent->getId(), onlyEquiped, false, false)) + const auto slot = fittingSet.getArtPos(part->getId(), onlyEquiped, false); + if(slot == ArtifactPosition::PRE_FIRST) { possible = false; break; } + fittingSet.lockSlot(slot); } if(possible) - arts.push_back(artifact); + arts.push_back(combinedArt); } return arts; } @@ -234,7 +236,7 @@ DLL_LINKAGE CArtifactInstance * ArtifactUtils::createArtifact(const ArtifactID & assert(art); auto * artInst = new CArtifactInstance(art); - if(art->isCombined()) + if(art->isCombined() && !art->isFused()) { for(const auto & part : art->getConstituents()) artInst->addPart(createArtInst(part), ArtifactPosition::PRE_FIRST); diff --git a/lib/BasicTypes.cpp b/lib/BasicTypes.cpp index a294a14a2..dec7cbaba 100644 --- a/lib/BasicTypes.cpp +++ b/lib/BasicTypes.cpp @@ -38,7 +38,7 @@ TerrainId AFactionMember::getNativeTerrain() const //this code is used in the CreatureTerrainLimiter::limit to setup battle bonuses //and in the CGHeroInstance::getNativeTerrain() to setup movement bonuses or/and penalties. return getBonusBearer()->hasBonus(selectorNoTerrainPenalty, cachingStringNoTerrainPenalty) - ? TerrainId::ANY_TERRAIN : VLC->factions()->getById(getFaction())->getNativeTerrain(); + ? TerrainId::ANY_TERRAIN : getFactionID().toEntity(VLC)->getNativeTerrain(); } int32_t AFactionMember::magicResistance() const @@ -102,7 +102,7 @@ int AFactionMember::moraleValAndBonusList(TConstBonusListPtr & bonusList) const return maxGoodMorale; } - static const auto unaffectedByMoraleSelector = Selector::type()(BonusType::NON_LIVING).Or(Selector::type()(BonusType::UNDEAD)) + static const auto unaffectedByMoraleSelector = Selector::type()(BonusType::NON_LIVING).Or(Selector::type()(BonusType::MECHANICAL)).Or(Selector::type()(BonusType::UNDEAD)) .Or(Selector::type()(BonusType::SIEGE_WEAPON)).Or(Selector::type()(BonusType::NO_MORALE)); static const std::string cachingStrUn = "AFactionMember::unaffectedByMoraleSelector"; @@ -187,6 +187,7 @@ bool ACreature::isLiving() const //TODO: theoreticaly there exists "LIVING" bonu static const std::string cachingStr = "ACreature::isLiving"; static const CSelector selector = Selector::type()(BonusType::UNDEAD) .Or(Selector::type()(BonusType::NON_LIVING)) + .Or(Selector::type()(BonusType::MECHANICAL)) .Or(Selector::type()(BonusType::GARGOYLE)) .Or(Selector::type()(BonusType::SIEGE_WEAPON)); diff --git a/lib/CArtHandler.cpp b/lib/CArtHandler.cpp index 4f944eb70..0232109c5 100644 --- a/lib/CArtHandler.cpp +++ b/lib/CArtHandler.cpp @@ -56,11 +56,26 @@ const std::vector & CCombinedArtifact::getConstituents() const return constituents; } -const std::vector & CCombinedArtifact::getPartOf() const +const std::set & CCombinedArtifact::getPartOf() const { return partOf; } +void CCombinedArtifact::setFused(bool isFused) +{ + fused = isFused; +} + +bool CCombinedArtifact::isFused() const +{ + return fused; +} + +bool CCombinedArtifact::hasParts() const +{ + return isCombined() && !isFused(); +} + bool CScrollArtifact::isScroll() const { return static_cast(this)->getId() == ArtifactID::SPELL_SCROLL; @@ -177,7 +192,6 @@ bool CArtifact::isTradable() const switch(id.toEnum()) { case ArtifactID::SPELLBOOK: - case ArtifactID::GRAIL: return false; default: return !isBig(); @@ -203,7 +217,7 @@ bool CArtifact::canBePutAt(const CArtifactSet * artSet, ArtifactPosition slot, b auto artCanBePutAt = [this, simpleArtCanBePutAt](const CArtifactSet * artSet, ArtifactPosition slot, bool assumeDestRemoved) -> bool { - if(isCombined()) + if(hasParts()) { if(!simpleArtCanBePutAt(artSet, slot, assumeDestRemoved)) return false; @@ -220,7 +234,7 @@ bool CArtifact::canBePutAt(const CArtifactSet * artSet, ArtifactPosition slot, b auto possibleSlot = ArtifactUtils::getArtAnyPosition(&fittingSet, art->getId()); if(ArtifactUtils::isSlotEquipment(possibleSlot)) { - fittingSet.setNewArtSlot(possibleSlot, nullptr, true); + fittingSet.lockSlot(possibleSlot); } else { @@ -431,9 +445,9 @@ std::shared_ptr CArtHandler::loadFromJson(const std::string & scope, const JsonNode & text = node["text"]; - VLC->generaltexth->registerString(scope, art->getNameTextID(), text["name"].String()); - VLC->generaltexth->registerString(scope, art->getDescriptionTextID(), text["description"].String()); - VLC->generaltexth->registerString(scope, art->getEventTextID(), text["event"].String()); + VLC->generaltexth->registerString(scope, art->getNameTextID(), text["name"]); + VLC->generaltexth->registerString(scope, art->getDescriptionTextID(), text["description"]); + VLC->generaltexth->registerString(scope, art->getEventTextID(), text["event"]); const JsonNode & graphics = node["graphics"]; art->image = graphics["image"].String(); @@ -606,19 +620,21 @@ void CArtHandler::loadType(CArtifact * art, const JsonNode & node) const void CArtHandler::loadComponents(CArtifact * art, const JsonNode & node) { - if (!node["components"].isNull()) + if(!node["components"].isNull()) { for(const auto & component : node["components"].Vector()) { - VLC->identifiers()->requestIdentifier("artifact", component, [=](si32 id) + VLC->identifiers()->requestIdentifier("artifact", component, [this, art](int32_t id) { // when this code is called both combinational art as well as component are loaded // so it is safe to access any of them art->constituents.push_back(ArtifactID(id).toArtifact()); - objects[id]->partOf.push_back(art); + objects[id]->partOf.insert(art); }); } } + if(!node["fusedComponents"].isNull()) + art->setFused(node["fusedComponents"].Bool()); } void CArtHandler::makeItCreatureArt(CArtifact * a, bool onlyCreature) @@ -691,9 +707,7 @@ void CArtHandler::afterLoadFinalization() CBonusSystemNode::treeHasChanged(); } -CArtifactSet::~CArtifactSet() = default; - -const CArtifactInstance * CArtifactSet::getArt(const ArtifactPosition & pos, bool excludeLocked) const +CArtifactInstance * CArtifactSet::getArt(const ArtifactPosition & pos, bool excludeLocked) const { if(const ArtSlotInfo * si = getSlot(pos)) { @@ -704,56 +718,34 @@ const CArtifactInstance * CArtifactSet::getArt(const ArtifactPosition & pos, boo return nullptr; } -CArtifactInstance * CArtifactSet::getArt(const ArtifactPosition & pos, bool excludeLocked) -{ - return const_cast((const_cast(this))->getArt(pos, excludeLocked)); -} - ArtifactPosition CArtifactSet::getArtPos(const ArtifactID & aid, bool onlyWorn, bool allowLocked) const { - const auto result = getAllArtPositions(aid, onlyWorn, allowLocked, false); - return result.empty() ? ArtifactPosition{ArtifactPosition::PRE_FIRST} : result[0]; -} - -std::vector CArtifactSet::getAllArtPositions(const ArtifactID & aid, bool onlyWorn, bool allowLocked, bool getAll) const -{ - std::vector result; - for(const auto & slotInfo : artifactsWorn) - if(slotInfo.second.artifact->getTypeId() == aid && (allowLocked || !slotInfo.second.locked)) - result.push_back(slotInfo.first); - - if(onlyWorn) - return result; - if(!getAll && !result.empty()) - return result; - - auto backpackPositions = getBackpackArtPositions(aid); - result.insert(result.end(), backpackPositions.begin(), backpackPositions.end()); - return result; -} - -std::vector CArtifactSet::getBackpackArtPositions(const ArtifactID & aid) const -{ - std::vector result; - - si32 backpackPosition = ArtifactPosition::BACKPACK_START; - for(const auto & artInfo : artifactsInBackpack) + for(const auto & [slot, slotInfo] : artifactsWorn) { - const auto * art = artInfo.getArt(); - if(art && art->artType->getId() == aid) - result.emplace_back(backpackPosition); - backpackPosition++; + if(slotInfo.artifact->getTypeId() == aid && (allowLocked || !slotInfo.locked)) + return slot; } - return result; + if(!onlyWorn) + { + size_t backpackPositionIdx = ArtifactPosition::BACKPACK_START; + for(const auto & artInfo : artifactsInBackpack) + { + const auto art = artInfo.getArt(); + if(art && art->getType()->getId() == aid) + return ArtifactPosition(backpackPositionIdx); + backpackPositionIdx++; + } + } + return ArtifactPosition::PRE_FIRST; } const CArtifactInstance * CArtifactSet::getArtByInstanceId(const ArtifactInstanceID & artInstId) const { - for(auto i : artifactsWorn) + for(const auto & i : artifactsWorn) if(i.second.artifact->getId() == artInstId) return i.second.artifact; - for(auto i : artifactsInBackpack) + for(const auto & i : artifactsInBackpack) if(i.artifact->getId() == artInstId) return i.artifact; @@ -764,7 +756,7 @@ ArtifactPosition CArtifactSet::getArtPos(const CArtifactInstance * artInst) cons { if(artInst) { - for(const auto & slot : artInst->artType->getPossibleSlots().at(bearerType())) + for(const auto & slot : artInst->getType()->getPossibleSlots().at(bearerType())) if(getArt(slot) == artInst) return slot; @@ -779,38 +771,44 @@ ArtifactPosition CArtifactSet::getArtPos(const CArtifactInstance * artInst) cons return ArtifactPosition::PRE_FIRST; } -bool CArtifactSet::hasArt(const ArtifactID & aid, bool onlyWorn, bool searchBackpackAssemblies, bool allowLocked) const +bool CArtifactSet::hasArt(const ArtifactID & aid, bool onlyWorn, bool searchCombinedParts) const { - return getArtPosCount(aid, onlyWorn, searchBackpackAssemblies, allowLocked) > 0; + if(searchCombinedParts && getCombinedArtWithPart(aid)) + return true; + if(getArtPos(aid, onlyWorn, searchCombinedParts) != ArtifactPosition::PRE_FIRST) + return true; + return false; } -bool CArtifactSet::hasArtBackpack(const ArtifactID & aid) const -{ - return !getBackpackArtPositions(aid).empty(); -} - -unsigned CArtifactSet::getArtPosCount(const ArtifactID & aid, bool onlyWorn, bool searchBackpackAssemblies, bool allowLocked) const -{ - const auto allPositions = getAllArtPositions(aid, onlyWorn, allowLocked, true); - if(!allPositions.empty()) - return allPositions.size(); - - if(searchBackpackAssemblies && getHiddenArt(aid)) - return 1; - - return 0; -} - -CArtifactSet::ArtPlacementMap CArtifactSet::putArtifact(ArtifactPosition slot, CArtifactInstance * art) +CArtifactSet::ArtPlacementMap CArtifactSet::putArtifact(const ArtifactPosition & slot, CArtifactInstance * art) { ArtPlacementMap resArtPlacement; + const auto putToSlot = [this](const ArtifactPosition & targetSlot, CArtifactInstance * targetArt, bool locked) + { + ArtSlotInfo * slotInfo; + if(targetSlot == ArtifactPosition::TRANSITION_POS) + { + slotInfo = &artifactsTransitionPos; + } + else if(ArtifactUtils::isSlotEquipment(targetSlot)) + { + slotInfo = &artifactsWorn[targetSlot]; + } + else + { + auto position = artifactsInBackpack.begin() + targetSlot - ArtifactPosition::BACKPACK_START; + slotInfo = &(*artifactsInBackpack.emplace(position)); + } + slotInfo->artifact = targetArt; + slotInfo->locked = locked; + }; - setNewArtSlot(slot, art, false); - if(art->artType->isCombined() && ArtifactUtils::isSlotEquipment(slot)) + putToSlot(slot, art, false); + if(art->getType()->isCombined() && ArtifactUtils::isSlotEquipment(slot)) { const CArtifactInstance * mainPart = nullptr; for(const auto & part : art->getPartsInfo()) - if(vstd::contains(part.art->artType->getPossibleSlots().at(bearerType()), slot) + if(vstd::contains(part.art->getType()->getPossibleSlots().at(bearerType()), slot) && (part.slot == ArtifactPosition::PRE_FIRST)) { mainPart = part.art; @@ -822,24 +820,43 @@ CArtifactSet::ArtPlacementMap CArtifactSet::putArtifact(ArtifactPosition slot, C if(part.art != mainPart) { auto partSlot = part.slot; - if(!part.art->artType->canBePutAt(this, partSlot)) + if(!part.art->getType()->canBePutAt(this, partSlot)) partSlot = ArtifactUtils::getArtAnyPosition(this, part.art->getTypeId()); assert(ArtifactUtils::isSlotEquipment(partSlot)); - setNewArtSlot(partSlot, part.art, true); - resArtPlacement.emplace(std::make_pair(part.art, partSlot)); + putToSlot(partSlot, part.art, true); + resArtPlacement.emplace(part.art, partSlot); } else { - resArtPlacement.emplace(std::make_pair(part.art, part.slot)); + resArtPlacement.emplace(part.art, part.slot); } } } return resArtPlacement; } -void CArtifactSet::removeArtifact(ArtifactPosition slot) +void CArtifactSet::removeArtifact(const ArtifactPosition & slot) { + const auto eraseArtSlot = [this](const ArtifactPosition & slotForErase) + { + if(slotForErase == ArtifactPosition::TRANSITION_POS) + { + artifactsTransitionPos.artifact = nullptr; + } + else if(ArtifactUtils::isSlotBackpack(slotForErase)) + { + auto backpackSlot = ArtifactPosition(slotForErase - ArtifactPosition::BACKPACK_START); + + assert(artifactsInBackpack.begin() + backpackSlot < artifactsInBackpack.end()); + artifactsInBackpack.erase(artifactsInBackpack.begin() + backpackSlot); + } + else + { + artifactsWorn.erase(slotForErase); + } + }; + if(const auto art = getArt(slot, false)) { if(art->isCombined()) @@ -858,7 +875,7 @@ void CArtifactSet::removeArtifact(ArtifactPosition slot) } } -std::pair CArtifactSet::searchForConstituent(const ArtifactID & aid) const +const CArtifactInstance * CArtifactSet::getCombinedArtWithPart(const ArtifactID & partId) const { for(const auto & slot : artifactsInBackpack) { @@ -867,24 +884,12 @@ std::pair CArtifactSet::se { for(auto & ci : art->getPartsInfo()) { - if(ci.art->getTypeId() == aid) - { - return {art, ci.art}; - } + if(ci.art->getTypeId() == partId) + return art; } } } - return {nullptr, nullptr}; -} - -const CArtifactInstance * CArtifactSet::getHiddenArt(const ArtifactID & aid) const -{ - return searchForConstituent(aid).second; -} - -const CArtifactInstance * CArtifactSet::getAssemblyByConstituent(const ArtifactID & aid) const -{ - return searchForConstituent(aid).first; + return nullptr; } const ArtSlotInfo * CArtifactSet::getSlot(const ArtifactPosition & pos) const @@ -905,6 +910,19 @@ const ArtSlotInfo * CArtifactSet::getSlot(const ArtifactPosition & pos) const return nullptr; } +void CArtifactSet::lockSlot(const ArtifactPosition & pos) +{ + if(pos == ArtifactPosition::TRANSITION_POS) + artifactsTransitionPos.locked = true; + else if(ArtifactUtils::isSlotEquipment(pos)) + artifactsWorn[pos].locked = true; + else + { + assert(artifactsInBackpack.size() > pos - ArtifactPosition::BACKPACK_START); + (artifactsInBackpack.begin() + pos - ArtifactPosition::BACKPACK_START)->locked = true; + } +} + bool CArtifactSet::isPositionFree(const ArtifactPosition & pos, bool onlyLockCheck) const { if(bearerType() == ArtBearer::ALTAR) @@ -916,47 +934,6 @@ bool CArtifactSet::isPositionFree(const ArtifactPosition & pos, bool onlyLockChe return true; //no slot means not used } -void CArtifactSet::setNewArtSlot(const ArtifactPosition & slot, ConstTransitivePtr art, bool locked) -{ - assert(!vstd::contains(artifactsWorn, slot)); - - ArtSlotInfo * slotInfo; - if(slot == ArtifactPosition::TRANSITION_POS) - { - slotInfo = &artifactsTransitionPos; - } - else if(ArtifactUtils::isSlotEquipment(slot)) - { - slotInfo = &artifactsWorn[slot]; - } - else - { - auto position = artifactsInBackpack.begin() + slot - ArtifactPosition::BACKPACK_START; - slotInfo = &(*artifactsInBackpack.emplace(position, ArtSlotInfo())); - } - slotInfo->artifact = art; - slotInfo->locked = locked; -} - -void CArtifactSet::eraseArtSlot(const ArtifactPosition & slot) -{ - if(slot == ArtifactPosition::TRANSITION_POS) - { - artifactsTransitionPos.artifact = nullptr; - } - else if(ArtifactUtils::isSlotBackpack(slot)) - { - auto backpackSlot = ArtifactPosition(slot - ArtifactPosition::BACKPACK_START); - - assert(artifactsInBackpack.begin() + backpackSlot < artifactsInBackpack.end()); - artifactsInBackpack.erase(artifactsInBackpack.begin() + backpackSlot); - } - else - { - artifactsWorn.erase(slot); - } -} - void CArtifactSet::artDeserializationFix(CBonusSystemNode *node) { for(auto & elem : artifactsWorn) @@ -1017,7 +994,7 @@ void CArtifactSet::serializeJsonHero(JsonSerializeFormat & handler) { auto * artifact = ArtifactUtils::createArtifact(artifactID); auto slot = ArtifactPosition::BACKPACK_START + artifactsInBackpack.size(); - if(artifact->artType->canBePutAt(this, slot)) + if(artifact->getType()->canBePutAt(this, slot)) { auto artsMap = putArtifact(slot, artifact); artifact->addPlacementMap(artsMap); @@ -1058,7 +1035,7 @@ void CArtifactSet::serializeJsonSlot(JsonSerializeFormat & handler, const Artifa { auto * artifact = ArtifactUtils::createArtifact(artifactID.toEnum()); - if(artifact->artType->canBePutAt(this, slot)) + if(artifact->getType()->canBePutAt(this, slot)) { auto artsMap = putArtifact(slot, artifact); artifact->addPlacementMap(artsMap); diff --git a/lib/CArtHandler.h b/lib/CArtHandler.h index 77b7d1d1f..a902f22c8 100644 --- a/lib/CArtHandler.h +++ b/lib/CArtHandler.h @@ -46,14 +46,19 @@ namespace ArtBearer class DLL_LINKAGE CCombinedArtifact { protected: - CCombinedArtifact() = default; + CCombinedArtifact() : fused(false) {}; std::vector constituents; // Artifacts IDs a combined artifact consists of, or nullptr. - std::vector partOf; // Reverse map of constituents - combined arts that include this art + std::set partOf; // Reverse map of constituents - combined arts that include this art + bool fused; + public: bool isCombined() const; const std::vector & getConstituents() const; - const std::vector & getPartOf() const; + const std::set & getPartOf() const; + void setFused(bool isFused); + bool isFused() const; + bool hasParts() const; }; class DLL_LINKAGE CScrollArtifact @@ -175,10 +180,10 @@ private: struct DLL_LINKAGE ArtSlotInfo { - ConstTransitivePtr artifact; - ui8 locked; //if locked, then artifact points to the combined artifact + CArtifactInstance * artifact; + bool locked; //if locked, then artifact points to the combined artifact - ArtSlotInfo() : locked(false) {} + ArtSlotInfo() : artifact(nullptr), locked(false) {} const CArtifactInstance * getArt() const; template void serialize(Handler & h) @@ -197,32 +202,20 @@ public: std::map artifactsWorn; //map; positions: 0 - head; 1 - shoulders; 2 - neck; 3 - right hand; 4 - left hand; 5 - torso; 6 - right ring; 7 - left ring; 8 - feet; 9 - misc1; 10 - misc2; 11 - misc3; 12 - misc4; 13 - mach1; 14 - mach2; 15 - mach3; 16 - mach4; 17 - spellbook; 18 - misc5 ArtSlotInfo artifactsTransitionPos; // Used as transition position for dragAndDrop artifact exchange - void setNewArtSlot(const ArtifactPosition & slot, ConstTransitivePtr art, bool locked); - void eraseArtSlot(const ArtifactPosition & slot); - const ArtSlotInfo * getSlot(const ArtifactPosition & pos) const; - const CArtifactInstance * getArt(const ArtifactPosition & pos, bool excludeLocked = true) const; //nullptr - no artifact - CArtifactInstance * getArt(const ArtifactPosition & pos, bool excludeLocked = true); //nullptr - no artifact - /// Looks for equipped artifact with given ID and returns its slot ID or -1 if none - /// (if more than one such artifact lower ID is returned) + void lockSlot(const ArtifactPosition & pos); + CArtifactInstance * getArt(const ArtifactPosition & pos, bool excludeLocked = true) const; + /// Looks for first artifact with given ID ArtifactPosition getArtPos(const ArtifactID & aid, bool onlyWorn = true, bool allowLocked = true) const; ArtifactPosition getArtPos(const CArtifactInstance * art) const; - std::vector getAllArtPositions(const ArtifactID & aid, bool onlyWorn, bool allowLocked, bool getAll) const; - std::vector getBackpackArtPositions(const ArtifactID & aid) const; const CArtifactInstance * getArtByInstanceId(const ArtifactInstanceID & artInstId) const; - /// Search for constituents of assemblies in backpack which do not have an ArtifactPosition - const CArtifactInstance * getHiddenArt(const ArtifactID & aid) const; - const CArtifactInstance * getAssemblyByConstituent(const ArtifactID & aid) const; - /// Checks if hero possess artifact of given id (either in backack or worn) - bool hasArt(const ArtifactID & aid, bool onlyWorn = false, bool searchBackpackAssemblies = false, bool allowLocked = true) const; - bool hasArtBackpack(const ArtifactID & aid) const; + bool hasArt(const ArtifactID & aid, bool onlyWorn = false, bool searchCombinedParts = false) const; bool isPositionFree(const ArtifactPosition & pos, bool onlyLockCheck = false) const; - unsigned getArtPosCount(const ArtifactID & aid, bool onlyWorn = true, bool searchBackpackAssemblies = true, bool allowLocked = true) const; virtual ArtBearer::ArtBearer bearerType() const = 0; - virtual ArtPlacementMap putArtifact(ArtifactPosition slot, CArtifactInstance * art); - virtual void removeArtifact(ArtifactPosition slot); - virtual ~CArtifactSet(); + virtual ArtPlacementMap putArtifact(const ArtifactPosition & slot, CArtifactInstance * art); + virtual void removeArtifact(const ArtifactPosition & slot); + virtual ~CArtifactSet() = default; template void serialize(Handler &h) { @@ -233,8 +226,7 @@ public: void artDeserializationFix(CBonusSystemNode *node); void serializeJsonArtifacts(JsonSerializeFormat & handler, const std::string & fieldName); -protected: - std::pair searchForConstituent(const ArtifactID & aid) const; + const CArtifactInstance * getCombinedArtWithPart(const ArtifactID & partId) const; private: void serializeJsonHero(JsonSerializeFormat & handler); diff --git a/lib/CArtifactInstance.cpp b/lib/CArtifactInstance.cpp index 0451e2d2f..e1be208dd 100644 --- a/lib/CArtifactInstance.cpp +++ b/lib/CArtifactInstance.cpp @@ -20,12 +20,12 @@ VCMI_LIB_NAMESPACE_BEGIN void CCombinedArtifactInstance::addPart(CArtifactInstance * art, const ArtifactPosition & slot) { auto artInst = static_cast(this); - assert(vstd::contains_if(artInst->artType->getConstituents(), + assert(vstd::contains_if(artInst->getType()->getConstituents(), [=](const CArtifact * partType) { return partType->getId() == art->getTypeId(); })); - assert(art->getParentNodes().size() == 1 && art->getParentNodes().front() == art->artType); + assert(art->getParentNodes().size() == 1 && art->getParentNodes().front() == art->getType()); partsInfo.emplace_back(art, slot); artInst->attachTo(*art); } @@ -44,18 +44,23 @@ bool CCombinedArtifactInstance::isPart(const CArtifactInstance * supposedPart) c return false; } +bool CCombinedArtifactInstance::hasParts() const +{ + return !partsInfo.empty(); +} + const std::vector & CCombinedArtifactInstance::getPartsInfo() const { return partsInfo; } -void CCombinedArtifactInstance::addPlacementMap(CArtifactSet::ArtPlacementMap & placementMap) +void CCombinedArtifactInstance::addPlacementMap(const CArtifactSet::ArtPlacementMap & placementMap) { if(!placementMap.empty()) for(auto & part : partsInfo) { - assert(placementMap.find(part.art) != placementMap.end()); - part.slot = placementMap.at(part.art); + if(placementMap.find(part.art) != placementMap.end()) + part.slot = placementMap.at(part.art); } } @@ -72,7 +77,7 @@ void CGrowingArtifactInstance::growingUp() { auto artInst = static_cast(this); - if(artInst->artType->isGrowing()) + if(artInst->getType()->isGrowing()) { auto bonus = std::make_shared(); @@ -81,7 +86,7 @@ void CGrowingArtifactInstance::growingUp() bonus->duration = BonusDuration::COMMANDER_KILLED; artInst->accumulateBonus(bonus); - for(const auto & bonus : artInst->artType->getBonusesPerLevel()) + for(const auto & bonus : artInst->getType()->getBonusesPerLevel()) { // Every n levels if(artInst->valOfBonuses(BonusType::LEVEL_COUNTER) % bonus.first == 0) @@ -89,7 +94,7 @@ void CGrowingArtifactInstance::growingUp() artInst->accumulateBonus(std::make_shared(bonus.second)); } } - for(const auto & bonus : artInst->artType->getThresholdBonuses()) + for(const auto & bonus : artInst->getType()->getThresholdBonuses()) { // At n level if(artInst->valOfBonuses(BonusType::LEVEL_COUNTER) == bonus.first) @@ -120,26 +125,23 @@ CArtifactInstance::CArtifactInstance() void CArtifactInstance::setType(const CArtifact * art) { - artType = art; + artTypeID = art->getId(); attachToSource(*art); } std::string CArtifactInstance::nodeName() const { - return "Artifact instance of " + (artType ? artType->getJsonKey() : std::string("uninitialized")) + " type"; -} - -std::string CArtifactInstance::getDescription() const -{ - std::string text = artType->getDescriptionTranslated(); - if(artType->isScroll()) - ArtifactUtils::insertScrrollSpellName(text, getScrollSpellID()); - return text; + return "Artifact instance of " + (getType() ? getType()->getJsonKey() : std::string("uninitialized")) + " type"; } ArtifactID CArtifactInstance::getTypeId() const { - return artType->getId(); + return artTypeID; +} + +const CArtifact * CArtifactInstance::getType() const +{ + return artTypeID.hasValue() ? artTypeID.toArtifact() : nullptr; } ArtifactInstanceID CArtifactInstance::getId() const @@ -154,44 +156,22 @@ void CArtifactInstance::setId(ArtifactInstanceID id) bool CArtifactInstance::canBePutAt(const CArtifactSet * artSet, ArtifactPosition slot, bool assumeDestRemoved) const { - return artType->canBePutAt(artSet, slot, assumeDestRemoved); + return getType()->canBePutAt(artSet, slot, assumeDestRemoved); } bool CArtifactInstance::isCombined() const { - return artType->isCombined(); + return getType()->isCombined(); } bool CArtifactInstance::isScroll() const { - return artType->isScroll(); -} - -void CArtifactInstance::putAt(CArtifactSet & set, const ArtifactPosition slot) -{ - auto placementMap = set.putArtifact(slot, this); - addPlacementMap(placementMap); -} - -void CArtifactInstance::removeFrom(CArtifactSet & set, const ArtifactPosition slot) -{ - set.removeArtifact(slot); - for(auto & part : partsInfo) - { - if(part.slot != ArtifactPosition::PRE_FIRST) - part.slot = ArtifactPosition::PRE_FIRST; - } -} - -void CArtifactInstance::move(CArtifactSet & srcSet, const ArtifactPosition srcSlot, CArtifactSet & dstSet, const ArtifactPosition dstSlot) -{ - removeFrom(srcSet, srcSlot); - putAt(dstSet, dstSlot); + return getType()->isScroll(); } void CArtifactInstance::deserializationFix() { - setType(artType); + setType(artTypeID.toArtifact()); for(PartInfo & part : partsInfo) attachTo(*part.art); } diff --git a/lib/CArtifactInstance.h b/lib/CArtifactInstance.h index 7c100a91f..1cb5f6508 100644 --- a/lib/CArtifactInstance.h +++ b/lib/CArtifactInstance.h @@ -25,7 +25,7 @@ protected: public: struct PartInfo { - ConstTransitivePtr art; + CArtifactInstance * art; ArtifactPosition slot; template void serialize(Handler & h) { @@ -38,8 +38,9 @@ public: void addPart(CArtifactInstance * art, const ArtifactPosition & slot); // Checks if supposed part inst is part of this combined art inst bool isPart(const CArtifactInstance * supposedPart) const; + bool hasParts() const; const std::vector & getPartsInfo() const; - void addPlacementMap(CArtifactSet::ArtPlacementMap & placementMap); + void addPlacementMap(const CArtifactSet::ArtPlacementMap & placementMap); template void serialize(Handler & h) { @@ -72,15 +73,15 @@ protected: void init(); ArtifactInstanceID id; + ArtifactID artTypeID; public: - const CArtifact * artType = nullptr; CArtifactInstance(const CArtifact * art); CArtifactInstance(); void setType(const CArtifact * art); std::string nodeName() const override; - std::string getDescription() const; ArtifactID getTypeId() const; + const CArtifact * getType() const; ArtifactInstanceID getId() const; void setId(ArtifactInstanceID id); @@ -88,16 +89,23 @@ public: bool assumeDestRemoved = false) const; bool isCombined() const; bool isScroll() const; - void putAt(CArtifactSet & set, const ArtifactPosition slot); - void removeFrom(CArtifactSet & set, const ArtifactPosition slot); - void move(CArtifactSet & srcSet, const ArtifactPosition srcSlot, CArtifactSet & dstSet, const ArtifactPosition dstSlot); void deserializationFix(); template void serialize(Handler & h) { h & static_cast(*this); h & static_cast(*this); - h & artType; + if (h.version >= Handler::Version::REMOVE_VLC_POINTERS) + { + h & artTypeID; + } + else + { + bool isNull = false; + h & isNull; + if (!isNull) + h & artTypeID; + } h & id; BONUS_TREE_DESERIALIZATION_FIX } diff --git a/lib/CBonusTypeHandler.cpp b/lib/CBonusTypeHandler.cpp index 799bf2632..3c142b063 100644 --- a/lib/CBonusTypeHandler.cpp +++ b/lib/CBonusTypeHandler.cpp @@ -15,12 +15,13 @@ #include "filesystem/Filesystem.h" -#include "GameConstants.h" #include "CCreatureHandler.h" +#include "GameConstants.h" +#include "VCMI_Lib.h" +#include "modding/ModScope.h" +#include "spells/CSpellHandler.h" #include "texts/CGeneralTextHandler.h" #include "json/JsonUtils.h" -#include "spells/CSpellHandler.h" -#include "VCMI_Lib.h" template class std::vector; @@ -200,8 +201,10 @@ ImagePath CBonusTypeHandler::bonusToGraphics(const std::shared_ptr & bonu void CBonusTypeHandler::load() { - const JsonNode gameConf(JsonPath::builtin("config/gameConfig.json")); - const JsonNode config(JsonUtils::assembleFromFiles(gameConf["bonuses"].convertTo>())); + JsonNode gameConf(JsonPath::builtin("config/gameConfig.json")); + gameConf.setModScope(ModScope::scopeBuiltin()); + JsonNode config(JsonUtils::assembleFromFiles(gameConf["bonuses"])); + config.setModScope("vcmi"); load(config); } @@ -240,8 +243,8 @@ void CBonusTypeHandler::loadItem(const JsonNode & source, CBonusType & dest, con if (!dest.hidden) { - VLC->generaltexth->registerString( "vcmi", dest.getNameTextID(), source["name"].String()); - VLC->generaltexth->registerString( "vcmi", dest.getDescriptionTextID(), source["description"].String()); + VLC->generaltexth->registerString( "vcmi", dest.getNameTextID(), source["name"]); + VLC->generaltexth->registerString( "vcmi", dest.getDescriptionTextID(), source["description"]); } const JsonNode & graphics = source["graphics"]; diff --git a/lib/CCreatureHandler.cpp b/lib/CCreatureHandler.cpp index 546aed9b7..ee445e617 100644 --- a/lib/CCreatureHandler.cpp +++ b/lib/CCreatureHandler.cpp @@ -117,7 +117,7 @@ int32_t CCreature::getHorde() const return hordeGrowth; } -FactionID CCreature::getFaction() const +FactionID CCreature::getFactionID() const { return FactionID(faction); } @@ -343,11 +343,6 @@ bool CCreature::isMyUpgrade(const CCreature *anotherCre) const return vstd::contains(upgrades, anotherCre->getId()); } -bool CCreature::valid() const -{ - return this == (*VLC->creh)[idNumber]; -} - std::string CCreature::nodeName() const { return "\"" + getNamePluralTextID() + "\""; @@ -467,56 +462,6 @@ void CCreatureHandler::loadCommanders() } } -void CCreatureHandler::loadBonuses(JsonNode & creature, std::string bonuses) const -{ - auto makeBonusNode = [&](const std::string & type, double val = 0) -> JsonNode - { - JsonNode ret; - ret["type"].String() = type; - ret["val"].Float() = val; - return ret; - }; - - static const std::map abilityMap = - { - {"FLYING_ARMY", makeBonusNode("FLYING")}, - {"SHOOTING_ARMY", makeBonusNode("SHOOTER")}, - {"SIEGE_WEAPON", makeBonusNode("SIEGE_WEAPON")}, - {"const_free_attack", makeBonusNode("BLOCKS_RETALIATION")}, - {"IS_UNDEAD", makeBonusNode("UNDEAD")}, - {"const_no_melee_penalty", makeBonusNode("NO_MELEE_PENALTY")}, - {"const_jousting", makeBonusNode("JOUSTING", 5)}, - {"KING_1", makeBonusNode("KING")}, // Slayer with no expertise - {"KING_2", makeBonusNode("KING", 2)}, // Advanced Slayer or better - {"KING_3", makeBonusNode("KING", 3)}, // Expert Slayer only - {"const_no_wall_penalty", makeBonusNode("NO_WALL_PENALTY")}, - {"MULTI_HEADED", makeBonusNode("ATTACKS_ALL_ADJACENT")}, - {"IMMUNE_TO_MIND_SPELLS", makeBonusNode("MIND_IMMUNITY")}, - {"HAS_EXTENDED_ATTACK", makeBonusNode("TWO_HEX_ATTACK_BREATH")} - }; - - auto hasAbility = [&](const std::string & name) -> bool - { - return boost::algorithm::find_first(bonuses, name); - }; - - for(const auto & a : abilityMap) - { - if(hasAbility(a.first)) - creature["abilities"][a.first] = a.second; - } - if(hasAbility("DOUBLE_WIDE")) - creature["doubleWide"].Bool() = true; - - if(hasAbility("const_raises_morale")) - { - JsonNode node = makeBonusNode("MORALE"); - node["val"].Float() = 1; - node["propagator"].String() = "HERO"; - creature["abilities"]["const_raises_morale"] = node; - } -} - std::vector CCreatureHandler::loadLegacyData() { size_t dataSize = VLC->engineSettings()->getInteger(EGameSettings::TEXTS_CREATURE); @@ -586,7 +531,7 @@ std::vector CCreatureHandler::loadLegacyData() // unused - ability text, not used since we no longer have original creature window parser.readString(); - loadBonuses(data, parser.readString()); //Attributes + parser.readString(); // unused - abilities, not used since we load them all from json configs h3Data.push_back(data); } @@ -617,9 +562,9 @@ std::shared_ptr CCreatureHandler::loadFromJson(const std::string & sc cre->cost = ResourceSet(node["cost"]); - VLC->generaltexth->registerString(scope, cre->getNameSingularTextID(), node["name"]["singular"].String()); - VLC->generaltexth->registerString(scope, cre->getNamePluralTextID(), node["name"]["plural"].String()); - VLC->generaltexth->registerString(scope, cre->getDescriptionTextID(), node["description"].String()); + VLC->generaltexth->registerString(scope, cre->getNameSingularTextID(), node["name"]["singular"]); + VLC->generaltexth->registerString(scope, cre->getNamePluralTextID(), node["name"]["plural"]); + VLC->generaltexth->registerString(scope, cre->getDescriptionTextID(), node["description"]); cre->addBonus(node["hitPoints"].Integer(), BonusType::STACK_HEALTH); cre->addBonus(node["speed"].Integer(), BonusType::STACKS_SPEED); diff --git a/lib/CCreatureHandler.h b/lib/CCreatureHandler.h index 7b09b8d65..16cf2ca16 100644 --- a/lib/CCreatureHandler.h +++ b/lib/CCreatureHandler.h @@ -127,7 +127,7 @@ public: std::string getNamePluralTextID() const override; std::string getNameSingularTextID() const override; - FactionID getFaction() const override; + FactionID getFactionID() const override; int32_t getIndex() const override; int32_t getIconIndex() const override; std::string getJsonKey() const override; @@ -166,8 +166,6 @@ public: static int estimateCreatureCount(ui32 countID); //reverse version of above function, returns middle of range bool isMyUpgrade(const CCreature *anotherCre) const; - bool valid() const; - void addBonus(int val, BonusType type); void addBonus(int val, BonusType type, BonusSubtypeID subtype); std::string nodeName() const override; @@ -197,8 +195,6 @@ private: void loadStackExperience(CCreature * creature, const JsonNode & input) const; void loadCreatureJson(CCreature * creature, const JsonNode & config) const; - /// adding abilities from ZCRTRAIT.TXT - void loadBonuses(JsonNode & creature, std::string bonuses) const; /// load all creatures from H3 files void load(); void loadCommanders(); diff --git a/lib/CCreatureSet.cpp b/lib/CCreatureSet.cpp index 9f74f35e5..46fa481cf 100644 --- a/lib/CCreatureSet.cpp +++ b/lib/CCreatureSet.cpp @@ -15,12 +15,12 @@ #include "CCreatureHandler.h" #include "VCMI_Lib.h" #include "IGameSettings.h" +#include "entities/hero/CHeroHandler.h" #include "mapObjects/CGHeroInstance.h" #include "modding/ModScope.h" #include "IGameCallback.h" #include "texts/CGeneralTextHandler.h" #include "spells/CSpellHandler.h" -#include "CHeroHandler.h" #include "IBonusTypeHandler.h" #include "serializer/JsonSerializeFormat.h" @@ -48,7 +48,7 @@ const CCreature * CCreatureSet::getCreature(const SlotID & slot) const { auto i = stacks.find(slot); if (i != stacks.end()) - return i->second->type; + return i->second->getCreature(); else return nullptr; } @@ -84,11 +84,10 @@ SlotID CCreatureSet::getSlotFor(const CreatureID & creature, ui32 slotsAmount) c SlotID CCreatureSet::getSlotFor(const CCreature *c, ui32 slotsAmount) const { - assert(c && c->valid()); + assert(c); for(const auto & elem : stacks) { - assert(elem.second->type->valid()); - if(elem.second->type == c) + if(elem.second->getType() == c) { return elem.first; //if there is already such creature we return its slot id } @@ -98,18 +97,16 @@ SlotID CCreatureSet::getSlotFor(const CCreature *c, ui32 slotsAmount) const bool CCreatureSet::hasCreatureSlots(const CCreature * c, const SlotID & exclude) const { - assert(c && c->valid()); + assert(c); for(const auto & elem : stacks) // elem is const { if(elem.first == exclude) // Check slot continue; - if(!elem.second || !elem.second->type) // Check creature + if(!elem.second || !elem.second->getType()) // Check creature continue; - assert(elem.second->type->valid()); - - if(elem.second->type == c) + if(elem.second->getType() == c) return true; } return false; @@ -117,7 +114,7 @@ bool CCreatureSet::hasCreatureSlots(const CCreature * c, const SlotID & exclude) std::vector CCreatureSet::getCreatureSlots(const CCreature * c, const SlotID & exclude, TQuantity ignoreAmount) const { - assert(c && c->valid()); + assert(c); std::vector result; for(const auto & elem : stacks) @@ -125,13 +122,12 @@ std::vector CCreatureSet::getCreatureSlots(const CCreature * c, const Sl if(elem.first == exclude) continue; - if(!elem.second || !elem.second->type || elem.second->type != c) + if(!elem.second || !elem.second->getType() || elem.second->getType() != c) continue; if(elem.second->count == ignoreAmount || elem.second->count < 1) continue; - assert(elem.second->type->valid()); result.push_back(elem.first); } return result; @@ -139,13 +135,13 @@ std::vector CCreatureSet::getCreatureSlots(const CCreature * c, const Sl bool CCreatureSet::isCreatureBalanced(const CCreature * c, TQuantity ignoreAmount) const { - assert(c && c->valid()); + assert(c); TQuantity max = 0; auto min = std::numeric_limits::max(); for(const auto & elem : stacks) { - if(!elem.second || !elem.second->type || elem.second->type != c) + if(!elem.second || !elem.second->getType() || elem.second->getType() != c) continue; const auto count = elem.second->count; @@ -153,7 +149,6 @@ bool CCreatureSet::isCreatureBalanced(const CCreature * c, TQuantity ignoreAmoun if(count == ignoreAmount || count < 1) continue; - assert(elem.second->type->valid()); if(count > max) max = count; @@ -214,7 +209,7 @@ TMapCreatureSlot CCreatureSet::getCreatureMap() const // https://www.cplusplus.com/reference/map/map/key_comp/ for(const auto & pair : stacks) { - const auto * creature = pair.second->type; + const auto * creature = pair.second->getCreature(); auto slot = pair.first; auto lb = creatureMap.lower_bound(creature); @@ -234,7 +229,7 @@ TCreatureQueue CCreatureSet::getCreatureQueue(const SlotID & exclude) const { if(pair.first == exclude) continue; - creatureQueue.push(std::make_pair(pair.second->type, pair.first)); + creatureQueue.push(std::make_pair(pair.second->getCreature(), pair.first)); } return creatureQueue; } @@ -262,10 +257,10 @@ bool CCreatureSet::mergeableStacks(std::pair & out, const SlotID //try to match creature to our preferred stack if(preferable.validSlot() && vstd::contains(stacks, preferable)) { - const CCreature *cr = stacks.find(preferable)->second->type; + const CCreature *cr = stacks.find(preferable)->second->getCreature(); for(const auto & elem : stacks) { - if(cr == elem.second->type && elem.first != preferable) + if(cr == elem.second->getType() && elem.first != preferable) { out.first = preferable; out.second = elem.first; @@ -278,7 +273,7 @@ bool CCreatureSet::mergeableStacks(std::pair & out, const SlotID { for(const auto & elem : stacks) { - if(stack.second->type == elem.second->type && stack.first != elem.first) + if(stack.second->getType() == elem.second->getType() && stack.first != elem.first) { out.first = stack.first; out.second = elem.first; @@ -328,7 +323,7 @@ void CCreatureSet::addToSlot(const SlotID & slot, CStackInstance * stack, bool a { putStack(slot, stack); } - else if(allowMerging && stack->type == getCreature(slot)) + else if(allowMerging && stack->getType() == getCreature(slot)) { joinStack(slot, stack); } @@ -366,6 +361,14 @@ ui64 CCreatureSet::getArmyStrength() const return ret; } +ui64 CCreatureSet::getArmyCost() const +{ + ui64 ret = 0; + for (const auto& elem : stacks) + ret += elem.second->getMarketValue(); + return ret; +} + ui64 CCreatureSet::getPower(const SlotID & slot) const { return getStack(slot).getPower(); @@ -514,7 +517,7 @@ void CCreatureSet::putStack(const SlotID & slot, CStackInstance * stack) void CCreatureSet::joinStack(const SlotID & slot, CStackInstance * stack) { [[maybe_unused]] const CCreature *c = getCreature(slot); - assert(c == stack->type); + assert(c == stack->getType()); assert(c); //TODO move stuff @@ -577,9 +580,9 @@ bool CCreatureSet::canBeMergedWith(const CCreatureSet &cs, bool allowMergingStac std::set cresToAdd; for(const auto & elem : cs.stacks) { - SlotID dest = getSlotFor(elem.second->type); + SlotID dest = getSlotFor(elem.second->getCreature()); if(!dest.validSlot() || hasStackAtSlot(dest)) - cresToAdd.insert(elem.second->type); + cresToAdd.insert(elem.second->getCreature()); } return cresToAdd.size() <= freeSlots; } @@ -590,13 +593,13 @@ bool CCreatureSet::canBeMergedWith(const CCreatureSet &cs, bool allowMergingStac //get types of creatures that need their own slot for(const auto & elem : cs.stacks) - if ((j = cres.getSlotFor(elem.second->type)).validSlot()) - cres.addToSlot(j, elem.second->type->getId(), 1, true); //merge if possible + if ((j = cres.getSlotFor(elem.second->getCreature())).validSlot()) + cres.addToSlot(j, elem.second->getId(), 1, true); //merge if possible //cres.addToSlot(elem.first, elem.second->type->getId(), 1, true); for(const auto & elem : stacks) { - if ((j = cres.getSlotFor(elem.second->type)).validSlot()) - cres.addToSlot(j, elem.second->type->getId(), 1, true); //merge if possible + if ((j = cres.getSlotFor(elem.second->getCreature())).validSlot()) + cres.addToSlot(j, elem.second->getId(), 1, true); //merge if possible else return false; //no place found } @@ -693,7 +696,7 @@ void CStackInstance::init() { experience = 0; count = 0; - type = nullptr; + setType(nullptr); _armyObj = nullptr; setNodeType(STACK_INSTANCE); } @@ -707,7 +710,7 @@ int CStackInstance::getExpRank() const { if (!VLC->engineSettings()->getBoolean(EGameSettings::MODULE_STACK_EXPERIENCE)) return 0; - int tier = type->getLevel(); + int tier = getType()->getLevel(); if (vstd::iswithin(tier, 1, 7)) { for(int i = static_cast(VLC->creh->expRanks[tier].size()) - 2; i > -1; --i) //sic! @@ -730,12 +733,12 @@ int CStackInstance::getExpRank() const int CStackInstance::getLevel() const { - return std::max(1, static_cast(type->getLevel())); + return std::max(1, getType()->getLevel()); } void CStackInstance::giveStackExp(TExpType exp) { - int level = type->getLevel(); + int level = getType()->getLevel(); if (!vstd::iswithin(level, 1, 7)) level = 0; @@ -756,17 +759,17 @@ void CStackInstance::setType(const CreatureID & creID) void CStackInstance::setType(const CCreature *c) { - if(type) + if(getCreature()) { - detachFromSource(*type); - if (type->isMyUpgrade(c) && VLC->engineSettings()->getBoolean(EGameSettings::MODULE_STACK_EXPERIENCE)) + detachFromSource(*getCreature()); + if (getCreature()->isMyUpgrade(c) && VLC->engineSettings()->getBoolean(EGameSettings::MODULE_STACK_EXPERIENCE)) experience = static_cast(experience * VLC->creh->expAfterUpgrade / 100.0); } CStackBasicDescriptor::setType(c); - if(type) - attachToSource(*type); + if(getCreature()) + attachToSource(*getCreature()); } std::string CStackInstance::bonusToString(const std::shared_ptr& bonus, bool description) const { @@ -808,7 +811,7 @@ bool CStackInstance::valid(bool allowUnrandomized) const { if(!randomStack) { - return (type && type == type->getId().toEntity(VLC)); + return (getType() && getType() == getId().toEntity(VLC)); } else return allowUnrandomized; @@ -818,8 +821,8 @@ std::string CStackInstance::nodeName() const { std::ostringstream oss; oss << "Stack of " << count << " of "; - if(type) - oss << type->getNamePluralTextID(); + if(getType()) + oss << getType()->getNamePluralTextID(); else oss << "[UNDEFINED TYPE]"; @@ -841,21 +844,27 @@ void CStackInstance::deserializationFix() CreatureID CStackInstance::getCreatureID() const { - if(type) - return type->getId(); + if(getType()) + return getType()->getId(); else return CreatureID::NONE; } std::string CStackInstance::getName() const { - return (count > 1) ? type->getNamePluralTranslated() : type->getNameSingularTranslated(); + return (count > 1) ? getType()->getNamePluralTranslated() : getType()->getNameSingularTranslated(); } ui64 CStackInstance::getPower() const { - assert(type); - return type->getAIValue() * count; + assert(getType()); + return static_cast(getType()->getAIValue()) * count; +} + +ui64 CStackInstance::getMarketValue() const +{ + assert(getType()); + return getType()->getFullRecruitCost().marketValue() * count; } ArtBearer::ArtBearer CStackInstance::bearerType() const @@ -863,7 +872,7 @@ ArtBearer::ArtBearer CStackInstance::bearerType() const return ArtBearer::CREATURE; } -CStackInstance::ArtPlacementMap CStackInstance::putArtifact(ArtifactPosition pos, CArtifactInstance * art) +CStackInstance::ArtPlacementMap CStackInstance::putArtifact(const ArtifactPosition & pos, CArtifactInstance * art) { assert(!getArt(pos)); assert(art->canBePutAt(this, pos)); @@ -872,7 +881,7 @@ CStackInstance::ArtPlacementMap CStackInstance::putArtifact(ArtifactPosition pos return CArtifactSet::putArtifact(pos, art); } -void CStackInstance::removeArtifact(ArtifactPosition pos) +void CStackInstance::removeArtifact(const ArtifactPosition & pos) { assert(getArt(pos)); @@ -899,7 +908,7 @@ void CStackInstance::serializeJson(JsonSerializeFormat & handler) else { //type set by CStackBasicDescriptor::serializeJson - if(type == nullptr) + if(getType() == nullptr) { uint8_t level = 0; uint8_t upgrade = 0; @@ -912,10 +921,10 @@ void CStackInstance::serializeJson(JsonSerializeFormat & handler) } } -FactionID CStackInstance::getFaction() const +FactionID CStackInstance::getFactionID() const { - if(type) - return type->getFaction(); + if(getType()) + return getType()->getFactionID(); return FactionID::NEUTRAL; } @@ -943,7 +952,7 @@ void CCommanderInstance::init() experience = 0; level = 1; count = 1; - type = nullptr; + setType(nullptr); _armyObj = nullptr; setNodeType (CBonusSystemNode::COMMANDER); secondarySkills.resize (ECommander::SPELL_POWER + 1); @@ -998,24 +1007,29 @@ bool CCommanderInstance::gainsLevel() const CStackBasicDescriptor::CStackBasicDescriptor() = default; CStackBasicDescriptor::CStackBasicDescriptor(const CreatureID & id, TQuantity Count): - type(id.toCreature()), + typeID(id), count(Count) { } CStackBasicDescriptor::CStackBasicDescriptor(const CCreature *c, TQuantity Count) - : type(c), count(Count) + : typeID(c ? c->getId() : CreatureID()), count(Count) { } +const CCreature * CStackBasicDescriptor::getCreature() const +{ + return typeID.hasValue() ? typeID.toCreature() : nullptr; +} + const Creature * CStackBasicDescriptor::getType() const { - return type; + return typeID.hasValue() ? typeID.toEntity(VLC) : nullptr; } CreatureID CStackBasicDescriptor::getId() const { - return type->getId(); + return typeID; } TQuantity CStackBasicDescriptor::getCount() const @@ -1023,18 +1037,14 @@ TQuantity CStackBasicDescriptor::getCount() const return count; } - void CStackBasicDescriptor::setType(const CCreature * c) { - type = c; + typeID = c ? c->getId() : CreatureID(); } bool operator== (const CStackBasicDescriptor & l, const CStackBasicDescriptor & r) { - return (!l.type && !r.type) - || (l.type && r.type - && l.type->getId() == r.type->getId() - && l.count == r.count); + return l.typeID == r.typeID && l.count == r.count; } void CStackBasicDescriptor::serializeJson(JsonSerializeFormat & handler) @@ -1043,9 +1053,9 @@ void CStackBasicDescriptor::serializeJson(JsonSerializeFormat & handler) if(handler.saving) { - if(type) + if(typeID.hasValue()) { - std::string typeName = type->getJsonKey(); + std::string typeName = typeID.toEntity(VLC)->getJsonKey(); handler.serializeString("type", typeName); } } diff --git a/lib/CCreatureSet.h b/lib/CCreatureSet.h index df7ab8dd4..c671bd547 100644 --- a/lib/CCreatureSet.h +++ b/lib/CCreatureSet.h @@ -31,8 +31,8 @@ class JsonSerializeFormat; class DLL_LINKAGE CStackBasicDescriptor { + CreatureID typeID; public: - const CCreature *type = nullptr; TQuantity count = -1; //exact quantity or quantity ID from CCreature::getQuantityID when getting info about enemy army CStackBasicDescriptor(); @@ -41,29 +41,27 @@ public: virtual ~CStackBasicDescriptor() = default; const Creature * getType() const; + const CCreature * getCreature() const; CreatureID getId() const; TQuantity getCount() const; virtual void setType(const CCreature * c); - + friend bool operator== (const CStackBasicDescriptor & l, const CStackBasicDescriptor & r); template void serialize(Handler &h) { if(h.saving) { - auto idNumber = type ? type->getId() : CreatureID(CreatureID::NONE); - h & idNumber; + h & typeID; } else { - CreatureID idNumber; - h & idNumber; - if(idNumber != CreatureID::NONE) - setType(dynamic_cast(VLC->creatures()->getById(idNumber))); - else - type = nullptr; + CreatureID creatureID; + h & creatureID; + setType(creatureID.toCreature()); } + h & count; } @@ -106,9 +104,11 @@ public: //IConstBonusProvider const IBonusBearer* getBonusBearer() const override; //INativeTerrainProvider - FactionID getFaction() const override; + FactionID getFactionID() const override; virtual ui64 getPower() const; + /// Returns total market value of resources needed to recruit this unit + virtual ui64 getMarketValue() const; CCreature::CreatureQuantityId getQuantityID() const; std::string getQuantityTXT(bool capitalized = true) const; virtual int getExpRank() const; @@ -126,8 +126,8 @@ public: void setArmyObj(const CArmedInstance *ArmyObj); virtual void giveStackExp(TExpType exp); bool valid(bool allowUnrandomized) const; - ArtPlacementMap putArtifact(ArtifactPosition pos, CArtifactInstance * art) override;//from CArtifactSet - void removeArtifact(ArtifactPosition pos) override; + ArtPlacementMap putArtifact(const ArtifactPosition & pos, CArtifactInstance * art) override;//from CArtifactSet + void removeArtifact(const ArtifactPosition & pos) override; ArtBearer::ArtBearer bearerType() const override; //from CArtifactSet std::string nodeName() const override; //from CBonusSystemnode void deserializationFix(); @@ -274,6 +274,7 @@ public: int stacksCount() const; virtual bool needsLastStack() const; //true if last stack cannot be taken ui64 getArmyStrength() const; //sum of AI values of creatures + ui64 getArmyCost() const; //sum of cost of creatures ui64 getPower(const SlotID & slot) const; //value of specific stack std::string getRoughAmount(const SlotID & slot, int mode = 0) const; //rough size of specific stack std::string getArmyDescription() const; diff --git a/lib/CGameInfoCallback.cpp b/lib/CGameInfoCallback.cpp index de5a77433..b426b5418 100644 --- a/lib/CGameInfoCallback.cpp +++ b/lib/CGameInfoCallback.cpp @@ -345,10 +345,10 @@ bool CGameInfoCallback::getHeroInfo(const CGObjectInstance * hero, InfoAboutHero for(auto & elem : info.army) { - if(static_cast(elem.second.type->getAIValue()) > maxAIValue) + if(static_cast(elem.second.getCreature()->getAIValue()) > maxAIValue) { - maxAIValue = elem.second.type->getAIValue(); - mostStrong = elem.second.type; + maxAIValue = elem.second.getCreature()->getAIValue(); + mostStrong = elem.second.getCreature(); } } @@ -357,7 +357,7 @@ bool CGameInfoCallback::getHeroInfo(const CGObjectInstance * hero, InfoAboutHero else for(auto & elem : info.army) { - elem.second.type = mostStrong; + elem.second.setType(mostStrong); } }; @@ -381,7 +381,7 @@ bool CGameInfoCallback::getHeroInfo(const CGObjectInstance * hero, InfoAboutHero for(const auto & creature : VLC->creh->objects) { - if(creature->getFaction() == factionIndex && static_cast(creature->getAIValue()) > maxAIValue) + if(creature->getFactionID() == factionIndex && static_cast(creature->getAIValue()) > maxAIValue) { maxAIValue = creature->getAIValue(); mostStrong = creature.get(); @@ -390,7 +390,7 @@ bool CGameInfoCallback::getHeroInfo(const CGObjectInstance * hero, InfoAboutHero if(nullptr != mostStrong) //possible, faction may have no creatures at all for(auto & elem : info.army) - elem.second.type = mostStrong; + elem.second.setType(mostStrong); }; @@ -479,6 +479,17 @@ std::vector CGameInfoCallback::getVisitableObjs(int3 return ret; } + +std::vector> CGameInfoCallback::getAllVisitableObjs() const +{ + std::vector> ret; + for(auto & obj : gs->map->objects) + if(obj && obj->isVisitable() && obj->ID != Obj::EVENT && isVisible(obj)) + ret.push_back(obj); + + return ret; +} + const CGObjectInstance * CGameInfoCallback::getTopObj (int3 pos) const { return vstd::backOrNull(getVisitableObjs(pos)); @@ -539,7 +550,7 @@ EDiggingStatus CGameInfoCallback::getTileDigStatus(int3 tile, bool verbose) cons for(const auto & object : gs->map->objects) { - if(object && object->ID == Obj::HOLE && object->pos == tile) + if(object && object->ID == Obj::HOLE && object->anchorPos() == tile) return EDiggingStatus::TILE_OCCUPIED; } return getTile(tile)->getDiggingStatus(); @@ -575,10 +586,10 @@ EBuildingState CGameInfoCallback::canBuildStructure( const CGTownInstance *t, Bu { ERROR_RET_VAL_IF(!canGetFullInfo(t), "Town is not owned!", EBuildingState::TOWN_NOT_OWNED); - if(!t->town->buildings.count(ID)) + if(!t->getTown()->buildings.count(ID)) return EBuildingState::BUILDING_ERROR; - const CBuilding * building = t->town->buildings.at(ID); + const CBuilding * building = t->getTown()->buildings.at(ID); if(t->hasBuilt(ID)) //already built @@ -619,7 +630,7 @@ EBuildingState CGameInfoCallback::canBuildStructure( const CGTownInstance *t, Bu { const TerrainTile *tile = getTile(t->bestLocation(), false); - if(!tile || !tile->terType->isWater()) + if(!tile || !tile->isWater()) return EBuildingState::NO_WATER; //lack of water } @@ -946,12 +957,12 @@ void CGameInfoCallback::calculatePaths( const CGHeroInstance *hero, CPathsInfo & const CArtifactInstance * CGameInfoCallback::getArtInstance( ArtifactInstanceID aid ) const { - return gs->map->artInstances[aid.num]; + return gs->map->artInstances.at(aid.num); } const CGObjectInstance * CGameInfoCallback::getObjInstance( ObjectInstanceID oid ) const { - return gs->map->objects[oid.num]; + return gs->map->objects.at(oid.num); } const CArtifactSet * CGameInfoCallback::getArtSet(const ArtifactLocation & loc) const @@ -964,7 +975,7 @@ std::vector CGameInfoCallback::getVisibleTeleportObjects(std:: vstd::erase_if(ids, [&](const ObjectInstanceID & id) -> bool { const auto * obj = getObj(id, false); - return player != PlayerColor::UNFLAGGABLE && (!obj || !isVisible(obj->pos, player)); + return player != PlayerColor::UNFLAGGABLE && (!obj || !isVisible(obj->visitablePos(), player)); }); return ids; } diff --git a/lib/CGameInfoCallback.h b/lib/CGameInfoCallback.h index 91c51b5a7..25f15d53e 100644 --- a/lib/CGameInfoCallback.h +++ b/lib/CGameInfoCallback.h @@ -11,6 +11,7 @@ #include "int3.h" #include "ResourceSet.h" // for Res +#include "ConstTransitivePtr.h" #define ASSERT_IF_CALLED_WITH_PLAYER if(!getPlayerID()) {logGlobal->error(BOOST_CURRENT_FUNCTION); assert(0);} @@ -189,6 +190,7 @@ public: const CGObjectInstance * getObj(ObjectInstanceID objid, bool verbose = true) const override; virtual std::vector getBlockingObjs(int3 pos)const; std::vector getVisitableObjs(int3 pos, bool verbose = true) const override; + std::vector> getAllVisitableObjs() const; virtual std::vector getFlaggableObjects(int3 pos) const; virtual const CGObjectInstance * getTopObj (int3 pos) const; virtual PlayerColor getOwner(ObjectInstanceID heroID) const; diff --git a/lib/CHeroHandler.cpp b/lib/CHeroHandler.cpp deleted file mode 100644 index 991cbbff2..000000000 --- a/lib/CHeroHandler.cpp +++ /dev/null @@ -1,829 +0,0 @@ -/* - * CHeroHandler.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 "CHeroHandler.h" - -#include "filesystem/Filesystem.h" -#include "VCMI_Lib.h" -#include "constants/StringConstants.h" -#include "battle/BattleHex.h" -#include "CCreatureHandler.h" -#include "IGameSettings.h" -#include "CSkillHandler.h" -#include "BattleFieldHandler.h" -#include "bonuses/Limiters.h" -#include "bonuses/Updaters.h" -#include "entities/faction/CFaction.h" -#include "entities/faction/CTown.h" -#include "entities/faction/CTownHandler.h" -#include "json/JsonBonus.h" -#include "json/JsonUtils.h" -#include "mapObjectConstructors/AObjectTypeHandler.h" -#include "mapObjectConstructors/CObjectClassesHandler.h" -#include "modding/IdentifierStorage.h" -#include "texts/CGeneralTextHandler.h" -#include "texts/CLegacyConfigParser.h" - -#include - -VCMI_LIB_NAMESPACE_BEGIN - -CHero::CHero() = default; -CHero::~CHero() = default; - -int32_t CHero::getIndex() const -{ - return ID.getNum(); -} - -int32_t CHero::getIconIndex() const -{ - return imageIndex; -} - -std::string CHero::getJsonKey() const -{ - return modScope + ':' + identifier; -} - -std::string CHero::getModScope() const -{ - return modScope; -} - -HeroTypeID CHero::getId() const -{ - return ID; -} - -std::string CHero::getNameTranslated() const -{ - return VLC->generaltexth->translate(getNameTextID()); -} - -std::string CHero::getBiographyTranslated() const -{ - return VLC->generaltexth->translate(getBiographyTextID()); -} - -std::string CHero::getSpecialtyNameTranslated() const -{ - return VLC->generaltexth->translate(getSpecialtyNameTextID()); -} - -std::string CHero::getSpecialtyDescriptionTranslated() const -{ - return VLC->generaltexth->translate(getSpecialtyDescriptionTextID()); -} - -std::string CHero::getSpecialtyTooltipTranslated() const -{ - return VLC->generaltexth->translate(getSpecialtyTooltipTextID()); -} - -std::string CHero::getNameTextID() const -{ - return TextIdentifier("hero", modScope, identifier, "name").get(); -} - -std::string CHero::getBiographyTextID() const -{ - return TextIdentifier("hero", modScope, identifier, "biography").get(); -} - -std::string CHero::getSpecialtyNameTextID() const -{ - return TextIdentifier("hero", modScope, identifier, "specialty", "name").get(); -} - -std::string CHero::getSpecialtyDescriptionTextID() const -{ - return TextIdentifier("hero", modScope, identifier, "specialty", "description").get(); -} - -std::string CHero::getSpecialtyTooltipTextID() const -{ - return TextIdentifier("hero", modScope, identifier, "specialty", "tooltip").get(); -} - -void CHero::registerIcons(const IconRegistar & cb) const -{ - cb(getIconIndex(), 0, "UN32", iconSpecSmall); - cb(getIconIndex(), 0, "UN44", iconSpecLarge); - cb(getIconIndex(), 0, "PORTRAITSLARGE", portraitLarge); - cb(getIconIndex(), 0, "PORTRAITSSMALL", portraitSmall); -} - -void CHero::updateFrom(const JsonNode & data) -{ - //todo: CHero::updateFrom -} - -void CHero::serializeJson(JsonSerializeFormat & handler) -{ - -} - - -SecondarySkill CHeroClass::chooseSecSkill(const std::set & possibles, vstd::RNG & rand) const //picks secondary skill out from given possibilities -{ - assert(!possibles.empty()); - - if (possibles.size() == 1) - return *possibles.begin(); - - int totalProb = 0; - for(const auto & possible : possibles) - if (secSkillProbability.count(possible) != 0) - totalProb += secSkillProbability.at(possible); - - if (totalProb == 0) // may trigger if set contains only banned skills (0 probability) - return *RandomGeneratorUtil::nextItem(possibles, rand); - - auto ran = rand.nextInt(totalProb - 1); - for(const auto & possible : possibles) - { - if (secSkillProbability.count(possible) != 0) - ran -= secSkillProbability.at(possible); - - if(ran < 0) - return possible; - } - - assert(0); // should not be possible - return *possibles.begin(); -} - -bool CHeroClass::isMagicHero() const -{ - return affinity == MAGIC; -} - -int CHeroClass::tavernProbability(FactionID targetFaction) const -{ - auto it = selectionProbability.find(targetFaction); - if (it != selectionProbability.end()) - return it->second; - return 0; -} - -EAlignment CHeroClass::getAlignment() const -{ - return VLC->factions()->getById(faction)->getAlignment(); -} - -int32_t CHeroClass::getIndex() const -{ - return id.getNum(); -} - -int32_t CHeroClass::getIconIndex() const -{ - return getIndex(); -} - -std::string CHeroClass::getJsonKey() const -{ - return modScope + ':' + identifier; -} - -std::string CHeroClass::getModScope() const -{ - return modScope; -} - -HeroClassID CHeroClass::getId() const -{ - return id; -} - -void CHeroClass::registerIcons(const IconRegistar & cb) const -{ - -} - -std::string CHeroClass::getNameTranslated() const -{ - return VLC->generaltexth->translate(getNameTextID()); -} - -std::string CHeroClass::getNameTextID() const -{ - return TextIdentifier("heroClass", modScope, identifier, "name").get(); -} - -void CHeroClass::updateFrom(const JsonNode & data) -{ - //TODO: CHeroClass::updateFrom -} - -void CHeroClass::serializeJson(JsonSerializeFormat & handler) -{ - -} - -CHeroClass::CHeroClass(): - faction(0), - affinity(0), - defaultTavernChance(0) -{ -} - -void CHeroClassHandler::fillPrimarySkillData(const JsonNode & node, CHeroClass * heroClass, PrimarySkill pSkill) const -{ - const auto & skillName = NPrimarySkill::names[pSkill.getNum()]; - auto currentPrimarySkillValue = static_cast(node["primarySkills"][skillName].Integer()); - int primarySkillLegalMinimum = VLC->engineSettings()->getVector(EGameSettings::HEROES_MINIMAL_PRIMARY_SKILLS)[pSkill.getNum()]; - - if(currentPrimarySkillValue < primarySkillLegalMinimum) - { - logMod->error("Hero class '%s' has incorrect initial value '%d' for skill '%s'. Value '%d' will be used instead.", - heroClass->getNameTranslated(), currentPrimarySkillValue, skillName, primarySkillLegalMinimum); - currentPrimarySkillValue = primarySkillLegalMinimum; - } - heroClass->primarySkillInitial.push_back(currentPrimarySkillValue); - heroClass->primarySkillLowLevel.push_back(static_cast(node["lowLevelChance"][skillName].Float())); - heroClass->primarySkillHighLevel.push_back(static_cast(node["highLevelChance"][skillName].Float())); -} - -const std::vector & CHeroClassHandler::getTypeNames() const -{ - static const std::vector typeNames = { "heroClass" }; - return typeNames; -} - -std::shared_ptr CHeroClassHandler::loadFromJson(const std::string & scope, const JsonNode & node, const std::string & identifier, size_t index) -{ - assert(identifier.find(':') == std::string::npos); - assert(!scope.empty()); - - std::string affinityStr[2] = { "might", "magic" }; - - auto heroClass = std::make_shared(); - - heroClass->id = HeroClassID(index); - heroClass->identifier = identifier; - heroClass->modScope = scope; - heroClass->imageBattleFemale = AnimationPath::fromJson(node["animation"]["battle"]["female"]); - heroClass->imageBattleMale = AnimationPath::fromJson(node["animation"]["battle"]["male"]); - //MODS COMPATIBILITY FOR 0.96 - heroClass->imageMapFemale = node["animation"]["map"]["female"].String(); - heroClass->imageMapMale = node["animation"]["map"]["male"].String(); - - VLC->generaltexth->registerString(scope, heroClass->getNameTextID(), node["name"].String()); - - if (vstd::contains(affinityStr, node["affinity"].String())) - { - heroClass->affinity = vstd::find_pos(affinityStr, node["affinity"].String()); - } - else - { - logGlobal->error("Mod '%s', hero class '%s': invalid affinity '%s'! Expected 'might' or 'magic'!", scope, identifier, node["affinity"].String()); - heroClass->affinity = CHeroClass::MIGHT; - } - - fillPrimarySkillData(node, heroClass.get(), PrimarySkill::ATTACK); - fillPrimarySkillData(node, heroClass.get(), PrimarySkill::DEFENSE); - fillPrimarySkillData(node, heroClass.get(), PrimarySkill::SPELL_POWER); - fillPrimarySkillData(node, heroClass.get(), PrimarySkill::KNOWLEDGE); - - auto percentSumm = std::accumulate(heroClass->primarySkillLowLevel.begin(), heroClass->primarySkillLowLevel.end(), 0); - if(percentSumm <= 0) - logMod->error("Hero class %s has wrong lowLevelChance values: must be above zero!", heroClass->identifier, percentSumm); - - percentSumm = std::accumulate(heroClass->primarySkillHighLevel.begin(), heroClass->primarySkillHighLevel.end(), 0); - if(percentSumm <= 0) - logMod->error("Hero class %s has wrong highLevelChance values: must be above zero!", heroClass->identifier, percentSumm); - - for(auto skillPair : node["secondarySkills"].Struct()) - { - int probability = static_cast(skillPair.second.Integer()); - VLC->identifiers()->requestIdentifier(skillPair.second.getModScope(), "skill", skillPair.first, [heroClass, probability](si32 skillID) - { - heroClass->secSkillProbability[skillID] = probability; - }); - } - - VLC->identifiers()->requestIdentifier ("creature", node["commander"], - [=](si32 commanderID) - { - heroClass->commander = CreatureID(commanderID); - }); - - heroClass->defaultTavernChance = static_cast(node["defaultTavern"].Float()); - for(const auto & tavern : node["tavern"].Struct()) - { - int value = static_cast(tavern.second.Float()); - - VLC->identifiers()->requestIdentifier(tavern.second.getModScope(), "faction", tavern.first, - [=](si32 factionID) - { - heroClass->selectionProbability[FactionID(factionID)] = value; - }); - } - - VLC->identifiers()->requestIdentifier("faction", node["faction"], - [=](si32 factionID) - { - heroClass->faction.setNum(factionID); - }); - - VLC->identifiers()->requestIdentifier(scope, "object", "hero", [=](si32 index) - { - JsonNode classConf = node["mapObject"]; - classConf["heroClass"].String() = identifier; - if (!node["compatibilityIdentifiers"].isNull()) - classConf["compatibilityIdentifiers"] = node["compatibilityIdentifiers"]; - classConf.setModScope(scope); - VLC->objtypeh->loadSubObject(identifier, classConf, index, heroClass->getIndex()); - }); - - return heroClass; -} - -std::vector CHeroClassHandler::loadLegacyData() -{ - size_t dataSize = VLC->engineSettings()->getInteger(EGameSettings::TEXTS_HERO_CLASS); - - objects.resize(dataSize); - std::vector h3Data; - h3Data.reserve(dataSize); - - CLegacyConfigParser parser(TextPath::builtin("DATA/HCTRAITS.TXT")); - - parser.endLine(); // header - parser.endLine(); - - for (size_t i=0; i set selection probability if it was not set before in tavern entries - for(auto & heroClass : objects) - { - for(auto & faction : VLC->townh->objects) - { - if (!faction->town) - continue; - if (heroClass->selectionProbability.count(faction->getId())) - continue; - - auto chance = static_cast(heroClass->defaultTavernChance * faction->town->defaultTavernChance); - heroClass->selectionProbability[faction->getId()] = static_cast(sqrt(chance) + 0.5); //FIXME: replace with std::round once MVS supports it - } - - // set default probabilities for gaining secondary skills where not loaded previously - for(int skillID = 0; skillID < VLC->skillh->size(); skillID++) - { - if(heroClass->secSkillProbability.count(skillID) == 0) - { - const CSkill * skill = (*VLC->skillh)[SecondarySkill(skillID)]; - logMod->trace("%s: no probability for %s, using default", heroClass->identifier, skill->getJsonKey()); - heroClass->secSkillProbability[skillID] = skill->gainChance[heroClass->affinity]; - } - } - } - - for(const auto & hc : objects) - { - if(!hc->imageMapMale.empty()) - { - JsonNode templ; - templ["animation"].String() = hc->imageMapMale; - VLC->objtypeh->getHandlerFor(Obj::HERO, hc->getIndex())->addTemplate(templ); - } - } -} - -CHeroClassHandler::~CHeroClassHandler() = default; - -CHeroHandler::~CHeroHandler() = default; - -CHeroHandler::CHeroHandler() -{ - loadExperience(); -} - -const std::vector & CHeroHandler::getTypeNames() const -{ - static const std::vector typeNames = { "hero" }; - return typeNames; -} - -std::shared_ptr CHeroHandler::loadFromJson(const std::string & scope, const JsonNode & node, const std::string & identifier, size_t index) -{ - assert(identifier.find(':') == std::string::npos); - assert(!scope.empty()); - - auto hero = std::make_shared(); - hero->ID = HeroTypeID(index); - hero->identifier = identifier; - hero->modScope = scope; - hero->gender = node["female"].Bool() ? EHeroGender::FEMALE : EHeroGender::MALE; - hero->special = node["special"].Bool(); - //Default - both false - hero->onlyOnWaterMap = node["onlyOnWaterMap"].Bool(); - hero->onlyOnMapWithoutWater = node["onlyOnMapWithoutWater"].Bool(); - - VLC->generaltexth->registerString(scope, hero->getNameTextID(), node["texts"]["name"].String()); - VLC->generaltexth->registerString(scope, hero->getBiographyTextID(), node["texts"]["biography"].String()); - VLC->generaltexth->registerString(scope, hero->getSpecialtyNameTextID(), node["texts"]["specialty"]["name"].String()); - VLC->generaltexth->registerString(scope, hero->getSpecialtyTooltipTextID(), node["texts"]["specialty"]["tooltip"].String()); - VLC->generaltexth->registerString(scope, hero->getSpecialtyDescriptionTextID(), node["texts"]["specialty"]["description"].String()); - - hero->iconSpecSmall = node["images"]["specialtySmall"].String(); - hero->iconSpecLarge = node["images"]["specialtyLarge"].String(); - hero->portraitSmall = node["images"]["small"].String(); - hero->portraitLarge = node["images"]["large"].String(); - hero->battleImage = AnimationPath::fromJson(node["battleImage"]); - - loadHeroArmy(hero.get(), node); - loadHeroSkills(hero.get(), node); - loadHeroSpecialty(hero.get(), node); - - VLC->identifiers()->requestIdentifier("heroClass", node["class"], - [=](si32 classID) - { - hero->heroClass = HeroClassID(classID).toHeroClass(); - }); - - return hero; -} - -void CHeroHandler::loadHeroArmy(CHero * hero, const JsonNode & node) const -{ - assert(node["army"].Vector().size() <= 3); // anything bigger is useless - army initialization uses up to 3 slots - - hero->initialArmy.resize(node["army"].Vector().size()); - - for (size_t i=0; i< hero->initialArmy.size(); i++) - { - const JsonNode & source = node["army"].Vector()[i]; - - hero->initialArmy[i].minAmount = static_cast(source["min"].Float()); - hero->initialArmy[i].maxAmount = static_cast(source["max"].Float()); - - if (hero->initialArmy[i].minAmount > hero->initialArmy[i].maxAmount) - { - logMod->error("Hero %s has minimal army size (%d) greater than maximal size (%d)!", hero->getJsonKey(), hero->initialArmy[i].minAmount, hero->initialArmy[i].maxAmount); - std::swap(hero->initialArmy[i].minAmount, hero->initialArmy[i].maxAmount); - } - - VLC->identifiers()->requestIdentifier("creature", source["creature"], [=](si32 creature) - { - hero->initialArmy[i].creature = CreatureID(creature); - }); - } -} - -void CHeroHandler::loadHeroSkills(CHero * hero, const JsonNode & node) const -{ - for(const JsonNode &set : node["skills"].Vector()) - { - int skillLevel = static_cast(boost::range::find(NSecondarySkill::levels, set["level"].String()) - std::begin(NSecondarySkill::levels)); - if (skillLevel < MasteryLevel::LEVELS_SIZE) - { - size_t currentIndex = hero->secSkillsInit.size(); - hero->secSkillsInit.emplace_back(SecondarySkill(-1), skillLevel); - - VLC->identifiers()->requestIdentifier("skill", set["skill"], [=](si32 id) - { - hero->secSkillsInit[currentIndex].first = SecondarySkill(id); - }); - } - else - { - logMod->error("Unknown skill level: %s", set["level"].String()); - } - } - - // spellbook is considered present if hero have "spellbook" entry even when this is an empty set (0 spells) - hero->haveSpellBook = !node["spellbook"].isNull(); - - for(const JsonNode & spell : node["spellbook"].Vector()) - { - VLC->identifiers()->requestIdentifier("spell", spell, - [=](si32 spellID) - { - hero->spells.insert(SpellID(spellID)); - }); - } -} - -/// creates standard H3 hero specialty for creatures -static std::vector> createCreatureSpecialty(CreatureID baseCreatureID) -{ - std::vector> result; - std::set targets; - targets.insert(baseCreatureID); - - // go through entire upgrade chain and collect all creatures to which baseCreatureID can be upgraded - for (;;) - { - std::set oldTargets = targets; - - for(const auto & upgradeSourceID : oldTargets) - { - const CCreature * upgradeSource = upgradeSourceID.toCreature(); - targets.insert(upgradeSource->upgrades.begin(), upgradeSource->upgrades.end()); - } - - if (oldTargets.size() == targets.size()) - break; - } - - for(CreatureID cid : targets) - { - const auto & specCreature = *cid.toCreature(); - int stepSize = specCreature.getLevel() ? specCreature.getLevel() : 5; - - { - auto bonus = std::make_shared(); - bonus->limiter.reset(new CCreatureTypeLimiter(specCreature, false)); - bonus->type = BonusType::STACKS_SPEED; - bonus->val = 1; - result.push_back(bonus); - } - - { - auto bonus = std::make_shared(); - bonus->type = BonusType::PRIMARY_SKILL; - bonus->subtype = BonusSubtypeID(PrimarySkill::ATTACK); - bonus->val = 0; - bonus->limiter.reset(new CCreatureTypeLimiter(specCreature, false)); - bonus->updater.reset(new GrowsWithLevelUpdater(specCreature.getAttack(false), stepSize)); - result.push_back(bonus); - } - - { - auto bonus = std::make_shared(); - bonus->type = BonusType::PRIMARY_SKILL; - bonus->subtype = BonusSubtypeID(PrimarySkill::DEFENSE); - bonus->val = 0; - bonus->limiter.reset(new CCreatureTypeLimiter(specCreature, false)); - bonus->updater.reset(new GrowsWithLevelUpdater(specCreature.getDefense(false), stepSize)); - result.push_back(bonus); - } - } - - return result; -} - -void CHeroHandler::beforeValidate(JsonNode & object) -{ - //handle "base" specialty info - JsonNode & specialtyNode = object["specialty"]; - if(specialtyNode.getType() == JsonNode::JsonType::DATA_STRUCT) - { - const JsonNode & base = specialtyNode["base"]; - if(!base.isNull()) - { - if(specialtyNode["bonuses"].isNull()) - { - logMod->warn("specialty has base without bonuses"); - } - else - { - JsonMap & bonuses = specialtyNode["bonuses"].Struct(); - for(std::pair keyValue : bonuses) - JsonUtils::inherit(bonuses[keyValue.first], base); - } - } - } -} - -void CHeroHandler::afterLoadFinalization() -{ - for(const auto & functor : callAfterLoadFinalization) - functor(); - - callAfterLoadFinalization.clear(); -} - -void CHeroHandler::loadHeroSpecialty(CHero * hero, const JsonNode & node) -{ - auto prepSpec = [=](std::shared_ptr bonus) - { - bonus->duration = BonusDuration::PERMANENT; - bonus->source = BonusSource::HERO_SPECIAL; - bonus->sid = BonusSourceID(hero->getId()); - return bonus; - }; - - //new format, using bonus system - const JsonNode & specialtyNode = node["specialty"]; - if(specialtyNode.getType() != JsonNode::JsonType::DATA_STRUCT) - { - logMod->error("Unsupported speciality format for hero %s!", hero->getNameTranslated()); - return; - } - - //creature specialty - alias for simplicity - if(!specialtyNode["creature"].isNull()) - { - JsonNode creatureNode = specialtyNode["creature"]; - - std::function specialtyLoader = [creatureNode, hero, prepSpec] - { - VLC->identifiers()->requestIdentifier("creature", creatureNode, [hero, prepSpec](si32 creature) - { - for (const auto & bonus : createCreatureSpecialty(CreatureID(creature))) - hero->specialty.push_back(prepSpec(bonus)); - }); - }; - - callAfterLoadFinalization.push_back(specialtyLoader); - } - - for(const auto & keyValue : specialtyNode["bonuses"].Struct()) - hero->specialty.push_back(prepSpec(JsonUtils::parseBonus(keyValue.second))); -} - -void CHeroHandler::loadExperience() -{ - expPerLevel.push_back(0); - expPerLevel.push_back(1000); - expPerLevel.push_back(2000); - expPerLevel.push_back(3200); - expPerLevel.push_back(4600); - expPerLevel.push_back(6200); - expPerLevel.push_back(8000); - expPerLevel.push_back(10000); - expPerLevel.push_back(12200); - expPerLevel.push_back(14700); - expPerLevel.push_back(17500); - expPerLevel.push_back(20600); - expPerLevel.push_back(24320); - expPerLevel.push_back(28784); - expPerLevel.push_back(34140); - - for (;;) - { - auto i = expPerLevel.size() - 1; - auto currExp = expPerLevel[i]; - auto prevExp = expPerLevel[i-1]; - auto prevDiff = currExp - prevExp; - auto nextDiff = prevDiff + prevDiff / 5; - auto maxExp = std::numeric_limits::max(); - - if (currExp > maxExp - nextDiff) - break; // overflow point reached - - expPerLevel.push_back (currExp + nextDiff); - } -} - -/// convert h3-style ID (e.g. Gobin Wolf Rider) to vcmi (e.g. goblinWolfRider) -static std::string genRefName(std::string input) -{ - boost::algorithm::replace_all(input, " ", ""); //remove spaces - input[0] = std::tolower(input[0]); // to camelCase - return input; -} - -std::vector CHeroHandler::loadLegacyData() -{ - size_t dataSize = VLC->engineSettings()->getInteger(EGameSettings::TEXTS_HERO); - - objects.resize(dataSize); - std::vector h3Data; - h3Data.reserve(dataSize); - - CLegacyConfigParser specParser(TextPath::builtin("DATA/HEROSPEC.TXT")); - CLegacyConfigParser bioParser(TextPath::builtin("DATA/HEROBIOS.TXT")); - CLegacyConfigParser parser(TextPath::builtin("DATA/HOTRAITS.TXT")); - - parser.endLine(); //ignore header - parser.endLine(); - - specParser.endLine(); //ignore header - specParser.endLine(); - - for (int i=0; iimageIndex = static_cast(index) + specialFramesCount; - - objects.emplace_back(object); - - registerObject(scope, "hero", name, object->getIndex()); - - for(const auto & compatID : data["compatibilityIdentifiers"].Vector()) - registerObject(scope, "hero", compatID.String(), object->getIndex()); -} - -void CHeroHandler::loadObject(std::string scope, std::string name, const JsonNode & data, size_t index) -{ - auto object = loadFromJson(scope, data, name, index); - object->imageIndex = static_cast(index); - - assert(objects[index] == nullptr); // ensure that this id was not loaded before - objects[index] = object; - - registerObject(scope, "hero", name, object->getIndex()); - for(const auto & compatID : data["compatibilityIdentifiers"].Vector()) - registerObject(scope, "hero", compatID.String(), object->getIndex()); -} - -ui32 CHeroHandler::level (TExpType experience) const -{ - return static_cast(boost::range::upper_bound(expPerLevel, experience) - std::begin(expPerLevel)); -} - -TExpType CHeroHandler::reqExp (ui32 level) const -{ - if(!level) - return 0; - - if (level <= expPerLevel.size()) - { - return expPerLevel[level-1]; - } - else - { - logGlobal->warn("A hero has reached unsupported amount of experience"); - return expPerLevel[expPerLevel.size()-1]; - } -} - -ui32 CHeroHandler::maxSupportedLevel() const -{ - return expPerLevel.size(); -} - -std::set CHeroHandler::getDefaultAllowed() const -{ - std::set result; - - for(auto & hero : objects) - if (hero && !hero->special) - result.insert(hero->getId()); - - return result; -} - -VCMI_LIB_NAMESPACE_END diff --git a/lib/CHeroHandler.h b/lib/CHeroHandler.h deleted file mode 100644 index c2bd9d220..000000000 --- a/lib/CHeroHandler.h +++ /dev/null @@ -1,221 +0,0 @@ -/* - * CHeroHandler.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 -#include -#include -#include - -#include "ConstTransitivePtr.h" -#include "GameConstants.h" -#include "bonuses/Bonus.h" -#include "bonuses/BonusList.h" -#include "IHandlerBase.h" -#include "filesystem/ResourcePath.h" - -VCMI_LIB_NAMESPACE_BEGIN - -namespace vstd -{ -class RNG; -} - -class CHeroClass; -class CGHeroInstance; -struct BattleHex; -class JsonNode; -class JsonSerializeFormat; -class BattleField; - -enum class EHeroGender : int8_t -{ - DEFAULT = -1, // from h3m, instance has same gender as hero type - MALE = 0, - FEMALE = 1, -}; - -class DLL_LINKAGE CHero : public HeroType -{ - friend class CHeroHandler; - - HeroTypeID ID; - std::string identifier; - std::string modScope; - -public: - struct InitialArmyStack - { - ui32 minAmount; - ui32 maxAmount; - CreatureID creature; - }; - si32 imageIndex = 0; - - std::vector initialArmy; - - const CHeroClass * heroClass = nullptr; - std::vector > secSkillsInit; //initial secondary skills; first - ID of skill, second - level of skill (1 - basic, 2 - adv., 3 - expert) - BonusList specialty; - std::set spells; - bool haveSpellBook = false; - bool special = false; // hero is special and won't be placed in game (unless preset on map), e.g. campaign heroes - bool onlyOnWaterMap; // hero will be placed only if the map contains water - bool onlyOnMapWithoutWater; // hero will be placed only if the map does not contain water - EHeroGender gender = EHeroGender::MALE; // default sex: 0=male, 1=female - - /// Graphics - std::string iconSpecSmall; - std::string iconSpecLarge; - std::string portraitSmall; - std::string portraitLarge; - AnimationPath battleImage; - - CHero(); - virtual ~CHero(); - - int32_t getIndex() const override; - int32_t getIconIndex() const override; - std::string getJsonKey() const override; - std::string getModScope() const override; - HeroTypeID getId() const override; - void registerIcons(const IconRegistar & cb) const override; - - std::string getNameTranslated() const override; - std::string getBiographyTranslated() const override; - std::string getSpecialtyNameTranslated() const override; - std::string getSpecialtyDescriptionTranslated() const override; - std::string getSpecialtyTooltipTranslated() const override; - - std::string getNameTextID() const override; - std::string getBiographyTextID() const override; - std::string getSpecialtyNameTextID() const override; - std::string getSpecialtyDescriptionTextID() const override; - std::string getSpecialtyTooltipTextID() const override; - - void updateFrom(const JsonNode & data); - void serializeJson(JsonSerializeFormat & handler); -}; - -class DLL_LINKAGE CHeroClass : public HeroClass -{ - friend class CHeroClassHandler; - HeroClassID id; // use getId instead - std::string modScope; - std::string identifier; // use getJsonKey instead - -public: - enum EClassAffinity - { - MIGHT, - MAGIC - }; - - //double aggression; // not used in vcmi. - FactionID faction; - ui8 affinity; // affinity, using EClassAffinity enum - - // default chance for hero of specific class to appear in tavern, if field "tavern" was not set - // resulting chance = sqrt(town.chance * heroClass.chance) - ui32 defaultTavernChance; - - CreatureID commander; - - std::vector primarySkillInitial; // initial primary skills - std::vector primarySkillLowLevel; // probability (%) of getting point of primary skill when getting level - std::vector primarySkillHighLevel;// same for high levels (> 10) - - std::map secSkillProbability; //probabilities of gaining secondary skills (out of 112), in id order - - std::map selectionProbability; //probability of selection in towns - - AnimationPath imageBattleMale; - AnimationPath imageBattleFemale; - std::string imageMapMale; - std::string imageMapFemale; - - CHeroClass(); - - int32_t getIndex() const override; - int32_t getIconIndex() const override; - std::string getJsonKey() const override; - std::string getModScope() const override; - HeroClassID getId() const override; - void registerIcons(const IconRegistar & cb) const override; - - std::string getNameTranslated() const override; - std::string getNameTextID() const override; - - bool isMagicHero() const; - SecondarySkill chooseSecSkill(const std::set & possibles, vstd::RNG & rand) const; //picks secondary skill out from given possibilities - - void updateFrom(const JsonNode & data); - void serializeJson(JsonSerializeFormat & handler); - - EAlignment getAlignment() const; - - int tavernProbability(FactionID faction) const; -}; - -class DLL_LINKAGE CHeroClassHandler : public CHandlerBase -{ - void fillPrimarySkillData(const JsonNode & node, CHeroClass * heroClass, PrimarySkill pSkill) const; - -public: - std::vector loadLegacyData() override; - - void afterLoadFinalization() override; - - ~CHeroClassHandler(); - -protected: - const std::vector & getTypeNames() const override; - std::shared_ptr loadFromJson(const std::string & scope, const JsonNode & node, const std::string & identifier, size_t index) override; - -}; - -class DLL_LINKAGE CHeroHandler : public CHandlerBase -{ - /// expPerLEvel[i] is amount of exp needed to reach level i; - /// consists of 196 values. Any higher levels require experience larger that TExpType can hold - std::vector expPerLevel; - - /// helpers for loading to avoid huge load functions - void loadHeroArmy(CHero * hero, const JsonNode & node) const; - void loadHeroSkills(CHero * hero, const JsonNode & node) const; - void loadHeroSpecialty(CHero * hero, const JsonNode & node); - - void loadExperience(); - - std::vector> callAfterLoadFinalization; - -public: - ui32 level(TExpType experience) const; //calculates level corresponding to given experience amount - TExpType reqExp(ui32 level) const; //calculates experience required for given level - ui32 maxSupportedLevel() const; - - std::vector loadLegacyData() override; - - void beforeValidate(JsonNode & object) override; - void loadObject(std::string scope, std::string name, const JsonNode & data) override; - void loadObject(std::string scope, std::string name, const JsonNode & data, size_t index) override; - void afterLoadFinalization() override; - - CHeroHandler(); - ~CHeroHandler(); - - std::set getDefaultAllowed() const; - -protected: - const std::vector & getTypeNames() const override; - std::shared_ptr loadFromJson(const std::string & scope, const JsonNode & node, const std::string & identifier, size_t index) override; -}; - -VCMI_LIB_NAMESPACE_END diff --git a/lib/CMakeLists.txt b/lib/CMakeLists.txt index d691daaa3..85e1c95a8 100644 --- a/lib/CMakeLists.txt +++ b/lib/CMakeLists.txt @@ -87,6 +87,10 @@ set(lib_MAIN_SRCS entities/faction/CFaction.cpp entities/faction/CTown.cpp entities/faction/CTownHandler.cpp + entities/hero/CHero.cpp + entities/hero/CHeroClass.cpp + entities/hero/CHeroClassHandler.cpp + entities/hero/CHeroHandler.cpp events/ApplyDamage.cpp events/GameResumed.cpp @@ -112,6 +116,7 @@ set(lib_MAIN_SRCS mapObjectConstructors/CommonConstructors.cpp mapObjectConstructors/CRewardableConstructor.cpp mapObjectConstructors/DwellingInstanceConstructor.cpp + mapObjectConstructors/FlaggableInstanceConstructor.cpp mapObjectConstructors/HillFortInstanceConstructor.cpp mapObjectConstructors/ShipyardInstanceConstructor.cpp @@ -128,6 +133,7 @@ set(lib_MAIN_SRCS mapObjects/CObjectHandler.cpp mapObjects/CQuest.cpp mapObjects/CRewardableObject.cpp + mapObjects/FlaggableMapObject.cpp mapObjects/IMarket.cpp mapObjects/IObjectInterface.cpp mapObjects/MiscObjects.cpp @@ -151,10 +157,11 @@ set(lib_MAIN_SRCS modding/ActiveModsInSaveList.cpp modding/CModHandler.cpp - modding/CModInfo.cpp modding/CModVersion.cpp modding/ContentTypeHandler.cpp modding/IdentifierStorage.cpp + modding/ModDescription.cpp + modding/ModManager.cpp modding/ModUtility.cpp modding/ModVerificationInfo.cpp @@ -184,6 +191,8 @@ set(lib_MAIN_SRCS rmg/TileInfo.cpp rmg/Zone.cpp rmg/Functions.cpp + rmg/ObjectInfo.cpp + rmg/ObjectConfig.cpp rmg/RmgMap.cpp rmg/PenroseTiling.cpp rmg/modificators/Modificator.cpp @@ -268,7 +277,6 @@ set(lib_MAIN_SRCS CCreatureSet.cpp CGameInfoCallback.cpp CGameInterface.cpp - CHeroHandler.cpp CPlayerState.cpp CRandomGenerator.cpp CScriptingModule.cpp @@ -456,6 +464,11 @@ set(lib_MAIN_HEADERS entities/faction/CFaction.h entities/faction/CTown.h entities/faction/CTownHandler.h + entities/hero/CHero.h + entities/hero/CHeroClass.h + entities/hero/CHeroClassHandler.h + entities/hero/CHeroHandler.h + entities/hero/EHeroGender.h events/ApplyDamage.h events/GameResumed.h @@ -487,6 +500,7 @@ set(lib_MAIN_HEADERS mapObjectConstructors/CRewardableConstructor.h mapObjectConstructors/DwellingInstanceConstructor.h mapObjectConstructors/HillFortInstanceConstructor.h + mapObjectConstructors/FlaggableInstanceConstructor.h mapObjectConstructors/IObjectInfo.h mapObjectConstructors/RandomMapInfo.h mapObjectConstructors/ShipyardInstanceConstructor.h @@ -505,11 +519,13 @@ set(lib_MAIN_HEADERS mapObjects/CObjectHandler.h mapObjects/CQuest.h mapObjects/CRewardableObject.h + mapObjects/FlaggableMapObject.h mapObjects/IMarket.h mapObjects/IObjectInterface.h mapObjects/IOwnableObject.h mapObjects/MapObjects.h mapObjects/MiscObjects.h + mapObjects/CompoundMapObjectID.h mapObjects/ObjectTemplate.h mapObjects/ObstacleSetHandler.h @@ -532,11 +548,12 @@ set(lib_MAIN_HEADERS modding/ActiveModsInSaveList.h modding/CModHandler.h - modding/CModInfo.h modding/CModVersion.h modding/ContentTypeHandler.h modding/IdentifierStorage.h + modding/ModDescription.h modding/ModIncompatibility.h + modding/ModManager.h modding/ModScope.h modding/ModUtility.h modding/ModVerificationInfo.h @@ -556,6 +573,7 @@ set(lib_MAIN_HEADERS networkPacks/PacksForServer.h networkPacks/SetRewardableConfiguration.h networkPacks/SetStackEffect.h + networkPacks/SaveLocalState.h networkPacks/StackLocation.h networkPacks/TradeItem.h @@ -587,6 +605,8 @@ set(lib_MAIN_HEADERS rmg/RmgMap.h rmg/float3.h rmg/Functions.h + rmg/ObjectInfo.h + rmg/ObjectConfig.h rmg/PenroseTiling.h rmg/modificators/Modificator.h rmg/modificators/ObjectManager.h @@ -677,7 +697,6 @@ set(lib_MAIN_HEADERS CCreatureSet.h CGameInfoCallback.h CGameInterface.h - CHeroHandler.h ConstTransitivePtr.h Color.h CPlayerState.h @@ -773,6 +792,16 @@ if(WIN32) ) endif() +# Use '-Wa,-mbig-obj' for files that generate very large object files +# when compiling with MinGW lest you get "too many sections" assembler errors +if(MINGW AND CMAKE_BUILD_TYPE STREQUAL "Debug") + set_source_files_properties( + serializer/SerializerReflection.cpp + IGameCallback.cpp + PROPERTIES + COMPILE_OPTIONS "-Wa,-mbig-obj") +endif() + vcmi_set_output_dir(vcmi "") enable_pch(vcmi) diff --git a/lib/CPlayerState.cpp b/lib/CPlayerState.cpp index 2242d5381..069be91c1 100644 --- a/lib/CPlayerState.cpp +++ b/lib/CPlayerState.cpp @@ -10,6 +10,7 @@ #include "StdInc.h" #include "CPlayerState.h" +#include "json/JsonNode.h" #include "mapObjects/CGDwelling.h" #include "mapObjects/CGTownInstance.h" #include "mapObjects/CGHeroInstance.h" @@ -20,8 +21,13 @@ VCMI_LIB_NAMESPACE_BEGIN PlayerState::PlayerState() - : color(-1), human(false), cheated(false), enteredWinningCheatCode(false), - enteredLosingCheatCode(false), status(EPlayerStatus::INGAME) + : color(-1) + , human(false) + , cheated(false) + , playerLocalSettings(std::make_unique()) + , enteredWinningCheatCode(false) + , enteredLosingCheatCode(false) + , status(EPlayerStatus::INGAME) { setNodeType(PLAYER); } diff --git a/lib/CPlayerState.h b/lib/CPlayerState.h index 96a0ba790..411b0945f 100644 --- a/lib/CPlayerState.h +++ b/lib/CPlayerState.h @@ -16,7 +16,6 @@ #include "bonuses/CBonusSystemNode.h" #include "ResourceSet.h" #include "TurnTimerInfo.h" -#include "ConstTransitivePtr.h" VCMI_LIB_NAMESPACE_BEGIN @@ -66,6 +65,7 @@ public: std::vector quests; //store info about all received quests std::vector battleBonuses; //additional bonuses to be added during battle with neutrals std::map> costumesArtifacts; + std::unique_ptr playerLocalSettings; // Json with client-defined data, such as order of heroes or current hero paths. Not used by client/lib bool cheated; bool enteredWinningCheatCode, enteredLosingCheatCode; //if true, this player has entered cheat codes for loss / victory @@ -116,6 +116,9 @@ public: h & status; h & turnTimer; + if (h.version >= Handler::Version::LOCAL_PLAYER_STATE_DATA) + h & *playerLocalSettings; + if (h.version >= Handler::Version::PLAYER_STATE_OWNED_OBJECTS) { h & ownedObjects; diff --git a/lib/CSkillHandler.cpp b/lib/CSkillHandler.cpp index 12901cd6d..e5436ba08 100644 --- a/lib/CSkillHandler.cpp +++ b/lib/CSkillHandler.cpp @@ -32,7 +32,9 @@ CSkill::CSkill(const SecondarySkill & id, std::string identifier, bool obligator id(id), identifier(std::move(identifier)), obligatoryMajor(obligatoryMajor), - obligatoryMinor(obligatoryMinor) + obligatoryMinor(obligatoryMinor), + special(false), + onlyOnWaterMap(false) { gainChance[0] = gainChance[1] = 0; //affects CHeroClassHandler::afterLoadFinalization() levels.resize(NSecondarySkill::levels.size() - 1); @@ -45,7 +47,12 @@ int32_t CSkill::getIndex() const int32_t CSkill::getIconIndex() const { - return getIndex(); //TODO: actual value with skill level + return getIndex() * 3 + 3; // Base master level +} + +int32_t CSkill::getIconIndex(uint8_t skillMasterLevel) const +{ + return getIconIndex() + skillMasterLevel; } std::string CSkill::getNameTextID() const @@ -122,7 +129,7 @@ CSkill::LevelInfo & CSkill::at(int level) DLL_LINKAGE std::ostream & operator<<(std::ostream & out, const CSkill::LevelInfo & info) { for(int i=0; i < info.effects.size(); i++) - out << (i ? "," : "") << info.effects[i]->Description(); + out << (i ? "," : "") << info.effects[i]->Description(nullptr); return out << "])"; } @@ -211,8 +218,9 @@ std::shared_ptr CSkillHandler::loadFromJson(const std::string & scope, c skill->modScope = scope; skill->onlyOnWaterMap = json["onlyOnWaterMap"].Bool(); + skill->special = json["special"].Bool(); - VLC->generaltexth->registerString(scope, skill->getNameTextID(), json["name"].String()); + VLC->generaltexth->registerString(scope, skill->getNameTextID(), json["name"]); switch(json["gainChance"].getType()) { case JsonNode::JsonType::DATA_INTEGER: @@ -237,7 +245,7 @@ std::shared_ptr CSkillHandler::loadFromJson(const std::string & scope, c skill->addNewBonus(bonus, level); } CSkill::LevelInfo & skillAtLevel = skill->at(level); - VLC->generaltexth->registerString(scope, skill->getDescriptionTextID(level), levelNode["description"].String()); + VLC->generaltexth->registerString(scope, skill->getDescriptionTextID(level), levelNode["description"]); skillAtLevel.iconSmall = levelNode["images"]["small"].String(); skillAtLevel.iconMedium = levelNode["images"]["medium"].String(); skillAtLevel.iconLarge = levelNode["images"]["large"].String(); @@ -270,7 +278,8 @@ std::set CSkillHandler::getDefaultAllowed() const std::set result; for (auto const & skill : objects) - result.insert(skill->getId()); + if (!skill->special) + result.insert(skill->getId()); return result; } diff --git a/lib/CSkillHandler.h b/lib/CSkillHandler.h index a3d22d59b..ae89435eb 100644 --- a/lib/CSkillHandler.h +++ b/lib/CSkillHandler.h @@ -34,6 +34,7 @@ public: private: std::vector levels; // bonuses provided by basic, advanced and expert level void addNewBonus(const std::shared_ptr & b, int level); + int32_t getIconIndex() const override; SecondarySkill id; std::string modScope; @@ -50,7 +51,7 @@ public: }; int32_t getIndex() const override; - int32_t getIconIndex() const override; + int32_t getIconIndex(uint8_t skillMasterLevel) const; std::string getJsonKey() const override; std::string getModScope() const override; void registerIcons(const IconRegistar & cb) const override; @@ -74,6 +75,7 @@ public: void serializeJson(JsonSerializeFormat & handler); bool onlyOnWaterMap; + bool special; friend class CSkillHandler; friend DLL_LINKAGE std::ostream & operator<<(std::ostream & out, const CSkill & skill); diff --git a/lib/CStack.cpp b/lib/CStack.cpp index 68e80ddd9..1819e57df 100644 --- a/lib/CStack.cpp +++ b/lib/CStack.cpp @@ -28,7 +28,7 @@ CStack::CStack(const CStackInstance * Base, const PlayerColor & O, int I, Battle CBonusSystemNode(STACK_BATTLE), base(Base), ID(I), - type(Base->type), + typeID(Base->getId()), baseAmount(Base->count), owner(O), slot(S), @@ -48,7 +48,7 @@ CStack::CStack(): CStack::CStack(const CStackBasicDescriptor * stack, const PlayerColor & O, int I, BattleSide Side, const SlotID & S): CBonusSystemNode(STACK_BATTLE), ID(I), - type(stack->type), + typeID(stack->getId()), baseAmount(stack->count), owner(O), slot(S), @@ -60,7 +60,7 @@ CStack::CStack(const CStackBasicDescriptor * stack, const PlayerColor & O, int I void CStack::localInit(BattleInfo * battleInfo) { battle = battleInfo; - assert(type); + assert(typeID.hasValue()); exportBonuses(); if(base) //stack originating from "real" stack in garrison -> attach to it @@ -72,7 +72,7 @@ void CStack::localInit(BattleInfo * battleInfo) CArmedInstance * army = battle->battleGetArmyObject(side); assert(army); attachTo(*army); - attachToSource(*type); + attachToSource(*typeID.toCreature()); } nativeTerrain = getNativeTerrain(); //save nativeTerrain in the variable on the battle start to avoid dead lock CUnitState::localInit(this); //it causes execution of the CStack::isOnNativeTerrain where nativeTerrain will be considered @@ -164,8 +164,8 @@ std::string CStack::nodeName() const std::ostringstream oss; oss << owner.toString(); oss << " battle stack [" << ID << "]: " << getCount() << " of "; - if(type) - oss << type->getNamePluralTextID(); + if(typeID.hasValue()) + oss << typeID.toEntity(VLC)->getNamePluralTextID(); else oss << "[UNDEFINED TYPE]"; @@ -304,7 +304,7 @@ bool CStack::isMeleeAttackPossible(const battle::Unit * attacker, const battle:: std::string CStack::getName() const { - return (getCount() == 1) ? type->getNameSingularTranslated() : type->getNamePluralTranslated(); //War machines can't use base + return (getCount() == 1) ? typeID.toEntity(VLC)->getNameSingularTranslated() : typeID.toEntity(VLC)->getNamePluralTranslated(); //War machines can't use base } bool CStack::canBeHealed() const @@ -326,7 +326,7 @@ bool CStack::isOnTerrain(TerrainId terrain) const const CCreature * CStack::unitType() const { - return type; + return typeID.toCreature(); } int32_t CStack::unitBaseAmount() const @@ -352,7 +352,7 @@ bool CStack::unitHasAmmoCart(const battle::Unit * unit) const const auto * ownerHero = battle->battleGetOwnerHero(unit); if(ownerHero && ownerHero->artifactsWorn.find(ArtifactPosition::MACH2) != ownerHero->artifactsWorn.end()) { - if(battle->battleGetOwnerHero(unit)->artifactsWorn.at(ArtifactPosition::MACH2).artifact->artType->getId() == ArtifactID::AMMO_CART) + if(battle->battleGetOwnerHero(unit)->artifactsWorn.at(ArtifactPosition::MACH2).artifact->getTypeId() == ArtifactID::AMMO_CART) { return true; } @@ -401,7 +401,7 @@ void CStack::spendMana(ServerCallback * server, const int spellCost) const ssp.which = BattleSetStackProperty::CASTS; ssp.val = -spellCost; ssp.absolute = false; - server->apply(&ssp); + server->apply(ssp); } VCMI_LIB_NAMESPACE_END diff --git a/lib/CStack.h b/lib/CStack.h index 456b44bc2..d339eba40 100644 --- a/lib/CStack.h +++ b/lib/CStack.h @@ -27,7 +27,7 @@ class DLL_LINKAGE CStack : public CBonusSystemNode, public battle::CUnitState, p { private: ui32 ID = -1; //unique ID of stack - const CCreature * type = nullptr; + CreatureID typeID; TerrainId nativeTerrain; //tmp variable to save native terrain value on battle init ui32 baseAmount = -1; @@ -98,7 +98,7 @@ public: //stackState is not serialized here assert(isIndependentNode()); h & static_cast(*this); - h & type; + h & typeID; h & ID; h & baseAmount; h & owner; @@ -133,7 +133,7 @@ public: else if(!army || extSlot == SlotID() || !army->hasStackAtSlot(extSlot)) { base = nullptr; - logGlobal->warn("%s doesn't have a base stack!", type->getNameSingularTranslated()); + logGlobal->warn("%s doesn't have a base stack!", typeID.toEntity(VLC)->getNameSingularTranslated()); } else { diff --git a/lib/GameSettings.cpp b/lib/GameSettings.cpp index 780c79f43..2e177d8ef 100644 --- a/lib/GameSettings.cpp +++ b/lib/GameSettings.cpp @@ -37,70 +37,76 @@ GameSettings::GameSettings() = default; GameSettings::~GameSettings() = default; const std::vector GameSettings::settingProperties = { - {EGameSettings::BANKS_SHOW_GUARDS_COMPOSITION, "banks", "showGuardsComposition" }, - {EGameSettings::BONUSES_GLOBAL, "bonuses", "global" }, - {EGameSettings::BONUSES_PER_HERO, "bonuses", "perHero" }, - {EGameSettings::COMBAT_AREA_SHOT_CAN_TARGET_EMPTY_HEX, "combat", "areaShotCanTargetEmptyHex" }, - {EGameSettings::COMBAT_ATTACK_POINT_DAMAGE_FACTOR, "combat", "attackPointDamageFactor" }, - {EGameSettings::COMBAT_ATTACK_POINT_DAMAGE_FACTOR_CAP, "combat", "attackPointDamageFactorCap" }, - {EGameSettings::COMBAT_BAD_LUCK_DICE, "combat", "badLuckDice" }, - {EGameSettings::COMBAT_BAD_MORALE_DICE, "combat", "badMoraleDice" }, - {EGameSettings::COMBAT_DEFENSE_POINT_DAMAGE_FACTOR, "combat", "defensePointDamageFactor" }, - {EGameSettings::COMBAT_DEFENSE_POINT_DAMAGE_FACTOR_CAP, "combat", "defensePointDamageFactorCap" }, - {EGameSettings::COMBAT_GOOD_LUCK_DICE, "combat", "goodLuckDice" }, - {EGameSettings::COMBAT_GOOD_MORALE_DICE, "combat", "goodMoraleDice" }, - {EGameSettings::COMBAT_LAYOUTS, "combat", "layouts" }, - {EGameSettings::COMBAT_ONE_HEX_TRIGGERS_OBSTACLES, "combat", "oneHexTriggersObstacles" }, - {EGameSettings::CREATURES_ALLOW_ALL_FOR_DOUBLE_MONTH, "creatures", "allowAllForDoubleMonth" }, - {EGameSettings::CREATURES_ALLOW_RANDOM_SPECIAL_WEEKS, "creatures", "allowRandomSpecialWeeks" }, - {EGameSettings::CREATURES_DAILY_STACK_EXPERIENCE, "creatures", "dailyStackExperience" }, - {EGameSettings::CREATURES_WEEKLY_GROWTH_CAP, "creatures", "weeklyGrowthCap" }, - {EGameSettings::CREATURES_WEEKLY_GROWTH_PERCENT, "creatures", "weeklyGrowthPercent" }, - {EGameSettings::DIMENSION_DOOR_EXPOSES_TERRAIN_TYPE, "spells", "dimensionDoorExposesTerrainType" }, - {EGameSettings::DIMENSION_DOOR_FAILURE_SPENDS_POINTS, "spells", "dimensionDoorFailureSpendsPoints" }, - {EGameSettings::DIMENSION_DOOR_ONLY_TO_UNCOVERED_TILES, "spells", "dimensionDoorOnlyToUncoveredTiles"}, - {EGameSettings::DIMENSION_DOOR_TOURNAMENT_RULES_LIMIT, "spells", "dimensionDoorTournamentRulesLimit"}, - {EGameSettings::DIMENSION_DOOR_TRIGGERS_GUARDS, "spells", "dimensionDoorTriggersGuards" }, - {EGameSettings::DWELLINGS_ACCUMULATE_WHEN_NEUTRAL, "dwellings", "accumulateWhenNeutral" }, - {EGameSettings::DWELLINGS_ACCUMULATE_WHEN_OWNED, "dwellings", "accumulateWhenOwned" }, - {EGameSettings::DWELLINGS_MERGE_ON_RECRUIT, "dwellings", "mergeOnRecruit" }, - {EGameSettings::HEROES_BACKPACK_CAP, "heroes", "backpackSize" }, - {EGameSettings::HEROES_MINIMAL_PRIMARY_SKILLS, "heroes", "minimalPrimarySkills" }, - {EGameSettings::HEROES_PER_PLAYER_ON_MAP_CAP, "heroes", "perPlayerOnMapCap" }, - {EGameSettings::HEROES_PER_PLAYER_TOTAL_CAP, "heroes", "perPlayerTotalCap" }, - {EGameSettings::HEROES_RETREAT_ON_WIN_WITHOUT_TROOPS, "heroes", "retreatOnWinWithoutTroops" }, - {EGameSettings::HEROES_STARTING_STACKS_CHANCES, "heroes", "startingStackChances" }, - {EGameSettings::HEROES_TAVERN_INVITE, "heroes", "tavernInvite" }, - {EGameSettings::MAP_FORMAT_ARMAGEDDONS_BLADE, "mapFormat", "armageddonsBlade" }, - {EGameSettings::MAP_FORMAT_CHRONICLES, "mapFormat", "chronicles" }, - {EGameSettings::MAP_FORMAT_HORN_OF_THE_ABYSS, "mapFormat", "hornOfTheAbyss" }, - {EGameSettings::MAP_FORMAT_IN_THE_WAKE_OF_GODS, "mapFormat", "inTheWakeOfGods" }, - {EGameSettings::MAP_FORMAT_JSON_VCMI, "mapFormat", "jsonVCMI" }, - {EGameSettings::MAP_FORMAT_RESTORATION_OF_ERATHIA, "mapFormat", "restorationOfErathia" }, - {EGameSettings::MAP_FORMAT_SHADOW_OF_DEATH, "mapFormat", "shadowOfDeath" }, - {EGameSettings::MARKETS_BLACK_MARKET_RESTOCK_PERIOD, "markets", "blackMarketRestockPeriod" }, - {EGameSettings::MODULE_COMMANDERS, "modules", "commanders" }, - {EGameSettings::MODULE_STACK_ARTIFACT, "modules", "stackArtifact" }, - {EGameSettings::MODULE_STACK_EXPERIENCE, "modules", "stackExperience" }, - {EGameSettings::PATHFINDER_IGNORE_GUARDS, "pathfinder", "ignoreGuards" }, - {EGameSettings::PATHFINDER_ORIGINAL_FLY_RULES, "pathfinder", "originalFlyRules" }, - {EGameSettings::PATHFINDER_USE_BOAT, "pathfinder", "useBoat" }, - {EGameSettings::PATHFINDER_USE_MONOLITH_ONE_WAY_RANDOM, "pathfinder", "useMonolithOneWayRandom" }, - {EGameSettings::PATHFINDER_USE_MONOLITH_ONE_WAY_UNIQUE, "pathfinder", "useMonolithOneWayUnique" }, - {EGameSettings::PATHFINDER_USE_MONOLITH_TWO_WAY, "pathfinder", "useMonolithTwoWay" }, - {EGameSettings::PATHFINDER_USE_WHIRLPOOL, "pathfinder", "useWhirlpool" }, - {EGameSettings::TEXTS_ARTIFACT, "textData", "artifact" }, - {EGameSettings::TEXTS_CREATURE, "textData", "creature" }, - {EGameSettings::TEXTS_FACTION, "textData", "faction" }, - {EGameSettings::TEXTS_HERO, "textData", "hero" }, - {EGameSettings::TEXTS_HERO_CLASS, "textData", "heroClass" }, - {EGameSettings::TEXTS_OBJECT, "textData", "object" }, - {EGameSettings::TEXTS_RIVER, "textData", "river" }, - {EGameSettings::TEXTS_ROAD, "textData", "road" }, - {EGameSettings::TEXTS_SPELL, "textData", "spell" }, - {EGameSettings::TEXTS_TERRAIN, "textData", "terrain" }, - {EGameSettings::TOWNS_BUILDINGS_PER_TURN_CAP, "towns", "buildingsPerTurnCap" }, - {EGameSettings::TOWNS_STARTING_DWELLING_CHANCES, "towns", "startingDwellingChances" }, + {EGameSettings::BANKS_SHOW_GUARDS_COMPOSITION, "banks", "showGuardsComposition" }, + {EGameSettings::BONUSES_GLOBAL, "bonuses", "global" }, + {EGameSettings::BONUSES_PER_HERO, "bonuses", "perHero" }, + {EGameSettings::COMBAT_AREA_SHOT_CAN_TARGET_EMPTY_HEX, "combat", "areaShotCanTargetEmptyHex" }, + {EGameSettings::COMBAT_ATTACK_POINT_DAMAGE_FACTOR, "combat", "attackPointDamageFactor" }, + {EGameSettings::COMBAT_ATTACK_POINT_DAMAGE_FACTOR_CAP, "combat", "attackPointDamageFactorCap" }, + {EGameSettings::COMBAT_BAD_LUCK_DICE, "combat", "badLuckDice" }, + {EGameSettings::COMBAT_BAD_MORALE_DICE, "combat", "badMoraleDice" }, + {EGameSettings::COMBAT_DEFENSE_POINT_DAMAGE_FACTOR, "combat", "defensePointDamageFactor" }, + {EGameSettings::COMBAT_DEFENSE_POINT_DAMAGE_FACTOR_CAP, "combat", "defensePointDamageFactorCap" }, + {EGameSettings::COMBAT_GOOD_LUCK_DICE, "combat", "goodLuckDice" }, + {EGameSettings::COMBAT_GOOD_MORALE_DICE, "combat", "goodMoraleDice" }, + {EGameSettings::COMBAT_LAYOUTS, "combat", "layouts" }, + {EGameSettings::COMBAT_ONE_HEX_TRIGGERS_OBSTACLES, "combat", "oneHexTriggersObstacles" }, + {EGameSettings::CREATURES_ALLOW_ALL_FOR_DOUBLE_MONTH, "creatures", "allowAllForDoubleMonth" }, + {EGameSettings::CREATURES_ALLOW_RANDOM_SPECIAL_WEEKS, "creatures", "allowRandomSpecialWeeks" }, + {EGameSettings::CREATURES_DAILY_STACK_EXPERIENCE, "creatures", "dailyStackExperience" }, + {EGameSettings::CREATURES_WEEKLY_GROWTH_CAP, "creatures", "weeklyGrowthCap" }, + {EGameSettings::CREATURES_WEEKLY_GROWTH_PERCENT, "creatures", "weeklyGrowthPercent" }, + {EGameSettings::DIMENSION_DOOR_EXPOSES_TERRAIN_TYPE, "spells", "dimensionDoorExposesTerrainType" }, + {EGameSettings::DIMENSION_DOOR_FAILURE_SPENDS_POINTS, "spells", "dimensionDoorFailureSpendsPoints" }, + {EGameSettings::DIMENSION_DOOR_ONLY_TO_UNCOVERED_TILES, "spells", "dimensionDoorOnlyToUncoveredTiles" }, + {EGameSettings::DIMENSION_DOOR_TOURNAMENT_RULES_LIMIT, "spells", "dimensionDoorTournamentRulesLimit" }, + {EGameSettings::DIMENSION_DOOR_TRIGGERS_GUARDS, "spells", "dimensionDoorTriggersGuards" }, + {EGameSettings::DWELLINGS_ACCUMULATE_WHEN_NEUTRAL, "dwellings", "accumulateWhenNeutral" }, + {EGameSettings::DWELLINGS_ACCUMULATE_WHEN_OWNED, "dwellings", "accumulateWhenOwned" }, + {EGameSettings::DWELLINGS_MERGE_ON_RECRUIT, "dwellings", "mergeOnRecruit" }, + {EGameSettings::HEROES_BACKPACK_CAP, "heroes", "backpackSize" }, + {EGameSettings::HEROES_MINIMAL_PRIMARY_SKILLS, "heroes", "minimalPrimarySkills" }, + {EGameSettings::HEROES_PER_PLAYER_ON_MAP_CAP, "heroes", "perPlayerOnMapCap" }, + {EGameSettings::HEROES_PER_PLAYER_TOTAL_CAP, "heroes", "perPlayerTotalCap" }, + {EGameSettings::HEROES_RETREAT_ON_WIN_WITHOUT_TROOPS, "heroes", "retreatOnWinWithoutTroops" }, + {EGameSettings::HEROES_STARTING_STACKS_CHANCES, "heroes", "startingStackChances" }, + {EGameSettings::HEROES_TAVERN_INVITE, "heroes", "tavernInvite" }, + {EGameSettings::MAP_FORMAT_ARMAGEDDONS_BLADE, "mapFormat", "armageddonsBlade" }, + {EGameSettings::MAP_FORMAT_CHRONICLES, "mapFormat", "chronicles" }, + {EGameSettings::MAP_FORMAT_HORN_OF_THE_ABYSS, "mapFormat", "hornOfTheAbyss" }, + {EGameSettings::MAP_FORMAT_IN_THE_WAKE_OF_GODS, "mapFormat", "inTheWakeOfGods" }, + {EGameSettings::MAP_FORMAT_JSON_VCMI, "mapFormat", "jsonVCMI" }, + {EGameSettings::MAP_FORMAT_RESTORATION_OF_ERATHIA, "mapFormat", "restorationOfErathia" }, + {EGameSettings::MAP_FORMAT_SHADOW_OF_DEATH, "mapFormat", "shadowOfDeath" }, + {EGameSettings::MARKETS_BLACK_MARKET_RESTOCK_PERIOD, "markets", "blackMarketRestockPeriod" }, + {EGameSettings::MODULE_COMMANDERS, "modules", "commanders" }, + {EGameSettings::MODULE_STACK_ARTIFACT, "modules", "stackArtifact" }, + {EGameSettings::MODULE_STACK_EXPERIENCE, "modules", "stackExperience" }, + {EGameSettings::PATHFINDER_IGNORE_GUARDS, "pathfinder", "ignoreGuards" }, + {EGameSettings::PATHFINDER_ORIGINAL_FLY_RULES, "pathfinder", "originalFlyRules" }, + {EGameSettings::PATHFINDER_USE_BOAT, "pathfinder", "useBoat" }, + {EGameSettings::PATHFINDER_USE_MONOLITH_ONE_WAY_RANDOM, "pathfinder", "useMonolithOneWayRandom" }, + {EGameSettings::PATHFINDER_USE_MONOLITH_ONE_WAY_UNIQUE, "pathfinder", "useMonolithOneWayUnique" }, + {EGameSettings::PATHFINDER_USE_MONOLITH_TWO_WAY, "pathfinder", "useMonolithTwoWay" }, + {EGameSettings::PATHFINDER_USE_WHIRLPOOL, "pathfinder", "useWhirlpool" }, + {EGameSettings::RESOURCES_WEEKLY_BONUSES_AI, "resources", "weeklyBonusesAI" }, + {EGameSettings::TEXTS_ARTIFACT, "textData", "artifact" }, + {EGameSettings::TEXTS_CREATURE, "textData", "creature" }, + {EGameSettings::TEXTS_FACTION, "textData", "faction" }, + {EGameSettings::TEXTS_HERO, "textData", "hero" }, + {EGameSettings::TEXTS_HERO_CLASS, "textData", "heroClass" }, + {EGameSettings::TEXTS_OBJECT, "textData", "object" }, + {EGameSettings::TEXTS_RIVER, "textData", "river" }, + {EGameSettings::TEXTS_ROAD, "textData", "road" }, + {EGameSettings::TEXTS_SPELL, "textData", "spell" }, + {EGameSettings::TEXTS_TERRAIN, "textData", "terrain" }, + {EGameSettings::TOWNS_BUILDINGS_PER_TURN_CAP, "towns", "buildingsPerTurnCap" }, + {EGameSettings::TOWNS_STARTING_DWELLING_CHANCES, "towns", "startingDwellingChances" }, + {EGameSettings::TOWNS_SPELL_RESEARCH, "towns", "spellResearch" }, + {EGameSettings::TOWNS_SPELL_RESEARCH_COST, "towns", "spellResearchCost" }, + {EGameSettings::TOWNS_SPELL_RESEARCH_PER_DAY, "towns", "spellResearchPerDay" }, + {EGameSettings::TOWNS_SPELL_RESEARCH_COST_EXPONENT_PER_RESEARCH, "towns", "spellResearchCostExponentPerResearch" }, + {EGameSettings::INTERFACE_PLAYER_COLORED_BACKGROUND, "interface", "playerColoredBackground" }, }; void GameSettings::loadBase(const JsonNode & input) diff --git a/lib/IGameCallback.cpp b/lib/IGameCallback.cpp index fa73d0101..7cfef68cf 100644 --- a/lib/IGameCallback.cpp +++ b/lib/IGameCallback.cpp @@ -10,7 +10,6 @@ #include "StdInc.h" #include "IGameCallback.h" -#include "CHeroHandler.h" // for CHeroHandler #include "spells/CSpellHandler.h"// for CSpell #include "CSkillHandler.h"// for CSkill #include "CBonusTypeHandler.h" @@ -20,6 +19,7 @@ #include "bonuses/Propagators.h" #include "bonuses/Updaters.h" #include "entities/building/CBuilding.h" +#include "entities/hero/CHero.h" #include "networkPacks/ArtifactLocation.h" #include "serializer/CLoadFile.h" #include "serializer/CSaveFile.h" @@ -41,7 +41,6 @@ #include "gameState/QuestInfo.h" #include "mapping/CMap.h" #include "modding/CModHandler.h" -#include "modding/CModInfo.h" #include "modding/IdentifierStorage.h" #include "modding/CModVersion.h" #include "modding/ActiveModsInSaveList.h" @@ -72,7 +71,7 @@ void CPrivilegedInfoCallback::getFreeTiles(std::vector & tiles) const for (int yd = 0; yd < gs->map->height; yd++) { tinfo = getTile(int3 (xd,yd,zd)); - if (tinfo->terType->isLand() && tinfo->terType->isPassable() && !tinfo->blocked) //land and free + if (tinfo->isLand() && tinfo->getTerrain()->isPassable() && !tinfo->blocked()) //land and free tiles.emplace_back(xd, yd, zd); } } @@ -149,14 +148,14 @@ void CPrivilegedInfoCallback::getAllTiles(std::unordered_set & tiles, std: } } -void CPrivilegedInfoCallback::pickAllowedArtsSet(std::vector & out, vstd::RNG & rand) +void CPrivilegedInfoCallback::pickAllowedArtsSet(std::vector & out, vstd::RNG & rand) { for (int j = 0; j < 3 ; j++) - out.push_back(gameState()->pickRandomArtifact(rand, CArtifact::ART_TREASURE).toArtifact()); + out.push_back(gameState()->pickRandomArtifact(rand, CArtifact::ART_TREASURE)); for (int j = 0; j < 3 ; j++) - out.push_back(gameState()->pickRandomArtifact(rand, CArtifact::ART_MINOR).toArtifact()); + out.push_back(gameState()->pickRandomArtifact(rand, CArtifact::ART_MINOR)); - out.push_back(gameState()->pickRandomArtifact(rand, CArtifact::ART_MAJOR).toArtifact()); + out.push_back(gameState()->pickRandomArtifact(rand, CArtifact::ART_MAJOR)); } void CPrivilegedInfoCallback::getAllowedSpells(std::vector & out, std::optional level) diff --git a/lib/IGameCallback.h b/lib/IGameCallback.h index a9cc7b655..c93f31d68 100644 --- a/lib/IGameCallback.h +++ b/lib/IGameCallback.h @@ -75,7 +75,7 @@ public: void getAllTiles(std::unordered_set &tiles, std::optional player, int level, std::function filter) const; //gives 3 treasures, 3 minors, 1 major -> used by Black Market and Artifact Merchant - void pickAllowedArtsSet(std::vector & out, vstd::RNG & rand); + void pickAllowedArtsSet(std::vector & out, vstd::RNG & rand); void getAllowedSpells(std::vector &out, std::optional level = std::nullopt); void saveCommonState(CSaveFile &out) const; //stores GS and VLC @@ -94,6 +94,7 @@ public: virtual void showInfoDialog(InfoWindow * iw) = 0; virtual void changeSpells(const CGHeroInstance * hero, bool give, const std::set &spells)=0; + virtual void setResearchedSpells(const CGTownInstance * town, int level, const std::vector & spells, bool accepted)=0; virtual bool removeObject(const CGObjectInstance * obj, const PlayerColor & initiator) = 0; virtual void createBoat(const int3 & visitablePosition, BoatId type, PlayerColor initiator) = 0; virtual void setOwner(const CGObjectInstance * objid, PlayerColor owner)=0; @@ -122,7 +123,7 @@ public: virtual bool giveHeroNewArtifact(const CGHeroInstance * h, const ArtifactID & artId, const ArtifactPosition & pos) = 0; virtual bool giveHeroNewScroll(const CGHeroInstance * h, const SpellID & spellId, const ArtifactPosition & pos) = 0; - virtual bool putArtifact(const ArtifactLocation & al, const CArtifactInstance * art, std::optional askAssemble = std::nullopt) = 0; + virtual bool putArtifact(const ArtifactLocation & al, const ArtifactInstanceID & id, std::optional askAssemble = std::nullopt) = 0; virtual void removeArtifact(const ArtifactLocation& al) = 0; virtual bool moveArtifact(const PlayerColor & player, const ArtifactLocation & al1, const ArtifactLocation & al2) = 0; @@ -139,7 +140,7 @@ public: virtual void setManaPoints(ObjectInstanceID hid, int val)=0; virtual void giveHero(ObjectInstanceID id, PlayerColor player, ObjectInstanceID boatId = ObjectInstanceID()) = 0; virtual void changeObjPos(ObjectInstanceID objid, int3 newPos, const PlayerColor & initiator)=0; - virtual void sendAndApply(CPackForClient * pack) = 0; + virtual void sendAndApply(CPackForClient & pack) = 0; virtual void heroExchange(ObjectInstanceID hero1, ObjectInstanceID hero2)=0; //when two heroes meet on adventure map virtual void changeFogOfWar(int3 center, ui32 radius, PlayerColor player, ETileVisibility mode) = 0; virtual void changeFogOfWar(const std::unordered_set &tiles, PlayerColor player, ETileVisibility mode) = 0; diff --git a/lib/IGameSettings.h b/lib/IGameSettings.h index 9f6a8a78b..0fcae51f5 100644 --- a/lib/IGameSettings.h +++ b/lib/IGameSettings.h @@ -67,6 +67,7 @@ enum class EGameSettings PATHFINDER_USE_MONOLITH_ONE_WAY_UNIQUE, PATHFINDER_USE_MONOLITH_TWO_WAY, PATHFINDER_USE_WHIRLPOOL, + RESOURCES_WEEKLY_BONUSES_AI, TEXTS_ARTIFACT, TEXTS_CREATURE, TEXTS_FACTION, @@ -79,6 +80,11 @@ enum class EGameSettings TEXTS_TERRAIN, TOWNS_BUILDINGS_PER_TURN_CAP, TOWNS_STARTING_DWELLING_CHANCES, + INTERFACE_PLAYER_COLORED_BACKGROUND, + TOWNS_SPELL_RESEARCH, + TOWNS_SPELL_RESEARCH_COST, + TOWNS_SPELL_RESEARCH_PER_DAY, + TOWNS_SPELL_RESEARCH_COST_EXPONENT_PER_RESEARCH, OPTIONS_COUNT, OPTIONS_BEGIN = BONUSES_GLOBAL diff --git a/lib/ResourceSet.h b/lib/ResourceSet.h index cc5cd7a64..9c33db5fc 100644 --- a/lib/ResourceSet.h +++ b/lib/ResourceSet.h @@ -148,6 +148,26 @@ public: return ret; } + //Returns how many items of "this" we can afford with provided funds + int maxPurchasableCount(const ResourceSet& availableFunds) { + int ret = 0; // Initialize to 0 because we want the maximum number of accumulations + + for (size_t i = 0; i < container.size(); ++i) { + if (container.at(i) > 0) { // We only care about fulfilling positive needs + if (availableFunds[i] == 0) { + // If income is 0 and we need a positive amount, it's impossible to fulfill + return INT_MAX; + } + else { + // Calculate the number of times we need to accumulate income to fulfill the need + int ceiledResult = vstd::divideAndCeil(container.at(i), availableFunds[i]); + ret = std::max(ret, ceiledResult); + } + } + } + return ret; + } + ResourceSet & operator=(const TResource &rhs) { for(int & i : container) @@ -169,17 +189,6 @@ public: return this->container == rhs.container; } -// WARNING: comparison operators are used for "can afford" relation: a <= b means that foreach i a[i] <= b[i] -// that doesn't work the other way: a > b doesn't mean that a cannot be afforded with b, it's still b can afford a -// bool operator<(const ResourceSet &rhs) -// { -// for(int i = 0; i < size(); i++) -// if(at(i) >= rhs[i]) -// return false; -// -// return true; -// } - template void serialize(Handler &h) { h & container; diff --git a/lib/RiverHandler.cpp b/lib/RiverHandler.cpp index 0a117e6f8..c903b18e3 100644 --- a/lib/RiverHandler.cpp +++ b/lib/RiverHandler.cpp @@ -50,7 +50,7 @@ std::shared_ptr RiverTypeHandler::loadFromJson( info->paletteAnimation.push_back(element); } - VLC->generaltexth->registerString(scope, info->getNameTextID(), json["text"].String()); + VLC->generaltexth->registerString(scope, info->getNameTextID(), json["text"]); return info; } diff --git a/lib/RoadHandler.cpp b/lib/RoadHandler.cpp index aed58730f..0d82d9da6 100644 --- a/lib/RoadHandler.cpp +++ b/lib/RoadHandler.cpp @@ -41,7 +41,7 @@ std::shared_ptr RoadTypeHandler::loadFromJson( info->shortIdentifier = json["shortIdentifier"].String(); info->movementCost = json["moveCost"].Integer(); - VLC->generaltexth->registerString(scope,info->getNameTextID(), json["text"].String()); + VLC->generaltexth->registerString(scope,info->getNameTextID(), json["text"]); return info; } diff --git a/lib/StartInfo.cpp b/lib/StartInfo.cpp index b1abfe7b6..46b365375 100644 --- a/lib/StartInfo.cpp +++ b/lib/StartInfo.cpp @@ -11,10 +11,10 @@ #include "StartInfo.h" #include "texts/CGeneralTextHandler.h" -#include "CHeroHandler.h" #include "VCMI_Lib.h" #include "entities/faction/CFaction.h" #include "entities/faction/CTownHandler.h" +#include "entities/hero/CHeroHandler.h" #include "rmg/CMapGenOptions.h" #include "mapping/CMapInfo.h" #include "campaign/CampaignState.h" @@ -90,18 +90,22 @@ std::string StartInfo::getCampaignName() const return VLC->generaltexth->allTexts[508]; } -bool StartInfo::isSteadwickFallCampaignMission() const +bool StartInfo::isRestorationOfErathiaCampaign() const { + constexpr std::array roeCampaigns = { + "DATA/GOOD1", + "DATA/EVIL1", + "DATA/GOOD2", + "DATA/NEUTRAL1", + "DATA/EVIL2", + "DATA/GOOD3", + "DATA/SECRET1", + }; + if (!campState) return false; - if (campState->getFilename() != "DATA/EVIL1") - return false; - - if (campState->currentScenario() != CampaignScenarioID(2)) - return false; - - return true; + return vstd::contains(roeCampaigns, campState->getFilename()); } void LobbyInfo::verifyStateBeforeStart(bool ignoreNoHuman) const diff --git a/lib/StartInfo.h b/lib/StartInfo.h index 6a79ff862..8a79dc344 100644 --- a/lib/StartInfo.h +++ b/lib/StartInfo.h @@ -146,7 +146,7 @@ struct DLL_LINKAGE StartInfo : public Serializeable using TPlayerInfos = std::map; TPlayerInfos playerInfos; //color indexed - std::string startTimeIso8601; + time_t startTime; std::string fileURI; SimturnsInfo simturnsInfo; TurnTimerInfo turnTimerInfo; @@ -164,8 +164,8 @@ struct DLL_LINKAGE StartInfo : public Serializeable // TODO: Must be client-side std::string getCampaignName() const; - /// Controls hardcoded check for "Steadwick's Fall" scenario from "Dungeon and Devils" campaign - bool isSteadwickFallCampaignMission() const; + /// Controls hardcoded check for handling of garrisons by AI in Restoration of Erathia campaigns to match H3 behavior + bool isRestorationOfErathiaCampaign() const; template void serialize(Handler &h) @@ -180,7 +180,17 @@ struct DLL_LINKAGE StartInfo : public Serializeable h & oldSeeds; h & oldSeeds; } - h & startTimeIso8601; + if (h.version < Handler::Version::FOLDER_NAME_REWORK) + { + std::string startTimeLegacy; + h & startTimeLegacy; + struct std::tm tm; + std::istringstream ss(startTimeLegacy); + ss >> std::get_time(&tm, "%Y%m%dT%H%M%S"); + startTime = mktime(&tm); + } + else + h & startTime; h & fileURI; h & simturnsInfo; h & turnTimerInfo; @@ -193,7 +203,7 @@ struct DLL_LINKAGE StartInfo : public Serializeable StartInfo() : mode(EStartMode::INVALID) , difficulty(1) - , startTimeIso8601(vstd::getDateTimeISO8601Basic(std::time(nullptr))) + , startTime(std::time(nullptr)) { } diff --git a/lib/TerrainHandler.cpp b/lib/TerrainHandler.cpp index 6b2e81aef..f3431394a 100644 --- a/lib/TerrainHandler.cpp +++ b/lib/TerrainHandler.cpp @@ -45,7 +45,7 @@ std::shared_ptr TerrainTypeHandler::loadFromJson( const std::string info->transitionRequired = json["transitionRequired"].Bool(); info->terrainViewPatterns = json["terrainViewPatterns"].String(); - VLC->generaltexth->registerString(scope, info->getNameTextID(), json["text"].String()); + VLC->generaltexth->registerString(scope, info->getNameTextID(), json["text"]); const JsonVector & unblockedVec = json["minimapUnblocked"].Vector(); info->minimapUnblocked = diff --git a/lib/VCMI_Lib.cpp b/lib/VCMI_Lib.cpp index c516947bd..b53dc06af 100644 --- a/lib/VCMI_Lib.cpp +++ b/lib/VCMI_Lib.cpp @@ -14,7 +14,6 @@ #include "CArtHandler.h" #include "CBonusTypeHandler.h" #include "CCreatureHandler.h" -#include "CHeroHandler.h" #include "CConfigHandler.h" #include "RoadHandler.h" #include "RiverHandler.h" @@ -23,9 +22,10 @@ #include "spells/effects/Registry.h" #include "CSkillHandler.h" #include "entities/faction/CTownHandler.h" +#include "entities/hero/CHeroClassHandler.h" +#include "entities/hero/CHeroHandler.h" #include "texts/CGeneralTextHandler.h" #include "modding/CModHandler.h" -#include "modding/CModInfo.h" #include "modding/IdentifierStorage.h" #include "modding/CModVersion.h" #include "IGameEventsReceiver.h" @@ -156,55 +156,44 @@ void LibClasses::loadModFilesystem() CStopWatch loadTime; modh = std::make_unique(); identifiersHandler = std::make_unique(); - modh->loadMods(); logGlobal->info("\tMod handler: %d ms", loadTime.getDiff()); modh->loadModFilesystems(); logGlobal->info("\tMod filesystems: %d ms", loadTime.getDiff()); } -static void logHandlerLoaded(const std::string & name, CStopWatch & timer) -{ - logGlobal->info("\t\t %s handler: %d ms", name, timer.getDiff()); -} - -template void createHandler(std::shared_ptr & handler, const std::string &name, CStopWatch &timer) +template void createHandler(std::shared_ptr & handler) { handler = std::make_shared(); - logHandlerLoaded(name, timer); } void LibClasses::init(bool onlyEssential) { - CStopWatch pomtime; - CStopWatch totalTime; - - createHandler(settingsHandler, "Game Settings", pomtime); + createHandler(settingsHandler); modh->initializeConfig(); - createHandler(generaltexth, "General text", pomtime); - createHandler(bth, "Bonus type", pomtime); - createHandler(roadTypeHandler, "Road", pomtime); - createHandler(riverTypeHandler, "River", pomtime); - createHandler(terrainTypeHandler, "Terrain", pomtime); - createHandler(heroh, "Hero", pomtime); - createHandler(heroclassesh, "Hero classes", pomtime); - createHandler(arth, "Artifact", pomtime); - createHandler(creh, "Creature", pomtime); - createHandler(townh, "Town", pomtime); - createHandler(biomeHandler, "Obstacle set", pomtime); - createHandler(objh, "Object", pomtime); - createHandler(objtypeh, "Object types information", pomtime); - createHandler(spellh, "Spell", pomtime); - createHandler(skillh, "Skill", pomtime); - createHandler(terviewh, "Terrain view pattern", pomtime); - createHandler(tplh, "Template", pomtime); //templates need already resolved identifiers (refactor?) + createHandler(generaltexth); + createHandler(bth); + createHandler(roadTypeHandler); + createHandler(riverTypeHandler); + createHandler(terrainTypeHandler); + createHandler(heroh); + createHandler(heroclassesh); + createHandler(arth); + createHandler(creh); + createHandler(townh); + createHandler(biomeHandler); + createHandler(objh); + createHandler(objtypeh); + createHandler(spellh); + createHandler(skillh); + createHandler(terviewh); + createHandler(tplh); //templates need already resolved identifiers (refactor?) #if SCRIPTING_ENABLED - createHandler(scriptHandler, "Script", pomtime); + createHandler(scriptHandler); #endif - createHandler(battlefieldsHandler, "Battlefields", pomtime); - createHandler(obstacleHandler, "Obstacles", pomtime); - logGlobal->info("\tInitializing handlers: %d ms", totalTime.getDiff()); + createHandler(battlefieldsHandler); + createHandler(obstacleHandler); modh->load(); modh->afterLoad(onlyEssential); diff --git a/lib/battle/BattleInfo.cpp b/lib/battle/BattleInfo.cpp index 5bbebc49c..309f253ff 100644 --- a/lib/battle/BattleInfo.cpp +++ b/lib/battle/BattleInfo.cpp @@ -15,7 +15,6 @@ #include "bonuses/Limiters.h" #include "bonuses/Updaters.h" #include "../CStack.h" -#include "../CHeroHandler.h" #include "../entities/building/TownFortifications.h" #include "../filesystem/Filesystem.h" #include "../mapObjects/CGTownInstance.h" @@ -162,54 +161,45 @@ struct RangeGenerator BattleInfo * BattleInfo::setupBattle(const int3 & tile, TerrainId terrain, const BattleField & battlefieldType, BattleSideArray armies, BattleSideArray heroes, const BattleLayout & layout, const CGTownInstance * town) { CMP_stack cmpst; - auto * curB = new BattleInfo(layout); + auto * currentBattle = new BattleInfo(layout); for(auto i : { BattleSide::LEFT_SIDE, BattleSide::RIGHT_SIDE}) - curB->sides[i].init(heroes[i], armies[i]); + currentBattle->sides[i].init(heroes[i], armies[i]); - std::vector & stacks = (curB->stacks); + std::vector & stacks = (currentBattle->stacks); - curB->tile = tile; - curB->battlefieldType = battlefieldType; - curB->round = -2; - curB->activeStack = -1; - curB->replayAllowed = false; - - if(town) - { - curB->town = town; - curB->terrainType = town->getNativeTerrain(); - } - else - { - curB->town = nullptr; - curB->terrainType = terrain; - } + currentBattle->tile = tile; + currentBattle->terrainType = terrain; + currentBattle->battlefieldType = battlefieldType; + currentBattle->round = -2; + currentBattle->activeStack = -1; + currentBattle->replayAllowed = false; + currentBattle->town = town; //setting up siege obstacles if (town && town->fortificationsLevel().wallsHealth != 0) { auto fortification = town->fortificationsLevel(); - curB->si.gateState = EGateState::CLOSED; + currentBattle->si.gateState = EGateState::CLOSED; - curB->si.wallState[EWallPart::GATE] = EWallState::INTACT; + currentBattle->si.wallState[EWallPart::GATE] = EWallState::INTACT; for(const auto wall : {EWallPart::BOTTOM_WALL, EWallPart::BELOW_GATE, EWallPart::OVER_GATE, EWallPart::UPPER_WALL}) - curB->si.wallState[wall] = static_cast(fortification.wallsHealth); + currentBattle->si.wallState[wall] = static_cast(fortification.wallsHealth); if (fortification.citadelHealth != 0) - curB->si.wallState[EWallPart::KEEP] = static_cast(fortification.citadelHealth); + currentBattle->si.wallState[EWallPart::KEEP] = static_cast(fortification.citadelHealth); if (fortification.upperTowerHealth != 0) - curB->si.wallState[EWallPart::UPPER_TOWER] = static_cast(fortification.upperTowerHealth); + currentBattle->si.wallState[EWallPart::UPPER_TOWER] = static_cast(fortification.upperTowerHealth); if (fortification.lowerTowerHealth != 0) - curB->si.wallState[EWallPart::BOTTOM_TOWER] = static_cast(fortification.lowerTowerHealth); + currentBattle->si.wallState[EWallPart::BOTTOM_TOWER] = static_cast(fortification.lowerTowerHealth); } //randomize obstacles - if (layout.obstaclesAllowed && !town) + if (layout.obstaclesAllowed && (!town || !town->hasFort())) { RandGen r{}; auto ourRand = [&](){ return r.rand(); }; @@ -222,12 +212,12 @@ BattleInfo * BattleInfo::setupBattle(const int3 & tile, TerrainId terrain, const auto appropriateAbsoluteObstacle = [&](int id) { const auto * info = Obstacle(id).getInfo(); - return info && info->isAbsoluteObstacle && info->isAppropriate(curB->terrainType, battlefieldType); + return info && info->isAbsoluteObstacle && info->isAppropriate(currentBattle->terrainType, battlefieldType); }; auto appropriateUsualObstacle = [&](int id) { const auto * info = Obstacle(id).getInfo(); - return info && !info->isAbsoluteObstacle && info->isAppropriate(curB->terrainType, battlefieldType); + return info && !info->isAbsoluteObstacle && info->isAppropriate(currentBattle->terrainType, battlefieldType); }; if(r.rand(1,100) <= 40) //put cliff-like obstacle @@ -238,8 +228,8 @@ BattleInfo * BattleInfo::setupBattle(const int3 & tile, TerrainId terrain, const auto obstPtr = std::make_shared(); obstPtr->obstacleType = CObstacleInstance::ABSOLUTE_OBSTACLE; obstPtr->ID = obidgen.getSuchNumber(appropriateAbsoluteObstacle); - obstPtr->uniqueID = static_cast(curB->obstacles.size()); - curB->obstacles.push_back(obstPtr); + obstPtr->uniqueID = static_cast(currentBattle->obstacles.size()); + currentBattle->obstacles.push_back(obstPtr); for(BattleHex blocked : obstPtr->getBlockedTiles()) blockedTiles.push_back(blocked); @@ -257,7 +247,7 @@ BattleInfo * BattleInfo::setupBattle(const int3 & tile, TerrainId terrain, const while(tilesToBlock > 0) { RangeGenerator obidgen(0, VLC->obstacleHandler->size() - 1, ourRand); - auto tileAccessibility = curB->getAccessibility(); + auto tileAccessibility = currentBattle->getAccessibility(); const int obid = obidgen.getSuchNumber(appropriateUsualObstacle); const ObstacleInfo &obi = *Obstacle(obid).getInfo(); @@ -291,8 +281,8 @@ BattleInfo * BattleInfo::setupBattle(const int3 & tile, TerrainId terrain, const auto obstPtr = std::make_shared(); obstPtr->ID = obid; obstPtr->pos = posgenerator.getSuchNumber(validPosition); - obstPtr->uniqueID = static_cast(curB->obstacles.size()); - curB->obstacles.push_back(obstPtr); + obstPtr->uniqueID = static_cast(currentBattle->obstacles.size()); + currentBattle->obstacles.push_back(obstPtr); for(BattleHex blocked : obstPtr->getBlockedTiles()) blockedTiles.push_back(blocked); @@ -313,10 +303,10 @@ BattleInfo * BattleInfo::setupBattle(const int3 & tile, TerrainId terrain, const if(nullptr != warMachineArt && hex.isValid()) { - CreatureID cre = warMachineArt->artType->getWarMachine(); + CreatureID cre = warMachineArt->getType()->getWarMachine(); if(cre != CreatureID::NONE) - curB->generateNewStack(curB->nextUnitId(), CStackBasicDescriptor(cre, 1), side, SlotID::WAR_MACHINES_SLOT, hex); + currentBattle->generateNewStack(currentBattle->nextUnitId(), CStackBasicDescriptor(cre, 1), side, SlotID::WAR_MACHINES_SLOT, hex); } }; @@ -354,7 +344,7 @@ BattleInfo * BattleInfo::setupBattle(const int3 & tile, TerrainId terrain, const const BattleHex & pos = layout.units.at(side).at(k); if (pos.isValid()) - curB->generateNewStack(curB->nextUnitId(), *i->second, side, i->first, pos); + currentBattle->generateNewStack(currentBattle->nextUnitId(), *i->second, side, i->first, pos); } } @@ -363,20 +353,20 @@ BattleInfo * BattleInfo::setupBattle(const int3 & tile, TerrainId terrain, const { if (heroes[i] && heroes[i]->commander && heroes[i]->commander->alive) { - curB->generateNewStack(curB->nextUnitId(), *heroes[i]->commander, i, SlotID::COMMANDER_SLOT_PLACEHOLDER, layout.commanders.at(i)); + currentBattle->generateNewStack(currentBattle->nextUnitId(), *heroes[i]->commander, i, SlotID::COMMANDER_SLOT_PLACEHOLDER, layout.commanders.at(i)); } } - if (curB->town) + if (currentBattle->town) { - if (curB->town->fortificationsLevel().citadelHealth != 0) - curB->generateNewStack(curB->nextUnitId(), CStackBasicDescriptor(CreatureID::ARROW_TOWERS, 1), BattleSide::DEFENDER, SlotID::ARROW_TOWERS_SLOT, BattleHex::CASTLE_CENTRAL_TOWER); + if (currentBattle->town->fortificationsLevel().citadelHealth != 0) + currentBattle->generateNewStack(currentBattle->nextUnitId(), CStackBasicDescriptor(CreatureID::ARROW_TOWERS, 1), BattleSide::DEFENDER, SlotID::ARROW_TOWERS_SLOT, BattleHex::CASTLE_CENTRAL_TOWER); - if (curB->town->fortificationsLevel().upperTowerHealth != 0) - curB->generateNewStack(curB->nextUnitId(), CStackBasicDescriptor(CreatureID::ARROW_TOWERS, 1), BattleSide::DEFENDER, SlotID::ARROW_TOWERS_SLOT, BattleHex::CASTLE_UPPER_TOWER); + if (currentBattle->town->fortificationsLevel().upperTowerHealth != 0) + currentBattle->generateNewStack(currentBattle->nextUnitId(), CStackBasicDescriptor(CreatureID::ARROW_TOWERS, 1), BattleSide::DEFENDER, SlotID::ARROW_TOWERS_SLOT, BattleHex::CASTLE_UPPER_TOWER); - if (curB->town->fortificationsLevel().lowerTowerHealth != 0) - curB->generateNewStack(curB->nextUnitId(), CStackBasicDescriptor(CreatureID::ARROW_TOWERS, 1), BattleSide::DEFENDER, SlotID::ARROW_TOWERS_SLOT, BattleHex::CASTLE_BOTTOM_TOWER); + if (currentBattle->town->fortificationsLevel().lowerTowerHealth != 0) + currentBattle->generateNewStack(currentBattle->nextUnitId(), CStackBasicDescriptor(CreatureID::ARROW_TOWERS, 1), BattleSide::DEFENDER, SlotID::ARROW_TOWERS_SLOT, BattleHex::CASTLE_BOTTOM_TOWER); //Moat generating is done on server } @@ -391,15 +381,15 @@ BattleInfo * BattleInfo::setupBattle(const int3 & tile, TerrainId terrain, const for(const std::shared_ptr & bonus : bgInfo->bonuses) { - curB->addNewBonus(bonus); + currentBattle->addNewBonus(bonus); } //native terrain bonuses auto nativeTerrain = std::make_shared(); - curB->addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::STACKS_SPEED, BonusSource::TERRAIN_NATIVE, 1, BonusSourceID())->addLimiter(nativeTerrain)); - curB->addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::PRIMARY_SKILL, BonusSource::TERRAIN_NATIVE, 1, BonusSourceID(), BonusSubtypeID(PrimarySkill::ATTACK))->addLimiter(nativeTerrain)); - curB->addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::PRIMARY_SKILL, BonusSource::TERRAIN_NATIVE, 1, BonusSourceID(), BonusSubtypeID(PrimarySkill::DEFENSE))->addLimiter(nativeTerrain)); + currentBattle->addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::STACKS_SPEED, BonusSource::TERRAIN_NATIVE, 1, BonusSourceID())->addLimiter(nativeTerrain)); + currentBattle->addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::PRIMARY_SKILL, BonusSource::TERRAIN_NATIVE, 1, BonusSourceID(), BonusSubtypeID(PrimarySkill::ATTACK))->addLimiter(nativeTerrain)); + currentBattle->addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::PRIMARY_SKILL, BonusSource::TERRAIN_NATIVE, 1, BonusSourceID(), BonusSubtypeID(PrimarySkill::DEFENSE))->addLimiter(nativeTerrain)); ////////////////////////////////////////////////////////////////////////// //tactics @@ -429,21 +419,21 @@ BattleInfo * BattleInfo::setupBattle(const int3 & tile, TerrainId terrain, const logGlobal->warn("Double tactics is not implemented, only attacker will have tactics!"); if(tacticsSkillDiffAttacker > 0) { - curB->tacticsSide = BattleSide::ATTACKER; + currentBattle->tacticsSide = BattleSide::ATTACKER; //bonus specifies distance you can move beyond base row; this allows 100% compatibility with HMM3 mechanics - curB->tacticDistance = 1 + tacticsSkillDiffAttacker; + currentBattle->tacticDistance = 1 + tacticsSkillDiffAttacker; } else if(tacticsSkillDiffDefender > 0) { - curB->tacticsSide = BattleSide::DEFENDER; + currentBattle->tacticsSide = BattleSide::DEFENDER; //bonus specifies distance you can move beyond base row; this allows 100% compatibility with HMM3 mechanics - curB->tacticDistance = 1 + tacticsSkillDiffDefender; + currentBattle->tacticDistance = 1 + tacticsSkillDiffDefender; } else - curB->tacticDistance = 0; + currentBattle->tacticDistance = 0; } - return curB; + return currentBattle; } const CGHeroInstance * BattleInfo::getHero(const PlayerColor & player) const @@ -886,12 +876,12 @@ void BattleInfo::addOrUpdateUnitBonus(CStack * sta, const Bonus & value, bool fo if(forceAdd || !sta->hasBonus(Selector::source(BonusSource::SPELL_EFFECT, value.sid).And(Selector::typeSubtypeValueType(value.type, value.subtype, value.valType)))) { //no such effect or cumulative - add new - logBonus->trace("%s receives a new bonus: %s", sta->nodeName(), value.Description()); + logBonus->trace("%s receives a new bonus: %s", sta->nodeName(), value.Description(nullptr)); sta->addNewBonus(std::make_shared(value)); } else { - logBonus->trace("%s updated bonus: %s", sta->nodeName(), value.Description()); + logBonus->trace("%s updated bonus: %s", sta->nodeName(), value.Description(nullptr)); for(const auto & stackBonus : sta->getExportedBonusList()) //TODO: optimize { diff --git a/lib/battle/CBattleInfoCallback.cpp b/lib/battle/CBattleInfoCallback.cpp index b8d7e881f..251c1f9b7 100644 --- a/lib/battle/CBattleInfoCallback.cpp +++ b/lib/battle/CBattleInfoCallback.cpp @@ -927,7 +927,7 @@ bool CBattleInfoCallback::handleObstacleTriggersForUnit(SpellCastEnvironment & s bocp.battleID = getBattle()->getBattleID(); bocp.changes.emplace_back(spellObstacle.uniqueID, operation); changedObstacle.toInfo(bocp.changes.back(), operation); - spellEnv.apply(&bocp); + spellEnv.apply(bocp); }; const auto side = unit.unitSide(); auto shouldReveal = !spellObstacle->hidden || !battleIsObstacleVisibleForSide(*obstacle, side); @@ -1392,7 +1392,7 @@ AttackableTiles CBattleInfoCallback::getPotentiallyAttackableHexes( at.friendlyCreaturePositions.insert(tile); } } - else if(attacker->hasBonusOfType(BonusType::TWO_HEX_ATTACK_BREATH)) + else if(attacker->hasBonusOfType(BonusType::TWO_HEX_ATTACK_BREATH) || attacker->hasBonusOfType(BonusType::PRISM_HEX_ATTACK_BREATH)) { auto direction = BattleHex::mutualPosition(attackOriginHex, destinationTile); @@ -1404,27 +1404,39 @@ AttackableTiles CBattleInfoCallback::getPotentiallyAttackableHexes( direction = BattleHex::mutualPosition(attackOriginHex, defender->occupiedHex(defenderPos)); } - if(direction != BattleHex::NONE) //only adjacent hexes are subject of dragon breath calculation + for(int i = 0; i < 3; i++) { - BattleHex nextHex = destinationTile.cloneInDirection(direction, false); - - if ( defender->doubleWide() ) + if(direction != BattleHex::NONE) //only adjacent hexes are subject of dragon breath calculation { - auto secondHex = destinationTile == defenderPos ? defender->occupiedHex(defenderPos) : defenderPos; + BattleHex nextHex = destinationTile.cloneInDirection(direction, false); - // if targeted double-wide creature is attacked from above or below ( -> second hex is also adjacent to attack origin) - // then dragon breath should target tile on the opposite side of targeted creature - if(BattleHex::mutualPosition(attackOriginHex, secondHex) != BattleHex::NONE) - nextHex = secondHex.cloneInDirection(direction, false); + if ( defender->doubleWide() ) + { + auto secondHex = destinationTile == defenderPos ? defender->occupiedHex(defenderPos) : defenderPos; + + // if targeted double-wide creature is attacked from above or below ( -> second hex is also adjacent to attack origin) + // then dragon breath should target tile on the opposite side of targeted creature + if(BattleHex::mutualPosition(attackOriginHex, secondHex) != BattleHex::NONE) + nextHex = secondHex.cloneInDirection(direction, false); + } + + if (nextHex.isValid()) + { + //friendly stacks can also be damaged by Dragon Breath + const auto * st = battleGetUnitByPos(nextHex, true); + if(st != nullptr && st != attacker) //but not unit itself (doublewide + prism attack) + at.friendlyCreaturePositions.insert(nextHex); + } } - if (nextHex.isValid()) - { - //friendly stacks can also be damaged by Dragon Breath - const auto * st = battleGetUnitByPos(nextHex, true); - if(st != nullptr) - at.friendlyCreaturePositions.insert(nextHex); - } + if(!attacker->hasBonusOfType(BonusType::PRISM_HEX_ATTACK_BREATH)) + break; + + // only needed for prism + int tmpDirection = static_cast(direction) + 2; + if(tmpDirection > static_cast(BattleHex::EDir::LEFT)) + tmpDirection -= static_cast(BattleHex::EDir::TOP); + direction = static_cast(tmpDirection); } } return at; diff --git a/lib/battle/CObstacleInstance.cpp b/lib/battle/CObstacleInstance.cpp index 098d980a2..6bb5c65f3 100644 --- a/lib/battle/CObstacleInstance.cpp +++ b/lib/battle/CObstacleInstance.cpp @@ -9,7 +9,6 @@ */ #include "StdInc.h" #include "CObstacleInstance.h" -#include "../CHeroHandler.h" #include "../ObstacleHandler.h" #include "../VCMI_Lib.h" diff --git a/lib/battle/CUnitState.cpp b/lib/battle/CUnitState.cpp index 131fc2a23..737a9813f 100644 --- a/lib/battle/CUnitState.cpp +++ b/lib/battle/CUnitState.cpp @@ -416,9 +416,9 @@ int32_t CUnitState::creatureIconIndex() const return unitType()->getIconIndex(); } -FactionID CUnitState::getFaction() const +FactionID CUnitState::getFactionID() const { - return unitType()->getFaction(); + return unitType()->getFactionID(); } int32_t CUnitState::getCasterUnitId() const diff --git a/lib/battle/CUnitState.h b/lib/battle/CUnitState.h index b5451ba89..9bb9570a1 100644 --- a/lib/battle/CUnitState.h +++ b/lib/battle/CUnitState.h @@ -253,7 +253,7 @@ public: void localInit(const IUnitEnvironment * env_); void serializeJson(JsonSerializeFormat & handler); - FactionID getFaction() const override; + FactionID getFactionID() const override; void afterAttack(bool ranged, bool counter); diff --git a/lib/bonuses/Bonus.cpp b/lib/bonuses/Bonus.cpp index 2f1cb519b..95d665d76 100644 --- a/lib/bonuses/Bonus.cpp +++ b/lib/bonuses/Bonus.cpp @@ -14,18 +14,21 @@ #include "Updaters.h" #include "Propagators.h" -#include "../VCMI_Lib.h" -#include "../spells/CSpellHandler.h" +#include "../CArtHandler.h" #include "../CCreatureHandler.h" #include "../CCreatureSet.h" -#include "../CHeroHandler.h" -#include "../texts/CGeneralTextHandler.h" #include "../CSkillHandler.h" -#include "../CArtHandler.h" +#include "../IGameCallback.h" #include "../TerrainHandler.h" -#include "../constants/StringConstants.h" +#include "../VCMI_Lib.h" +#include "../mapObjects/CGObjectInstance.h" +#include "../mapObjectConstructors/CObjectClassesHandler.h" #include "../battle/BattleInfo.h" +#include "../constants/StringConstants.h" +#include "../entities/hero/CHero.h" #include "../modding/ModUtility.h" +#include "../spells/CSpellHandler.h" +#include "../texts/CGeneralTextHandler.h" VCMI_LIB_NAMESPACE_BEGIN @@ -87,7 +90,7 @@ JsonNode CAddInfo::toJsonNode() const return node; } } -std::string Bonus::Description(std::optional customValue) const +std::string Bonus::Description(const IGameInfoCallback * cb, std::optional customValue) const { MetaString descriptionHelper = description; auto valueToShow = customValue.value_or(val); @@ -112,6 +115,10 @@ std::string Bonus::Description(std::optional customValue) const case BonusSource::HERO_SPECIAL: descriptionHelper.appendTextID(sid.as().toEntity(VLC)->getNameTextID()); break; + case BonusSource::OBJECT_INSTANCE: + const auto * object = cb->getObj(sid.as()); + if (object) + descriptionHelper.appendTextID(VLC->objtypeh->getObjectName(object->ID, object->subID)); } } diff --git a/lib/bonuses/Bonus.h b/lib/bonuses/Bonus.h index 63b4d6b1d..caefbc46a 100644 --- a/lib/bonuses/Bonus.h +++ b/lib/bonuses/Bonus.h @@ -26,6 +26,7 @@ class IPropagator; class IUpdater; class BonusList; class CSelector; +class IGameInfoCallback; using BonusSubtypeID = VariantIdentifier; using BonusSourceID = VariantIdentifier; @@ -177,7 +178,7 @@ struct DLL_LINKAGE Bonus : public std::enable_shared_from_this, public Se val += Val; } - std::string Description(std::optional customValue = {}) const; + std::string Description(const IGameInfoCallback * cb, std::optional customValue = {}) const; JsonNode toJsonNode() const; std::shared_ptr addLimiter(const TLimiterPtr & Limiter); //returns this for convenient chain-calls diff --git a/lib/bonuses/BonusEnum.h b/lib/bonuses/BonusEnum.h index e68450c08..1ef515e1f 100644 --- a/lib/bonuses/BonusEnum.h +++ b/lib/bonuses/BonusEnum.h @@ -180,6 +180,8 @@ class JsonNode; BONUS_NAME(RESOURCES_TOWN_MULTIPLYING_BOOST) /*Bonus that does not account for propagation and gives extra resources per day with amount multiplied by number of owned towns. val - base resource amount to be multiplied times number of owned towns, subtype - resource type*/ \ BONUS_NAME(DISINTEGRATE) /* after death no corpse remains */ \ BONUS_NAME(INVINCIBLE) /* cannot be target of attacks or spells */ \ + BONUS_NAME(MECHANICAL) /*eg. factory creatures, cannot be rised or healed, only neutral morale, repairable by engineer */ \ + BONUS_NAME(PRISM_HEX_ATTACK_BREATH) /*eg. dragons*/ \ /* end of list */ diff --git a/lib/bonuses/CBonusSystemNode.cpp b/lib/bonuses/CBonusSystemNode.cpp index 7b944eefb..b5a2d582d 100644 --- a/lib/bonuses/CBonusSystemNode.cpp +++ b/lib/bonuses/CBonusSystemNode.cpp @@ -378,7 +378,7 @@ void CBonusSystemNode::propagateBonus(const std::shared_ptr & b, const CB ? source.getUpdatedBonus(b, b->propagationUpdater) : b; bonuses.push_back(propagated); - logBonus->trace("#$# %s #propagated to# %s", propagated->Description(), nodeName()); + logBonus->trace("#$# %s #propagated to# %s", propagated->Description(nullptr), nodeName()); } TNodes lchildren; @@ -392,9 +392,9 @@ void CBonusSystemNode::unpropagateBonus(const std::shared_ptr & b) if(b->propagator->shouldBeAttached(this)) { if (bonuses -= b) - logBonus->trace("#$# %s #is no longer propagated to# %s", b->Description(), nodeName()); + logBonus->trace("#$# %s #is no longer propagated to# %s", b->Description(nullptr), nodeName()); else - logBonus->warn("Attempt to remove #$# %s, which is not propagated to %s", b->Description(), nodeName()); + logBonus->warn("Attempt to remove #$# %s, which is not propagated to %s", b->Description(nullptr), nodeName()); bonuses.remove_if([b](const auto & bonus) { diff --git a/lib/bonuses/Limiters.cpp b/lib/bonuses/Limiters.cpp index 4f1786a20..e5aa2fae3 100644 --- a/lib/bonuses/Limiters.cpp +++ b/lib/bonuses/Limiters.cpp @@ -17,7 +17,6 @@ #include "../spells/CSpellHandler.h" #include "../CCreatureHandler.h" #include "../CCreatureSet.h" -#include "../CHeroHandler.h" #include "../texts/CGeneralTextHandler.h" #include "../CSkillHandler.h" #include "../CStack.h" @@ -76,7 +75,7 @@ static const CCreature * retrieveCreature(const CBonusSystemNode *node) default: const CStackInstance * csi = retrieveStackInstance(node); if(csi) - return csi->type; + return csi->getCreature(); return nullptr; } } @@ -104,25 +103,25 @@ ILimiter::EDecision CCreatureTypeLimiter::limit(const BonusLimitationContext &co if(!c) return ILimiter::EDecision::DISCARD; - auto accept = c->getId() == creature->getId() || (includeUpgrades && creature->isMyUpgrade(c)); + auto accept = c->getId() == creatureID || (includeUpgrades && creatureID.toCreature()->isMyUpgrade(c)); return accept ? ILimiter::EDecision::ACCEPT : ILimiter::EDecision::DISCARD; //drop bonus if it's not our creature and (we don`t check upgrades or its not our upgrade) } CCreatureTypeLimiter::CCreatureTypeLimiter(const CCreature & creature_, bool IncludeUpgrades) - : creature(&creature_), includeUpgrades(IncludeUpgrades) + : creatureID(creature_.getId()), includeUpgrades(IncludeUpgrades) { } void CCreatureTypeLimiter::setCreature(const CreatureID & id) { - creature = id.toCreature(); + creatureID = id; } std::string CCreatureTypeLimiter::toString() const { boost::format fmt("CCreatureTypeLimiter(creature=%s, includeUpgrades=%s)"); - fmt % creature->getJsonKey() % (includeUpgrades ? "true" : "false"); + fmt % creatureID.toEntity(VLC)->getJsonKey() % (includeUpgrades ? "true" : "false"); return fmt.str(); } @@ -131,7 +130,7 @@ JsonNode CCreatureTypeLimiter::toJsonNode() const JsonNode root; root["type"].String() = "CREATURE_TYPE_LIMITER"; - root["parameters"].Vector().emplace_back(creature->getJsonKey()); + root["parameters"].Vector().emplace_back(creatureID.toEntity(VLC)->getJsonKey()); root["parameters"].Vector().emplace_back(includeUpgrades); return root; @@ -300,15 +299,15 @@ ILimiter::EDecision FactionLimiter::limit(const BonusLimitationContext &context) if(bearer) { if(faction != FactionID::DEFAULT) - return bearer->getFaction() == faction ? ILimiter::EDecision::ACCEPT : ILimiter::EDecision::DISCARD; + return bearer->getFactionID() == faction ? ILimiter::EDecision::ACCEPT : ILimiter::EDecision::DISCARD; switch(context.b.source) { case BonusSource::CREATURE_ABILITY: - return bearer->getFaction() == context.b.sid.as().toCreature()->getFaction() ? ILimiter::EDecision::ACCEPT : ILimiter::EDecision::DISCARD; + return bearer->getFactionID() == context.b.sid.as().toCreature()->getFactionID() ? ILimiter::EDecision::ACCEPT : ILimiter::EDecision::DISCARD; case BonusSource::TOWN_STRUCTURE: - return bearer->getFaction() == context.b.sid.as().getFaction() ? ILimiter::EDecision::ACCEPT : ILimiter::EDecision::DISCARD; + return bearer->getFactionID() == context.b.sid.as().getFaction() ? ILimiter::EDecision::ACCEPT : ILimiter::EDecision::DISCARD; //TODO: other sources of bonuses } diff --git a/lib/bonuses/Limiters.h b/lib/bonuses/Limiters.h index f485559b7..9ad0e56d2 100644 --- a/lib/bonuses/Limiters.h +++ b/lib/bonuses/Limiters.h @@ -94,7 +94,7 @@ public: class DLL_LINKAGE CCreatureTypeLimiter : public ILimiter //affect only stacks of given creature (and optionally it's upgrades) { public: - const CCreature * creature = nullptr; + CreatureID creatureID; bool includeUpgrades = false; CCreatureTypeLimiter() = default; @@ -108,7 +108,16 @@ public: template void serialize(Handler &h) { h & static_cast(*this); - h & creature; + + if (h.version < Handler::Version::REMOVE_TOWN_PTR) + { + bool isNull = false; + h & isNull; + if(!isNull) + h & creatureID; + } + else + h & creatureID; h & includeUpgrades; } }; diff --git a/lib/campaign/CampaignHandler.cpp b/lib/campaign/CampaignHandler.cpp index 563437cb0..7d94bb1b8 100644 --- a/lib/campaign/CampaignHandler.cpp +++ b/lib/campaign/CampaignHandler.cpp @@ -25,7 +25,6 @@ #include "../modding/IdentifierStorage.h" #include "../modding/ModScope.h" #include "../texts/CGeneralTextHandler.h" -#include "../texts/Languages.h" #include "../texts/TextOperations.h" VCMI_LIB_NAMESPACE_BEGIN @@ -64,8 +63,7 @@ std::unique_ptr CampaignHandler::getHeader( const std::string & name) { ResourcePath resourceID(name, EResType::CAMPAIGN); std::string modName = VLC->modh->findResourceOrigin(resourceID); - std::string language = VLC->modh->getModLanguage(modName); - std::string encoding = Languages::getLanguageOptions(language).encoding; + std::string encoding = VLC->modh->findResourceEncoding(resourceID); auto ret = std::make_unique(); auto fileStream = CResourceHandler::get(modName)->load(resourceID); @@ -80,8 +78,7 @@ std::shared_ptr CampaignHandler::getCampaign( const std::string & { ResourcePath resourceID(name, EResType::CAMPAIGN); std::string modName = VLC->modh->findResourceOrigin(resourceID); - std::string language = VLC->modh->getModLanguage(modName); - std::string encoding = Languages::getLanguageOptions(language).encoding; + std::string encoding = VLC->modh->findResourceEncoding(resourceID); auto ret = std::make_unique(); diff --git a/lib/campaign/CampaignState.cpp b/lib/campaign/CampaignState.cpp index 132d6e904..5dc6a2e10 100644 --- a/lib/campaign/CampaignState.cpp +++ b/lib/campaign/CampaignState.cpp @@ -339,7 +339,7 @@ void CampaignState::setCurrentMapAsConquered(std::vector heroe { range::sort(heroes, [](const CGHeroInstance * a, const CGHeroInstance * b) { - return a->getHeroStrength() > b->getHeroStrength(); + return a->getHeroStrengthForCampaign() > b->getHeroStrengthForCampaign(); }); logGlobal->info("Scenario %d of campaign %s (%s) has been completed", currentMap->getNum(), getFilename(), getNameTranslated()); @@ -351,14 +351,14 @@ void CampaignState::setCurrentMapAsConquered(std::vector heroe { JsonNode node = CampaignState::crossoverSerialize(hero); - if (reservedHeroes.count(hero->getHeroType())) + if (reservedHeroes.count(hero->getHeroTypeID())) { - logGlobal->info("Hero crossover: %d (%s) exported to global pool", hero->getHeroType(), hero->getNameTranslated()); - globalHeroPool[hero->getHeroType()] = node; + logGlobal->info("Hero crossover: %d (%s) exported to global pool", hero->getHeroTypeID(), hero->getNameTranslated()); + globalHeroPool[hero->getHeroTypeID()] = node; } else { - logGlobal->info("Hero crossover: %d (%s) exported to scenario pool", hero->getHeroType(), hero->getNameTranslated()); + logGlobal->info("Hero crossover: %d (%s) exported to scenario pool", hero->getHeroTypeID(), hero->getNameTranslated()); scenarioHeroPool[*currentMap].push_back(node); } } diff --git a/lib/constants/EntityIdentifiers.cpp b/lib/constants/EntityIdentifiers.cpp index 599920523..2c3f183c5 100644 --- a/lib/constants/EntityIdentifiers.cpp +++ b/lib/constants/EntityIdentifiers.cpp @@ -29,12 +29,13 @@ #include "modding/IdentifierStorage.h" #include "modding/ModScope.h" #include "VCMI_Lib.h" -#include "CHeroHandler.h" #include "CArtHandler.h"//todo: remove #include "CCreatureHandler.h"//todo: remove #include "spells/CSpellHandler.h" //todo: remove #include "CSkillHandler.h"//todo: remove #include "entities/faction/CFaction.h" +#include "entities/hero/CHero.h" +#include "entities/hero/CHeroClass.h" #include "mapObjectConstructors/AObjectTypeHandler.h" #include "constants/StringConstants.h" #include "texts/CGeneralTextHandler.h" diff --git a/lib/constants/EntityIdentifiers.h b/lib/constants/EntityIdentifiers.h index c4a1f6272..3bc547247 100644 --- a/lib/constants/EntityIdentifiers.h +++ b/lib/constants/EntityIdentifiers.h @@ -298,7 +298,7 @@ public: HORDE_2_UPGR, GRAIL, EXTRA_TOWN_HALL, EXTRA_CITY_HALL, EXTRA_CAPITOL, DWELL_FIRST=30, DWELL_LVL_2, DWELL_LVL_3, DWELL_LVL_4, DWELL_LVL_5, DWELL_LVL_6, DWELL_LAST=36, DWELL_UP_FIRST=37, DWELL_LVL_2_UP, DWELL_LVL_3_UP, DWELL_LVL_4_UP, DWELL_LVL_5_UP, - DWELL_LVL_6_UP, DWELL_UP_LAST=43, DWELL_LVL_8=150, DWELL_LVL_8_UP=151, + DWELL_LVL_6_UP, DWELL_UP_LAST=43, DWELL_LVL_8=150, DWELL_LVL_8_UP=151, //150-154 reserved for 8. creature with potential upgrades DWELL_LVL_1 = DWELL_FIRST, DWELL_LVL_7 = DWELL_LAST, @@ -337,7 +337,10 @@ public: if (it != tmp.end()) return std::distance(tmp.begin(), it); } - return (dwelling - DWELL_FIRST) % (GameConstants::CREATURES_PER_TOWN - 1); + if(dwelling >= BuildingIDBase::DWELL_LVL_8 && dwelling < BuildingIDBase::DWELL_LVL_8 + 5) + return 7; + else + return (dwelling - DWELL_FIRST) % (GameConstants::CREATURES_PER_TOWN - 1); } static int getUpgradedFromDwelling(BuildingIDBase dwelling) @@ -349,15 +352,18 @@ public: if (it != tmp.end()) return i; } - return (dwelling - DWELL_FIRST) / (GameConstants::CREATURES_PER_TOWN - 1); + if(dwelling >= BuildingIDBase::DWELL_LVL_8 && dwelling < BuildingIDBase::DWELL_LVL_8 + 5) + return dwelling - BuildingIDBase::DWELL_LVL_8; + else + return (dwelling - DWELL_FIRST) / (GameConstants::CREATURES_PER_TOWN - 1); } static void advanceDwelling(BuildingIDBase & dwelling) { - if(dwelling != BuildingIDBase::DWELL_LVL_8) - dwelling.advance(GameConstants::CREATURES_PER_TOWN - 1); - else + if(dwelling >= BuildingIDBase::DWELL_LVL_8 && dwelling < BuildingIDBase::DWELL_LVL_8 + 5) dwelling.advance(1); + else + dwelling.advance(GameConstants::CREATURES_PER_TOWN - 1); } bool IsSpecialOrGrail() const diff --git a/lib/entities/faction/CFaction.cpp b/lib/entities/faction/CFaction.cpp index 4ed5d1ad2..3ef88afae 100644 --- a/lib/entities/faction/CFaction.cpp +++ b/lib/entities/faction/CFaction.cpp @@ -92,7 +92,7 @@ FactionID CFaction::getId() const return FactionID(index); } -FactionID CFaction::getFaction() const +FactionID CFaction::getFactionID() const { return FactionID(index); } diff --git a/lib/entities/faction/CFaction.h b/lib/entities/faction/CFaction.h index 294a77529..27d6a2239 100644 --- a/lib/entities/faction/CFaction.h +++ b/lib/entities/faction/CFaction.h @@ -39,7 +39,7 @@ class DLL_LINKAGE CFaction : public Faction FactionID index = FactionID::NEUTRAL; - FactionID getFaction() const override; //This function should not be used + FactionID getFactionID() const override; //This function should not be used public: TerrainId nativeTerrain; diff --git a/lib/entities/faction/CTownHandler.cpp b/lib/entities/faction/CTownHandler.cpp index 036c2f6ef..91d05852b 100644 --- a/lib/entities/faction/CTownHandler.cpp +++ b/lib/entities/faction/CTownHandler.cpp @@ -13,9 +13,9 @@ #include "CTown.h" #include "CFaction.h" #include "../building/CBuilding.h" +#include "../hero/CHeroClassHandler.h" #include "../../CCreatureHandler.h" -#include "../../CHeroHandler.h" #include "../../IGameSettings.h" #include "../../TerrainHandler.h" #include "../../VCMI_Lib.h" @@ -292,8 +292,8 @@ void CTownHandler::loadBuilding(CTown * town, const std::string & stringID, cons ret->modScope = source.getModScope(); ret->town = town; - VLC->generaltexth->registerString(source.getModScope(), ret->getNameTextID(), source["name"].String()); - VLC->generaltexth->registerString(source.getModScope(), ret->getDescriptionTextID(), source["description"].String()); + VLC->generaltexth->registerString(source.getModScope(), ret->getNameTextID(), source["name"]); + VLC->generaltexth->registerString(source.getModScope(), ret->getDescriptionTextID(), source["description"]); ret->subId = vstd::find_or(MappedKeys::SPECIAL_BUILDINGS, source["type"].String(), BuildingSubID::NONE); ret->resources = TResources(source["cost"]); @@ -603,7 +603,7 @@ void CTownHandler::loadTown(CTown * town, const JsonNode & source) town->namesCount = 0; for(const auto & name : source["names"].Vector()) { - VLC->generaltexth->registerString(town->faction->modScope, town->getRandomNameTextID(town->namesCount), name.String()); + VLC->generaltexth->registerString(town->faction->modScope, town->getRandomNameTextID(town->namesCount), name); town->namesCount += 1; } @@ -718,8 +718,8 @@ std::shared_ptr CTownHandler::loadFromJson(const std::string & scope, faction->modScope = scope; faction->identifier = identifier; - VLC->generaltexth->registerString(scope, faction->getNameTextID(), source["name"].String()); - VLC->generaltexth->registerString(scope, faction->getDescriptionTextID(), source["description"].String()); + VLC->generaltexth->registerString(scope, faction->getNameTextID(), source["name"]); + VLC->generaltexth->registerString(scope, faction->getDescriptionTextID(), source["description"]); faction->creatureBg120 = ImagePath::fromJson(source["creatureBackground"]["120px"]); faction->creatureBg130 = ImagePath::fromJson(source["creatureBackground"]["130px"]); @@ -883,8 +883,8 @@ void CTownHandler::beforeValidate(JsonNode & object) if (building.second.Struct().count("onVisitBonuses")) { building.second["configuration"]["visitMode"] = JsonNode("bonus"); - building.second["configuration"]["visitMode"]["rewards"][0]["message"] = building.second["description"]; - building.second["configuration"]["visitMode"]["rewards"][0]["bonuses"] = building.second["onVisitBonuses"]; + building.second["configuration"]["rewards"][0]["message"] = building.second["description"]; + building.second["configuration"]["rewards"][0]["bonuses"] = building.second["onVisitBonuses"]; } } } diff --git a/lib/entities/hero/CHero.cpp b/lib/entities/hero/CHero.cpp new file mode 100644 index 000000000..4b9b363f1 --- /dev/null +++ b/lib/entities/hero/CHero.cpp @@ -0,0 +1,114 @@ +/* + * CHero.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 "CHero.h" + +#include "../../VCMI_Lib.h" +#include "../../texts/CGeneralTextHandler.h" + +VCMI_LIB_NAMESPACE_BEGIN + +CHero::CHero() = default; +CHero::~CHero() = default; + +int32_t CHero::getIndex() const +{ + return ID.getNum(); +} + +int32_t CHero::getIconIndex() const +{ + return imageIndex; +} + +std::string CHero::getJsonKey() const +{ + return modScope + ':' + identifier; +} + +std::string CHero::getModScope() const +{ + return modScope; +} + +HeroTypeID CHero::getId() const +{ + return ID; +} + +std::string CHero::getNameTranslated() const +{ + return VLC->generaltexth->translate(getNameTextID()); +} + +std::string CHero::getBiographyTranslated() const +{ + return VLC->generaltexth->translate(getBiographyTextID()); +} + +std::string CHero::getSpecialtyNameTranslated() const +{ + return VLC->generaltexth->translate(getSpecialtyNameTextID()); +} + +std::string CHero::getSpecialtyDescriptionTranslated() const +{ + return VLC->generaltexth->translate(getSpecialtyDescriptionTextID()); +} + +std::string CHero::getSpecialtyTooltipTranslated() const +{ + return VLC->generaltexth->translate(getSpecialtyTooltipTextID()); +} + +std::string CHero::getNameTextID() const +{ + return TextIdentifier("hero", modScope, identifier, "name").get(); +} + +std::string CHero::getBiographyTextID() const +{ + return TextIdentifier("hero", modScope, identifier, "biography").get(); +} + +std::string CHero::getSpecialtyNameTextID() const +{ + return TextIdentifier("hero", modScope, identifier, "specialty", "name").get(); +} + +std::string CHero::getSpecialtyDescriptionTextID() const +{ + return TextIdentifier("hero", modScope, identifier, "specialty", "description").get(); +} + +std::string CHero::getSpecialtyTooltipTextID() const +{ + return TextIdentifier("hero", modScope, identifier, "specialty", "tooltip").get(); +} + +void CHero::registerIcons(const IconRegistar & cb) const +{ + cb(getIconIndex(), 0, "UN32", iconSpecSmall); + cb(getIconIndex(), 0, "UN44", iconSpecLarge); + cb(getIconIndex(), 0, "PORTRAITSLARGE", portraitLarge); + cb(getIconIndex(), 0, "PORTRAITSSMALL", portraitSmall); +} + +void CHero::updateFrom(const JsonNode & data) +{ + //todo: CHero::updateFrom +} + +void CHero::serializeJson(JsonSerializeFormat & handler) +{ + +} + +VCMI_LIB_NAMESPACE_END diff --git a/lib/entities/hero/CHero.h b/lib/entities/hero/CHero.h new file mode 100644 index 000000000..1b96cc341 --- /dev/null +++ b/lib/entities/hero/CHero.h @@ -0,0 +1,87 @@ +/* + * CHero.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 + +#include "EHeroGender.h" + +#include "../../bonuses/BonusList.h" +#include "../../constants/EntityIdentifiers.h" +#include "../../filesystem/ResourcePath.h" + +VCMI_LIB_NAMESPACE_BEGIN + +class DLL_LINKAGE CHero : public HeroType +{ + friend class CHeroHandler; + + HeroTypeID ID; + std::string identifier; + std::string modScope; + +public: + struct InitialArmyStack + { + ui32 minAmount; + ui32 maxAmount; + CreatureID creature; + }; + si32 imageIndex = 0; + + std::vector initialArmy; + + const CHeroClass * heroClass = nullptr; + + //initial secondary skills; first - ID of skill, second - level of skill (1 - basic, 2 - adv., 3 - expert) + std::vector> secSkillsInit; + + BonusList specialty; + std::set spells; + bool haveSpellBook = false; + bool special = false; // hero is special and won't be placed in game (unless preset on map), e.g. campaign heroes + bool onlyOnWaterMap; // hero will be placed only if the map contains water + bool onlyOnMapWithoutWater; // hero will be placed only if the map does not contain water + EHeroGender gender = EHeroGender::MALE; // default sex: 0=male, 1=female + + /// Graphics + std::string iconSpecSmall; + std::string iconSpecLarge; + std::string portraitSmall; + std::string portraitLarge; + AnimationPath battleImage; + + CHero(); + virtual ~CHero(); + + int32_t getIndex() const override; + int32_t getIconIndex() const override; + std::string getJsonKey() const override; + std::string getModScope() const override; + HeroTypeID getId() const override; + void registerIcons(const IconRegistar & cb) const override; + + std::string getNameTranslated() const override; + std::string getBiographyTranslated() const override; + std::string getSpecialtyNameTranslated() const override; + std::string getSpecialtyDescriptionTranslated() const override; + std::string getSpecialtyTooltipTranslated() const override; + + std::string getNameTextID() const override; + std::string getBiographyTextID() const override; + std::string getSpecialtyNameTextID() const override; + std::string getSpecialtyDescriptionTextID() const override; + std::string getSpecialtyTooltipTextID() const override; + + void updateFrom(const JsonNode & data); + void serializeJson(JsonSerializeFormat & handler); +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/entities/hero/CHeroClass.cpp b/lib/entities/hero/CHeroClass.cpp new file mode 100644 index 000000000..80e06cabf --- /dev/null +++ b/lib/entities/hero/CHeroClass.cpp @@ -0,0 +1,120 @@ +/* + * CHeroClass.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 "CHeroClass.h" + +#include "../faction/CFaction.h" + +#include "../../VCMI_Lib.h" +#include "../../texts/CGeneralTextHandler.h" + +#include + +VCMI_LIB_NAMESPACE_BEGIN + +SecondarySkill CHeroClass::chooseSecSkill(const std::set & possibles, vstd::RNG & rand) const //picks secondary skill out from given possibilities +{ + assert(!possibles.empty()); + + std::vector weights; + std::vector skills; + + for(const auto & possible : possibles) + { + skills.push_back(possible); + if (secSkillProbability.count(possible) != 0) + { + int weight = secSkillProbability.at(possible); + weights.push_back(std::max(1, weight)); + } + else + weights.push_back(1); // H3 behavior - banned skills have minimal (1) chance to be picked + } + + int selectedIndex = RandomGeneratorUtil::nextItemWeighted(weights, rand); + return skills.at(selectedIndex); +} + +bool CHeroClass::isMagicHero() const +{ + return affinity == MAGIC; +} + +int CHeroClass::tavernProbability(FactionID targetFaction) const +{ + auto it = selectionProbability.find(targetFaction); + if (it != selectionProbability.end()) + return it->second; + return 0; +} + +EAlignment CHeroClass::getAlignment() const +{ + return faction.toEntity(VLC)->getAlignment(); +} + +int32_t CHeroClass::getIndex() const +{ + return id.getNum(); +} + +int32_t CHeroClass::getIconIndex() const +{ + return getIndex(); +} + +std::string CHeroClass::getJsonKey() const +{ + return modScope + ':' + identifier; +} + +std::string CHeroClass::getModScope() const +{ + return modScope; +} + +HeroClassID CHeroClass::getId() const +{ + return id; +} + +void CHeroClass::registerIcons(const IconRegistar & cb) const +{ + +} + +std::string CHeroClass::getNameTranslated() const +{ + return VLC->generaltexth->translate(getNameTextID()); +} + +std::string CHeroClass::getNameTextID() const +{ + return TextIdentifier("heroClass", modScope, identifier, "name").get(); +} + +void CHeroClass::updateFrom(const JsonNode & data) +{ + //TODO: CHeroClass::updateFrom +} + +void CHeroClass::serializeJson(JsonSerializeFormat & handler) +{ + +} + +CHeroClass::CHeroClass(): + faction(0), + affinity(0), + defaultTavernChance(0) +{ +} + +VCMI_LIB_NAMESPACE_END diff --git a/lib/entities/hero/CHeroClass.h b/lib/entities/hero/CHeroClass.h new file mode 100644 index 000000000..0d4edc5a6 --- /dev/null +++ b/lib/entities/hero/CHeroClass.h @@ -0,0 +1,85 @@ +/* + * CHeroClass.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 + +#include "../../constants/EntityIdentifiers.h" +#include "../../constants/Enumerations.h" +#include "../../filesystem/ResourcePath.h" + +VCMI_LIB_NAMESPACE_BEGIN + +namespace vstd +{ +class RNG; +} + +class DLL_LINKAGE CHeroClass : public HeroClass +{ + friend class CHeroClassHandler; + HeroClassID id; // use getId instead + std::string modScope; + std::string identifier; // use getJsonKey instead + +public: + enum EClassAffinity + { + MIGHT, + MAGIC + }; + + //double aggression; // not used in vcmi. + FactionID faction; + ui8 affinity; // affinity, using EClassAffinity enum + + // default chance for hero of specific class to appear in tavern, if field "tavern" was not set + // resulting chance = sqrt(town.chance * heroClass.chance) + ui32 defaultTavernChance; + + CreatureID commander; + + std::vector primarySkillInitial; // initial primary skills + std::vector primarySkillLowLevel; // probability (%) of getting point of primary skill when getting level + std::vector primarySkillHighLevel; // same for high levels (> 10) + + std::map secSkillProbability; //probabilities of gaining secondary skills (out of 112), in id order + + std::map selectionProbability; //probability of selection in towns + + AnimationPath imageBattleMale; + AnimationPath imageBattleFemale; + std::string imageMapMale; + std::string imageMapFemale; + + CHeroClass(); + + int32_t getIndex() const override; + int32_t getIconIndex() const override; + std::string getJsonKey() const override; + std::string getModScope() const override; + HeroClassID getId() const override; + void registerIcons(const IconRegistar & cb) const override; + + std::string getNameTranslated() const override; + std::string getNameTextID() const override; + + bool isMagicHero() const; + SecondarySkill chooseSecSkill(const std::set & possibles, vstd::RNG & rand) const; //picks secondary skill out from given possibilities + + void updateFrom(const JsonNode & data); + void serializeJson(JsonSerializeFormat & handler); + + EAlignment getAlignment() const; + + int tavernProbability(FactionID faction) const; +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/entities/hero/CHeroClassHandler.cpp b/lib/entities/hero/CHeroClassHandler.cpp new file mode 100644 index 000000000..e9207a229 --- /dev/null +++ b/lib/entities/hero/CHeroClassHandler.cpp @@ -0,0 +1,226 @@ +/* + * CHeroClassHandler.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 "CHeroClassHandler.h" + +#include "CHeroClass.h" + +#include "../faction/CTown.h" +#include "../faction/CTownHandler.h" + +#include "../../CSkillHandler.h" +#include "../../IGameSettings.h" +#include "../../VCMI_Lib.h" +#include "../../constants/StringConstants.h" +#include "../../json/JsonNode.h" +#include "../../mapObjectConstructors/AObjectTypeHandler.h" +#include "../../mapObjectConstructors/CObjectClassesHandler.h" +#include "../../modding/IdentifierStorage.h" +#include "../../texts/CGeneralTextHandler.h" +#include "../../texts/CLegacyConfigParser.h" + +VCMI_LIB_NAMESPACE_BEGIN + +void CHeroClassHandler::fillPrimarySkillData(const JsonNode & node, CHeroClass * heroClass, PrimarySkill pSkill) const +{ + const auto & skillName = NPrimarySkill::names[pSkill.getNum()]; + auto currentPrimarySkillValue = static_cast(node["primarySkills"][skillName].Integer()); + int primarySkillLegalMinimum = VLC->engineSettings()->getVector(EGameSettings::HEROES_MINIMAL_PRIMARY_SKILLS)[pSkill.getNum()]; + + if(currentPrimarySkillValue < primarySkillLegalMinimum) + { + logMod->error("Hero class '%s' has incorrect initial value '%d' for skill '%s'. Value '%d' will be used instead.", + heroClass->getNameTranslated(), currentPrimarySkillValue, skillName, primarySkillLegalMinimum); + currentPrimarySkillValue = primarySkillLegalMinimum; + } + heroClass->primarySkillInitial.push_back(currentPrimarySkillValue); + heroClass->primarySkillLowLevel.push_back(static_cast(node["lowLevelChance"][skillName].Float())); + heroClass->primarySkillHighLevel.push_back(static_cast(node["highLevelChance"][skillName].Float())); +} + +const std::vector & CHeroClassHandler::getTypeNames() const +{ + static const std::vector typeNames = { "heroClass" }; + return typeNames; +} + +std::shared_ptr CHeroClassHandler::loadFromJson(const std::string & scope, const JsonNode & node, const std::string & identifier, size_t index) +{ + assert(identifier.find(':') == std::string::npos); + assert(!scope.empty()); + + std::string affinityStr[2] = { "might", "magic" }; + + auto heroClass = std::make_shared(); + + heroClass->id = HeroClassID(index); + heroClass->identifier = identifier; + heroClass->modScope = scope; + heroClass->imageBattleFemale = AnimationPath::fromJson(node["animation"]["battle"]["female"]); + heroClass->imageBattleMale = AnimationPath::fromJson(node["animation"]["battle"]["male"]); + //MODS COMPATIBILITY FOR 0.96 + heroClass->imageMapFemale = node["animation"]["map"]["female"].String(); + heroClass->imageMapMale = node["animation"]["map"]["male"].String(); + + VLC->generaltexth->registerString(scope, heroClass->getNameTextID(), node["name"].String()); + + if (vstd::contains(affinityStr, node["affinity"].String())) + { + heroClass->affinity = vstd::find_pos(affinityStr, node["affinity"].String()); + } + else + { + logGlobal->error("Mod '%s', hero class '%s': invalid affinity '%s'! Expected 'might' or 'magic'!", scope, identifier, node["affinity"].String()); + heroClass->affinity = CHeroClass::MIGHT; + } + + fillPrimarySkillData(node, heroClass.get(), PrimarySkill::ATTACK); + fillPrimarySkillData(node, heroClass.get(), PrimarySkill::DEFENSE); + fillPrimarySkillData(node, heroClass.get(), PrimarySkill::SPELL_POWER); + fillPrimarySkillData(node, heroClass.get(), PrimarySkill::KNOWLEDGE); + + auto percentSumm = std::accumulate(heroClass->primarySkillLowLevel.begin(), heroClass->primarySkillLowLevel.end(), 0); + if(percentSumm <= 0) + logMod->error("Hero class %s has wrong lowLevelChance values: must be above zero!", heroClass->identifier, percentSumm); + + percentSumm = std::accumulate(heroClass->primarySkillHighLevel.begin(), heroClass->primarySkillHighLevel.end(), 0); + if(percentSumm <= 0) + logMod->error("Hero class %s has wrong highLevelChance values: must be above zero!", heroClass->identifier, percentSumm); + + for(auto skillPair : node["secondarySkills"].Struct()) + { + int probability = static_cast(skillPair.second.Integer()); + VLC->identifiers()->requestIdentifier(skillPair.second.getModScope(), "skill", skillPair.first, [heroClass, probability](si32 skillID) + { + heroClass->secSkillProbability[skillID] = probability; + }); + } + + VLC->identifiers()->requestIdentifier ("creature", node["commander"], + [=](si32 commanderID) + { + heroClass->commander = CreatureID(commanderID); + }); + + heroClass->defaultTavernChance = static_cast(node["defaultTavern"].Float()); + for(const auto & tavern : node["tavern"].Struct()) + { + int value = static_cast(tavern.second.Float()); + + VLC->identifiers()->requestIdentifier(tavern.second.getModScope(), "faction", tavern.first, + [=](si32 factionID) + { + heroClass->selectionProbability[FactionID(factionID)] = value; + }); + } + + VLC->identifiers()->requestIdentifier("faction", node["faction"], + [=](si32 factionID) + { + heroClass->faction.setNum(factionID); + }); + + VLC->identifiers()->requestIdentifier(scope, "object", "hero", [=](si32 index) + { + JsonNode classConf = node["mapObject"]; + classConf["heroClass"].String() = identifier; + if (!node["compatibilityIdentifiers"].isNull()) + classConf["compatibilityIdentifiers"] = node["compatibilityIdentifiers"]; + classConf.setModScope(scope); + VLC->objtypeh->loadSubObject(identifier, classConf, index, heroClass->getIndex()); + }); + + return heroClass; +} + +std::vector CHeroClassHandler::loadLegacyData() +{ + size_t dataSize = VLC->engineSettings()->getInteger(EGameSettings::TEXTS_HERO_CLASS); + + objects.resize(dataSize); + std::vector h3Data; + h3Data.reserve(dataSize); + + CLegacyConfigParser parser(TextPath::builtin("DATA/HCTRAITS.TXT")); + + parser.endLine(); // header + parser.endLine(); + + for (size_t i=0; i set selection probability if it was not set before in tavern entries + for(auto & heroClass : objects) + { + for(auto & faction : VLC->townh->objects) + { + if (!faction->town) + continue; + if (heroClass->selectionProbability.count(faction->getId())) + continue; + + auto chance = static_cast(heroClass->defaultTavernChance * faction->town->defaultTavernChance); + heroClass->selectionProbability[faction->getId()] = static_cast(sqrt(chance) + 0.5); //FIXME: replace with std::round once MVS supports it + } + + // set default probabilities for gaining secondary skills where not loaded previously + for(int skillID = 0; skillID < VLC->skillh->size(); skillID++) + { + if(heroClass->secSkillProbability.count(skillID) == 0) + { + const CSkill * skill = (*VLC->skillh)[SecondarySkill(skillID)]; + logMod->trace("%s: no probability for %s, using default", heroClass->identifier, skill->getJsonKey()); + heroClass->secSkillProbability[skillID] = skill->gainChance[heroClass->affinity]; + } + } + } + + for(const auto & hc : objects) + { + if(!hc->imageMapMale.empty()) + { + JsonNode templ; + templ["animation"].String() = hc->imageMapMale; + VLC->objtypeh->getHandlerFor(Obj::HERO, hc->getIndex())->addTemplate(templ); + } + } +} + +CHeroClassHandler::~CHeroClassHandler() = default; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/entities/hero/CHeroClassHandler.h b/lib/entities/hero/CHeroClassHandler.h new file mode 100644 index 000000000..e04a6dbf3 --- /dev/null +++ b/lib/entities/hero/CHeroClassHandler.h @@ -0,0 +1,37 @@ +/* + * CHeroClassHandler.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 + +#include "CHeroClass.h" // convenience include - users of handler generally also use its entity + +#include "../../IHandlerBase.h" +#include "../../constants/EntityIdentifiers.h" + +VCMI_LIB_NAMESPACE_BEGIN + +class DLL_LINKAGE CHeroClassHandler : public CHandlerBase +{ + void fillPrimarySkillData(const JsonNode & node, CHeroClass * heroClass, PrimarySkill pSkill) const; + +public: + std::vector loadLegacyData() override; + + void afterLoadFinalization() override; + + ~CHeroClassHandler(); + +protected: + const std::vector & getTypeNames() const override; + std::shared_ptr loadFromJson(const std::string & scope, const JsonNode & node, const std::string & identifier, size_t index) override; +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/entities/hero/CHeroHandler.cpp b/lib/entities/hero/CHeroHandler.cpp new file mode 100644 index 000000000..8b05df0eb --- /dev/null +++ b/lib/entities/hero/CHeroHandler.cpp @@ -0,0 +1,424 @@ +/* + * CHeroHandler.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 "CHeroHandler.h" + +#include "CHero.h" + +#include "../../VCMI_Lib.h" +#include "../../constants/StringConstants.h" +#include "../../CCreatureHandler.h" +#include "../../IGameSettings.h" +#include "../../bonuses/Limiters.h" +#include "../../bonuses/Updaters.h" +#include "../../json/JsonBonus.h" +#include "../../json/JsonUtils.h" +#include "../../modding/IdentifierStorage.h" +#include "../../texts/CGeneralTextHandler.h" +#include "../../texts/CLegacyConfigParser.h" + +VCMI_LIB_NAMESPACE_BEGIN + +CHeroHandler::~CHeroHandler() = default; + +CHeroHandler::CHeroHandler() +{ + loadExperience(); +} + +const std::vector & CHeroHandler::getTypeNames() const +{ + static const std::vector typeNames = { "hero" }; + return typeNames; +} + +std::shared_ptr CHeroHandler::loadFromJson(const std::string & scope, const JsonNode & node, const std::string & identifier, size_t index) +{ + assert(identifier.find(':') == std::string::npos); + assert(!scope.empty()); + + auto hero = std::make_shared(); + hero->ID = HeroTypeID(index); + hero->identifier = identifier; + hero->modScope = scope; + hero->gender = node["female"].Bool() ? EHeroGender::FEMALE : EHeroGender::MALE; + hero->special = node["special"].Bool(); + //Default - both false + hero->onlyOnWaterMap = node["onlyOnWaterMap"].Bool(); + hero->onlyOnMapWithoutWater = node["onlyOnMapWithoutWater"].Bool(); + + VLC->generaltexth->registerString(scope, hero->getNameTextID(), node["texts"]["name"]); + VLC->generaltexth->registerString(scope, hero->getBiographyTextID(), node["texts"]["biography"]); + VLC->generaltexth->registerString(scope, hero->getSpecialtyNameTextID(), node["texts"]["specialty"]["name"]); + VLC->generaltexth->registerString(scope, hero->getSpecialtyTooltipTextID(), node["texts"]["specialty"]["tooltip"]); + VLC->generaltexth->registerString(scope, hero->getSpecialtyDescriptionTextID(), node["texts"]["specialty"]["description"]); + + hero->iconSpecSmall = node["images"]["specialtySmall"].String(); + hero->iconSpecLarge = node["images"]["specialtyLarge"].String(); + hero->portraitSmall = node["images"]["small"].String(); + hero->portraitLarge = node["images"]["large"].String(); + hero->battleImage = AnimationPath::fromJson(node["battleImage"]); + + loadHeroArmy(hero.get(), node); + loadHeroSkills(hero.get(), node); + loadHeroSpecialty(hero.get(), node); + + VLC->identifiers()->requestIdentifier("heroClass", node["class"], + [=](si32 classID) + { + hero->heroClass = HeroClassID(classID).toHeroClass(); + }); + + return hero; +} + +void CHeroHandler::loadHeroArmy(CHero * hero, const JsonNode & node) const +{ + assert(node["army"].Vector().size() <= 3); // anything bigger is useless - army initialization uses up to 3 slots + + hero->initialArmy.resize(node["army"].Vector().size()); + + for (size_t i=0; i< hero->initialArmy.size(); i++) + { + const JsonNode & source = node["army"].Vector()[i]; + + hero->initialArmy[i].minAmount = static_cast(source["min"].Float()); + hero->initialArmy[i].maxAmount = static_cast(source["max"].Float()); + + if (hero->initialArmy[i].minAmount > hero->initialArmy[i].maxAmount) + { + logMod->error("Hero %s has minimal army size (%d) greater than maximal size (%d)!", hero->getJsonKey(), hero->initialArmy[i].minAmount, hero->initialArmy[i].maxAmount); + std::swap(hero->initialArmy[i].minAmount, hero->initialArmy[i].maxAmount); + } + + VLC->identifiers()->requestIdentifier("creature", source["creature"], [=](si32 creature) + { + hero->initialArmy[i].creature = CreatureID(creature); + }); + } +} + +void CHeroHandler::loadHeroSkills(CHero * hero, const JsonNode & node) const +{ + for(const JsonNode &set : node["skills"].Vector()) + { + int skillLevel = static_cast(boost::range::find(NSecondarySkill::levels, set["level"].String()) - std::begin(NSecondarySkill::levels)); + if (skillLevel < MasteryLevel::LEVELS_SIZE) + { + size_t currentIndex = hero->secSkillsInit.size(); + hero->secSkillsInit.emplace_back(SecondarySkill(-1), skillLevel); + + VLC->identifiers()->requestIdentifier("skill", set["skill"], [=](si32 id) + { + hero->secSkillsInit[currentIndex].first = SecondarySkill(id); + }); + } + else + { + logMod->error("Unknown skill level: %s", set["level"].String()); + } + } + + // spellbook is considered present if hero have "spellbook" entry even when this is an empty set (0 spells) + hero->haveSpellBook = !node["spellbook"].isNull(); + + for(const JsonNode & spell : node["spellbook"].Vector()) + { + VLC->identifiers()->requestIdentifier("spell", spell, + [=](si32 spellID) + { + hero->spells.insert(SpellID(spellID)); + }); + } +} + +/// creates standard H3 hero specialty for creatures +static std::vector> createCreatureSpecialty(CreatureID baseCreatureID) +{ + std::vector> result; + std::set targets; + targets.insert(baseCreatureID); + + // go through entire upgrade chain and collect all creatures to which baseCreatureID can be upgraded + for (;;) + { + std::set oldTargets = targets; + + for(const auto & upgradeSourceID : oldTargets) + { + const CCreature * upgradeSource = upgradeSourceID.toCreature(); + targets.insert(upgradeSource->upgrades.begin(), upgradeSource->upgrades.end()); + } + + if (oldTargets.size() == targets.size()) + break; + } + + for(CreatureID cid : targets) + { + const auto & specCreature = *cid.toCreature(); + int stepSize = specCreature.getLevel() ? specCreature.getLevel() : 5; + + { + auto bonus = std::make_shared(); + bonus->limiter.reset(new CCreatureTypeLimiter(specCreature, false)); + bonus->type = BonusType::STACKS_SPEED; + bonus->val = 1; + result.push_back(bonus); + } + + { + auto bonus = std::make_shared(); + bonus->type = BonusType::PRIMARY_SKILL; + bonus->subtype = BonusSubtypeID(PrimarySkill::ATTACK); + bonus->val = 0; + bonus->limiter.reset(new CCreatureTypeLimiter(specCreature, false)); + bonus->updater.reset(new GrowsWithLevelUpdater(specCreature.getAttack(false), stepSize)); + result.push_back(bonus); + } + + { + auto bonus = std::make_shared(); + bonus->type = BonusType::PRIMARY_SKILL; + bonus->subtype = BonusSubtypeID(PrimarySkill::DEFENSE); + bonus->val = 0; + bonus->limiter.reset(new CCreatureTypeLimiter(specCreature, false)); + bonus->updater.reset(new GrowsWithLevelUpdater(specCreature.getDefense(false), stepSize)); + result.push_back(bonus); + } + } + + return result; +} + +void CHeroHandler::beforeValidate(JsonNode & object) +{ + //handle "base" specialty info + JsonNode & specialtyNode = object["specialty"]; + if(specialtyNode.getType() == JsonNode::JsonType::DATA_STRUCT) + { + const JsonNode & base = specialtyNode["base"]; + if(!base.isNull()) + { + if(specialtyNode["bonuses"].isNull()) + { + logMod->warn("specialty has base without bonuses"); + } + else + { + JsonMap & bonuses = specialtyNode["bonuses"].Struct(); + for(std::pair keyValue : bonuses) + JsonUtils::inherit(bonuses[keyValue.first], base); + } + } + } +} + +void CHeroHandler::afterLoadFinalization() +{ + for(const auto & functor : callAfterLoadFinalization) + functor(); + + callAfterLoadFinalization.clear(); +} + +void CHeroHandler::loadHeroSpecialty(CHero * hero, const JsonNode & node) +{ + auto prepSpec = [=](std::shared_ptr bonus) + { + bonus->duration = BonusDuration::PERMANENT; + bonus->source = BonusSource::HERO_SPECIAL; + bonus->sid = BonusSourceID(hero->getId()); + return bonus; + }; + + //new format, using bonus system + const JsonNode & specialtyNode = node["specialty"]; + if(specialtyNode.getType() != JsonNode::JsonType::DATA_STRUCT) + { + logMod->error("Unsupported speciality format for hero %s!", hero->getNameTranslated()); + return; + } + + //creature specialty - alias for simplicity + if(!specialtyNode["creature"].isNull()) + { + JsonNode creatureNode = specialtyNode["creature"]; + + std::function specialtyLoader = [creatureNode, hero, prepSpec] + { + VLC->identifiers()->requestIdentifier("creature", creatureNode, [hero, prepSpec](si32 creature) + { + for (const auto & bonus : createCreatureSpecialty(CreatureID(creature))) + hero->specialty.push_back(prepSpec(bonus)); + }); + }; + + callAfterLoadFinalization.push_back(specialtyLoader); + } + + for(const auto & keyValue : specialtyNode["bonuses"].Struct()) + hero->specialty.push_back(prepSpec(JsonUtils::parseBonus(keyValue.second))); +} + +void CHeroHandler::loadExperience() +{ + expPerLevel.push_back(0); + expPerLevel.push_back(1000); + expPerLevel.push_back(2000); + expPerLevel.push_back(3200); + expPerLevel.push_back(4600); + expPerLevel.push_back(6200); + expPerLevel.push_back(8000); + expPerLevel.push_back(10000); + expPerLevel.push_back(12200); + expPerLevel.push_back(14700); + expPerLevel.push_back(17500); + expPerLevel.push_back(20600); + expPerLevel.push_back(24320); + expPerLevel.push_back(28784); + expPerLevel.push_back(34140); + + for (;;) + { + auto i = expPerLevel.size() - 1; + auto currExp = expPerLevel[i]; + auto prevExp = expPerLevel[i-1]; + auto prevDiff = currExp - prevExp; + auto nextDiff = prevDiff + prevDiff / 5; + auto maxExp = std::numeric_limits::max(); + + if (currExp > maxExp - nextDiff) + break; // overflow point reached + + expPerLevel.push_back (currExp + nextDiff); + } +} + +/// convert h3-style ID (e.g. Gobin Wolf Rider) to vcmi (e.g. goblinWolfRider) +static std::string genRefName(std::string input) +{ + boost::algorithm::replace_all(input, " ", ""); //remove spaces + input[0] = std::tolower(input[0]); // to camelCase + return input; +} + +std::vector CHeroHandler::loadLegacyData() +{ + size_t dataSize = VLC->engineSettings()->getInteger(EGameSettings::TEXTS_HERO); + + objects.resize(dataSize); + std::vector h3Data; + h3Data.reserve(dataSize); + + CLegacyConfigParser specParser(TextPath::builtin("DATA/HEROSPEC.TXT")); + CLegacyConfigParser bioParser(TextPath::builtin("DATA/HEROBIOS.TXT")); + CLegacyConfigParser parser(TextPath::builtin("DATA/HOTRAITS.TXT")); + + parser.endLine(); //ignore header + parser.endLine(); + + specParser.endLine(); //ignore header + specParser.endLine(); + + for (int i=0; iimageIndex = static_cast(index) + specialFramesCount; + + objects.emplace_back(object); + + registerObject(scope, "hero", name, object->getIndex()); + + for(const auto & compatID : data["compatibilityIdentifiers"].Vector()) + registerObject(scope, "hero", compatID.String(), object->getIndex()); +} + +void CHeroHandler::loadObject(std::string scope, std::string name, const JsonNode & data, size_t index) +{ + auto object = loadFromJson(scope, data, name, index); + object->imageIndex = static_cast(index); + + assert(objects[index] == nullptr); // ensure that this id was not loaded before + objects[index] = object; + + registerObject(scope, "hero", name, object->getIndex()); + for(const auto & compatID : data["compatibilityIdentifiers"].Vector()) + registerObject(scope, "hero", compatID.String(), object->getIndex()); +} + +ui32 CHeroHandler::level (TExpType experience) const +{ + return static_cast(boost::range::upper_bound(expPerLevel, experience) - std::begin(expPerLevel)); +} + +TExpType CHeroHandler::reqExp (ui32 level) const +{ + if(!level) + return 0; + + if (level <= expPerLevel.size()) + { + return expPerLevel[level-1]; + } + else + { + logGlobal->warn("A hero has reached unsupported amount of experience"); + return expPerLevel[expPerLevel.size()-1]; + } +} + +ui32 CHeroHandler::maxSupportedLevel() const +{ + return expPerLevel.size(); +} + +std::set CHeroHandler::getDefaultAllowed() const +{ + std::set result; + + for(auto & hero : objects) + if (hero && !hero->special) + result.insert(hero->getId()); + + return result; +} + +VCMI_LIB_NAMESPACE_END diff --git a/lib/entities/hero/CHeroHandler.h b/lib/entities/hero/CHeroHandler.h new file mode 100644 index 000000000..d62911599 --- /dev/null +++ b/lib/entities/hero/CHeroHandler.h @@ -0,0 +1,59 @@ +/* + * CHeroHandler.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 + +#include "CHero.h" // convenience include - users of handler generally also use its entity + + +#include "../../GameConstants.h" +#include "../../IHandlerBase.h" + +VCMI_LIB_NAMESPACE_BEGIN + +class DLL_LINKAGE CHeroHandler : public CHandlerBase +{ + /// expPerLEvel[i] is amount of exp needed to reach level i; + /// consists of 196 values. Any higher levels require experience larger that TExpType can hold + std::vector expPerLevel; + + /// helpers for loading to avoid huge load functions + void loadHeroArmy(CHero * hero, const JsonNode & node) const; + void loadHeroSkills(CHero * hero, const JsonNode & node) const; + void loadHeroSpecialty(CHero * hero, const JsonNode & node); + + void loadExperience(); + + std::vector> callAfterLoadFinalization; + +public: + ui32 level(TExpType experience) const; //calculates level corresponding to given experience amount + TExpType reqExp(ui32 level) const; //calculates experience required for given level + ui32 maxSupportedLevel() const; + + std::vector loadLegacyData() override; + + void beforeValidate(JsonNode & object) override; + void loadObject(std::string scope, std::string name, const JsonNode & data) override; + void loadObject(std::string scope, std::string name, const JsonNode & data, size_t index) override; + void afterLoadFinalization() override; + + CHeroHandler(); + ~CHeroHandler(); + + std::set getDefaultAllowed() const; + +protected: + const std::vector & getTypeNames() const override; + std::shared_ptr loadFromJson(const std::string & scope, const JsonNode & node, const std::string & identifier, size_t index) override; +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/entities/hero/EHeroGender.h b/lib/entities/hero/EHeroGender.h new file mode 100644 index 000000000..2b9dfb61f --- /dev/null +++ b/lib/entities/hero/EHeroGender.h @@ -0,0 +1,21 @@ +/* + * EHeroGender.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 + +VCMI_LIB_NAMESPACE_BEGIN + + enum class EHeroGender : int8_t +{ + DEFAULT = -1, // from h3m, instance has same gender as hero type + MALE = 0, + FEMALE = 1, +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/filesystem/CCompressedStream.cpp b/lib/filesystem/CCompressedStream.cpp index 9837c78bc..8c3970e1f 100644 --- a/lib/filesystem/CCompressedStream.cpp +++ b/lib/filesystem/CCompressedStream.cpp @@ -136,6 +136,9 @@ si64 CCompressedStream::readMore(ui8 *data, si64 size) { if (inflateState->avail_in == 0) { + if (gzipStream == nullptr) + throw std::runtime_error("Potentially truncated gzip file"); + //inflate ran out of available data or was not initialized yet // get new input data and update state accordingly si64 availSize = gzipStream->read(compressedBuffer.data(), compressedBuffer.size()); diff --git a/lib/filesystem/Filesystem.cpp b/lib/filesystem/Filesystem.cpp index e3df56e34..4840b3f87 100644 --- a/lib/filesystem/Filesystem.cpp +++ b/lib/filesystem/Filesystem.cpp @@ -212,6 +212,7 @@ ISimpleResourceLoader * CResourceHandler::get() ISimpleResourceLoader * CResourceHandler::get(const std::string & identifier) { + assert(knownLoaders.count(identifier)); return knownLoaders.at(identifier); } diff --git a/lib/gameState/CGameState.cpp b/lib/gameState/CGameState.cpp index 31bd4e2de..7df3c63e9 100644 --- a/lib/gameState/CGameState.cpp +++ b/lib/gameState/CGameState.cpp @@ -18,7 +18,6 @@ #include "../ArtifactUtils.h" #include "../texts/CGeneralTextHandler.h" -#include "../CHeroHandler.h" #include "../CPlayerState.h" #include "../CStopWatch.h" #include "../IGameSettings.h" @@ -30,6 +29,8 @@ #include "../campaign/CampaignState.h" #include "../constants/StringConstants.h" #include "../entities/faction/CTownHandler.h" +#include "../entities/hero/CHero.h" +#include "../entities/hero/CHeroClass.h" #include "../filesystem/ResourcePath.h" #include "../json/JsonBonus.h" #include "../json/JsonUtils.h" @@ -61,7 +62,7 @@ boost::shared_mutex CGameState::mutex; HeroTypeID CGameState::pickNextHeroType(const PlayerColor & owner) { const PlayerSettings &ps = scenarioOps->getIthPlayersSettings(owner); - if(ps.hero >= HeroTypeID(0) && !isUsedHero(HeroTypeID(ps.hero))) //we haven't used selected hero + if(ps.hero.isValid() && !isUsedHero(HeroTypeID(ps.hero))) //we haven't used selected hero { return HeroTypeID(ps.hero); } @@ -267,6 +268,8 @@ void CGameState::updateOnLoad(StartInfo * si) for(auto & i : si->playerInfos) gs->players[i.first].human = i.second.isControlledByHuman(); scenarioOps->extraOptionsInfo = si->extraOptionsInfo; + scenarioOps->turnTimerInfo = si->turnTimerInfo; + scenarioOps->simturnsInfo = si->simturnsInfo; } void CGameState::initNewGame(const IMapService * mapService, bool allowSavingRandomMap, Load::ProgressAccumulator & progressTracking) @@ -434,10 +437,10 @@ void CGameState::initGrailPosition() for(int y = BORDER_WIDTH; y < map->height - BORDER_WIDTH; y++) { const TerrainTile &t = map->getTile(int3(x, y, z)); - if(!t.blocked - && !t.visitable - && t.terType->isLand() - && t.terType->isPassable() + if(!t.blocked() + && !t.visitable() + && t.isLand() + && t.getTerrain()->isPassable() && (int)map->grailPos.dist2dSQ(int3(x, y, z)) <= (map->grailRadius * map->grailRadius)) allowedPos.emplace_back(x, y, z); } @@ -447,7 +450,7 @@ void CGameState::initGrailPosition() //remove tiles with holes for(auto & elem : map->objects) if(elem && elem->ID == Obj::HOLE) - allowedPos -= elem->pos; + allowedPos -= elem->anchorPos(); if(!allowedPos.empty()) { @@ -493,7 +496,7 @@ void CGameState::randomizeMapObjects() { for (int j = 0; j < object->getHeight() ; j++) { - int3 pos = object->pos - int3(i,j,0); + int3 pos = object->anchorPos() - int3(i,j,0); if(map->isInTheMap(pos)) map->getTile(pos).extTileFlags |= 128; } } @@ -528,7 +531,7 @@ void CGameState::placeStartingHero(const PlayerColor & playerColor, const HeroTy { for(auto town : map->towns) { - if(town->getPosition() == townPos) + if(town->anchorPos() == townPos) { townPos = town->visitablePos(); break; @@ -543,8 +546,7 @@ void CGameState::placeStartingHero(const PlayerColor & playerColor, const HeroTy hero->setHeroType(heroTypeId); hero->tempOwner = playerColor; - hero->pos = townPos; - hero->pos += hero->getVisitableOffset(); + hero->setAnchorPos(townPos + hero->getVisitableOffset()); map->getEditManager()->insertObject(hero); } @@ -598,7 +600,7 @@ void CGameState::initHeroes() } hero->initHero(getRandomGenerator()); - map->allHeroes[hero->getHeroType().getNum()] = hero; + map->allHeroes[hero->getHeroTypeID().getNum()] = hero; } // generate boats for all heroes on water @@ -606,13 +608,13 @@ void CGameState::initHeroes() { assert(map->isInTheMap(hero->visitablePos())); const auto & tile = map->getTile(hero->visitablePos()); - if (tile.terType->isWater()) + if (tile.isWater()) { auto handler = VLC->objtypeh->getHandlerFor(Obj::BOAT, hero->getBoatType().getNum()); auto boat = dynamic_cast(handler->create(callback, nullptr)); handler->configureObject(boat, gs->getRandomGenerator()); - boat->pos = hero->pos; + boat->setAnchorPos(hero->anchorPos()); boat->appearance = handler->getTemplates().front(); boat->id = ObjectInstanceID(static_cast(gs->map->objects.size())); @@ -628,20 +630,20 @@ void CGameState::initHeroes() { auto * hero = dynamic_cast(obj.get()); hero->initHero(getRandomGenerator()); - map->allHeroes[hero->getHeroType().getNum()] = hero; + map->allHeroes[hero->getHeroTypeID().getNum()] = hero; } } std::set heroesToCreate = getUnusedAllowedHeroes(); //ids of heroes to be created and put into the pool for(auto ph : map->predefinedHeroes) { - if(!vstd::contains(heroesToCreate, ph->getHeroType())) + if(!vstd::contains(heroesToCreate, ph->getHeroTypeID())) continue; ph->initHero(getRandomGenerator()); heroesPool->addHeroToPool(ph); - heroesToCreate.erase(ph->type->getId()); + heroesToCreate.erase(ph->getHeroTypeID()); - map->allHeroes[ph->getHeroType().getNum()] = ph; + map->allHeroes[ph->getHeroTypeID().getNum()] = ph; } for(const HeroTypeID & htype : heroesToCreate) //all not used allowed heroes go with default state into the pool @@ -755,12 +757,12 @@ void CGameState::initTownNames() for(auto & vti : map->towns) { - assert(vti->town); + assert(vti->getTown()); if(!vti->getNameTextID().empty()) continue; - FactionID faction = vti->getFaction(); + FactionID faction = vti->getFactionID(); if(availableNames.empty()) { @@ -797,8 +799,8 @@ void CGameState::initTowns() for (auto & vti : map->towns) { - assert(vti->town); - assert(vti->town->creatures.size() <= GameConstants::CREATURES_PER_TOWN); + assert(vti->getTown()); + assert(vti->getTown()->creatures.size() <= GameConstants::CREATURES_PER_TOWN); constexpr std::array basicDwellings = { BuildingID::DWELL_FIRST, BuildingID::DWELL_LVL_2, BuildingID::DWELL_LVL_3, BuildingID::DWELL_LVL_4, BuildingID::DWELL_LVL_5, BuildingID::DWELL_LVL_6, BuildingID::DWELL_LVL_7, BuildingID::DWELL_LVL_8 }; constexpr std::array upgradedDwellings = { BuildingID::DWELL_UP_FIRST, BuildingID::DWELL_LVL_2_UP, BuildingID::DWELL_LVL_3_UP, BuildingID::DWELL_LVL_4_UP, BuildingID::DWELL_LVL_5_UP, BuildingID::DWELL_LVL_6_UP, BuildingID::DWELL_LVL_7_UP, BuildingID::DWELL_LVL_8_UP }; @@ -827,7 +829,7 @@ void CGameState::initTowns() vti->addBuilding(BuildingID::VILLAGE_HALL); //init hordes - for (int i = 0; i < vti->town->creatures.size(); i++) + for (int i = 0; i < vti->getTown()->creatures.size(); i++) { if(vti->hasBuilt(hordes[i])) //if we have horde for this level { @@ -893,7 +895,7 @@ void CGameState::initTowns() int sel = -1; for(ui32 ps=0;pspossibleSpells.size();ps++) - total += vti->possibleSpells[ps].toSpell()->getProbability(vti->getFaction()); + total += vti->possibleSpells[ps].toSpell()->getProbability(vti->getFactionID()); if (total == 0) // remaining spells have 0 probability break; @@ -901,7 +903,7 @@ void CGameState::initTowns() auto r = getRandomGenerator().nextInt(total - 1); for(ui32 ps=0; pspossibleSpells.size();ps++) { - r -= vti->possibleSpells[ps].toSpell()->getProbability(vti->getFaction()); + r -= vti->possibleSpells[ps].toSpell()->getProbability(vti->getFactionID()); if(r<0) { sel = ps; @@ -962,22 +964,18 @@ void CGameState::placeHeroesInTowns() { for(CGTownInstance * t : player.second.getTowns()) { - if(h->visitablePos().z != t->visitablePos().z) - continue; - - bool heroOnTownBlockableTile = t->blockingAt(h->visitablePos().x, h->visitablePos().y); + bool heroOnTownBlockableTile = t->blockingAt(h->visitablePos()); // current hero position is at one of blocking tiles of current town // assume that this hero should be visiting the town (H3M format quirk) and move hero to correct position if (heroOnTownBlockableTile) { - int3 correctedPos = h->convertFromVisitablePos(t->visitablePos()); - map->removeBlockVisTiles(h); - h->pos = correctedPos; + int3 correctedPos = h->convertFromVisitablePos(t->visitablePos()); + h->setAnchorPos(correctedPos); map->addBlockVisTiles(h); - assert(t->visitableAt(h->visitablePos().x, h->visitablePos().y)); + assert(t->visitableAt(h->visitablePos())); } } } @@ -999,7 +997,7 @@ void CGameState::initVisitingAndGarrisonedHeroes() if(h->visitablePos().z != t->visitablePos().z) continue; - if (t->visitableAt(h->visitablePos().x, h->visitablePos().y)) + if (t->visitableAt(h->visitablePos())) { assert(t->visitingHero == nullptr); t->setVisitingHero(h); @@ -1064,7 +1062,7 @@ BattleField CGameState::battleGetBattlefieldType(int3 tile, vstd::RNG & rand) for(auto &obj : map->objects) { //look only for objects covering given tile - if( !obj || obj->pos.z != tile.z || !obj->coveringAt(tile.x, tile.y)) + if( !obj || !obj->coveringAt(tile)) continue; auto customBattlefield = obj->getBattlefield(); @@ -1076,10 +1074,10 @@ BattleField CGameState::battleGetBattlefieldType(int3 tile, vstd::RNG & rand) if(map->isCoastalTile(tile)) //coastal tile is always ground return BattleField(*VLC->identifiers()->getIdentifier("core", "battlefield.sand_shore")); - if (t.terType->battleFields.empty()) - throw std::runtime_error("Failed to find battlefield for terrain " + t.terType->getJsonKey()); + if (t.getTerrain()->battleFields.empty()) + throw std::runtime_error("Failed to find battlefield for terrain " + t.getTerrain()->getJsonKey()); - return BattleField(*RandomGeneratorUtil::nextItem(t.terType->battleFields, rand)); + return BattleField(*RandomGeneratorUtil::nextItem(t.getTerrain()->battleFields, rand)); } void CGameState::fillUpgradeInfo(const CArmedInstance *obj, SlotID stackPos, UpgradeInfo &out) const @@ -1093,7 +1091,7 @@ void CGameState::fillUpgradeInfo(const CArmedInstance *obj, SlotID stackPos, Upg UpgradeInfo CGameState::fillUpgradeInfo(const CStackInstance &stack) const { UpgradeInfo ret; - const CCreature *base = stack.type; + const CCreature *base = stack.getCreature(); if (stack.armyObj->ID == Obj::HERO) { @@ -1141,9 +1139,9 @@ PlayerRelations CGameState::getPlayerRelations( PlayerColor color1, PlayerColor return PlayerRelations::ENEMIES; } -void CGameState::apply(CPackForClient *pack) +void CGameState::apply(CPackForClient & pack) { - pack->applyGs(this); + pack.applyGs(this); } void CGameState::calculatePaths(const CGHeroInstance *hero, CPathsInfo &out) @@ -1173,7 +1171,7 @@ std::vector CGameState::guardingCreatures (int3 pos) const return guards; const TerrainTile &posTile = map->getTile(pos); - if (posTile.visitable) + if (posTile.visitable()) { for (CGObjectInstance* obj : posTile.visitableObjects) { @@ -1192,7 +1190,7 @@ std::vector CGameState::guardingCreatures (int3 pos) const if (map->isInTheMap(pos)) { const auto & tile = map->getTile(pos); - if (tile.visitable && (tile.isWater() == posTile.isWater())) + if (tile.visitable() && (tile.isWater() == posTile.isWater())) { for (CGObjectInstance* obj : tile.visitableObjects) { @@ -1248,10 +1246,10 @@ bool CGameState::isVisible(const CGObjectInstance * obj, const std::optionalgetWidth(); ++fx) { - int3 pos = obj->pos + int3(-fx, -fy, 0); + int3 pos = obj->anchorPos() + int3(-fx, -fy, 0); if ( map->isInTheMap(pos) && - obj->coveringAt(pos.x, pos.y) && + obj->coveringAt(pos) && isVisible(pos, *player)) return true; } @@ -1573,7 +1571,7 @@ void CGameState::obtainPlayersStats(SThievesGuildInfo & tgi, int level) { for(const auto & it : elem->Slots()) { - CreatureID toCmp = it.second->type->getId(); //ID of creature we should compare with the best one + CreatureID toCmp = it.second->getId(); //ID of creature we should compare with the best one if(bestCre == CreatureID::NONE || bestCre.toEntity(VLC)->getAIValue() < toCmp.toEntity(VLC)->getAIValue()) { bestCre = toCmp; @@ -1638,7 +1636,7 @@ bool CGameState::giveHeroArtifact(CGHeroInstance * h, const ArtifactID & aid) auto slot = ArtifactUtils::getArtAnyPosition(h, aid); if(ArtifactUtils::isSlotEquipment(slot) || ArtifactUtils::isSlotBackpack(slot)) { - ai->putAt(*h, slot); + map->putArtifactInstance(*h, ai, slot); return true; } else @@ -1658,18 +1656,13 @@ std::set CGameState::getUnusedAllowedHeroes(bool alsoIncludeNotAllow } for(auto hero : map->heroesOnMap) //heroes instances initialization - { - if(hero->type) - ret -= hero->type->getId(); - else - ret -= hero->getHeroType(); - } + ret -= hero->getHeroTypeID(); for(auto obj : map->objects) //prisons { auto * hero = dynamic_cast(obj.get()); if(hero && hero->ID == Obj::PRISON) - ret -= hero->getHeroType(); + ret -= hero->getHeroTypeID(); } return ret; @@ -1693,7 +1686,7 @@ CGHeroInstance * CGameState::getUsedHero(const HeroTypeID & hid) const auto * hero = dynamic_cast(obj.get()); assert(hero); - if (hero->getHeroType() == hid) + if (hero->getHeroTypeID() == hid) return hero; } diff --git a/lib/gameState/CGameState.h b/lib/gameState/CGameState.h index d247eb47b..a149575b4 100644 --- a/lib/gameState/CGameState.h +++ b/lib/gameState/CGameState.h @@ -98,7 +98,7 @@ public: /// picks next free hero type of the H3 hero init sequence -> chosen starting hero, then unused hero type randomly HeroTypeID pickNextHeroType(const PlayerColor & owner); - void apply(CPackForClient *pack); + void apply(CPackForClient & pack); BattleField battleGetBattlefieldType(int3 tile, vstd::RNG & rand); void fillUpgradeInfo(const CArmedInstance *obj, SlotID stackPos, UpgradeInfo &out) const override; diff --git a/lib/gameState/CGameStateCampaign.cpp b/lib/gameState/CGameStateCampaign.cpp index 8705d2d6f..892435cc6 100644 --- a/lib/gameState/CGameStateCampaign.cpp +++ b/lib/gameState/CGameStateCampaign.cpp @@ -16,6 +16,8 @@ #include "../campaign/CampaignState.h" #include "../entities/building/CBuilding.h" #include "../entities/building/CBuildingHandler.h" +#include "../entities/hero/CHeroClass.h" +#include "../entities/hero/CHero.h" #include "../mapping/CMapEditManager.h" #include "../mapObjects/CGHeroInstance.h" #include "../mapObjects/CGTownInstance.h" @@ -23,13 +25,13 @@ #include "../mapObjectConstructors/AObjectTypeHandler.h" #include "../mapObjectConstructors/CObjectClassesHandler.h" #include "../StartInfo.h" -#include "../CHeroHandler.h" #include "../mapping/CMap.h" #include "../ArtifactUtils.h" #include "../CPlayerState.h" #include "../serializer/CMemorySerializer.h" #include +#include VCMI_LIB_NAMESPACE_BEGIN @@ -86,7 +88,7 @@ void CGameStateCampaign::trimCrossoverHeroesParameters(const CampaignTravel & tr .And(Selector::subtype()(BonusSubtypeID(g))) .And(Selector::sourceType()(BonusSource::HERO_BASE_SKILL)); - hero.hero->getLocalBonus(sel)->val = hero.hero->type->heroClass->primarySkillInitial[g.getNum()]; + hero.hero->getLocalBonus(sel)->val = hero.hero->getHeroClass()->primarySkillInitial[g.getNum()]; } } } @@ -96,7 +98,7 @@ void CGameStateCampaign::trimCrossoverHeroesParameters(const CampaignTravel & tr //trimming sec skills for(auto & hero : campaignHeroReplacements) { - hero.hero->secSkills = hero.hero->type->secSkillsInit; + hero.hero->secSkills = hero.hero->getHeroType()->secSkillsInit; hero.hero->recreateSecondarySkillsBonuses(); } } @@ -135,19 +137,19 @@ void CGameStateCampaign::trimCrossoverHeroesParameters(const CampaignTravel & tr ArtifactLocation al(hero.hero->id, artifactPosition); - bool takeable = travelOptions.artifactsKeptByHero.count(art->artType->getId()); + bool takeable = travelOptions.artifactsKeptByHero.count(art->getTypeId()); bool locked = hero.hero->getSlot(al.slot)->locked; if (!locked && takeable) { - logGlobal->debug("Artifact %s from slot %d of hero %s will be transferred to next scenario", art->artType->getJsonKey(), al.slot.getNum(), hero.hero->getHeroTypeName()); + logGlobal->debug("Artifact %s from slot %d of hero %s will be transferred to next scenario", art->getType()->getJsonKey(), al.slot.getNum(), hero.hero->getHeroTypeName()); hero.transferrableArtifacts.push_back(artifactPosition); } if (!locked && !takeable) { - logGlobal->debug("Removing artifact %s from slot %d of hero %s", art->artType->getJsonKey(), al.slot.getNum(), hero.hero->getHeroTypeName()); - hero.hero->getArt(al.slot)->removeFrom(*hero.hero, al.slot); + logGlobal->debug("Removing artifact %s from slot %d of hero %s", art->getType()->getJsonKey(), al.slot.getNum(), hero.hero->getHeroTypeName()); + gameState->map->removeArtifactInstance(*hero.hero, al.slot); return true; } return false; @@ -240,7 +242,7 @@ void CGameStateCampaign::placeCampaignHeroes() for(auto & replacement : campaignHeroReplacements) if (replacement.heroPlaceholderId.hasValue()) - heroesToRemove.insert(replacement.hero->getHeroType()); + heroesToRemove.insert(replacement.hero->getHeroTypeID()); for(auto & heroID : heroesToRemove) { @@ -327,7 +329,7 @@ void CGameStateCampaign::giveCampaignBonusToHero(CGHeroInstance * hero) CArtifactInstance * scroll = ArtifactUtils::createScroll(SpellID(curBonus->info2)); const auto slot = ArtifactUtils::getArtAnyPosition(hero, scroll->getTypeId()); if(ArtifactUtils::isSlotEquipment(slot) || ArtifactUtils::isSlotBackpack(slot)) - scroll->putAt(*hero, slot); + gameState->map->putArtifactInstance(*hero, scroll, slot); else logGlobal->error("Cannot give starting scroll - no free slots!"); break; @@ -368,9 +370,9 @@ void CGameStateCampaign::replaceHeroesPlaceholders() heroToPlace->id = campaignHeroReplacement.heroPlaceholderId; if(heroPlaceholder->tempOwner.isValidPlayer()) heroToPlace->tempOwner = heroPlaceholder->tempOwner; - heroToPlace->pos = heroPlaceholder->pos; - heroToPlace->type = heroToPlace->getHeroType().toHeroType(); - heroToPlace->appearance = VLC->objtypeh->getHandlerFor(Obj::HERO, heroToPlace->type->heroClass->getIndex())->getTemplates().front(); + heroToPlace->setAnchorPos(heroPlaceholder->anchorPos()); + heroToPlace->setHeroType(heroToPlace->getHeroTypeID()); + heroToPlace->appearance = heroToPlace->getObjectHandler()->getTemplates().front(); gameState->map->removeBlockVisTiles(heroPlaceholder, true); gameState->map->objects[heroPlaceholder->id.getNum()] = nullptr; @@ -422,16 +424,16 @@ void CGameStateCampaign::transferMissingArtifacts(const CampaignTravel & travelO { auto * artifact = donorHero->getArt(artLocation); - logGlobal->debug("Removing artifact %s from slot %d of hero %s for transfer", artifact->artType->getJsonKey(), artLocation.getNum(), donorHero->getHeroTypeName()); - artifact->removeFrom(*donorHero, artLocation); + logGlobal->debug("Removing artifact %s from slot %d of hero %s for transfer", artifact->getType()->getJsonKey(), artLocation.getNum(), donorHero->getHeroTypeName()); + gameState->map->removeArtifactInstance(*donorHero, artLocation); if (receiver) { - logGlobal->debug("Granting artifact %s to hero %s for transfer", artifact->artType->getJsonKey(), receiver->getHeroTypeName()); + logGlobal->debug("Granting artifact %s to hero %s for transfer", artifact->getType()->getJsonKey(), receiver->getHeroTypeName()); const auto slot = ArtifactUtils::getArtAnyPosition(receiver, artifact->getTypeId()); if(ArtifactUtils::isSlotEquipment(slot) || ArtifactUtils::isSlotBackpack(slot)) - artifact->putAt(*receiver, slot); + gameState->map->putArtifactInstance(*receiver, artifact, slot); else logGlobal->error("Cannot transfer artifact - no free slots!"); } @@ -563,7 +565,7 @@ void CGameStateCampaign::initHeroes() { for (auto & hero : heroes) { - if (hero->getHeroType().getNum() == chosenBonus->info1) + if (hero->getHeroTypeID().getNum() == chosenBonus->info1) { giveCampaignBonusToHero(hero); break; @@ -655,14 +657,14 @@ void CGameStateCampaign::initTowns() if (!owner->human) continue; - if (town->pos != pi.posOfMainTown) + if (town->anchorPos() != pi.posOfMainTown) continue; BuildingID newBuilding; if(gameState->scenarioOps->campState->formatVCMI()) newBuilding = BuildingID(chosenBonus->info1); else - newBuilding = CBuildingHandler::campToERMU(chosenBonus->info1, town->getFaction(), town->getBuildings()); + newBuilding = CBuildingHandler::campToERMU(chosenBonus->info1, town->getFactionID(), town->getBuildings()); // Build granted building & all prerequisites - e.g. Mages Guild Lvl 3 should also give Mages Guild Lvl 1 & 2 while(true) @@ -675,7 +677,7 @@ void CGameStateCampaign::initTowns() town->addBuilding(newBuilding); - auto building = town->town->buildings.at(newBuilding); + auto building = town->getTown()->buildings.at(newBuilding); newBuilding = building->upgrade; } break; diff --git a/lib/gameState/GameStatistics.cpp b/lib/gameState/GameStatistics.cpp index 237da8f54..46b797845 100644 --- a/lib/gameState/GameStatistics.cpp +++ b/lib/gameState/GameStatistics.cpp @@ -14,7 +14,6 @@ #include "../VCMIDirs.h" #include "CGameState.h" #include "TerrainHandler.h" -#include "CHeroHandler.h" #include "StartInfo.h" #include "HighScore.h" #include "../mapObjects/CGHeroInstance.h" @@ -292,7 +291,7 @@ float Statistic::getMapExploredRatio(const CGameState * gs, PlayerColor player) { TerrainTile tile = gs->map->getTile(int3(x, y, layer)); - if(tile.blocked && (!tile.visitable)) + if(tile.blocked() && !tile.visitable()) continue; if(gs->isVisible(int3(x, y, layer), player)) @@ -381,7 +380,7 @@ float Statistic::getTownBuiltRatio(const PlayerState * ps) for(const auto & t : ps->getTowns()) { built += t->getBuildings().size(); - for(const auto & b : t->town->buildings) + for(const auto & b : t->getTown()->buildings) if(!t->forbiddenBuildings.count(b.first)) total += 1; } diff --git a/lib/gameState/InfoAboutArmy.cpp b/lib/gameState/InfoAboutArmy.cpp index cc3bdc205..8e2a4cf0c 100644 --- a/lib/gameState/InfoAboutArmy.cpp +++ b/lib/gameState/InfoAboutArmy.cpp @@ -12,7 +12,9 @@ #include "../mapObjects/CGHeroInstance.h" #include "../mapObjects/CGTownInstance.h" -#include "../CHeroHandler.h" + +#include +#include VCMI_LIB_NAMESPACE_BEGIN @@ -24,7 +26,7 @@ ArmyDescriptor::ArmyDescriptor(const CArmedInstance *army, bool detailed) if(detailed) (*this)[elem.first] = *elem.second; else - (*this)[elem.first] = CStackBasicDescriptor(elem.second->type, (int)elem.second->getQuantityID()); + (*this)[elem.first] = CStackBasicDescriptor(elem.second->getCreature(), (int)elem.second->getQuantityID()); } } @@ -40,12 +42,12 @@ int ArmyDescriptor::getStrength() const if(isDetailed) { for(const auto & elem : *this) - ret += elem.second.type->getAIValue() * elem.second.count; + ret += elem.second.getType()->getAIValue() * elem.second.count; } else { for(const auto & elem : *this) - ret += elem.second.type->getAIValue() * CCreature::estimateCreatureCount(elem.second.count); + ret += elem.second.getType()->getAIValue() * CCreature::estimateCreatureCount(elem.second.count); } return static_cast(ret); } @@ -115,7 +117,7 @@ void InfoAboutHero::initFromHero(const CGHeroInstance *h, InfoAboutHero::EInfoLe initFromArmy(h, detailed); - hclass = h->type->heroClass; + hclass = h->getHeroClass(); name = h->getNameTranslated(); portraitSource = h->getPortraitSource(); diff --git a/lib/gameState/TavernHeroesPool.cpp b/lib/gameState/TavernHeroesPool.cpp index bafaf28b0..3c0dfadbf 100644 --- a/lib/gameState/TavernHeroesPool.cpp +++ b/lib/gameState/TavernHeroesPool.cpp @@ -11,7 +11,6 @@ #include "TavernHeroesPool.h" #include "../mapObjects/CGHeroInstance.h" -#include "../CHeroHandler.h" VCMI_LIB_NAMESPACE_BEGIN @@ -25,7 +24,7 @@ std::map TavernHeroesPool::unusedHeroesFromPool() c { std::map pool = heroesPool; for(const auto & slot : currentTavern) - pool.erase(slot.hero->getHeroType()); + pool.erase(slot.hero->getHeroTypeID()); return pool; } @@ -34,7 +33,7 @@ TavernSlotRole TavernHeroesPool::getSlotRole(HeroTypeID hero) const { for (auto const & slot : currentTavern) { - if (slot.hero->getHeroType() == hero) + if (slot.hero->getHeroTypeID() == hero) return slot.role; } return TavernSlotRole::NONE; @@ -106,7 +105,7 @@ CGHeroInstance * TavernHeroesPool::takeHeroFromPool(HeroTypeID hero) heroesPool.erase(hero); vstd::erase_if(currentTavern, [&](const TavernSlot & entry){ - return entry.hero->type->getId() == hero; + return entry.hero->getHeroTypeID() == hero; }); assert(result); @@ -138,7 +137,7 @@ void TavernHeroesPool::onNewDay() void TavernHeroesPool::addHeroToPool(CGHeroInstance * hero) { - heroesPool[hero->getHeroType()] = hero; + heroesPool[hero->getHeroTypeID()] = hero; } void TavernHeroesPool::setAvailability(HeroTypeID hero, std::set mask) diff --git a/lib/json/JsonNode.cpp b/lib/json/JsonNode.cpp index 4d0e7ddd5..3fd979e00 100644 --- a/lib/json/JsonNode.cpp +++ b/lib/json/JsonNode.cpp @@ -110,17 +110,17 @@ JsonNode::JsonNode(const JsonPath & fileURI, const JsonParsingSettings & parserS *this = parser.parse(fileURI.getName()); } -JsonNode::JsonNode(const JsonPath & fileURI, const std::string & idx) +JsonNode::JsonNode(const JsonPath & fileURI, const std::string & modName) { - auto file = CResourceHandler::get(idx)->load(fileURI)->readAll(); + auto file = CResourceHandler::get(modName)->load(fileURI)->readAll(); JsonParser parser(reinterpret_cast(file.first.get()), file.second, JsonParsingSettings()); *this = parser.parse(fileURI.getName()); } -JsonNode::JsonNode(const JsonPath & fileURI, bool & isValidSyntax) +JsonNode::JsonNode(const JsonPath & fileURI, const std::string & modName, bool & isValidSyntax) { - auto file = CResourceHandler::get()->load(fileURI)->readAll(); + auto file = CResourceHandler::get(modName)->load(fileURI)->readAll(); JsonParser parser(reinterpret_cast(file.first.get()), file.second, JsonParsingSettings()); *this = parser.parse(fileURI.getName()); diff --git a/lib/json/JsonNode.h b/lib/json/JsonNode.h index f4338423a..037148e9b 100644 --- a/lib/json/JsonNode.h +++ b/lib/json/JsonNode.h @@ -79,7 +79,7 @@ public: explicit JsonNode(const JsonPath & fileURI); explicit JsonNode(const JsonPath & fileURI, const JsonParsingSettings & parserSettings); explicit JsonNode(const JsonPath & fileURI, const std::string & modName); - explicit JsonNode(const JsonPath & fileURI, bool & isValidSyntax); + explicit JsonNode(const JsonPath & fileURI, const std::string & modName, bool & isValidSyntax); bool operator==(const JsonNode & other) const; bool operator!=(const JsonNode & other) const; @@ -187,7 +187,7 @@ void convert(std::map & value, const JsonNode & node) { value.clear(); for(const JsonMap::value_type & entry : node.Struct()) - value.insert(entry.first, entry.second.convertTo()); + value.emplace(entry.first, entry.second.convertTo()); } template diff --git a/lib/json/JsonParser.cpp b/lib/json/JsonParser.cpp index eb45d69d8..32b5de0d2 100644 --- a/lib/json/JsonParser.cpp +++ b/lib/json/JsonParser.cpp @@ -158,40 +158,58 @@ bool JsonParser::extractEscaping(std::string & str) switch(input[pos]) { + case '\r': + if(settings.mode == JsonParsingSettings::JsonFormatMode::JSON5 && input.size() > pos && input[pos+1] == '\n') + { + pos += 2; + return true; + } + break; + case '\n': + if(settings.mode == JsonParsingSettings::JsonFormatMode::JSON5) + { + pos += 1; + return true; + } + break; case '\"': str += '\"'; - break; + pos++; + return true; case '\\': str += '\\'; - break; + pos++; + return true; case 'b': str += '\b'; - break; + pos++; + return true; case 'f': str += '\f'; - break; + pos++; + return true; case 'n': str += '\n'; - break; + pos++; + return true; case 'r': str += '\r'; - break; + pos++; + return true; case 't': str += '\t'; - break; + pos++; + return true; case '/': str += '/'; - break; - default: - return error("Unknown escape sequence!", true); + pos++; + return true; } - return true; + return error("Unknown escape sequence!", true); } bool JsonParser::extractString(std::string & str) { - //TODO: JSON5 - line breaks escaping - if(settings.mode < JsonParsingSettings::JsonFormatMode::JSON5) { if(input[pos] != '\"') @@ -216,27 +234,30 @@ bool JsonParser::extractString(std::string & str) pos++; return true; } - if(input[pos] == '\\') // Escaping + else if(input[pos] == '\\') // Escaping { str.append(&input[first], pos - first); pos++; if(pos == input.size()) break; + extractEscaping(str); - first = pos + 1; + first = pos; } - if(input[pos] == '\n') // end-of-line + else if(input[pos] == '\n') // end-of-line { str.append(&input[first], pos - first); return error("Closing quote not found!", true); } - if(static_cast(input[pos]) < ' ') // control character + else if(static_cast(input[pos]) < ' ') // control character { str.append(&input[first], pos - first); - first = pos + 1; + pos++; + first = pos; error("Illegal character in the string!", true); } - pos++; + else + pos++; } return error("Unterminated string!"); } diff --git a/lib/json/JsonRandom.cpp b/lib/json/JsonRandom.cpp index 57f7a97a2..7b1f9dabd 100644 --- a/lib/json/JsonRandom.cpp +++ b/lib/json/JsonRandom.cpp @@ -13,6 +13,8 @@ #include #include +#include +#include #include "JsonBonus.h" @@ -23,8 +25,9 @@ #include "../CCreatureSet.h" #include "../spells/CSpellHandler.h" #include "../CSkillHandler.h" -#include "../CHeroHandler.h" #include "../IGameCallback.h" +#include "../entities/hero/CHero.h" +#include "../entities/hero/CHeroClass.h" #include "../gameState/CGameState.h" #include "../mapObjects/IObjectInterface.h" #include "../modding/IdentifierStorage.h" @@ -482,13 +485,13 @@ VCMI_LIB_NAMESPACE_BEGIN else logMod->warn("Failed to select suitable random creature!"); - stack.type = pickedCreature.toCreature(); + stack.setType(pickedCreature.toCreature()); stack.count = loadValue(value, rng, variables); - if (!value["upgradeChance"].isNull() && !stack.type->upgrades.empty()) + if (!value["upgradeChance"].isNull() && !stack.getCreature()->upgrades.empty()) { if (int(value["upgradeChance"].Float()) > rng.nextInt(99)) // select random upgrade { - stack.type = RandomGeneratorUtil::nextItem(stack.type->upgrades, rng)->toCreature(); + stack.setType(RandomGeneratorUtil::nextItem(stack.getCreature()->upgrades, rng)->toCreature()); } } return stack; diff --git a/lib/json/JsonUtils.cpp b/lib/json/JsonUtils.cpp index f1680e11f..b335e3e5d 100644 --- a/lib/json/JsonUtils.cpp +++ b/lib/json/JsonUtils.cpp @@ -230,13 +230,36 @@ void JsonUtils::inherit(JsonNode & descendant, const JsonNode & base) std::swap(descendant, inheritedNode); } -JsonNode JsonUtils::assembleFromFiles(const std::vector & files) +JsonNode JsonUtils::assembleFromFiles(const JsonNode & files, bool & isValid) +{ + if (files.isVector()) + { + assert(!files.getModScope().empty()); + auto configList = files.convertTo >(); + JsonNode result = JsonUtils::assembleFromFiles(configList, files.getModScope(), isValid); + + return result; + } + else + { + isValid = true; + return files; + } +} + +JsonNode JsonUtils::assembleFromFiles(const JsonNode & files) { bool isValid = false; return assembleFromFiles(files, isValid); } -JsonNode JsonUtils::assembleFromFiles(const std::vector & files, bool & isValid) +JsonNode JsonUtils::assembleFromFiles(const std::vector & files) +{ + bool isValid = false; + return assembleFromFiles(files, "", isValid); +} + +JsonNode JsonUtils::assembleFromFiles(const std::vector & files, std::string modName, bool & isValid) { isValid = true; JsonNode result; @@ -245,10 +268,10 @@ JsonNode JsonUtils::assembleFromFiles(const std::vector & files, bo { JsonPath path = JsonPath::builtinTODO(file); - if (CResourceHandler::get()->existsResource(path)) + if (CResourceHandler::get(modName)->existsResource(path)) { bool isValidFile = false; - JsonNode section(JsonPath::builtinTODO(file), isValidFile); + JsonNode section(JsonPath::builtinTODO(file), modName, isValidFile); merge(result, section); isValid |= isValidFile; } @@ -275,4 +298,28 @@ JsonNode JsonUtils::assembleFromFiles(const std::string & filename) return result; } +void JsonUtils::detectConflicts(JsonNode & result, const JsonNode & left, const JsonNode & right, const std::string & keyName) +{ + switch (left.getType()) + { + case JsonNode::JsonType::DATA_NULL: + case JsonNode::JsonType::DATA_BOOL: + case JsonNode::JsonType::DATA_FLOAT: + case JsonNode::JsonType::DATA_INTEGER: + case JsonNode::JsonType::DATA_STRING: + case JsonNode::JsonType::DATA_VECTOR: // NOTE: comparing vectors as whole - since merge will overwrite it in its entirety + { + result[keyName][left.getModScope()] = left; + result[keyName][right.getModScope()] = right; + return; + } + case JsonNode::JsonType::DATA_STRUCT: + { + for(const auto & node : left.Struct()) + if (!right[node.first].isNull()) + detectConflicts(result, node.second, right[node.first], keyName + "/" + node.first); + } + } +} + VCMI_LIB_NAMESPACE_END diff --git a/lib/json/JsonUtils.h b/lib/json/JsonUtils.h index 954ee0b16..17d8941a0 100644 --- a/lib/json/JsonUtils.h +++ b/lib/json/JsonUtils.h @@ -44,8 +44,10 @@ namespace JsonUtils * @brief generate one Json structure from multiple files * @param files - list of filenames with parts of json structure */ + DLL_LINKAGE JsonNode assembleFromFiles(const JsonNode & files); + DLL_LINKAGE JsonNode assembleFromFiles(const JsonNode & files, bool & isValid); DLL_LINKAGE JsonNode assembleFromFiles(const std::vector & files); - DLL_LINKAGE JsonNode assembleFromFiles(const std::vector & files, bool & isValid); + DLL_LINKAGE JsonNode assembleFromFiles(const std::vector & files, std::string modName, bool & isValid); /// This version loads all files with same name (overridden by mods) DLL_LINKAGE JsonNode assembleFromFiles(const std::string & filename); @@ -72,6 +74,12 @@ namespace JsonUtils /// get schema by json URI: vcmi:# /// example: schema "vcmi:settings" is used to check user settings DLL_LINKAGE const JsonNode & getSchema(const std::string & URI); + + /// detects potential conflicts - json entries present in both nodes + /// returns JsonNode that contains list of conflicting keys + /// For each conflict - list of conflicting mods and list of conflicting json values + /// result[pathToKey][modID] -> node that was conflicting + DLL_LINKAGE void detectConflicts(JsonNode & result, const JsonNode & left, const JsonNode & right, const std::string & keyName); } VCMI_LIB_NAMESPACE_END diff --git a/lib/json/JsonValidator.cpp b/lib/json/JsonValidator.cpp index 0114f113d..2dde7b338 100644 --- a/lib/json/JsonValidator.cpp +++ b/lib/json/JsonValidator.cpp @@ -74,7 +74,7 @@ static int getLevenshteinDistance(const std::string & s, const std::string & t) /// Searches for keys similar to 'target' in 'candidates' map /// Returns closest match or empty string if no suitable candidates are found -static std::string findClosestMatch(JsonMap candidates, std::string target) +static std::string findClosestMatch(const JsonMap & candidates, const std::string & target) { // Maximum distance at which we can consider strings to be similar // If strings have more different symbols than this number then it is not a typo, but a completely different word @@ -422,7 +422,7 @@ static std::string requiredCheck(JsonValidator & validator, const JsonNode & bas std::string errors; for(const auto & required : schema.Vector()) { - if (data[required.String()].isNull()) + if (data[required.String()].isNull() && data.getModScope() != "core") errors += validator.makeErrorMessage("Required entry " + required.String() + " is missing"); } return errors; diff --git a/lib/mapObjectConstructors/AObjectTypeHandler.cpp b/lib/mapObjectConstructors/AObjectTypeHandler.cpp index c431900aa..d842d9953 100644 --- a/lib/mapObjectConstructors/AObjectTypeHandler.cpp +++ b/lib/mapObjectConstructors/AObjectTypeHandler.cpp @@ -133,8 +133,6 @@ void AObjectTypeHandler::preInitObject(CGObjectInstance * obj) const { obj->ID = Obj(type); obj->subID = subtype; - obj->typeName = typeName; - obj->subTypeName = getJsonKey(); obj->blockVisit = blockVisit; obj->removable = removable; } diff --git a/lib/mapObjectConstructors/AObjectTypeHandler.h b/lib/mapObjectConstructors/AObjectTypeHandler.h index 402aa2aec..52a2c7c1d 100644 --- a/lib/mapObjectConstructors/AObjectTypeHandler.h +++ b/lib/mapObjectConstructors/AObjectTypeHandler.h @@ -90,7 +90,7 @@ public: /// returns preferred template for this object, if present (e.g. one of 3 possible templates for town - village, fort and castle) /// note that appearance will not be changed - this must be done separately (either by assignment or via pack from server) - std::shared_ptr getOverride(TerrainId terrainType, const CGObjectInstance * object) const; + virtual std::shared_ptr getOverride(TerrainId terrainType, const CGObjectInstance * object) const; BattleField getBattlefield() const; diff --git a/lib/mapObjectConstructors/CBankInstanceConstructor.cpp b/lib/mapObjectConstructors/CBankInstanceConstructor.cpp index c35a2cf28..71abc90b0 100644 --- a/lib/mapObjectConstructors/CBankInstanceConstructor.cpp +++ b/lib/mapObjectConstructors/CBankInstanceConstructor.cpp @@ -28,7 +28,7 @@ void CBankInstanceConstructor::initTypeData(const JsonNode & input) if (input.Struct().count("name") == 0) logMod->warn("Bank %s missing name!", getJsonKey()); - VLC->generaltexth->registerString(input.getModScope(), getNameTextID(), input["name"].String()); + VLC->generaltexth->registerString(input.getModScope(), getNameTextID(), input["name"]); levels = input["levels"].Vector(); bankResetDuration = static_cast(input["resetDuration"].Float()); diff --git a/lib/mapObjectConstructors/CObjectClassesHandler.cpp b/lib/mapObjectConstructors/CObjectClassesHandler.cpp index 51c9c8fbb..422c8e2cf 100644 --- a/lib/mapObjectConstructors/CObjectClassesHandler.cpp +++ b/lib/mapObjectConstructors/CObjectClassesHandler.cpp @@ -23,23 +23,29 @@ #include "../mapObjectConstructors/CRewardableConstructor.h" #include "../mapObjectConstructors/CommonConstructors.h" #include "../mapObjectConstructors/DwellingInstanceConstructor.h" +#include "../mapObjectConstructors/FlaggableInstanceConstructor.h" #include "../mapObjectConstructors/HillFortInstanceConstructor.h" #include "../mapObjectConstructors/ShipyardInstanceConstructor.h" + #include "../mapObjects/CGCreature.h" -#include "../mapObjects/CGPandoraBox.h" -#include "../mapObjects/CQuest.h" -#include "../mapObjects/ObjectTemplate.h" -#include "../mapObjects/CGMarket.h" -#include "../mapObjects/MiscObjects.h" #include "../mapObjects/CGHeroInstance.h" +#include "../mapObjects/CGMarket.h" +#include "../mapObjects/CGPandoraBox.h" #include "../mapObjects/CGTownInstance.h" +#include "../mapObjects/CQuest.h" +#include "../mapObjects/FlaggableMapObject.h" +#include "../mapObjects/MiscObjects.h" +#include "../mapObjects/ObjectTemplate.h" #include "../mapObjects/ObstacleSetHandler.h" + #include "../modding/IdentifierStorage.h" #include "../modding/CModHandler.h" #include "../modding/ModScope.h" #include "../texts/CGeneralTextHandler.h" #include "../texts/CLegacyConfigParser.h" +#include + VCMI_LIB_NAMESPACE_BEGIN CObjectClassesHandler::CObjectClassesHandler() @@ -55,6 +61,7 @@ CObjectClassesHandler::CObjectClassesHandler() SET_HANDLER_CLASS("town", CTownInstanceConstructor); SET_HANDLER_CLASS("bank", CBankInstanceConstructor); SET_HANDLER_CLASS("boat", BoatInstanceConstructor); + SET_HANDLER_CLASS("flaggable", FlaggableInstanceConstructor); SET_HANDLER_CLASS("market", MarketInstanceConstructor); SET_HANDLER_CLASS("hillFort", HillFortInstanceConstructor); SET_HANDLER_CLASS("shipyard", ShipyardInstanceConstructor); @@ -80,7 +87,6 @@ CObjectClassesHandler::CObjectClassesHandler() SET_HANDLER("garrison", CGGarrison); SET_HANDLER("heroPlaceholder", CGHeroPlaceholder); SET_HANDLER("keymaster", CGKeymasterTent); - SET_HANDLER("lighthouse", CGLighthouse); SET_HANDLER("magi", CGMagi); SET_HANDLER("mine", CGMine); SET_HANDLER("obelisk", CGObelisk); @@ -205,8 +211,13 @@ TObjectTypeHandler CObjectClassesHandler::loadSubObjectFromJson(const std::strin // Compatibility with 1.5 mods for 1.6. To be removed in 1.7 // Detect banks that use old format and load them using old bank hander - if (baseObject->id == Obj::CREATURE_BANK && entry.Struct().count("levels") && !entry.Struct().count("rewards")) - handler = "bank"; + if (baseObject->id == Obj::CREATURE_BANK) + { + if (entry.Struct().count("levels") && !entry.Struct().count("rewards")) + handler = "bank"; + else + handler = "configurable"; + } auto createdObject = handlerConstructors.at(handler)(); @@ -276,7 +287,7 @@ std::unique_ptr CObjectClassesHandler::loadFromJson(const std::stri newObject->base = json["base"]; newObject->id = index; - VLC->generaltexth->registerString(scope, newObject->getNameTextID(), json["name"].String()); + VLC->generaltexth->registerString(scope, newObject->getNameTextID(), json["name"]); newObject->objectTypeHandlers.resize(json["lastReservedIndex"].Float() + 1); @@ -351,7 +362,7 @@ TObjectTypeHandler CObjectClassesHandler::getHandlerFor(MapObjectID type, MapObj return mapObjectTypes.front()->objectTypeHandlers.front(); auto subID = subtype.getNum(); - if (type == Obj::PRISON || type == Obj::HERO_PLACEHOLDER) + if (type == Obj::PRISON || type == Obj::HERO_PLACEHOLDER || type == Obj::SPELL_SCROLL) subID = 0; auto result = mapObjectTypes.at(type.getNum())->objectTypeHandlers.at(subID); @@ -390,6 +401,62 @@ TObjectTypeHandler CObjectClassesHandler::getHandlerFor(CompoundMapObjectID comp return getHandlerFor(compoundIdentifier.primaryID, compoundIdentifier.secondaryID); } +CompoundMapObjectID CObjectClassesHandler::getCompoundIdentifier(const std::string & scope, const std::string & type, const std::string & subtype) const +{ + std::optional id; + if (scope.empty()) + { + id = VLC->identifiers()->getIdentifier("object", type); + } + else + { + id = VLC->identifiers()->getIdentifier(scope, "object", type); + } + + if(id) + { + if (subtype.empty()) + return CompoundMapObjectID(id.value(), 0); + + const auto & object = mapObjectTypes.at(id.value()); + std::optional subID = VLC->identifiers()->getIdentifier(scope, object->getJsonKey(), subtype); + + if (subID) + return CompoundMapObjectID(id.value(), subID.value()); + } + + std::string errorString = "Failed to get id for object of type " + type + "." + subtype; + logGlobal->error(errorString); + throw std::runtime_error(errorString); +} + +CompoundMapObjectID CObjectClassesHandler::getCompoundIdentifier(const std::string & objectName) const +{ + std::string subtype = "object"; //Default for objects with no subIds + std::string type; + + auto scopeAndFullName = vstd::splitStringToPair(objectName, ':'); + logGlobal->debug("scopeAndFullName: %s, %s", scopeAndFullName.first, scopeAndFullName.second); + + auto typeAndName = vstd::splitStringToPair(scopeAndFullName.second, '.'); + logGlobal->debug("typeAndName: %s, %s", typeAndName.first, typeAndName.second); + + auto nameAndSubtype = vstd::splitStringToPair(typeAndName.second, '.'); + logGlobal->debug("nameAndSubtype: %s, %s", nameAndSubtype.first, nameAndSubtype.second); + + if (!nameAndSubtype.first.empty()) + { + type = nameAndSubtype.first; + subtype = nameAndSubtype.second; + } + else + { + type = typeAndName.second; + } + + return getCompoundIdentifier(boost::to_lower_copy(scopeAndFullName.first), type, subtype); +} + std::set CObjectClassesHandler::knownObjects() const { std::set ret; @@ -456,9 +523,21 @@ void CObjectClassesHandler::afterLoadFinalization() obj->afterLoadFinalization(); if(obj->getTemplates().empty()) - logGlobal->warn("No templates found for %s:%s", entry->getJsonKey(), obj->getJsonKey()); + logMod->debug("No templates found for %s:%s", entry->getJsonKey(), obj->getJsonKey()); } } + + for(auto & entry : objectIdHandlers) + { + // Call function for each object id + entry.second(entry.first); + } +} + +void CObjectClassesHandler::resolveObjectCompoundId(const std::string & id, std::function callback) +{ + auto compoundId = getCompoundIdentifier(id); + objectIdHandlers.push_back(std::make_pair(compoundId, callback)); } void CObjectClassesHandler::generateExtraMonolithsForRMG(ObjectClass * container) diff --git a/lib/mapObjectConstructors/CObjectClassesHandler.h b/lib/mapObjectConstructors/CObjectClassesHandler.h index 0b898c0a9..264b6f1ee 100644 --- a/lib/mapObjectConstructors/CObjectClassesHandler.h +++ b/lib/mapObjectConstructors/CObjectClassesHandler.h @@ -9,7 +9,7 @@ */ #pragma once -#include "../constants/EntityIdentifiers.h" +#include "../mapObjects/CompoundMapObjectID.h" #include "../IHandlerBase.h" #include "../json/JsonNode.h" @@ -19,27 +19,6 @@ class AObjectTypeHandler; class ObjectTemplate; struct SObjectSounds; -struct DLL_LINKAGE CompoundMapObjectID -{ - si32 primaryID; - si32 secondaryID; - - CompoundMapObjectID(si32 primID, si32 secID) : primaryID(primID), secondaryID(secID) {}; - - bool operator<(const CompoundMapObjectID& other) const - { - if(this->primaryID != other.primaryID) - return this->primaryID < other.primaryID; - else - return this->secondaryID < other.secondaryID; - } - - bool operator==(const CompoundMapObjectID& other) const - { - return (this->primaryID == other.primaryID) && (this->secondaryID == other.secondaryID); - } -}; - class CGObjectInstance; using TObjectTypeHandler = std::shared_ptr; @@ -74,6 +53,8 @@ class DLL_LINKAGE CObjectClassesHandler : public IHandlerBase, boost::noncopyabl /// map that is filled during construction with all known handlers. Not serializeable due to usage of std::function std::map > handlerConstructors; + std::vector>> objectIdHandlers; + /// container with H3 templates, used only during loading, no need to serialize it using TTemplatesContainer = std::multimap, std::shared_ptr>; TTemplatesContainer legacyTemplates; @@ -110,15 +91,19 @@ public: TObjectTypeHandler getHandlerFor(MapObjectID type, MapObjectSubID subtype) const; TObjectTypeHandler getHandlerFor(const std::string & scope, const std::string & type, const std::string & subtype) const; TObjectTypeHandler getHandlerFor(CompoundMapObjectID compoundIdentifier) const; + CompoundMapObjectID getCompoundIdentifier(const std::string & scope, const std::string & type, const std::string & subtype) const; + CompoundMapObjectID getCompoundIdentifier(const std::string & objectName) const; std::string getObjectName(MapObjectID type, MapObjectSubID subtype) const; SObjectSounds getObjectSounds(MapObjectID type, MapObjectSubID subtype) const; + void resolveObjectCompoundId(const std::string & id, std::function callback); + /// Returns handler string describing the handler (for use in client) std::string getObjectHandlerName(MapObjectID type) const; std::string getJsonKey(MapObjectID type) const; }; -VCMI_LIB_NAMESPACE_END +VCMI_LIB_NAMESPACE_END \ No newline at end of file diff --git a/lib/mapObjectConstructors/CRewardableConstructor.cpp b/lib/mapObjectConstructors/CRewardableConstructor.cpp index 3fbcba73d..ed0c92142 100644 --- a/lib/mapObjectConstructors/CRewardableConstructor.cpp +++ b/lib/mapObjectConstructors/CRewardableConstructor.cpp @@ -14,6 +14,7 @@ #include "../mapObjects/CRewardableObject.h" #include "../texts/CGeneralTextHandler.h" #include "../IGameCallback.h" +#include "../CConfigHandler.h" VCMI_LIB_NAMESPACE_BEGIN @@ -23,9 +24,10 @@ void CRewardableConstructor::initTypeData(const JsonNode & config) blockVisit = config["blockedVisitable"].Bool(); if (!config["name"].isNull()) - VLC->generaltexth->registerString( config.getModScope(), getNameTextID(), config["name"].String()); + VLC->generaltexth->registerString( config.getModScope(), getNameTextID(), config["name"]); - JsonUtils::validate(config, "vcmi:rewardable", getJsonKey()); + if (settings["mods"]["validation"].String() != "off") + JsonUtils::validate(config, "vcmi:rewardable", getJsonKey()); } @@ -43,9 +45,10 @@ CGObjectInstance * CRewardableConstructor::create(IGameCallback * cb, std::share return ret; } -Rewardable::Configuration CRewardableConstructor::generateConfiguration(IGameCallback * cb, vstd::RNG & rand, MapObjectID objectID) const +Rewardable::Configuration CRewardableConstructor::generateConfiguration(IGameCallback * cb, vstd::RNG & rand, MapObjectID objectID, const std::map & presetVariables) const { Rewardable::Configuration result; + result.variables.preset = presetVariables; objectInfo.configureObject(result, rand, cb); for(auto & rewardInfo : result.info) @@ -67,7 +70,7 @@ void CRewardableConstructor::configureObject(CGObjectInstance * object, vstd::RN if (!rewardableObject) throw std::runtime_error("Object " + std::to_string(object->getObjGroupIndex()) + ", " + std::to_string(object->getObjTypeIndex()) + " is not a rewardable object!" ); - rewardableObject->configuration = generateConfiguration(object->cb, rng, object->ID); + rewardableObject->configuration = generateConfiguration(object->cb, rng, object->ID, rewardableObject->configuration.variables.preset); rewardableObject->initializeGuards(); if (rewardableObject->configuration.info.empty()) diff --git a/lib/mapObjectConstructors/CRewardableConstructor.h b/lib/mapObjectConstructors/CRewardableConstructor.h index eab0e500a..a9f30d500 100644 --- a/lib/mapObjectConstructors/CRewardableConstructor.h +++ b/lib/mapObjectConstructors/CRewardableConstructor.h @@ -31,7 +31,7 @@ public: std::unique_ptr getObjectInfo(std::shared_ptr tmpl) const override; - Rewardable::Configuration generateConfiguration(IGameCallback * cb, vstd::RNG & rand, MapObjectID objectID) const; + Rewardable::Configuration generateConfiguration(IGameCallback * cb, vstd::RNG & rand, MapObjectID objectID, const std::map & presetVariables) const; }; VCMI_LIB_NAMESPACE_END diff --git a/lib/mapObjectConstructors/CommonConstructors.cpp b/lib/mapObjectConstructors/CommonConstructors.cpp index d90ac8ef2..a3986304a 100644 --- a/lib/mapObjectConstructors/CommonConstructors.cpp +++ b/lib/mapObjectConstructors/CommonConstructors.cpp @@ -11,14 +11,16 @@ #include "CommonConstructors.h" #include "../texts/CGeneralTextHandler.h" -#include "../CHeroHandler.h" #include "../IGameCallback.h" #include "../json/JsonRandom.h" #include "../constants/StringConstants.h" #include "../TerrainHandler.h" #include "../VCMI_Lib.h" +#include "../CConfigHandler.h" #include "../entities/faction/CTownHandler.h" +#include "../entities/hero/CHeroClass.h" +#include "../json/JsonUtils.h" #include "../mapObjects/CGHeroInstance.h" #include "../mapObjects/CGMarket.h" #include "../mapObjects/CGTownInstance.h" @@ -96,13 +98,12 @@ bool CTownInstanceConstructor::objectFilter(const CGObjectInstance * object, std void CTownInstanceConstructor::initializeObject(CGTownInstance * obj) const { - obj->town = faction->town; obj->tempOwner = PlayerColor::NEUTRAL; } void CTownInstanceConstructor::randomizeObject(CGTownInstance * object, vstd::RNG & rng) const { - auto templ = getOverride(object->cb->getTile(object->pos)->terType->getId(), object); + auto templ = getOverride(object->cb->getTile(object->pos)->getTerrainID(), object); if(templ) object->appearance = templ; } @@ -124,39 +125,71 @@ void CHeroInstanceConstructor::initTypeData(const JsonNode & input) input["heroClass"], [&](si32 index) { heroClass = HeroClassID(index).toHeroClass(); }); - filtersJson = input["filters"]; -} - -void CHeroInstanceConstructor::afterLoadFinalization() -{ - for(const auto & entry : filtersJson.Struct()) + for (const auto & [name, config] : input["filters"].Struct()) { - filters[entry.first] = LogicalExpression(entry.second, [](const JsonNode & node) + HeroFilter filter; + filter.allowFemale = config["female"].Bool(); + filter.allowMale = config["male"].Bool(); + filters[name] = filter; + + if (!config["hero"].isNull()) { - return HeroTypeID(VLC->identifiers()->getIdentifier("hero", node.Vector()[0]).value_or(-1)); - }); + VLC->identifiers()->requestIdentifier( "hero", config["hero"], [this, templateName = name](si32 index) { + filters.at(templateName).fixedHero = HeroTypeID(index); + }); + } } } -bool CHeroInstanceConstructor::objectFilter(const CGObjectInstance * object, std::shared_ptr templ) const +std::shared_ptr CHeroInstanceConstructor::getOverride(TerrainId terrainType, const CGObjectInstance * object) const { const auto * hero = dynamic_cast(object); - auto heroTest = [&](const HeroTypeID & id) - { - return hero->type->getId() == id; - }; + std::vector> allTemplates = getTemplates(); + std::shared_ptr candidateFullMatch; + std::shared_ptr candidateGenderMatch; + std::shared_ptr candidateBase; - if(filters.count(templ->stringID)) + assert(hero->gender != EHeroGender::DEFAULT); + + for (const auto & templ : allTemplates) { - return filters.at(templ->stringID).test(heroTest); + if (filters.count(templ->stringID)) + { + const auto & filter = filters.at(templ->stringID); + if (filter.fixedHero.hasValue()) + { + if (filter.fixedHero == hero->getHeroTypeID()) + candidateFullMatch = templ; + } + else if (filter.allowMale) + { + if (hero->gender == EHeroGender::MALE) + candidateGenderMatch = templ; + } + else if (filter.allowFemale) + { + if (hero->gender == EHeroGender::FEMALE) + candidateGenderMatch = templ; + } + else + { + candidateBase = templ; + } + } + else + { + candidateBase = templ; + } } - return false; -} -void CHeroInstanceConstructor::initializeObject(CGHeroInstance * obj) const -{ - obj->type = nullptr; //FIXME: set to valid value. somehow. + if (candidateFullMatch) + return candidateFullMatch; + + if (candidateGenderMatch) + return candidateGenderMatch; + + return candidateBase; } void CHeroInstanceConstructor::randomizeObject(CGHeroInstance * object, vstd::RNG & rng) const @@ -211,6 +244,30 @@ AnimationPath BoatInstanceConstructor::getBoatAnimationName() const void MarketInstanceConstructor::initTypeData(const JsonNode & input) { + if (settings["mods"]["validation"].String() != "off") + JsonUtils::validate(input, "vcmi:market", getJsonKey()); + + if (!input["description"].isNull()) + { + std::string description = input["description"].String(); + descriptionTextID = TextIdentifier(getBaseTextID(), "description").get(); + VLC->generaltexth->registerString( input.getModScope(), descriptionTextID, input["description"]); + } + + if (!input["speech"].isNull()) + { + std::string speech = input["speech"].String(); + if (!speech.empty() && speech.at(0) == '@') + { + speechTextID = speech.substr(1); + } + else + { + speechTextID = TextIdentifier(getBaseTextID(), "speech").get(); + VLC->generaltexth->registerString( input.getModScope(), speechTextID, input["speech"]); + } + } + for(auto & element : input["modes"].Vector()) { if(MappedKeys::MARKET_NAMES_TO_TYPES.count(element.String())) @@ -219,9 +276,11 @@ void MarketInstanceConstructor::initTypeData(const JsonNode & input) marketEfficiency = input["efficiency"].isNull() ? 5 : input["efficiency"].Integer(); predefinedOffer = input["offer"]; - - title = input["title"].String(); - speech = input["speech"].String(); +} + +bool MarketInstanceConstructor::hasDescription() const +{ + return !descriptionTextID.empty(); } CGMarket * MarketInstanceConstructor::createObject(IGameCallback * cb) const @@ -241,21 +300,6 @@ CGMarket * MarketInstanceConstructor::createObject(IGameCallback * cb) const return new CGMarket(cb); } -void MarketInstanceConstructor::initializeObject(CGMarket * market) const -{ - market->marketEfficiency = marketEfficiency; - - if(auto university = dynamic_cast(market)) - { - university->title = market->getObjectName(); - if(!title.empty()) - university->title = VLC->generaltexth->translate(title); - - if(!speech.empty()) - university->speech = VLC->generaltexth->translate(speech); - } -} - const std::set & MarketInstanceConstructor::availableModes() const { return marketModes; @@ -273,4 +317,15 @@ void MarketInstanceConstructor::randomizeObject(CGMarket * object, vstd::RNG & r } } +std::string MarketInstanceConstructor::getSpeechTranslated() const +{ + assert(marketModes.count(EMarketMode::RESOURCE_SKILL)); + return VLC->generaltexth->translate(speechTextID); +} + +int MarketInstanceConstructor::getMarketEfficiency() const +{ + return marketEfficiency; +} + VCMI_LIB_NAMESPACE_END diff --git a/lib/mapObjectConstructors/CommonConstructors.h b/lib/mapObjectConstructors/CommonConstructors.h index d7ce61f5e..73ba43874 100644 --- a/lib/mapObjectConstructors/CommonConstructors.h +++ b/lib/mapObjectConstructors/CommonConstructors.h @@ -72,18 +72,21 @@ public: class CHeroInstanceConstructor : public CDefaultObjectTypeHandler { - JsonNode filtersJson; -protected: - bool objectFilter(const CGObjectInstance * obj, std::shared_ptr tmpl) const override; + struct HeroFilter + { + HeroTypeID fixedHero; + bool allowMale; + bool allowFemale; + }; + + std::map filters; + const CHeroClass * heroClass = nullptr; + + std::shared_ptr getOverride(TerrainId terrainType, const CGObjectInstance * object) const override; void initTypeData(const JsonNode & input) override; public: - const CHeroClass * heroClass = nullptr; - std::map> filters; - - void initializeObject(CGHeroInstance * object) const override; void randomizeObject(CGHeroInstance * object, vstd::RNG & rng) const override; - void afterLoadFinalization() override; bool hasNameTextID() const override; std::string getNameTextID() const override; @@ -112,23 +115,23 @@ public: class MarketInstanceConstructor : public CDefaultObjectTypeHandler { -protected: - void initTypeData(const JsonNode & config) override; + std::string descriptionTextID; + std::string speechTextID; std::set marketModes; JsonNode predefinedOffer; int marketEfficiency; - - std::string title; - std::string speech; - + + void initTypeData(const JsonNode & config) override; public: CGMarket * createObject(IGameCallback * cb) const override; - void initializeObject(CGMarket * object) const override; void randomizeObject(CGMarket * object, vstd::RNG & rng) const override; const std::set & availableModes() const; + bool hasDescription() const; + std::string getSpeechTranslated() const; + int getMarketEfficiency() const; }; VCMI_LIB_NAMESPACE_END diff --git a/lib/mapObjectConstructors/DwellingInstanceConstructor.cpp b/lib/mapObjectConstructors/DwellingInstanceConstructor.cpp index 609a57efb..3572dfb8a 100644 --- a/lib/mapObjectConstructors/DwellingInstanceConstructor.cpp +++ b/lib/mapObjectConstructors/DwellingInstanceConstructor.cpp @@ -29,7 +29,7 @@ void DwellingInstanceConstructor::initTypeData(const JsonNode & input) if (input.Struct().count("name") == 0) logMod->warn("Dwelling %s missing name!", getJsonKey()); - VLC->generaltexth->registerString( input.getModScope(), getNameTextID(), input["name"].String()); + VLC->generaltexth->registerString( input.getModScope(), getNameTextID(), input["name"]); const JsonVector & levels = input["creatures"].Vector(); const auto totalLevels = levels.size(); @@ -88,25 +88,28 @@ void DwellingInstanceConstructor::randomizeObject(CGDwelling * dwelling, vstd::R dwelling->creatures.back().second.push_back(cre->getId()); } - bool guarded = false; //TODO: serialize for sanity + bool guarded = false; - if(guards.getType() == JsonNode::JsonType::DATA_BOOL) //simple switch + if(guards.getType() == JsonNode::JsonType::DATA_BOOL) { + //simple switch if(guards.Bool()) { guarded = true; } } - else if(guards.getType() == JsonNode::JsonType::DATA_VECTOR) //custom guards (eg. Elemental Conflux) + else if(guards.getType() == JsonNode::JsonType::DATA_VECTOR) { + //custom guards (eg. Elemental Conflux) JsonRandom::Variables emptyVariables; for(auto & stack : randomizer.loadCreatures(guards, rng, emptyVariables)) { - dwelling->putStack(SlotID(dwelling->stacksCount()), new CStackInstance(stack.type->getId(), stack.count)); + dwelling->putStack(SlotID(dwelling->stacksCount()), new CStackInstance(stack.getId(), stack.count)); } } - else //default condition - creatures are of level 5 or higher + else if (dwelling->ID == Obj::CREATURE_GENERATOR1 || dwelling->ID == Obj::CREATURE_GENERATOR4) { + //default condition - this is dwelling with creatures of level 5 or higher for(auto creatureEntry : availableCreatures) { if(creatureEntry.at(0)->getLevel() >= 5) diff --git a/lib/mapObjectConstructors/FlaggableInstanceConstructor.cpp b/lib/mapObjectConstructors/FlaggableInstanceConstructor.cpp new file mode 100644 index 000000000..ed5eac661 --- /dev/null +++ b/lib/mapObjectConstructors/FlaggableInstanceConstructor.cpp @@ -0,0 +1,65 @@ +/* +* FlaggableInstanceConstructor.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 "FlaggableInstanceConstructor.h" + +#include "../CConfigHandler.h" +#include "../VCMI_Lib.h" +#include "../json/JsonBonus.h" +#include "../json/JsonUtils.h" +#include "../texts/CGeneralTextHandler.h" + +VCMI_LIB_NAMESPACE_BEGIN + +void FlaggableInstanceConstructor::initTypeData(const JsonNode & config) +{ + if (settings["mods"]["validation"].String() != "off") + JsonUtils::validate(config, "vcmi:flaggable", getJsonKey()); + + for (const auto & bonusJson : config["bonuses"].Struct()) + providedBonuses.push_back(JsonUtils::parseBonus(bonusJson.second)); + + if (!config["message"].isNull()) + { + std::string message = config["message"].String(); + if (!message.empty() && message.at(0) == '@') + { + visitMessageTextID = message.substr(1); + } + else + { + visitMessageTextID = TextIdentifier(getBaseTextID(), "onVisit").get(); + VLC->generaltexth->registerString( config.getModScope(), visitMessageTextID, config["message"]); + } + } + + dailyIncome = ResourceSet(config["dailyIncome"]); +} + +void FlaggableInstanceConstructor::initializeObject(FlaggableMapObject * flaggable) const +{ +} + +const std::string & FlaggableInstanceConstructor::getVisitMessageTextID() const +{ + return visitMessageTextID; +} + +const std::vector> & FlaggableInstanceConstructor::getProvidedBonuses() const +{ + return providedBonuses; +} + +const ResourceSet & FlaggableInstanceConstructor::getDailyIncome() const +{ + return dailyIncome; +} + +VCMI_LIB_NAMESPACE_END diff --git a/lib/mapObjectConstructors/FlaggableInstanceConstructor.h b/lib/mapObjectConstructors/FlaggableInstanceConstructor.h new file mode 100644 index 000000000..c0ad722fe --- /dev/null +++ b/lib/mapObjectConstructors/FlaggableInstanceConstructor.h @@ -0,0 +1,41 @@ +/* +* FlaggableInstanceConstructor.h, part of VCMI engine +* +* Authors: listed in file AUTHORS in main folder +* +* License: GNU General Public License v2.0 or later +* Full text of license available in license.txt file, in main folder +* +*/ +#pragma once + +#include "CDefaultObjectTypeHandler.h" + +#include "../ResourceSet.h" +#include "../bonuses/Bonus.h" +#include "../mapObjects/FlaggableMapObject.h" + +VCMI_LIB_NAMESPACE_BEGIN + +class FlaggableInstanceConstructor final : public CDefaultObjectTypeHandler +{ + /// List of bonuses that are provided by every map object of this type + std::vector> providedBonuses; + + /// ID of message to show on hero visit + std::string visitMessageTextID; + + /// Amount of resources granted by this object to owner every day + ResourceSet dailyIncome; + +protected: + void initTypeData(const JsonNode & config) override; + void initializeObject(FlaggableMapObject * object) const override; + +public: + const std::string & getVisitMessageTextID() const; + const std::vector> & getProvidedBonuses() const; + const ResourceSet & getDailyIncome() const; +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/mapObjectConstructors/HillFortInstanceConstructor.cpp b/lib/mapObjectConstructors/HillFortInstanceConstructor.cpp index b325edde3..889868b0a 100644 --- a/lib/mapObjectConstructors/HillFortInstanceConstructor.cpp +++ b/lib/mapObjectConstructors/HillFortInstanceConstructor.cpp @@ -11,12 +11,17 @@ #include "HillFortInstanceConstructor.h" #include "../mapObjects/MiscObjects.h" +#include "../texts/CGeneralTextHandler.h" VCMI_LIB_NAMESPACE_BEGIN void HillFortInstanceConstructor::initTypeData(const JsonNode & config) { parameters = config; + if(!parameters["unavailableUpgradeMessage"].isNull()) + VLC->generaltexth->registerString(parameters.getModScope(), TextIdentifier(getBaseTextID(), "unavailableUpgradeMessage"), parameters["unavailableUpgradeMessage"].String()); + + VLC->generaltexth->registerString(parameters.getModScope(), TextIdentifier(getBaseTextID(), "description"), parameters["description"].String()); } void HillFortInstanceConstructor::initializeObject(HillFort * fort) const diff --git a/lib/mapObjectConstructors/IObjectInfo.h b/lib/mapObjectConstructors/IObjectInfo.h index 280402fa8..7a88b5fef 100644 --- a/lib/mapObjectConstructors/IObjectInfo.h +++ b/lib/mapObjectConstructors/IObjectInfo.h @@ -49,7 +49,10 @@ public: virtual bool givesBonuses() const { return false; } + virtual bool hasGuards() const { return false; } + virtual ~IObjectInfo() = default; + }; VCMI_LIB_NAMESPACE_END diff --git a/lib/mapObjects/CArmedInstance.cpp b/lib/mapObjects/CArmedInstance.cpp index 5a8b0cae8..23d026cd9 100644 --- a/lib/mapObjects/CArmedInstance.cpp +++ b/lib/mapObjects/CArmedInstance.cpp @@ -78,7 +78,7 @@ void CArmedInstance::updateMoraleBonusFromArmy() const CStackInstance * inst = slot.second; const auto * creature = inst->getCreatureID().toEntity(VLC); - factions.insert(creature->getFaction()); + factions.insert(creature->getFactionID()); // Check for undead flag instead of faction (undead mummies are neutral) if (!hasUndead) { diff --git a/lib/mapObjects/CBank.cpp b/lib/mapObjects/CBank.cpp index ac65f7fa8..716390e6e 100644 --- a/lib/mapObjects/CBank.cpp +++ b/lib/mapObjects/CBank.cpp @@ -94,7 +94,7 @@ void CBank::setConfig(const BankConfig & config) clearSlots(); // remove all stacks, if any for(const auto & stack : config.guards) - setCreature (SlotID(stacksCount()), stack.type->getId(), stack.count); + setCreature (SlotID(stacksCount()), stack.getId(), stack.count); daycounter = 1; //yes, 1 since "today" daycounter won't be incremented } @@ -138,13 +138,13 @@ bool CBank::wasVisited (PlayerColor player) const void CBank::onHeroVisit(const CGHeroInstance * h) const { ChangeObjectVisitors cov(ChangeObjectVisitors::VISITOR_ADD_PLAYER, id, h->id); - cb->sendAndApply(&cov); + cb->sendAndApply(cov); BlockingDialog bd(true, false); bd.player = h->getOwner(); bd.text.appendLocalString(EMetaText::ADVOB_TXT, 32); bd.components = getPopupComponents(h->getOwner()); - bd.text.replaceRawString(getObjectName()); + bd.text.replaceTextID(getObjectHandler()->getNameTextID()); cb->showBlockingDialog(this, &bd); } @@ -158,7 +158,7 @@ void CBank::doVisit(const CGHeroInstance * hero) const if (!bankConfig) { iw.text.appendRawString(VLC->generaltexth->advobtxt[33]);// This was X, now is completely empty - iw.text.replaceRawString(getObjectName()); + iw.text.replaceTextID(getObjectHandler()->getNameTextID()); cb->showInfoDialog(&iw); } @@ -190,8 +190,8 @@ void CBank::doVisit(const CGHeroInstance * hero) const iw.text.appendLocalString(EMetaText::ADVOB_TXT, 34); const auto * strongest = boost::range::max_element(bankConfig->guards, [](const CStackBasicDescriptor & a, const CStackBasicDescriptor & b) { - return a.type->getFightValue() < b.type->getFightValue(); - })->type; + return a.getType()->getFightValue() < b.getType()->getFightValue(); + })->getType(); iw.text.replaceNamePlural(strongest->getId()); iw.text.replaceRawString(loot.buildList()); @@ -244,7 +244,7 @@ void CBank::doVisit(const CGHeroInstance * hero) const CCreatureSet ourArmy; for(const auto & slot : bankConfig->creatures) { - ourArmy.addToSlot(ourArmy.getSlotFor(slot.type->getId()), slot.type->getId(), slot.count); + ourArmy.addToSlot(ourArmy.getSlotFor(slot.getId()), slot.getId(), slot.count); } for(const auto & elem : ourArmy.Slots()) diff --git a/lib/mapObjects/CGCreature.cpp b/lib/mapObjects/CGCreature.cpp index d34b5792c..b1589c8e3 100644 --- a/lib/mapObjects/CGCreature.cpp +++ b/lib/mapObjects/CGCreature.cpp @@ -33,7 +33,7 @@ std::string CGCreature::getHoverText(PlayerColor player) const if(stacks.empty()) { //should not happen... - logGlobal->error("Invalid stack at tile %s: subID=%d; id=%d", pos.toString(), getCreature(), id.getNum()); + logGlobal->error("Invalid stack at tile %s: subID=%d; id=%d", anchorPos().toString(), getCreature(), id.getNum()); return "INVALID_STACK"; } @@ -45,7 +45,7 @@ std::string CGCreature::getHoverText(PlayerColor player) const else ms.appendLocalString(EMetaText::ARRAY_TXT, quantityTextIndex); ms.appendRawString(" "); - ms.appendNamePlural(getCreature()); + ms.appendNamePlural(getCreatureID()); return ms.toString(); } @@ -57,7 +57,7 @@ std::string CGCreature::getHoverText(const CGHeroInstance * hero) const MetaString ms; ms.appendNumber(stacks.begin()->second->count); ms.appendRawString(" "); - ms.appendName(getCreature(), stacks.begin()->second->count); + ms.appendName(getCreatureID(), stacks.begin()->second->count); return ms.toString(); } else @@ -66,6 +66,18 @@ std::string CGCreature::getHoverText(const CGHeroInstance * hero) const } } +std::string CGCreature::getMonsterLevelText() const +{ + std::string monsterLevel = VLC->generaltexth->translate("vcmi.adventureMap.monsterLevel"); + bool isRanged = getCreature()->getBonusBearer()->hasBonusOfType(BonusType::SHOOTER); + std::string attackTypeKey = isRanged ? "vcmi.adventureMap.monsterRangedType" : "vcmi.adventureMap.monsterMeleeType"; + std::string attackType = VLC->generaltexth->translate(attackTypeKey); + boost::replace_first(monsterLevel, "%TOWN", getCreature()->getFactionID().toEntity(VLC)->getNameTranslated()); + boost::replace_first(monsterLevel, "%LEVEL", std::to_string(getCreature()->getLevel())); + boost::replace_first(monsterLevel, "%ATTACK_TYPE", attackType); + return monsterLevel; +} + std::string CGCreature::getPopupText(const CGHeroInstance * hero) const { std::string hoverName; @@ -102,15 +114,13 @@ std::string CGCreature::getPopupText(const CGHeroInstance * hero) const if (settings["general"]["enableUiEnhancements"].Bool()) { - std::string monsterLevel = VLC->generaltexth->translate("vcmi.adventureMap.monsterLevel"); - boost::replace_first(monsterLevel, "%TOWN", (*VLC->townh)[VLC->creatures()->getById(getCreature())->getFaction()]->getNameTranslated()); - boost::replace_first(monsterLevel, "%LEVEL", std::to_string(VLC->creatures()->getById(getCreature())->getLevel())); - hoverName += monsterLevel; - + hoverName += getMonsterLevelText(); hoverName += VLC->generaltexth->translate("vcmi.adventureMap.monsterThreat.title"); int choice; - double ratio = (static_cast(getArmyStrength()) / hero->getTotalStrength()); + uint64_t armyStrength = getArmyStrength(); + uint64_t heroStrength = hero->getTotalStrength(); + double ratio = static_cast(armyStrength) / heroStrength; if (ratio < 0.1) choice = 0; else if (ratio < 0.25) choice = 1; else if (ratio < 0.6) choice = 2; @@ -131,13 +141,16 @@ std::string CGCreature::getPopupText(const CGHeroInstance * hero) const std::string CGCreature::getPopupText(PlayerColor player) const { - return getHoverText(player); + std::string hoverName = getHoverText(player); + if (settings["general"]["enableUiEnhancements"].Bool()) + hoverName += getMonsterLevelText(); + return hoverName; } std::vector CGCreature::getPopupComponents(PlayerColor player) const { return { - Component(ComponentType::CREATURE, getCreature()) + Component(ComponentType::CREATURE, getCreatureID()) }; } @@ -169,7 +182,7 @@ void CGCreature::onHeroVisit( const CGHeroInstance * h ) const BlockingDialog ynd(true,false); ynd.player = h->tempOwner; ynd.text.appendLocalString(EMetaText::ADVOB_TXT, 86); - ynd.text.replaceName(getCreature(), getStackCount(SlotID(0))); + ynd.text.replaceName(getCreatureID(), getStackCount(SlotID(0))); cb->showBlockingDialog(this, &ynd); break; } @@ -184,7 +197,7 @@ void CGCreature::onHeroVisit( const CGHeroInstance * h ) const std::string tmp = VLC->generaltexth->advobtxt[90]; boost::algorithm::replace_first(tmp, "%d", std::to_string(getStackCount(SlotID(0)))); boost::algorithm::replace_first(tmp, "%d", std::to_string(action)); - boost::algorithm::replace_first(tmp,"%s",VLC->creatures()->getById(getCreature())->getNamePluralTranslated()); + boost::algorithm::replace_first(tmp,"%s",getCreature()->getNamePluralTranslated()); ynd.text.appendRawString(tmp); cb->showBlockingDialog(this, &ynd); break; @@ -192,11 +205,16 @@ void CGCreature::onHeroVisit( const CGHeroInstance * h ) const } } -CreatureID CGCreature::getCreature() const +CreatureID CGCreature::getCreatureID() const { return CreatureID(getObjTypeIndex().getNum()); } +const CCreature * CGCreature::getCreature() const +{ + return getCreatureID().toCreature(); +} + void CGCreature::pickRandomObject(vstd::RNG & rand) { switch(ID.toEnum()) @@ -266,7 +284,7 @@ void CGCreature::initObj(vstd::RNG & rand) stacks[SlotID(0)]->setType(getCreature()); TQuantity &amount = stacks[SlotID(0)]->count; - const Creature * c = VLC->creatures()->getById(getCreature()); + const Creature * c = getCreature(); if(amount == 0) { amount = rand.nextInt(c->getAdvMapAmountMin(), c->getAdvMapAmountMax()); @@ -340,8 +358,8 @@ int CGCreature::takenAction(const CGHeroInstance *h, bool allowJoin) const for(const auto & elem : h->Slots()) { - bool isOurUpgrade = vstd::contains(getCreature().toCreature()->upgrades, elem.second->getCreatureID()); - bool isOurDowngrade = vstd::contains(elem.second->type->upgrades, getCreature()); + bool isOurUpgrade = vstd::contains(getCreature()->upgrades, elem.second->getCreatureID()); + bool isOurDowngrade = vstd::contains(elem.second->getCreature()->upgrades, getCreatureID()); if(isOurUpgrade || isOurDowngrade) count += elem.second->count; @@ -367,7 +385,7 @@ int CGCreature::takenAction(const CGHeroInstance *h, bool allowJoin) const if(diplomacy * 2 + sympathy + 1 >= character) { - int32_t recruitCost = VLC->creatures()->getById(getCreature())->getRecruitCost(EGameResID::GOLD); + int32_t recruitCost = getCreature()->getRecruitCost(EGameResID::GOLD); int32_t stackCount = getStackCount(SlotID(0)); return recruitCost * stackCount; //join for gold } @@ -462,7 +480,7 @@ void CGCreature::fight( const CGHeroInstance *h ) const if (containsUpgradedStack()) //upgrade { SlotID slotID = SlotID(static_cast(std::floor(static_cast(stacks.size()) / 2.0f))); - const auto & upgrades = getStack(slotID).type->upgrades; + const auto & upgrades = getStack(slotID).getCreature()->upgrades; if(!upgrades.empty()) { auto it = RandomGeneratorUtil::nextItem(upgrades, cb->gameState()->getRandomGenerator()); @@ -480,7 +498,7 @@ void CGCreature::flee( const CGHeroInstance * h ) const BlockingDialog ynd(true,false); ynd.player = h->tempOwner; ynd.text.appendLocalString(EMetaText::ADVOB_TXT,91); - ynd.text.replaceName(getCreature(), getStackCount(SlotID(0))); + ynd.text.replaceName(getCreatureID(), getStackCount(SlotID(0))); cb->showBlockingDialog(this, &ynd); } @@ -500,10 +518,10 @@ void CGCreature::battleFinished(const CGHeroInstance *hero, const BattleResult & { //merge stacks into one TSlots::const_iterator i; - const CCreature * cre = getCreature().toCreature(); + const CCreature * cre = getCreature(); for(i = stacks.begin(); i != stacks.end(); i++) { - if(cre->isMyUpgrade(i->second->type)) + if(cre->isMyUpgrade(i->second->getCreature())) { cb->changeStackType(StackLocation(this, i->first), cre); //un-upgrade creatures } @@ -518,7 +536,7 @@ void CGCreature::battleFinished(const CGHeroInstance *hero, const BattleResult & // TODO it's either overcomplicated (if we assume there'll be only one stack) or buggy (if we allow multiple stacks... but that'll also cause troubles elsewhere) i = stacks.end(); i--; - SlotID slot = getSlotFor(i->second->type); + SlotID slot = getSlotFor(i->second->getCreature()); if(slot == i->first) //no reason to move stack to its own slot break; else @@ -549,7 +567,7 @@ bool CGCreature::containsUpgradedStack() const float c = 5325.181015f; float d = 32788.727920f; - int val = static_cast(std::floor(a * pos.x + b * pos.y + c * pos.z + d)); + int val = static_cast(std::floor(a * visitablePos().x + b * visitablePos().y + c * visitablePos().z + d)); return ((val % 32768) % 100) < 50; } @@ -578,7 +596,7 @@ int CGCreature::getNumberOfStacks(const CGHeroInstance *hero) const ui32 c = 1943276003u; ui32 d = 3174620878u; - ui32 R1 = a * static_cast(pos.x) + b * static_cast(pos.y) + c * static_cast(pos.z) + d; + ui32 R1 = a * static_cast(visitablePos().x) + b * static_cast(visitablePos().y) + c * static_cast(visitablePos().z) + d; ui32 R2 = (R1 >> 16) & 0x7fff; int R4 = R2 % 100 + 1; diff --git a/lib/mapObjects/CGCreature.h b/lib/mapObjects/CGCreature.h index 2b88d367d..daa554a2e 100644 --- a/lib/mapObjects/CGCreature.h +++ b/lib/mapObjects/CGCreature.h @@ -49,7 +49,8 @@ public: void newTurn(vstd::RNG & rand) const override; void battleFinished(const CGHeroInstance *hero, const BattleResult &result) const override; void blockingDialogAnswered(const CGHeroInstance *hero, int32_t answer) const override; - CreatureID getCreature() const; + CreatureID getCreatureID() const; + const CCreature * getCreature() const; //stack formation depends on position, bool containsUpgradedStack() const; @@ -81,7 +82,7 @@ private: int takenAction(const CGHeroInstance *h, bool allowJoin=true) const; //action on confrontation: -2 - fight, -1 - flee, >=0 - will join for given value of gold (may be 0) void giveReward(const CGHeroInstance * h) const; - + std::string getMonsterLevelText() const; }; VCMI_LIB_NAMESPACE_END diff --git a/lib/mapObjects/CGDwelling.cpp b/lib/mapObjects/CGDwelling.cpp index 1ec3e492c..ac9ca1905 100644 --- a/lib/mapObjects/CGDwelling.cpp +++ b/lib/mapObjects/CGDwelling.cpp @@ -93,7 +93,7 @@ FactionID CGDwelling::randomizeFaction(vstd::RNG & rand) assert(linkedTown->ID == Obj::TOWN); if(linkedTown->ID==Obj::TOWN) - return linkedTown->getFaction(); + return linkedTown->getFactionID(); } if(!randomizationInfo->allowedFactions.empty()) @@ -180,6 +180,7 @@ void CGDwelling::initObj(vstd::RNG & rand) { case Obj::CREATURE_GENERATOR1: case Obj::CREATURE_GENERATOR4: + case Obj::WAR_MACHINE_FACTORY: { getObjectHandler()->configureObject(this, rand); assert(!creatures.empty()); @@ -190,13 +191,6 @@ void CGDwelling::initObj(vstd::RNG & rand) //is handled within newturn func break; - case Obj::WAR_MACHINE_FACTORY: - creatures.resize(3); - creatures[0].second.emplace_back(CreatureID::BALLISTA); - creatures[1].second.emplace_back(CreatureID::FIRST_AID_TENT); - creatures[2].second.emplace_back(CreatureID::AMMO_CART); - break; - default: assert(0); break; @@ -224,7 +218,7 @@ void CGDwelling::onHeroVisit( const CGHeroInstance * h ) const iw.player = h->tempOwner; iw.text.appendLocalString(EMetaText::ADVOB_TXT, 44); //{%s} \n\n The camp is deserted. Perhaps you should try next week. iw.text.replaceName(ID); - cb->sendAndApply(&iw); + cb->sendAndApply(iw); return; } @@ -324,7 +318,7 @@ void CGDwelling::newTurn(vstd::RNG & rand) const } if(change) - cb->sendAndApply(&sac); + cb->sendAndApply(sac); updateGuards(); } @@ -392,7 +386,7 @@ void CGDwelling::updateGuards() const csc.slot = slot; csc.count = crea->getGrowth() * 3; csc.absoluteValue = true; - cb->sendAndApply(&csc); + cb->sendAndApply(csc); } else //slot is empty, create whole new stack { @@ -401,7 +395,7 @@ void CGDwelling::updateGuards() const ns.slot = slot; ns.type = crea->getId(); ns.count = crea->getGrowth() * 3; - cb->sendAndApply(&ns); + cb->sendAndApply(ns); } } } @@ -458,7 +452,7 @@ void CGDwelling::heroAcceptsCreatures( const CGHeroInstance *h) const iw.text.replaceNamePlural(crid); cb->showInfoDialog(&iw); - cb->sendAndApply(&sac); + cb->sendAndApply(sac); cb->addToSlot(StackLocation(h, slot), crs, count); } } @@ -469,7 +463,7 @@ void CGDwelling::heroAcceptsCreatures( const CGHeroInstance *h) const iw.text.appendLocalString(EMetaText::GENERAL_TXT, 422); //There are no %s here to recruit. iw.text.replaceNamePlural(crid); iw.player = h->tempOwner; - cb->sendAndApply(&iw); + cb->sendAndApply(iw); } } else @@ -483,7 +477,7 @@ void CGDwelling::heroAcceptsCreatures( const CGHeroInstance *h) const sac.creatures[0].first = !h->getArt(ArtifactPosition::MACH1); //ballista sac.creatures[1].first = !h->getArt(ArtifactPosition::MACH3); //first aid tent sac.creatures[2].first = !h->getArt(ArtifactPosition::MACH2); //ammo cart - cb->sendAndApply(&sac); + cb->sendAndApply(sac); } auto windowMode = (ID == Obj::CREATURE_GENERATOR1 || ID == Obj::REFUGEE_CAMP) ? EOpenWindowMode::RECRUITMENT_FIRST : EOpenWindowMode::RECRUITMENT_ALL; diff --git a/lib/mapObjects/CGHeroInstance.cpp b/lib/mapObjects/CGHeroInstance.cpp index 8cf3871b0..6e84d0baf 100644 --- a/lib/mapObjects/CGHeroInstance.cpp +++ b/lib/mapObjects/CGHeroInstance.cpp @@ -17,7 +17,6 @@ #include "../texts/CGeneralTextHandler.h" #include "../ArtifactUtils.h" -#include "../CHeroHandler.h" #include "../TerrainHandler.h" #include "../RoadHandler.h" #include "../IGameSettings.h" @@ -31,6 +30,8 @@ #include "../StartInfo.h" #include "CGTownInstance.h" #include "../entities/faction/CTownHandler.h" +#include "../entities/hero/CHeroHandler.h" +#include "../entities/hero/CHeroClass.h" #include "../battle/CBattleInfoEssentials.h" #include "../campaign/CampaignState.h" #include "../json/JsonBonus.h" @@ -48,6 +49,8 @@ VCMI_LIB_NAMESPACE_BEGIN +const ui32 CGHeroInstance::NO_PATROLLING = std::numeric_limits::max(); + void CGHeroPlaceholder::serializeJsonOptions(JsonSerializeFormat & handler) { serializeJsonOwner(handler); @@ -99,16 +102,16 @@ ui32 CGHeroInstance::getTileMovementCost(const TerrainTile & dest, const Terrain int64_t ret = GameConstants::BASE_MOVEMENT_COST; //if there is road both on dest and src tiles - use src road movement cost - if(dest.roadType->getId() != Road::NO_ROAD && from.roadType->getId() != Road::NO_ROAD) + if(dest.hasRoad() && from.hasRoad()) { - ret = from.roadType->movementCost; + ret = from.getRoad()->movementCost; } - else if(ti->nativeTerrain != from.terType->getId() &&//the terrain is not native + 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.terType->getId()))) //no special movement bonus + !ti->hasBonusOfType(BonusType::NO_TERRAIN_PENALTY, BonusSubtypeID(from.getTerrainID()))) //no special movement bonus { - ret = VLC->terrainTypeHandler->getById(from.terType->getId())->moveCost; + ret = VLC->terrainTypeHandler->getById(from.getTerrainID())->moveCost; ret -= ti->valOfBonuses(BonusType::ROUGH_TERRAIN_DISCOUNT); if(ret < GameConstants::BASE_MOVEMENT_COST) ret = GameConstants::BASE_MOVEMENT_COST; @@ -116,9 +119,9 @@ ui32 CGHeroInstance::getTileMovementCost(const TerrainTile & dest, const Terrain return static_cast(ret); } -FactionID CGHeroInstance::getFaction() const +FactionID CGHeroInstance::getFactionID() const { - return FactionID(type->heroClass->faction); + return getHeroClass()->faction; } const IBonusBearer* CGHeroInstance::getBonusBearer() const @@ -229,10 +232,10 @@ bool CGHeroInstance::canLearnSkill(const SecondarySkill & which) const if (getSecSkillLevel(which) > 0) return false; - if (type->heroClass->secSkillProbability.count(which) == 0) + if (getHeroClass()->secSkillProbability.count(which) == 0) return false; - if (type->heroClass->secSkillProbability.at(which) == 0) + if (getHeroClass()->secSkillProbability.at(which) == 0) return false; return true; @@ -282,7 +285,6 @@ int CGHeroInstance::movementPointsLimitCached(bool onLand, const TurnInfo * ti) CGHeroInstance::CGHeroInstance(IGameCallback * cb) : CArmedInstance(cb), - type(nullptr), tacticFormationEnabled(false), inTownGarrison(false), moveDir(4), @@ -303,36 +305,83 @@ PlayerColor CGHeroInstance::getOwner() const return tempOwner; } -HeroTypeID CGHeroInstance::getHeroType() const +const CHeroClass * CGHeroInstance::getHeroClass() const { + return getHeroType()->heroClass; +} + +HeroClassID CGHeroInstance::getHeroClassID() const +{ + auto heroType = getHeroTypeID(); + if (heroType.hasValue()) + return getHeroType()->heroClass->getId(); + else + return HeroClassID(); +} + +const CHero * CGHeroInstance::getHeroType() const +{ + return getHeroTypeID().toHeroType(); +} + +HeroTypeID CGHeroInstance::getHeroTypeID() const +{ + if (ID == Obj::RANDOM_HERO) + return HeroTypeID::NONE; return HeroTypeID(getObjTypeIndex().getNum()); } void CGHeroInstance::setHeroType(HeroTypeID heroType) { - assert(type == nullptr); subID = heroType; } +void CGHeroInstance::initObj(vstd::RNG & rand) +{ + if (ID == Obj::HERO) + updateAppearance(); +} + void CGHeroInstance::initHero(vstd::RNG & rand, const HeroTypeID & SUBID) { subID = SUBID.getNum(); initHero(rand); } +TObjectTypeHandler CGHeroInstance::getObjectHandler() const +{ + if (ID == Obj::HERO) + return VLC->objtypeh->getHandlerFor(ID, getHeroClass()->getIndex()); + else // prison or random hero + return VLC->objtypeh->getHandlerFor(ID, 0); +} + +void CGHeroInstance::updateAppearance() +{ + auto handler = VLC->objtypeh->getHandlerFor(Obj::HERO, getHeroClass()->getIndex());; + auto terrain = cb->gameState()->getTile(visitablePos())->getTerrainID(); + auto app = handler->getOverride(terrain, this); + if (app) + appearance = app; +} + void CGHeroInstance::initHero(vstd::RNG & rand) { assert(validTypes(true)); - if(!type) - type = getHeroType().toHeroType(); + + if (gender == EHeroGender::DEFAULT) + gender = getHeroType()->gender; if (ID == Obj::HERO) - appearance = VLC->objtypeh->getHandlerFor(Obj::HERO, type->heroClass->getIndex())->getTemplates().front(); + { + auto handler = VLC->objtypeh->getHandlerFor(Obj::HERO, getHeroClass()->getIndex());; + appearance = handler->getTemplates().front(); + } if(!vstd::contains(spells, SpellID::PRESET)) { // hero starts with default spells - for(const auto & spellID : type->spells) + for(const auto & spellID : getHeroType()->spells) spells.insert(spellID); } else //remove placeholder @@ -341,7 +390,7 @@ void CGHeroInstance::initHero(vstd::RNG & rand) if(!vstd::contains(spells, SpellID::SPELLBOOK_PRESET)) { // hero starts with default spellbook presence status - if(!getArt(ArtifactPosition::SPELLBOOK) && type->haveSpellBook) + if(!getArt(ArtifactPosition::SPELLBOOK) && getHeroType()->haveSpellBook) { auto artifact = ArtifactUtils::createArtifact(ArtifactID::SPELLBOOK); putArtifact(ArtifactPosition::SPELLBOOK, artifact); @@ -360,14 +409,11 @@ void CGHeroInstance::initHero(vstd::RNG & rand) { for(int g=0; g(g), type->heroClass->primarySkillInitial[g]); + pushPrimSkill(static_cast(g), getHeroClass()->primarySkillInitial[g]); } } if(secSkills.size() == 1 && secSkills[0] == std::pair(SecondarySkill::NONE, -1)) //set secondary skills to default - secSkills = type->secSkillsInit; - - if (gender == EHeroGender::DEFAULT) - gender = type->gender; + secSkills = getHeroType()->secSkillsInit; setFormation(EArmyFormation::LOOSE); if (!stacksCount()) //standard army//initial army @@ -403,9 +449,9 @@ void CGHeroInstance::initHero(vstd::RNG & rand) addNewBonus(bonus); } - if (cb->getSettings().getBoolean(EGameSettings::MODULE_COMMANDERS) && !commander && type->heroClass->commander.hasValue()) + if (cb->getSettings().getBoolean(EGameSettings::MODULE_COMMANDERS) && !commander && getHeroClass()->commander.hasValue()) { - commander = new CCommanderInstance(type->heroClass->commander); + commander = new CCommanderInstance(getHeroClass()->commander); commander->setArmyObj (castToArmyObj()); //TODO: separate function for setting commanders commander->giveStackExp (exp); //after our exp is set } @@ -413,7 +459,7 @@ void CGHeroInstance::initHero(vstd::RNG & rand) skillsInfo = SecondarySkillsInfo(); //copy active (probably growing) bonuses from hero prototype to hero object - for(const std::shared_ptr & b : type->specialty) + for(const std::shared_ptr & b : getHeroType()->specialty) addNewBonus(b); //initialize bonuses @@ -433,14 +479,14 @@ void CGHeroInstance::initArmy(vstd::RNG & rand, IArmyDescriptor * dst) auto stacksCountChances = cb->getSettings().getVector(EGameSettings::HEROES_STARTING_STACKS_CHANCES); int stacksCountInitRandomNumber = rand.nextInt(1, 100); - size_t maxStacksCount = std::min(stacksCountChances.size(), type->initialArmy.size()); + size_t maxStacksCount = std::min(stacksCountChances.size(), getHeroType()->initialArmy.size()); for(int stackNo=0; stackNo < maxStacksCount; stackNo++) { if (stacksCountInitRandomNumber > stacksCountChances[stackNo]) continue; - auto & stack = type->initialArmy[stackNo]; + auto & stack = getHeroType()->initialArmy[stackNo]; int count = rand.nextInt(stack.minAmount, stack.maxAmount); @@ -588,11 +634,11 @@ std::string CGHeroInstance::getMovementPointsTextIfOwner(PlayerColor player) con ui8 CGHeroInstance::maxlevelsToMagicSchool() const { - return type->heroClass->isMagicHero() ? 3 : 4; + return getHeroClass()->isMagicHero() ? 3 : 4; } ui8 CGHeroInstance::maxlevelsToWisdom() const { - return type->heroClass->isMagicHero() ? 3 : 6; + return getHeroClass()->isMagicHero() ? 3 : 6; } CGHeroInstance::SecondarySkillsInfo::SecondarySkillsInfo(): @@ -615,13 +661,12 @@ void CGHeroInstance::pickRandomObject(vstd::RNG & rand) if (ID == Obj::RANDOM_HERO) { + auto selectedHero = cb->gameState()->pickNextHeroType(getOwner()); + ID = Obj::HERO; - subID = cb->gameState()->pickNextHeroType(getOwner()); - type = getHeroType().toHeroType(); - randomizeArmy(type->heroClass->faction); + subID = selectedHero; + randomizeArmy(getHeroClass()->faction); } - else - type = getHeroType().toHeroType(); auto oldSubID = subID; @@ -629,7 +674,7 @@ void CGHeroInstance::pickRandomObject(vstd::RNG & rand) // after setType subID used to store unique hero identify id. Check issue 2277 for details // exclude prisons which should use appearance as set in map, via map editor or RMG if (ID != Obj::PRISON) - setType(ID, type->heroClass->getIndex()); + setType(ID, getHeroClass()->getIndex()); this->subID = oldSubID; } @@ -666,7 +711,25 @@ double CGHeroInstance::getFightingStrength() const double CGHeroInstance::getMagicStrength() const { - return sqrt((1.0 + 0.05*getPrimSkillLevel(PrimarySkill::KNOWLEDGE)) * (1.0 + 0.05*getPrimSkillLevel(PrimarySkill::SPELL_POWER))); + if (!hasSpellbook()) + return 1; + bool atLeastOneCombatSpell = false; + for (auto spell : spells) + { + if (spellbookContainsSpell(spell) && spell.toSpell()->isCombat()) + { + atLeastOneCombatSpell = true; + break; + } + } + if (!atLeastOneCombatSpell) + return 1; + return sqrt((1.0 + 0.05*getPrimSkillLevel(PrimarySkill::KNOWLEDGE) * mana / manaLimit()) * (1.0 + 0.05*getPrimSkillLevel(PrimarySkill::SPELL_POWER) * mana / manaLimit())); +} + +double CGHeroInstance::getMagicStrengthForCampaign() const +{ + return sqrt((1.0 + 0.05 * getPrimSkillLevel(PrimarySkill::KNOWLEDGE)) * (1.0 + 0.05 * getPrimSkillLevel(PrimarySkill::SPELL_POWER))); } double CGHeroInstance::getHeroStrength() const @@ -674,9 +737,14 @@ double CGHeroInstance::getHeroStrength() const return sqrt(pow(getFightingStrength(), 2.0) * pow(getMagicStrength(), 2.0)); } +double CGHeroInstance::getHeroStrengthForCampaign() const +{ + return sqrt(pow(getFightingStrength(), 2.0) * pow(getMagicStrengthForCampaign(), 2.0)); +} + ui64 CGHeroInstance::getTotalStrength() const { - double ret = getFightingStrength() * getArmyStrength(); + double ret = getHeroStrength() * getArmyStrength(); return static_cast(ret); } @@ -804,7 +872,7 @@ void CGHeroInstance::spendMana(ServerCallback * server, const int spellCost) con sm.hid = id; sm.val = -spellCost; - server->apply(&sm); + server->apply(sm); } } @@ -1041,7 +1109,7 @@ si32 CGHeroInstance::getManaNewTurn() const BoatId CGHeroInstance::getBoatType() const { - return BoatId(VLC->townh->getById(type->heroClass->faction)->getBoatType()); + return BoatId(VLC->townh->getById(getHeroClass()->faction)->getBoatType()); } void CGHeroInstance::getOutOffsets(std::vector &offsets) const @@ -1080,7 +1148,7 @@ void CGHeroInstance::pushPrimSkill( PrimarySkill which, int val ) EAlignment CGHeroInstance::getAlignment() const { - return type->heroClass->getAlignment(); + return getHeroClass()->getAlignment(); } void CGHeroInstance::initExp(vstd::RNG & rand) @@ -1104,12 +1172,12 @@ HeroTypeID CGHeroInstance::getPortraitSource() const if (customPortraitSource.isValid()) return customPortraitSource; else - return getHeroType(); + return getHeroTypeID(); } int32_t CGHeroInstance::getIconIndex() const { - return VLC->heroTypes()->getById(getPortraitSource())->getIconIndex(); + return getPortraitSource().toEntity(VLC)->getIconIndex(); } std::string CGHeroInstance::getNameTranslated() const @@ -1126,15 +1194,15 @@ std::string CGHeroInstance::getClassNameTextID() const { if (isCampaignGem()) return "core.genrltxt.735"; - return type->heroClass->getNameTextID(); + return getHeroClass()->getNameTextID(); } std::string CGHeroInstance::getNameTextID() const { if (!nameCustomTextId.empty()) return nameCustomTextId; - if (type) - return type->getNameTextID(); + if (getHeroTypeID().hasValue()) + return getHeroType()->getNameTextID(); // FIXME: called by logging from some specialties (mods?) before type is set on deserialization // assert(0); @@ -1150,13 +1218,13 @@ std::string CGHeroInstance::getBiographyTextID() const { if (!biographyCustomTextId.empty()) return biographyCustomTextId; - if (type) - return type->getBiographyTextID(); + if (getHeroTypeID().hasValue()) + return getHeroType()->getBiographyTextID(); return ""; //for random hero } -CGHeroInstance::ArtPlacementMap CGHeroInstance::putArtifact(ArtifactPosition pos, CArtifactInstance * art) +CGHeroInstance::ArtPlacementMap CGHeroInstance::putArtifact(const ArtifactPosition & pos, CArtifactInstance * art) { assert(art->canBePutAt(this, pos)); @@ -1165,7 +1233,7 @@ CGHeroInstance::ArtPlacementMap CGHeroInstance::putArtifact(ArtifactPosition pos return CArtifactSet::putArtifact(pos, art); } -void CGHeroInstance::removeArtifact(ArtifactPosition pos) +void CGHeroInstance::removeArtifact(const ArtifactPosition & pos) { auto art = getArt(pos); assert(art); @@ -1201,7 +1269,7 @@ void CGHeroInstance::removeSpellbook() if(hasSpellbook()) { - getArt(ArtifactPosition::SPELLBOOK)->removeFrom(*this, ArtifactPosition::SPELLBOOK); + cb->gameState()->map->removeArtifactInstance(*this, ArtifactPosition::SPELLBOOK); } } @@ -1349,11 +1417,11 @@ std::vector CGHeroInstance::getLevelUpProposedSecondarySkills(vs SecondarySkill selection; if (selectWisdom) - selection = type->heroClass->chooseSecSkill(intersect(options, wisdomList), rand); + selection = getHeroClass()->chooseSecSkill(intersect(options, wisdomList), rand); else if (selectSchool) - selection = type->heroClass->chooseSecSkill(intersect(options, schoolList), rand); + selection = getHeroClass()->chooseSecSkill(intersect(options, schoolList), rand); else - selection = type->heroClass->chooseSecSkill(options, rand); + selection = getHeroClass()->chooseSecSkill(options, rand); skills.push_back(selection); options.erase(selection); @@ -1384,7 +1452,7 @@ PrimarySkill CGHeroInstance::nextPrimarySkill(vstd::RNG & rand) const { assert(gainsLevel()); const auto isLowLevelHero = level < GameConstants::HERO_HIGH_LEVEL; - const auto & skillChances = isLowLevelHero ? type->heroClass->primarySkillLowLevel : type->heroClass->primarySkillHighLevel; + const auto & skillChances = isLowLevelHero ? getHeroClass()->primarySkillLowLevel : getHeroClass()->primarySkillHighLevel; if (isCampaignYog()) { @@ -1514,35 +1582,25 @@ bool CGHeroInstance::hasVisions(const CGObjectInstance * target, BonusSubtypeID if (visionsMultiplier > 0) vstd::amax(visionsRange, 3); //minimum range is 3 tiles, but only if VISIONS bonus present - const int distance = static_cast(target->pos.dist2d(visitablePos())); + const int distance = static_cast(target->anchorPos().dist2d(visitablePos())); //logGlobal->debug(boost::str(boost::format("Visions: dist %d, mult %d, range %d") % distance % visionsMultiplier % visionsRange)); - return (distance < visionsRange) && (target->pos.z == pos.z); + return (distance < visionsRange) && (target->anchorPos().z == anchorPos().z); } std::string CGHeroInstance::getHeroTypeName() const { if(ID == Obj::HERO || ID == Obj::PRISON) - { - if(type) - { - return type->getJsonKey(); - } - else - { - return getHeroType().toEntity(VLC)->getJsonKey(); - } - } + return getHeroType()->getJsonKey(); + return ""; } void CGHeroInstance::afterAddToMap(CMap * map) { if(ID != Obj::PRISON) - { map->heroesOnMap.emplace_back(this); - } } void CGHeroInstance::afterRemoveFromMap(CMap* map) { @@ -1729,29 +1787,27 @@ void CGHeroInstance::serializeJsonOptions(JsonSerializeFormat & handler) if(!appearance) { // crossoverDeserialize - type = getHeroType().toHeroType(); - appearance = VLC->objtypeh->getHandlerFor(Obj::HERO, type->heroClass->getIndex())->getTemplates().front(); + appearance = VLC->objtypeh->getHandlerFor(Obj::HERO, getHeroClassID())->getTemplates().front(); } } CArmedInstance::serializeJsonOptions(handler); { - static constexpr int NO_PATROLING = -1; - int rawPatrolRadius = NO_PATROLING; + ui32 rawPatrolRadius = NO_PATROLLING; if(handler.saving) { - rawPatrolRadius = patrol.patrolling ? patrol.patrolRadius : NO_PATROLING; + rawPatrolRadius = patrol.patrolling ? patrol.patrolRadius : NO_PATROLLING; } - handler.serializeInt("patrolRadius", rawPatrolRadius, NO_PATROLING); + handler.serializeInt("patrolRadius", rawPatrolRadius, NO_PATROLLING); if(!handler.saving) { - patrol.patrolling = (rawPatrolRadius > NO_PATROLING); + patrol.patrolling = (rawPatrolRadius != NO_PATROLLING); patrol.initialPos = visitablePos(); - patrol.patrolRadius = (rawPatrolRadius > NO_PATROLING) ? rawPatrolRadius : 0; + patrol.patrolRadius = patrol.patrolling ? rawPatrolRadius : 0; } } } @@ -1793,14 +1849,14 @@ bool CGHeroInstance::isMissionCritical() const void CGHeroInstance::fillUpgradeInfo(UpgradeInfo & info, const CStackInstance &stack) const { - TConstBonusListPtr lista = getBonuses(Selector::typeSubtype(BonusType::SPECIAL_UPGRADE, BonusSubtypeID(stack.type->getId()))); + TConstBonusListPtr lista = getBonuses(Selector::typeSubtype(BonusType::SPECIAL_UPGRADE, BonusSubtypeID(stack.getId()))); for(const auto & it : *lista) { auto nid = CreatureID(it->additionalInfo[0]); - if (nid != stack.type->getId()) //in very specific case the upgrade is available by default (?) + if (nid != stack.getId()) //in very specific case the upgrade is available by default (?) { info.newID.push_back(nid); - info.cost.push_back(nid.toCreature()->getFullRecruitCost() - stack.type->getFullRecruitCost()); + info.cost.push_back(nid.toCreature()->getFullRecruitCost() - stack.getType()->getFullRecruitCost()); } } } @@ -1817,7 +1873,7 @@ bool CGHeroInstance::isCampaignYog() const if (!boost::starts_with(campaign, "DATA/YOG")) // "Birth of a Barbarian" return false; - if (getHeroType() != HeroTypeID::SOLMYR) // Yog (based on Solmyr) + if (getHeroTypeID() != HeroTypeID::SOLMYR) // Yog (based on Solmyr) return false; return true; @@ -1835,7 +1891,7 @@ bool CGHeroInstance::isCampaignGem() const if (!boost::starts_with(campaign, "DATA/GEM") && !boost::starts_with(campaign, "DATA/FINAL")) // "New Beginning" and "Unholy Alliance" return false; - if (getHeroType() != HeroTypeID::GEM) // Yog (based on Solmyr) + if (getHeroTypeID() != HeroTypeID::GEM) // Yog (based on Solmyr) return false; return true; @@ -1863,5 +1919,11 @@ const IOwnableObject * CGHeroInstance::asOwnable() const return this; } +int CGHeroInstance::getBasePrimarySkillValue(PrimarySkill which) const +{ + std::string cachingStr = "type_PRIMARY_SKILL_base_" + std::to_string(static_cast(which)); + auto selector = Selector::typeSubtype(BonusType::PRIMARY_SKILL, BonusSubtypeID(which)).And(Selector::sourceType()(BonusSource::HERO_BASE_SKILL)); + return valOfBonuses(selector, cachingStr); +} VCMI_LIB_NAMESPACE_END diff --git a/lib/mapObjects/CGHeroInstance.h b/lib/mapObjects/CGHeroInstance.h index a4dffa8af..e453760b8 100644 --- a/lib/mapObjects/CGHeroInstance.h +++ b/lib/mapObjects/CGHeroInstance.h @@ -14,6 +14,7 @@ #include "CArmedInstance.h" #include "IOwnableObject.h" +#include "../entities/hero/EHeroGender.h" #include "../CArtHandler.h" // For CArtifactSet VCMI_LIB_NAMESPACE_BEGIN @@ -24,7 +25,6 @@ class CGTownInstance; class CMap; struct TerrainTile; struct TurnInfo; -enum class EHeroGender : int8_t; class DLL_LINKAGE CGHeroPlaceholder : public CGObjectInstance { @@ -72,7 +72,6 @@ public: ////////////////////////////////////////////////////////////////////////// - const CHero * type; TExpType exp; //experience points ui32 level; //current level of hero @@ -93,6 +92,7 @@ public: static constexpr si32 UNINITIALIZED_MANA = -1; static constexpr ui32 UNINITIALIZED_MOVEMENT = -1; static constexpr auto UNINITIALIZED_EXPERIENCE = std::numeric_limits::max(); + static const ui32 NO_PATROLLING; //std::vector artifacts; //hero's artifacts from bag //std::map artifWorn; //map; positions: 0 - head; 1 - shoulders; 2 - neck; 3 - right hand; 4 - left hand; 5 - torso; 6 - right ring; 7 - left ring; 8 - feet; 9 - misc1; 10 - misc2; 11 - misc3; 12 - misc4; 13 - mach1; 14 - mach2; 15 - mach3; 16 - mach4; 17 - spellbook; 18 - misc5 @@ -100,10 +100,9 @@ public: struct DLL_LINKAGE Patrol { - Patrol(){patrolling=false;initialPos=int3();patrolRadius=-1;}; - bool patrolling; + bool patrolling{false}; int3 initialPos; - ui32 patrolRadius; + ui32 patrolRadius{NO_PATROLLING}; template void serialize(Handler &h) { h & patrolling; @@ -171,7 +170,7 @@ public: const IOwnableObject * asOwnable() const final; //INativeTerrainProvider - FactionID getFaction() const override; + FactionID getFactionID() const override; TerrainId getNativeTerrain() const override; int getLowestCreatureSpeed() const; si32 manaRegain() const; //how many points of mana can hero regain "naturally" in one day @@ -224,10 +223,13 @@ public: int movementPointsAfterEmbark(int MPsBefore, int basicCost, bool disembark = false, const TurnInfo * ti = nullptr) const; double getFightingStrength() const; // takes attack / defense skill into account - double getMagicStrength() const; // takes knowledge / spell power 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 + double getMagicStrengthForCampaign() const; // takes knowledge / spell power skill into account double getHeroStrength() const; // includes fighting and magic strength + double getHeroStrengthForCampaign() const; // includes fighting and the for-campaign-version of magic strength ui64 getTotalStrength() const; // includes fighting strength and army strength TExpType calculateXp(TExpType exp) const; //apply learning skill + int getBasePrimarySkillValue(PrimarySkill which) const; //the value of a base-skill without items or temporary bonuses CStackBasicDescriptor calculateNecromancy (const BattleResult &battleResult) const; void showNecromancyDialog(const CStackBasicDescriptor &raisedStack, vstd::RNG & rand) const; @@ -235,14 +237,19 @@ public: ////////////////////////////////////////////////////////////////////////// - HeroTypeID getHeroType() const; + const CHeroClass * getHeroClass() const; + HeroClassID getHeroClassID() const; + + const CHero * getHeroType() const; + HeroTypeID getHeroTypeID() const; void setHeroType(HeroTypeID type); + void initObj(vstd::RNG & rand) override; void initHero(vstd::RNG & rand); void initHero(vstd::RNG & rand, const HeroTypeID & SUBID); - ArtPlacementMap putArtifact(ArtifactPosition pos, CArtifactInstance * art) override; - void removeArtifact(ArtifactPosition pos) override; + ArtPlacementMap putArtifact(const ArtifactPosition & pos, CArtifactInstance * art) override; + void removeArtifact(const ArtifactPosition & pos) override; void initExp(vstd::RNG & rand); void initArmy(vstd::RNG & rand, IArmyDescriptor *dst = nullptr); void pushPrimSkill(PrimarySkill which, int val); @@ -297,6 +304,7 @@ public: void attachToBoat(CGBoat* newBoat); void boatDeserializationFix(); void deserializationFix(); + void updateAppearance(); void pickRandomObject(vstd::RNG & rand) override; void onHeroVisit(const CGHeroInstance * h) const override; @@ -304,6 +312,8 @@ public: std::string getHoverText(PlayerColor player) const override; std::string getMovementPointsTextIfOwner(PlayerColor player) const; + TObjectTypeHandler getObjectHandler() const override; + void afterAddToMap(CMap * map) override; void afterRemoveFromMap(CMap * map) override; @@ -352,7 +362,14 @@ public: h & skillsInfo; h & visitedTown; h & boat; - h & type; + if (h.version < Handler::Version::REMOVE_TOWN_PTR) + { + HeroTypeID type; + bool isNull = false; + h & isNull; + if(!isNull) + h & type; + } h & commander; h & visitedObjects; BONUS_TREE_DESERIALIZATION_FIX diff --git a/lib/mapObjects/CGMarket.cpp b/lib/mapObjects/CGMarket.cpp index 448eedf20..a7eef18ad 100644 --- a/lib/mapObjects/CGMarket.cpp +++ b/lib/mapObjects/CGMarket.cpp @@ -39,9 +39,25 @@ void CGMarket::onHeroVisit(const CGHeroInstance * h) const cb->showObjectWindow(this, EOpenWindowMode::MARKET_WINDOW, h, true); } +std::string CGMarket::getPopupText(PlayerColor player) const +{ + if (!getMarketHandler()->hasDescription()) + return getHoverText(player); + + MetaString message = MetaString::createFromRawString("{%s}\r\n\r\n%s"); + message.replaceName(ID); + message.replaceTextID(TextIdentifier(getObjectHandler()->getBaseTextID(), "description").get()); + return message.toString(); +} + +std::string CGMarket::getPopupText(const CGHeroInstance * hero) const +{ + return getPopupText(hero->getOwner()); +} + int CGMarket::getMarketEfficiency() const { - return marketEfficiency; + return getMarketHandler()->getMarketEfficiency(); } int CGMarket::availableUnits(EMarketMode mode, int marketItemSerial) const @@ -49,12 +65,16 @@ int CGMarket::availableUnits(EMarketMode mode, int marketItemSerial) const return -1; } -std::set CGMarket::availableModes() const +std::shared_ptr CGMarket::getMarketHandler() const { const auto & baseHandler = getObjectHandler(); const auto & ourHandler = std::dynamic_pointer_cast(baseHandler); + return ourHandler; +} - return ourHandler->availableModes(); +std::set CGMarket::availableModes() const +{ + return getMarketHandler()->availableModes(); } CGMarket::CGMarket(IGameCallback *cb): @@ -68,11 +88,8 @@ std::vector CGBlackMarket::availableItemsIds(EMarketMode mode) con case EMarketMode::RESOURCE_ARTIFACT: { std::vector ret; - for(const CArtifact *a : artifacts) - if(a) - ret.push_back(a->getId()); - else - ret.push_back(ArtifactID{}); + for(const auto & a : artifacts) + ret.push_back(a); return ret; } default: @@ -93,7 +110,7 @@ void CGBlackMarket::newTurn(vstd::RNG & rand) const SetAvailableArtifacts saa; saa.id = id; cb->pickAllowedArtsSet(saa.arts, rand); - cb->sendAndApply(&saa); + cb->sendAndApply(saa); } std::vector CGUniversity::availableItemsIds(EMarketMode mode) const @@ -108,6 +125,11 @@ std::vector CGUniversity::availableItemsIds(EMarketMode mode) cons } } +std::string CGUniversity::getSpeechTranslated() const +{ + return getMarketHandler()->getSpeechTranslated(); +} + void CGUniversity::onHeroVisit(const CGHeroInstance * h) const { cb->showObjectWindow(this, EOpenWindowMode::UNIVERSITY_WINDOW, h, true); diff --git a/lib/mapObjects/CGMarket.h b/lib/mapObjects/CGMarket.h index e1bc5b7df..6f4f92a42 100644 --- a/lib/mapObjects/CGMarket.h +++ b/lib/mapObjects/CGMarket.h @@ -15,16 +15,22 @@ VCMI_LIB_NAMESPACE_BEGIN +class MarketInstanceConstructor; + class DLL_LINKAGE CGMarket : public CGObjectInstance, public IMarket { +protected: + std::shared_ptr getMarketHandler() const; + public: - int marketEfficiency; - CGMarket(IGameCallback *cb); ///IObjectInterface void onHeroVisit(const CGHeroInstance * h) const override; //open trading window void initObj(vstd::RNG & rand) override;//set skills for trade + std::string getPopupText(PlayerColor player) const override; + std::string getPopupText(const CGHeroInstance * hero) const override; + ///IMarket ObjectInstanceID getObjInstanceID() const override; int getMarketEfficiency() const override; @@ -41,7 +47,12 @@ public: h & marketModes; } - h & marketEfficiency; + if (h.version < Handler::Version::MARKET_TRANSLATION_FIX) + { + int unused = 0; + h & unused; + } + if (h.version < Handler::Version::NEW_MARKETS) { std::string speech; @@ -63,7 +74,7 @@ class DLL_LINKAGE CGBlackMarket : public CGMarket public: using CGMarket::CGMarket; - std::vector artifacts; //available artifacts + std::vector artifacts; //available artifacts void newTurn(vstd::RNG & rand) const override; //reset artifacts for black market every month std::vector availableItemsIds(EMarketMode mode) const override; @@ -71,7 +82,24 @@ public: template void serialize(Handler &h) { h & static_cast(*this); - h & artifacts; + if (h.version < Handler::Version::REMOVE_VLC_POINTERS) + { + int32_t size = 0; + h & size; + for (int32_t i = 0; i < size; ++i) + { + bool isNull = false; + ArtifactID artifact; + h & isNull; + if (!isNull) + h & artifact; + artifacts.push_back(artifact); + } + } + else + { + h & artifacts; + } } }; @@ -79,8 +107,8 @@ class DLL_LINKAGE CGUniversity : public CGMarket { public: using CGMarket::CGMarket; - std::string speech; //currently shown only in university - std::string title; + + std::string getSpeechTranslated() const; std::vector skills; //available skills @@ -91,10 +119,11 @@ public: { h & static_cast(*this); h & skills; - if (h.version >= Handler::Version::NEW_MARKETS) + if (h.version >= Handler::Version::NEW_MARKETS && h.version < Handler::Version::MARKET_TRANSLATION_FIX) { - h & speech; - h & title; + std::string temp; + h & temp; + h & temp; } } }; diff --git a/lib/mapObjects/CGObjectInstance.cpp b/lib/mapObjects/CGObjectInstance.cpp index 5cee078fe..8511a9875 100644 --- a/lib/mapObjects/CGObjectInstance.cpp +++ b/lib/mapObjects/CGObjectInstance.cpp @@ -54,14 +54,14 @@ MapObjectSubID CGObjectInstance::getObjTypeIndex() const return subID; } -int3 CGObjectInstance::getPosition() const +int3 CGObjectInstance::anchorPos() const { return pos; } int3 CGObjectInstance::getTopVisiblePos() const { - return pos - appearance->getTopVisibleOffset(); + return anchorPos() - appearance->getTopVisibleOffset(); } void CGObjectInstance::setOwner(const PlayerColor & ow) @@ -69,6 +69,11 @@ void CGObjectInstance::setOwner(const PlayerColor & ow) tempOwner = ow; } +void CGObjectInstance::setAnchorPos(int3 newPos) +{ + pos = newPos; +} + int CGObjectInstance::getWidth() const { return appearance->getWidth(); @@ -79,32 +84,19 @@ int CGObjectInstance::getHeight() const return appearance->getHeight(); } -bool CGObjectInstance::visitableAt(int x, int y) const -{ - return appearance->isVisitableAt(pos.x - x, pos.y - y); -} -bool CGObjectInstance::blockingAt(int x, int y) const -{ - return appearance->isBlockedAt(pos.x - x, pos.y - y); -} - -bool CGObjectInstance::coveringAt(int x, int y) const -{ - return appearance->isVisibleAt(pos.x - x, pos.y - y); -} - bool CGObjectInstance::visitableAt(const int3 & testPos) const { - return pos.z == testPos.z && appearance->isVisitableAt(pos.x - testPos.x, pos.y - testPos.y); + return anchorPos().z == testPos.z && appearance->isVisitableAt(anchorPos().x - testPos.x, anchorPos().y - testPos.y); } + bool CGObjectInstance::blockingAt(const int3 & testPos) const { - return pos.z == testPos.z && appearance->isBlockedAt(pos.x - testPos.x, pos.y - testPos.y); + return anchorPos().z == testPos.z && appearance->isBlockedAt(anchorPos().x - testPos.x, anchorPos().y - testPos.y); } bool CGObjectInstance::coveringAt(const int3 & testPos) const { - return pos.z == testPos.z && appearance->isVisibleAt(pos.x - testPos.x, pos.y - testPos.y); + return anchorPos().z == testPos.z && appearance->isVisibleAt(anchorPos().x - testPos.x, anchorPos().y - testPos.y); } std::set CGObjectInstance::getBlockedPos() const @@ -115,7 +107,7 @@ std::set CGObjectInstance::getBlockedPos() const for(int h=0; hisBlockedAt(w, h)) - ret.insert(int3(pos.x - w, pos.y - h, pos.z)); + ret.insert(int3(anchorPos().x - w, anchorPos().y - h, anchorPos().z)); } } return ret; @@ -136,13 +128,13 @@ void CGObjectInstance::setType(MapObjectID newID, MapObjectSubID newSubID) cb->gameState()->map->removeBlockVisTiles(this, true); auto handler = VLC->objtypeh->getHandlerFor(newID, newSubID); - if(!handler->getTemplates(tile.terType->getId()).empty()) + if(!handler->getTemplates(tile.getTerrainID()).empty()) { - appearance = handler->getTemplates(tile.terType->getId())[0]; + appearance = handler->getTemplates(tile.getTerrainID())[0]; } else { - logGlobal->warn("Object %d:%d at %s has no templates suitable for terrain %s", newID, newSubID, visitablePos().toString(), tile.terType->getNameTranslated()); + logGlobal->warn("Object %d:%d at %s has no templates suitable for terrain %s", newID, newSubID, visitablePos().toString(), tile.getTerrain()->getNameTranslated()); appearance = handler->getTemplates()[0]; // get at least some appearance since alternative is crash } @@ -200,6 +192,16 @@ TObjectTypeHandler CGObjectInstance::getObjectHandler() const return VLC->objtypeh->getHandlerFor(ID, subID); } +std::string CGObjectInstance::getTypeName() const +{ + return getObjectHandler()->getTypeName(); +} + +std::string CGObjectInstance::getSubtypeName() const +{ + return getObjectHandler()->getSubTypeName(); +} + void CGObjectInstance::setPropertyDer( ObjProperty what, ObjPropertyID identifier ) {} @@ -215,6 +217,8 @@ int CGObjectInstance::getSightRadius() const int3 CGObjectInstance::getVisitableOffset() const { + if (!isVisitable()) + logGlobal->debug("Attempt to access visitable offset on a non-visitable object!"); return appearance->getVisitableOffset(); } @@ -313,6 +317,9 @@ void CGObjectInstance::onHeroVisit( const CGHeroInstance * h ) const int3 CGObjectInstance::visitablePos() const { + if (!isVisitable()) + logGlobal->debug("Attempt to access visitable position on a non-visitable object!"); + return pos - getVisitableOffset(); } @@ -353,8 +360,11 @@ void CGObjectInstance::serializeJson(JsonSerializeFormat & handler) //only save here, loading is handled by map loader if(handler.saving) { - handler.serializeString("type", typeName); - handler.serializeString("subtype", subTypeName); + std::string ourTypeName = getTypeName(); + std::string ourSubtypeName = getSubtypeName(); + + handler.serializeString("type", ourTypeName); + handler.serializeString("subtype", ourSubtypeName); handler.serializeInt("x", pos.x); handler.serializeInt("y", pos.y); diff --git a/lib/mapObjects/CGObjectInstance.h b/lib/mapObjects/CGObjectInstance.h index 130077ae1..c201dd4db 100644 --- a/lib/mapObjects/CGObjectInstance.h +++ b/lib/mapObjects/CGObjectInstance.h @@ -28,8 +28,6 @@ using TObjectTypeHandler = std::shared_ptr; class DLL_LINKAGE CGObjectInstance : public IObjectInterface { public: - /// Position of bottom-right corner of object on map - int3 pos; /// Type of object, e.g. town, hero, creature. MapObjectID ID; /// Subtype of object, depends on type @@ -41,9 +39,10 @@ public: /// Defines appearance of object on map (animation, blocked tiles, blit order, etc) std::shared_ptr appearance; + /// Position of bottom-right corner of object on map + int3 pos; + std::string instanceName; - std::string typeName; - std::string subTypeName; CGObjectInstance(IGameCallback *cb); ~CGObjectInstance() override; @@ -51,6 +50,9 @@ public: MapObjectID getObjGroupIndex() const override; MapObjectSubID getObjTypeIndex() const override; + std::string getTypeName() const; + std::string getSubtypeName() const; + /// "center" tile from which the sight distance is calculated int3 getSightCenter() const; /// If true hero can visit this object only from neighbouring tiles and can't stand on this object @@ -62,21 +64,19 @@ public: return this->tempOwner; } void setOwner(const PlayerColor & ow); + void setAnchorPos(int3 pos); /** APPEARANCE ACCESSORS **/ int getWidth() const; //returns width of object graphic in tiles int getHeight() const; //returns height of object graphic in tiles int3 visitablePos() const override; - int3 getPosition() const override; + int3 anchorPos() const override; int3 getTopVisiblePos() const; - bool visitableAt(int x, int y) const; //returns true if object is visitable at location (x, y) (h3m pos) - bool blockingAt(int x, int y) const; //returns true if object is blocking location (x, y) (h3m pos) - bool coveringAt(int x, int y) const; //returns true if object covers with picture location (x, y) (h3m pos) - bool visitableAt(const int3 & pos) const; //returns true if object is visitable at location (x, y) (h3m pos) - bool blockingAt (const int3 & pos) const; //returns true if object is blocking location (x, y) (h3m pos) - bool coveringAt (const int3 & pos) const; //returns true if object covers with picture location (x, y) (h3m pos) + bool visitableAt(const int3 & pos) const; //returns true if object is visitable at location + bool blockingAt (const int3 & pos) const; //returns true if object is blocking location + bool coveringAt (const int3 & pos) const; //returns true if object covers with picture location std::set getBlockedPos() const; //returns set of positions blocked by this object const std::set & getBlockedOffsets() const; //returns set of relative positions blocked by this object @@ -101,7 +101,7 @@ public: std::optional getVisitSound(vstd::RNG & rng) const; std::optional getRemovalSound(vstd::RNG & rng) const; - TObjectTypeHandler getObjectHandler() const; + virtual TObjectTypeHandler getObjectHandler() const; /** VIRTUAL METHODS **/ @@ -143,8 +143,12 @@ public: template void serialize(Handler &h) { h & instanceName; - h & typeName; - h & subTypeName; + if (h.version < Handler::Version::REMOVE_OBJECT_TYPENAME) + { + std::string unused; + h & unused; + h & unused; + } h & pos; h & ID; subID.serializeIdentifier(h, ID); diff --git a/lib/mapObjects/CGTownInstance.cpp b/lib/mapObjects/CGTownInstance.cpp index b76287d22..e1543f553 100644 --- a/lib/mapObjects/CGTownInstance.cpp +++ b/lib/mapObjects/CGTownInstance.cpp @@ -45,7 +45,7 @@ int CGTownInstance::getSightRadius() const //returns sight distance for(const auto & bid : builtBuildings) { - auto height = town->buildings.at(bid)->height; + auto height = getTown()->buildings.at(bid)->height; if(ret < height) ret = height; } @@ -115,7 +115,7 @@ int CGTownInstance::mageGuildLevel() const int CGTownInstance::getHordeLevel(const int & HID) const//HID - 0 or 1; returns creature level or -1 if that horde structure is not present { - return town->hordeLvl.at(HID); + return getTown()->hordeLvl.at(HID); } int CGTownInstance::creatureGrowth(const int & level) const @@ -127,7 +127,7 @@ GrowthInfo CGTownInstance::getGrowthInfo(int level) const { GrowthInfo ret; - if (level<0 || level >=town->creatures.size()) + if (level<0 || level >=getTown()->creatures.size()) return ret; if (creatures[level].second.empty()) return ret; //no dwelling @@ -151,11 +151,11 @@ GrowthInfo CGTownInstance::getGrowthInfo(int level) const else if (hasBuilt(BuildingID::CITADEL)) ret.entries.emplace_back(subID, BuildingID::CITADEL, castleBonus = base / 2); - if(town->hordeLvl.at(0) == level)//horde 1 + if(getTown()->hordeLvl.at(0) == level)//horde 1 if(hasBuilt(BuildingID::HORDE_1)) ret.entries.emplace_back(subID, BuildingID::HORDE_1, creature->getHorde()); - if(town->hordeLvl.at(1) == level)//horde 2 + if(getTown()->hordeLvl.at(1) == level)//horde 2 if(hasBuilt(BuildingID::HORDE_2)) ret.entries.emplace_back(subID, BuildingID::HORDE_2, creature->getHorde()); @@ -166,7 +166,7 @@ GrowthInfo CGTownInstance::getGrowthInfo(int level) const const auto growth = b->val * (base + castleBonus) / 100; if (growth) { - ret.entries.emplace_back(growth, b->Description(growth)); + ret.entries.emplace_back(growth, b->Description(cb, growth)); } } @@ -174,7 +174,7 @@ GrowthInfo CGTownInstance::getGrowthInfo(int level) const // Note: bonus uses 1-based levels (Pikeman is level 1), town list uses 0-based (Pikeman in 0-th creatures entry) TConstBonusListPtr bonuses = getBonuses(Selector::typeSubtype(BonusType::CREATURE_GROWTH, BonusCustomSubtype::creatureLevel(level+1))); for(const auto & b : *bonuses) - ret.entries.emplace_back(b->val, b->Description()); + ret.entries.emplace_back(b->val, b->Description(cb)); int dwellingBonus = 0; if(const PlayerState *p = cb->getPlayerState(tempOwner, false)) @@ -209,11 +209,11 @@ int CGTownInstance::getDwellingBonus(const std::vector& creatureIds, TResources CGTownInstance::dailyIncome() const { TResources ret; - for(const auto & p : town->buildings) + for(const auto & p : getTown()->buildings) { BuildingID buildingUpgrade; - for(const auto & p2 : town->buildings) + for(const auto & p2 : getTown()->buildings) { if (p2.second->upgrade == p.first) { @@ -251,10 +251,10 @@ bool CGTownInstance::hasCapitol() const TownFortifications CGTownInstance::fortificationsLevel() const { - auto result = town->fortifications; + auto result = getTown()->fortifications; for (auto const & buildingID : builtBuildings) - result += town->buildings.at(buildingID)->fortifications; + result += getTown()->buildings.at(buildingID)->fortifications; if (result.wallsHealth == 0) return TownFortifications(); @@ -264,11 +264,13 @@ TownFortifications CGTownInstance::fortificationsLevel() const CGTownInstance::CGTownInstance(IGameCallback *cb): CGDwelling(cb), - town(nullptr), built(0), destroyed(0), identifier(0), - alignmentToPlayer(PlayerColor::NEUTRAL) + alignmentToPlayer(PlayerColor::NEUTRAL), + spellResearchCounterDay(0), + spellResearchAcceptedCounter(0), + spellResearchAllowed(true) { this->setNodeType(CBonusSystemNode::TOWN); } @@ -347,7 +349,7 @@ void CGTownInstance::onHeroVisit(const CGHeroInstance * h) const scp.heroid = h->id; scp.which = SetCommanderProperty::ALIVE; scp.amount = 1; - cb->sendAndApply(&scp); + cb->sendAndApply(scp); } cb->heroVisitCastle(this, h); // TODO(vmarkovtsev): implement payment for rising the commander @@ -376,17 +378,17 @@ void CGTownInstance::onHeroLeave(const CGHeroInstance * h) const std::string CGTownInstance::getObjectName() const { - return getNameTranslated() + ", " + (ID == Obj::RANDOM_TOWN ? "Random town" : getFaction().toEntity(VLC)->getNameTranslated()); + return getNameTranslated() + ", " + (ID == Obj::RANDOM_TOWN ? "Random town" : getTown()->faction->getNameTranslated()); } bool CGTownInstance::townEnvisagesBuilding(BuildingSubID::EBuildingSubID subId) const { - return town->getBuildingType(subId) != BuildingID::NONE; + return getTown()->getBuildingType(subId) != BuildingID::NONE; } void CGTownInstance::initializeConfigurableBuildings(vstd::RNG & rand) { - for(const auto & kvp : town->buildings) + for(const auto & kvp : getTown()->buildings) { if(!kvp.second->rewardableObjectInfo.getParameters().isNull()) rewardableBuildings[kvp.first] = new TownRewardableBuildingInstance(this, kvp.second->bid, rand); @@ -454,8 +456,7 @@ void CGTownInstance::pickRandomObject(vstd::RNG & rand) assert(ID == Obj::TOWN); // just in case setType(ID, subID); - town = (*VLC->townh)[getFaction()]->town; - randomizeArmy(getFaction()); + randomizeArmy(getFactionID()); updateAppearance(); } @@ -464,19 +465,19 @@ void CGTownInstance::initObj(vstd::RNG & rand) ///initialize town structures blockVisit = true; if(townEnvisagesBuilding(BuildingSubID::PORTAL_OF_SUMMONING)) //Dungeon for example - creatures.resize(town->creatures.size() + 1); + creatures.resize(getTown()->creatures.size() + 1); else - creatures.resize(town->creatures.size()); + creatures.resize(getTown()->creatures.size()); - for (int level = 0; level < town->creatures.size(); level++) + for (int level = 0; level < getTown()->creatures.size(); level++) { BuildingID buildID = BuildingID(BuildingID::getDwellingFromLevel(level, 0)); int upgradeNum = 0; - for (; town->buildings.count(buildID); upgradeNum++, BuildingID::advanceDwelling(buildID)) + for (; getTown()->buildings.count(buildID); upgradeNum++, BuildingID::advanceDwelling(buildID)) { - if (hasBuilt(buildID) && town->creatures.at(level).size() > upgradeNum) - creatures[level].second.push_back(town->creatures[level][upgradeNum]); + if (hasBuilt(buildID) && getTown()->creatures.at(level).size() > upgradeNum) + creatures[level].second.push_back(getTown()->creatures[level][upgradeNum]); } } initializeConfigurableBuildings(rand); @@ -620,15 +621,15 @@ void CGTownInstance::removeCapitols(const PlayerColor & owner) const if (hasCapitol()) // search if there's an older capitol { PlayerState* state = cb->gameState()->getPlayerState(owner); //get all towns owned by player - for (const auto & town : state->getTowns()) + for (const auto & otherTown : state->getTowns()) { - if (town != this && town->hasCapitol()) + if (otherTown != this && otherTown->hasCapitol()) { RazeStructures rs; rs.tid = id; rs.bid.insert(BuildingID::CAPITOL); rs.destroyed = destroyed; - cb->sendAndApply(&rs); + cb->sendAndApply(rs); return; } } @@ -645,7 +646,7 @@ void CGTownInstance::clearArmy() const BoatId CGTownInstance::getBoatType() const { - return town->faction->boatType; + return getTown()->faction->boatType; } int CGTownInstance::getMarketEfficiency() const @@ -669,11 +670,9 @@ std::vector CGTownInstance::availableItemsIds(EMarketMode mode) co if(mode == EMarketMode::RESOURCE_ARTIFACT) { std::vector ret; - for(const CArtifact *a : cb->gameState()->map->townMerchantArtifacts) - if(a) - ret.push_back(a->getId()); - else - ret.push_back(ArtifactID{}); + for(const ArtifactID a : cb->gameState()->map->townMerchantArtifacts) + ret.push_back(a); + return ret; } else if ( mode == EMarketMode::RESOURCE_SKILL ) @@ -691,7 +690,7 @@ ObjectInstanceID CGTownInstance::getObjInstanceID() const void CGTownInstance::updateAppearance() { - auto terrain = cb->gameState()->getTile(visitablePos())->terType->getId(); + auto terrain = cb->gameState()->getTile(visitablePos())->getTerrainID(); //FIXME: not the best way to do this auto app = getObjectHandler()->getOverride(terrain, this); if (app) @@ -700,7 +699,7 @@ void CGTownInstance::updateAppearance() std::string CGTownInstance::nodeName() const { - return "Town (" + (town ? town->faction->getNameTranslated() : "unknown") + ") of " + getNameTranslated(); + return "Town (" + getTown()->faction->getNameTranslated() + ") of " + getNameTranslated(); } void CGTownInstance::deserializationFix() @@ -749,7 +748,7 @@ void CGTownInstance::recreateBuildingsBonuses() for(const auto & upgradeID : builtBuildings) { - const auto & upgrade = town->buildings.at(upgradeID); + const auto & upgrade = getTown()->buildings.at(upgradeID); if (upgrade->getBase() == bid && upgrade->upgradeReplacesBonuses) bonusesReplacedByUpgrade = true; } @@ -758,7 +757,7 @@ void CGTownInstance::recreateBuildingsBonuses() if (bonusesReplacedByUpgrade) continue; - auto building = town->buildings.at(bid); + auto building = getTown()->buildings.at(bid); if(building->buildingBonuses.empty()) continue; @@ -825,21 +824,6 @@ bool CGTownInstance::armedGarrison() const return !stacks.empty() || garrisonHero; } -const CTown * CGTownInstance::getTown() const -{ - if(ID == Obj::RANDOM_TOWN) - return VLC->townh->randomTown; - else - { - if(nullptr == town) - { - return (*VLC->townh)[getFaction()]->town; - } - else - return town; - } -} - int CGTownInstance::getTownLevel() const { // count all buildings that are not upgrades @@ -847,7 +831,7 @@ int CGTownInstance::getTownLevel() const for(const auto & bid : builtBuildings) { - if(town->buildings.at(bid)->upgrade == BuildingID::NONE) + if(getTown()->buildings.at(bid)->upgrade == BuildingID::NONE) level++; } return level; @@ -889,7 +873,7 @@ bool CGTownInstance::hasBuilt(BuildingSubID::EBuildingSubID buildingID) const { for(const auto & bid : builtBuildings) { - if(town->buildings.at(bid)->subId == buildingID) + if(getTown()->buildings.at(bid)->subId == buildingID) return true; } return false; @@ -902,7 +886,7 @@ bool CGTownInstance::hasBuilt(const BuildingID & buildingID) const bool CGTownInstance::hasBuilt(const BuildingID & buildingID, FactionID townID) const { - if (townID == town->faction->getId() || townID == FactionID::ANY) + if (townID == getTown()->faction->getId() || townID == FactionID::ANY) return hasBuilt(buildingID); return false; } @@ -920,7 +904,7 @@ std::set CGTownInstance::availableModes() const std::set result; for (const auto & buildingID : builtBuildings) { - const auto * buildingPtr = town->buildings.at(buildingID).get(); + const auto * buildingPtr = getTown()->buildings.at(buildingID).get(); result.insert(buildingPtr->marketModes.begin(), buildingPtr->marketModes.end()); } @@ -947,11 +931,11 @@ std::set CGTownInstance::getBuildings() const TResources CGTownInstance::getBuildingCost(const BuildingID & buildingID) const { - if (vstd::contains(town->buildings, buildingID)) - return town->buildings.at(buildingID)->resources; + if (vstd::contains(getTown()->buildings, buildingID)) + return getTown()->buildings.at(buildingID)->resources; else { - logGlobal->error("Town %s at %s has no possible building %d!", getNameTranslated(), pos.toString(), buildingID.toEnum()); + logGlobal->error("Town %s at %s has no possible building %d!", getNameTranslated(), anchorPos().toString(), buildingID.toEnum()); return TResources(); } @@ -959,7 +943,7 @@ TResources CGTownInstance::getBuildingCost(const BuildingID & buildingID) const CBuilding::TRequired CGTownInstance::genBuildingRequirements(const BuildingID & buildID, bool deep) const { - const CBuilding * building = town->buildings.at(buildID); + const CBuilding * building = getTown()->buildings.at(buildID); //TODO: find better solution to prevent infinite loops std::set processed; @@ -967,13 +951,13 @@ CBuilding::TRequired CGTownInstance::genBuildingRequirements(const BuildingID & std::function dependTest = [&](const BuildingID & id) -> CBuilding::TRequired::Variant { - if (town->buildings.count(id) == 0) + if (getTown()->buildings.count(id) == 0) { logMod->error("Invalid building ID %d in building dependencies!", id.getNum()); return CBuilding::TRequired::OperatorAll(); } - const CBuilding * build = town->buildings.at(id); + const CBuilding * build = getTown()->buildings.at(id); CBuilding::TRequired::OperatorAll requirements; if (!hasBuilt(id)) @@ -998,7 +982,7 @@ CBuilding::TRequired CGTownInstance::genBuildingRequirements(const BuildingID & CBuilding::TRequired::OperatorAll requirements; if (building->upgrade != BuildingID::NONE) { - const CBuilding * upgr = town->buildings.at(building->upgrade); + const CBuilding * upgr = getTown()->buildings.at(building->upgrade); requirements.expressions.push_back(dependTest(upgr->bid)); processed.clear(); @@ -1148,14 +1132,27 @@ void CGTownInstance::serializeJsonOptions(JsonSerializeFormat & handler) } } -FactionID CGTownInstance::getFaction() const +const CFaction * CGTownInstance::getFaction() const { - return FactionID(subID.getNum()); + return getFactionID().toFaction(); +} + +const CTown * CGTownInstance::getTown() const +{ + if(ID == Obj::RANDOM_TOWN) + return VLC->townh->randomTown; + + return getFaction()->town; +} + +FactionID CGTownInstance::getFactionID() const +{ + return FactionID(subID.getNum()); } TerrainId CGTownInstance::getNativeTerrain() const { - return town->faction->getNativeTerrain(); + return getTown()->faction->getNativeTerrain(); } ArtifactID CGTownInstance::getWarMachineInBuilding(BuildingID building) const @@ -1163,21 +1160,21 @@ ArtifactID CGTownInstance::getWarMachineInBuilding(BuildingID building) const if (builtBuildings.count(building) == 0) return ArtifactID::NONE; - if (building == BuildingID::BLACKSMITH && town->warMachineDeprecated.hasValue()) - return town->warMachineDeprecated.toCreature()->warMachine; + if (building == BuildingID::BLACKSMITH && getTown()->warMachineDeprecated.hasValue()) + return getTown()->warMachineDeprecated.toCreature()->warMachine; - return town->buildings.at(building)->warMachine; + return getTown()->buildings.at(building)->warMachine; } bool CGTownInstance::isWarMachineAvailable(ArtifactID warMachine) const { for (auto const & buildingID : builtBuildings) - if (town->buildings.at(buildingID)->warMachine == warMachine) + if (getTown()->buildings.at(buildingID)->warMachine == warMachine) return true; if (builtBuildings.count(BuildingID::BLACKSMITH) && - town->warMachineDeprecated.hasValue() && - town->warMachineDeprecated.toCreature()->warMachine == warMachine) + getTown()->warMachineDeprecated.hasValue() && + getTown()->warMachineDeprecated.toCreature()->warMachine == warMachine) return true; return false; @@ -1197,7 +1194,7 @@ GrowthInfo::Entry::Entry(int subID, const BuildingID & building, int _count): co { MetaString formatter; formatter.appendRawString("%s %+d"); - formatter.replaceRawString((*VLC->townh)[subID]->town->buildings.at(building)->getNameTranslated()); + formatter.replaceRawString(FactionID(subID).toFaction()->town->buildings.at(building)->getNameTranslated()); formatter.replacePositiveNumber(count); description = formatter.toString(); @@ -1228,14 +1225,14 @@ void CGTownInstance::fillUpgradeInfo(UpgradeInfo & info, const CStackInstance &s { for(const CGTownInstance::TCreaturesSet::value_type & dwelling : creatures) { - if (vstd::contains(dwelling.second, stack.type->getId())) //Dwelling with our creature + if (vstd::contains(dwelling.second, stack.getId())) //Dwelling with our creature { for(const auto & upgrID : dwelling.second) { - if(vstd::contains(stack.type->upgrades, upgrID)) //possible upgrade + if(vstd::contains(stack.getCreature()->upgrades, upgrID)) //possible upgrade { info.newID.push_back(upgrID); - info.cost.push_back(upgrID.toCreature()->getFullRecruitCost() - stack.type->getFullRecruitCost()); + info.cost.push_back(upgrID.toCreature()->getFullRecruitCost() - stack.getType()->getFullRecruitCost()); } } } diff --git a/lib/mapObjects/CGTownInstance.h b/lib/mapObjects/CGTownInstance.h index fab98714e..64eeaf9e2 100644 --- a/lib/mapObjects/CGTownInstance.h +++ b/lib/mapObjects/CGTownInstance.h @@ -50,18 +50,16 @@ struct DLL_LINKAGE GrowthInfo class DLL_LINKAGE CGTownInstance : public CGDwelling, public IShipyard, public IMarket, public INativeTerrainProvider, public ICreatureUpgrader { + friend class CTownInstanceConstructor; std::string nameTextId; // name of town std::map convertOldBuildings(std::vector oldVector); std::set builtBuildings; public: - using CGDwelling::getPosition; - enum EFortLevel {NONE = 0, FORT = 1, CITADEL = 2, CASTLE = 3}; CTownAndVisitingHero townAndVis; - const CTown * town; si32 built; //how many buildings has been built this turn si32 destroyed; //how many buildings has been destroyed this turn ConstTransitivePtr garrisonHero, visitingHero; @@ -73,6 +71,9 @@ public: std::vector > spells; //spells[level] -> vector of spells, first will be available in guild std::vector events; std::pair bonusValue;//var to store town bonuses (rampart = resources from mystic pond, factory = save debts); + int spellResearchCounterDay; + int spellResearchAcceptedCounter; + bool spellResearchAllowed; ////////////////////////////////////////////////////////////////////////// template void serialize(Handler &h) @@ -93,6 +94,13 @@ public: h & spells; h & events; + if (h.version >= Handler::Version::SPELL_RESEARCH) + { + h & spellResearchCounterDay; + h & spellResearchAcceptedCounter; + h & spellResearchAllowed; + } + if (h.version >= Handler::Version::NEW_TOWN_BUILDINGS) { h & rewardableBuildings; @@ -104,16 +112,13 @@ public: rewardableBuildings = convertOldBuildings(oldVector); } - if (h.saving) + if (h.version < Handler::Version::REMOVE_TOWN_PTR) { - CFaction * faction = town ? town->faction : nullptr; - h & faction; - } - else - { - CFaction * faction = nullptr; - h & faction; - town = faction ? faction->town : nullptr; + FactionID faction; + bool isNull = false; + h & isNull; + if (!isNull) + h & faction; } h & townAndVis; @@ -205,9 +210,10 @@ public: DamageRange getKeepDamageRange() const; const CTown * getTown() const; + const CFaction * getFaction() const; /// INativeTerrainProvider - FactionID getFaction() const override; + FactionID getFactionID() const override; TerrainId getNativeTerrain() const override; /// Returns ID of war machine that is produced by specified building or NONE if this is not built or if building does not produce war machines diff --git a/lib/mapObjects/CQuest.cpp b/lib/mapObjects/CQuest.cpp index 15a9153e8..9e2e83c47 100644 --- a/lib/mapObjects/CQuest.cpp +++ b/lib/mapObjects/CQuest.cpp @@ -16,9 +16,9 @@ #include "../ArtifactUtils.h" #include "../CSoundBase.h" #include "../texts/CGeneralTextHandler.h" -#include "../CHeroHandler.h" #include "CGCreature.h" #include "../IGameCallback.h" +#include "../entities/hero/CHeroHandler.h" #include "../mapObjectConstructors/CObjectClassesHandler.h" #include "../serializer/JsonSerializeFormat.h" #include "../GameConstants.h" @@ -110,7 +110,7 @@ bool CQuest::checkMissionArmy(const CQuest * q, const CCreatureSet * army) { for(count = 0, it = army->Slots().begin(); it != army->Slots().end(); ++it) { - if(it->second->type == cre->type) + if(it->second->getType() == cre->getType()) { count += it->second->count; slotsCount++; @@ -152,7 +152,7 @@ void CQuest::completeQuest(IGameCallback * cb, const CGHeroInstance *h) const } else { - const auto * assembly = h->getAssemblyByConstituent(elem); + const auto * assembly = h->getCombinedArtWithPart(elem); assert(assembly); auto parts = assembly->getPartsInfo(); @@ -431,7 +431,7 @@ void CGSeerHut::setObjToKill() if(getCreatureToKill(true)) { - quest->stackToKill = getCreatureToKill(false)->getCreature(); + quest->stackToKill = getCreatureToKill(false)->getCreatureID(); assert(quest->stackToKill != CreatureID::NONE); quest->stackDirection = checkDirection(); } @@ -588,7 +588,7 @@ void CGSeerHut::onHeroVisit(const CGHeroInstance * h) const AddQuest aq; aq.quest = QuestInfo (quest, this, visitablePos()); aq.player = h->tempOwner; - cb->sendAndApply(&aq); //TODO: merge with setObjProperty? + cb->sendAndApply(aq); //TODO: merge with setObjProperty? } if(firstVisit || failRequirements) @@ -614,7 +614,7 @@ void CGSeerHut::onHeroVisit(const CGHeroInstance * h) const int CGSeerHut::checkDirection() const { - int3 cord = getCreatureToKill(false)->pos; + int3 cord = getCreatureToKill(false)->visitablePos(); if(static_cast(cord.x) / static_cast(cb->getMapSize().x) < 0.34) //north { if(static_cast(cord.y) / static_cast(cb->getMapSize().y) < 0.34) //northwest @@ -811,7 +811,7 @@ void CGKeymasterTent::onHeroVisit( const CGHeroInstance * h ) const cow.mode = ChangeObjectVisitors::VISITOR_GLOBAL; cow.hero = h->id; cow.object = id; - cb->sendAndApply(&cow); + cb->sendAndApply(cow); txt_id=19; } else @@ -860,7 +860,7 @@ void CGBorderGuard::onHeroVisit(const CGHeroInstance * h) const AddQuest aq; aq.quest = QuestInfo (quest, this, visitablePos()); aq.player = h->tempOwner; - cb->sendAndApply (&aq); + cb->sendAndApply(aq); //TODO: add this quest only once OR check for multiple instances later } } @@ -885,7 +885,7 @@ void CGBorderGate::onHeroVisit(const CGHeroInstance * h) const //TODO: passabili AddQuest aq; aq.quest = QuestInfo (quest, this, visitablePos()); aq.player = h->tempOwner; - cb->sendAndApply (&aq); + cb->sendAndApply(aq); } } diff --git a/lib/mapObjects/CRewardableObject.cpp b/lib/mapObjects/CRewardableObject.cpp index 90c02aacb..9c0d67378 100644 --- a/lib/mapObjects/CRewardableObject.cpp +++ b/lib/mapObjects/CRewardableObject.cpp @@ -12,99 +12,33 @@ #include "CRewardableObject.h" #include "../CPlayerState.h" -#include "../GameSettings.h" #include "../IGameCallback.h" +#include "../IGameSettings.h" #include "../battle/BattleLayout.h" #include "../gameState/CGameState.h" #include "../mapObjectConstructors/AObjectTypeHandler.h" -#include "../mapObjectConstructors/CObjectClassesHandler.h" #include "../mapObjectConstructors/CRewardableConstructor.h" #include "../mapObjects/CGHeroInstance.h" #include "../networkPacks/PacksForClient.h" #include "../networkPacks/PacksForClientBattle.h" #include "../serializer/JsonSerializeFormat.h" -#include "../texts/CGeneralTextHandler.h" #include VCMI_LIB_NAMESPACE_BEGIN -void CRewardableObject::grantRewardWithMessage(const CGHeroInstance * contextHero, int index, bool markAsVisit) const +const IObjectInterface * CRewardableObject::getObject() const { - auto vi = configuration.info.at(index); - logGlobal->debug("Granting reward %d. Message says: %s", index, vi.message.toString()); - // show message only if it is not empty or in infobox - if (configuration.infoWindowType != EInfoWindowMode::MODAL || !vi.message.toString().empty()) - { - InfoWindow iw; - iw.player = contextHero->tempOwner; - iw.text = vi.message; - vi.reward.loadComponents(iw.components, contextHero); - iw.type = configuration.infoWindowType; - if(!iw.components.empty() || !iw.text.toString().empty()) - cb->showInfoDialog(&iw); - } - // grant reward afterwards. Note that it may remove object - if(markAsVisit) - markAsVisited(contextHero); - grantReward(index, contextHero); + return this; } -void CRewardableObject::selectRewardWithMessage(const CGHeroInstance * contextHero, const std::vector & rewardIndices, const MetaString & dialog) const +void CRewardableObject::markAsScouted(const CGHeroInstance * hero) const { - BlockingDialog sd(configuration.canRefuse, rewardIndices.size() > 1); - sd.player = contextHero->tempOwner; - sd.text = dialog; - sd.components = loadComponents(contextHero, rewardIndices); - cb->showBlockingDialog(this, &sd); - + ChangeObjectVisitors cov(ChangeObjectVisitors::VISITOR_ADD_PLAYER, id, hero->id); + cb->sendAndApply(cov); } -void CRewardableObject::grantAllRewardsWithMessage(const CGHeroInstance * contextHero, const std::vector & rewardIndices, bool markAsVisit) const -{ - if (rewardIndices.empty()) - return; - - for (auto index : rewardIndices) - { - // TODO: Merge all rewards of same type, with single message? - grantRewardWithMessage(contextHero, index, false); - } - // Mark visited only after all rewards were processed - if(markAsVisit) - markAsVisited(contextHero); -} - -std::vector CRewardableObject::loadComponents(const CGHeroInstance * contextHero, const std::vector & rewardIndices) const -{ - std::vector result; - - if (rewardIndices.empty()) - return result; - - if (configuration.selectMode != Rewardable::SELECT_FIRST && rewardIndices.size() > 1) - { - for (auto index : rewardIndices) - result.push_back(configuration.info.at(index).reward.getDisplayedComponent(contextHero)); - } - else - { - configuration.info.at(rewardIndices.front()).reward.loadComponents(result, contextHero); - } - - return result; -} - -bool CRewardableObject::guardedPotentially() const -{ - for (auto const & visitInfo : configuration.info) - if (!visitInfo.reward.guards.empty()) - return true; - - return false; -} - -bool CRewardableObject::guardedPresently() const +bool CRewardableObject::isGuarded() const { return stacksCount() > 0; } @@ -114,10 +48,10 @@ void CRewardableObject::onHeroVisit(const CGHeroInstance *hero) const if(!wasScouted(hero->getOwner())) { ChangeObjectVisitors cov(ChangeObjectVisitors::VISITOR_SCOUTED, id, hero->id); - cb->sendAndApply(&cov); + cb->sendAndApply(cov); } - if (guardedPresently()) + if (isGuarded()) { auto guardedIndexes = getAvailableRewards(hero, Rewardable::EEventType::EVENT_GUARDED); auto guardedReward = configuration.info.at(guardedIndexes.at(0)); @@ -136,94 +70,9 @@ void CRewardableObject::onHeroVisit(const CGHeroInstance *hero) const } } -void CRewardableObject::doHeroVisit(const CGHeroInstance *h) const -{ - if(!wasVisitedBefore(h)) - { - auto rewards = getAvailableRewards(h, Rewardable::EEventType::EVENT_FIRST_VISIT); - bool objectRemovalPossible = false; - for(auto index : rewards) - { - if(configuration.info.at(index).reward.removeObject) - objectRemovalPossible = true; - } - - logGlobal->debug("Visiting object with %d possible rewards", rewards.size()); - switch (rewards.size()) - { - case 0: // no available rewards, e.g. visiting School of War without gold - { - auto emptyRewards = getAvailableRewards(h, Rewardable::EEventType::EVENT_NOT_AVAILABLE); - if (!emptyRewards.empty()) - grantRewardWithMessage(h, emptyRewards[0], false); - else - logMod->warn("No applicable message for visiting empty object!"); - break; - } - case 1: // one reward. Just give it with message - { - if (configuration.canRefuse) - selectRewardWithMessage(h, rewards, configuration.info.at(rewards.front()).message); - else - grantRewardWithMessage(h, rewards.front(), true); - break; - } - default: // multiple rewards. Act according to select mode - { - switch (configuration.selectMode) { - case Rewardable::SELECT_PLAYER: // player must select - selectRewardWithMessage(h, rewards, configuration.onSelect); - break; - case Rewardable::SELECT_FIRST: // give first available - if (configuration.canRefuse) - selectRewardWithMessage(h, { rewards.front() }, configuration.info.at(rewards.front()).message); - else - grantRewardWithMessage(h, rewards.front(), true); - break; - case Rewardable::SELECT_RANDOM: // give random - { - ui32 rewardIndex = *RandomGeneratorUtil::nextItem(rewards, cb->gameState()->getRandomGenerator()); - if (configuration.canRefuse) - selectRewardWithMessage(h, { rewardIndex }, configuration.info.at(rewardIndex).message); - else - grantRewardWithMessage(h, rewardIndex, true); - break; - } - case Rewardable::SELECT_ALL: // grant all possible - grantAllRewardsWithMessage(h, rewards, true); - break; - } - break; - } - } - - if(!objectRemovalPossible && getAvailableRewards(h, Rewardable::EEventType::EVENT_FIRST_VISIT).empty()) - { - ChangeObjectVisitors cov(ChangeObjectVisitors::VISITOR_ADD_PLAYER, id, h->id); - cb->sendAndApply(&cov); - } - } - else - { - logGlobal->debug("Revisiting already visited object"); - - if (!wasVisited(h->getOwner())) - { - ChangeObjectVisitors cov(ChangeObjectVisitors::VISITOR_ADD_PLAYER, id, h->id); - cb->sendAndApply(&cov); - } - - auto visitedRewards = getAvailableRewards(h, Rewardable::EEventType::EVENT_ALREADY_VISITED); - if (!visitedRewards.empty()) - grantRewardWithMessage(h, visitedRewards[0], false); - else - logMod->warn("No applicable message for visiting already visited object!"); - } -} - void CRewardableObject::heroLevelUpDone(const CGHeroInstance *hero) const { - grantRewardAfterLevelup(cb, configuration.info.at(selectedReward), this, hero); + grantRewardAfterLevelup(configuration.info.at(selectedReward), this, hero); } void CRewardableObject::battleFinished(const CGHeroInstance *hero, const BattleResult &result) const @@ -236,7 +85,7 @@ void CRewardableObject::battleFinished(const CGHeroInstance *hero, const BattleR void CRewardableObject::blockingDialogAnswered(const CGHeroInstance * hero, int32_t answer) const { - if(guardedPresently()) + if(isGuarded()) { if (answer) { @@ -246,19 +95,7 @@ void CRewardableObject::blockingDialogAnswered(const CGHeroInstance * hero, int3 } else { - if (answer == 0) - return; //Player refused - - if(answer > 0 && answer - 1 < configuration.info.size()) - { - auto list = getAvailableRewards(hero, Rewardable::EEventType::EVENT_FIRST_VISIT); - markAsVisited(hero); - grantReward(list[answer - 1], hero); - } - else - { - throw std::runtime_error("Unhandled choice"); - } + onBlockingDialogAnswered(hero, answer); } } @@ -267,18 +104,18 @@ void CRewardableObject::markAsVisited(const CGHeroInstance * hero) const cb->setObjPropertyValue(id, ObjProperty::REWARD_CLEARED, true); ChangeObjectVisitors cov(ChangeObjectVisitors::VISITOR_ADD_HERO, id, hero->id); - cb->sendAndApply(&cov); + cb->sendAndApply(cov); } void CRewardableObject::grantReward(ui32 rewardID, const CGHeroInstance * hero) const { cb->setObjPropertyValue(id, ObjProperty::REWARD_SELECT, rewardID); - grantRewardBeforeLevelup(cb, configuration.info.at(rewardID), hero); + grantRewardBeforeLevelup(configuration.info.at(rewardID), hero); // hero is not blocked by levelup dialog - grant remainder immediately if(!cb->isVisitCoveredByAnotherQuery(this, hero)) { - grantRewardAfterLevelup(cb, configuration.info.at(rewardID), this, hero); + grantRewardAfterLevelup(configuration.info.at(rewardID), this, hero); } } @@ -410,7 +247,7 @@ std::vector CRewardableObject::getPopupComponentsImpl(PlayerColor pla if (!wasScouted(player)) return {}; - if (guardedPresently()) + if (isGuarded()) { if (!cb->getSettings().getBoolean(EGameSettings::BANKS_SHOW_GUARDS_COMPOSITION)) return {}; @@ -480,14 +317,14 @@ void CRewardableObject::newTurn(vstd::RNG & rand) const if (configuration.resetParameters.rewards) { auto handler = std::dynamic_pointer_cast(getObjectHandler()); - auto newConfiguration = handler->generateConfiguration(cb, rand, ID); + auto newConfiguration = handler->generateConfiguration(cb, rand, ID, configuration.variables.preset); cb->setRewardableObjectConfiguration(id, newConfiguration); } if (configuration.resetParameters.visitors) { cb->setObjPropertyValue(id, ObjProperty::REWARD_CLEARED, false); ChangeObjectVisitors cov(ChangeObjectVisitors::VISITOR_CLEAR, id); - cb->sendAndApply(&cov); + cb->sendAndApply(cov); } } } diff --git a/lib/mapObjects/CRewardableObject.h b/lib/mapObjects/CRewardableObject.h index 39a658ad2..14bd52af2 100644 --- a/lib/mapObjects/CRewardableObject.h +++ b/lib/mapObjects/CRewardableObject.h @@ -25,32 +25,24 @@ protected: /// reward selected by player, no serialize ui16 selectedReward = 0; - void grantReward(ui32 rewardID, const CGHeroInstance * hero) const; - void markAsVisited(const CGHeroInstance * hero) const; + void grantReward(ui32 rewardID, const CGHeroInstance * hero) const override; + void markAsVisited(const CGHeroInstance * hero) const override; + + const IObjectInterface * getObject() const override; + void markAsScouted(const CGHeroInstance * hero) const override; /// return true if this object was "cleared" before and no longer has rewards applicable to selected hero /// unlike wasVisited, this method uses information not available to player owner, for example, if object was cleared by another player before - bool wasVisitedBefore(const CGHeroInstance * contextHero) const; + bool wasVisitedBefore(const CGHeroInstance * contextHero) const override; void serializeJsonOptions(JsonSerializeFormat & handler) override; - virtual void grantRewardWithMessage(const CGHeroInstance * contextHero, int rewardIndex, bool markAsVisit) const; - virtual void selectRewardWithMessage(const CGHeroInstance * contextHero, const std::vector & rewardIndices, const MetaString & dialog) const; - - virtual void grantAllRewardsWithMessage(const CGHeroInstance * contextHero, const std::vector& rewardIndices, bool markAsVisit) const; - - std::vector loadComponents(const CGHeroInstance * contextHero, const std::vector & rewardIndices) const; - std::string getDisplayTextImpl(PlayerColor player, const CGHeroInstance * hero, bool includeDescription) const; std::string getDescriptionMessage(PlayerColor player, const CGHeroInstance * hero) const; std::vector getPopupComponentsImpl(PlayerColor player, const CGHeroInstance * hero) const; - void doHeroVisit(const CGHeroInstance *h) const; - - /// Returns true if this object might have guards present, whether they were cleared or not - bool guardedPotentially() const; /// Returns true if this object is currently guarded - bool guardedPresently() const; + bool isGuarded() const; public: /// Visitability checks. Note that hero check includes check for hero owner (returns true if object was visited by player) diff --git a/lib/mapObjects/CompoundMapObjectID.h b/lib/mapObjects/CompoundMapObjectID.h new file mode 100644 index 000000000..4c067548b --- /dev/null +++ b/lib/mapObjects/CompoundMapObjectID.h @@ -0,0 +1,37 @@ +/* + * CompoundMapObjectID.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 "../constants/EntityIdentifiers.h" + +VCMI_LIB_NAMESPACE_BEGIN + +struct DLL_LINKAGE CompoundMapObjectID +{ + si32 primaryID; + si32 secondaryID; + + CompoundMapObjectID(si32 primID, si32 secID) : primaryID(primID), secondaryID(secID) {}; + + bool operator<(const CompoundMapObjectID& other) const + { + if(this->primaryID != other.primaryID) + return this->primaryID < other.primaryID; + else + return this->secondaryID < other.secondaryID; + } + + bool operator==(const CompoundMapObjectID& other) const + { + return (this->primaryID == other.primaryID) && (this->secondaryID == other.secondaryID); + } +}; + +VCMI_LIB_NAMESPACE_END \ No newline at end of file diff --git a/lib/mapObjects/FlaggableMapObject.cpp b/lib/mapObjects/FlaggableMapObject.cpp new file mode 100644 index 000000000..5b22650ae --- /dev/null +++ b/lib/mapObjects/FlaggableMapObject.cpp @@ -0,0 +1,105 @@ +/* + * FlaggableMapObject.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 "FlaggableMapObject.h" + +#include "../IGameCallback.h" +#include "CGHeroInstance.h" +#include "../networkPacks/PacksForClient.h" +#include "../mapObjectConstructors/FlaggableInstanceConstructor.h" + +VCMI_LIB_NAMESPACE_BEGIN + +const IOwnableObject * FlaggableMapObject::asOwnable() const +{ + return this; +} + +ResourceSet FlaggableMapObject::dailyIncome() const +{ + return getFlaggableHandler()->getDailyIncome(); +} + +std::vector FlaggableMapObject::providedCreatures() const +{ + return {}; +} + +void FlaggableMapObject::onHeroVisit( const CGHeroInstance * h ) const +{ + if (cb->getPlayerRelations(h->getOwner(), getOwner()) != PlayerRelations::ENEMIES) + return; // H3 behavior - revisiting owned Lighthouse is a no-op + + if (getOwner().isValidPlayer()) + takeBonusFrom(getOwner()); + + cb->setOwner(this, h->getOwner()); //not ours? flag it! + + InfoWindow iw; + iw.player = h->getOwner(); + iw.text.appendTextID(getFlaggableHandler()->getVisitMessageTextID()); + cb->showInfoDialog(&iw); + + giveBonusTo(h->getOwner()); +} + +void FlaggableMapObject::initObj(vstd::RNG & rand) +{ + if(getOwner().isValidPlayer()) + { + // FIXME: This is dirty hack + giveBonusTo(getOwner(), true); + } +} + +std::shared_ptr FlaggableMapObject::getFlaggableHandler() const +{ + return std::dynamic_pointer_cast(getObjectHandler()); +} + +void FlaggableMapObject::giveBonusTo(const PlayerColor & player, bool onInit) const +{ + for (auto const & bonus : getFlaggableHandler()->getProvidedBonuses()) + { + GiveBonus gb(GiveBonus::ETarget::PLAYER); + gb.id = player; + gb.bonus = *bonus; + + // FIXME: better place for this code? + gb.bonus.duration = BonusDuration::PERMANENT; + gb.bonus.source = BonusSource::OBJECT_INSTANCE; + gb.bonus.sid = BonusSourceID(id); + + // FIXME: This is really dirty hack + // Proper fix would be to make FlaggableMapObject into bonus system node + // Unfortunately this will cause saves breakage + if(onInit) + gb.applyGs(cb->gameState()); + else + cb->sendAndApply(gb); + } +} + +void FlaggableMapObject::takeBonusFrom(const PlayerColor & player) const +{ + RemoveBonus rb(GiveBonus::ETarget::PLAYER); + rb.whoID = player; + rb.source = BonusSource::OBJECT_INSTANCE; + rb.id = BonusSourceID(id); + cb->sendAndApply(rb); +} + +void FlaggableMapObject::serializeJsonOptions(JsonSerializeFormat& handler) +{ + serializeJsonOwner(handler); +} + +VCMI_LIB_NAMESPACE_END diff --git a/lib/mapObjects/FlaggableMapObject.h b/lib/mapObjects/FlaggableMapObject.h new file mode 100644 index 000000000..f3c86e0d2 --- /dev/null +++ b/lib/mapObjects/FlaggableMapObject.h @@ -0,0 +1,41 @@ +/* + * FlaggableMapObject.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 "CGObjectInstance.h" +#include "IOwnableObject.h" + +VCMI_LIB_NAMESPACE_BEGIN + +struct Bonus; +class FlaggableInstanceConstructor; + +class DLL_LINKAGE FlaggableMapObject : public CGObjectInstance, public IOwnableObject +{ + std::shared_ptr getFlaggableHandler() const; + + void giveBonusTo(const PlayerColor & player, bool onInit = false) const; + void takeBonusFrom(const PlayerColor & player) const; + +public: + using CGObjectInstance::CGObjectInstance; + + void onHeroVisit(const CGHeroInstance * h) const override; + void initObj(vstd::RNG & rand) override; + + const IOwnableObject * asOwnable() const final; + ResourceSet dailyIncome() const override; + std::vector providedCreatures() const override; + +protected: + void serializeJsonOptions(JsonSerializeFormat & handler) override; +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/mapObjects/IObjectInterface.cpp b/lib/mapObjects/IObjectInterface.cpp index 11d9e294c..acf4eed14 100644 --- a/lib/mapObjects/IObjectInterface.cpp +++ b/lib/mapObjects/IObjectInterface.cpp @@ -28,7 +28,7 @@ void IObjectInterface::showInfoDialog(const ui32 txtID, const ui16 soundID, EInf iw.player = getOwner(); iw.type = mode; iw.text.appendLocalString(EMetaText::ADVOB_TXT,txtID); - cb->sendAndApply(&iw); + cb->sendAndApply(iw); } ///IObjectInterface @@ -90,10 +90,10 @@ int3 IBoatGenerator::bestLocation() const if(!tile) continue; // tile not visible / outside the map - if(!tile->terType->isWater()) + if(!tile->isWater()) continue; - if (tile->blocked) + if (tile->blocked()) { bool hasBoat = false; for (auto const * object : tile->blockingObjects) @@ -145,7 +145,7 @@ void IBoatGenerator::getProblemText(MetaString &out, const CGHeroInstance *visit out.appendLocalString(EMetaText::ADVOB_TXT, 189); break; case NO_WATER: - logGlobal->error("Shipyard without water at tile %s! ", getObject()->getPosition().toString()); + logGlobal->error("Shipyard without water at tile %s! ", getObject()->anchorPos().toString()); return; } } diff --git a/lib/mapObjects/IObjectInterface.h b/lib/mapObjects/IObjectInterface.h index 9ccda2c7c..4398c5a11 100644 --- a/lib/mapObjects/IObjectInterface.h +++ b/lib/mapObjects/IObjectInterface.h @@ -47,7 +47,7 @@ public: virtual PlayerColor getOwner() const = 0; virtual int3 visitablePos() const = 0; - virtual int3 getPosition() const = 0; + virtual int3 anchorPos() const = 0; virtual void onHeroVisit(const CGHeroInstance * h) const; virtual void onHeroLeave(const CGHeroInstance * h) const; diff --git a/lib/mapObjects/MiscObjects.cpp b/lib/mapObjects/MiscObjects.cpp index ab5fc3d50..910baa639 100644 --- a/lib/mapObjects/MiscObjects.cpp +++ b/lib/mapObjects/MiscObjects.cpp @@ -111,7 +111,7 @@ void CGMine::initObj(vstd::RNG & rand) } else { - logGlobal->error("Abandoned mine at (%s) has no valid resource candidates!", pos.toString()); + logGlobal->error("Abandoned mine at (%s) has no valid resource candidates!", anchorPos().toString()); producedResource = GameResID::GOLD; } } @@ -510,11 +510,11 @@ void CGMonolith::onHeroVisit( const CGHeroInstance * h ) const if(cb->isTeleportChannelImpassable(channel)) { - logGlobal->debug("Cannot find corresponding exit monolith for %d at %s", id.getNum(), pos.toString()); + logGlobal->debug("Cannot find corresponding exit monolith for %d at %s", id.getNum(), anchorPos().toString()); td.impassable = true; } else if(getRandomExit(h) == ObjectInstanceID()) - logGlobal->debug("All exits blocked for monolith %d at %s", id.getNum(), pos.toString()); + logGlobal->debug("All exits blocked for monolith %d at %s", id.getNum(), anchorPos().toString()); } else h->showInfoDialog(70); @@ -574,7 +574,7 @@ void CGSubterraneanGate::onHeroVisit( const CGHeroInstance * h ) const if(cb->isTeleportChannelImpassable(channel)) { h->showInfoDialog(153);//Just inside the entrance you find a large pile of rubble blocking the tunnel. You leave discouraged. - logGlobal->debug("Cannot find exit subterranean gate for %d at %s", id.getNum(), pos.toString()); + logGlobal->debug("Cannot find exit subterranean gate for %d at %s", id.getNum(), anchorPos().toString()); td.impassable = true; } else @@ -602,13 +602,13 @@ void CGSubterraneanGate::postInit(IGameCallback * cb) //matches subterranean gat auto * hlp = dynamic_cast(cb->gameState()->getObjInstance(obj->id)); if(hlp) - gatesSplit[hlp->pos.z].push_back(hlp); + gatesSplit[hlp->visitablePos().z].push_back(hlp); } //sort by position std::sort(gatesSplit[0].begin(), gatesSplit[0].end(), [](const CGObjectInstance * a, const CGObjectInstance * b) { - return a->pos < b->pos; + return a->visitablePos() < b->visitablePos(); }); auto assignToChannel = [&](CGSubterraneanGate * obj) @@ -631,7 +631,7 @@ void CGSubterraneanGate::postInit(IGameCallback * cb) //matches subterranean gat CGSubterraneanGate *checked = gatesSplit[1][j]; if(checked->channel != TeleportChannelID()) continue; - si32 hlp = checked->pos.dist2dSQ(objCurrent->pos); + si32 hlp = checked->visitablePos().dist2dSQ(objCurrent->visitablePos()); if(hlp < best.second) { best.first = j; @@ -657,11 +657,11 @@ void CGWhirlpool::onHeroVisit( const CGHeroInstance * h ) const TeleportDialog td(h->id, channel); if(cb->isTeleportChannelImpassable(channel)) { - logGlobal->debug("Cannot find exit whirlpool for %d at %s", id.getNum(), pos.toString()); + logGlobal->debug("Cannot find exit whirlpool for %d at %s", id.getNum(), anchorPos().toString()); td.impassable = true; } else if(getRandomExit(h) == ObjectInstanceID()) - logGlobal->debug("All exits are blocked for whirlpool %d at %s", id.getNum(), pos.toString()); + logGlobal->debug("All exits are blocked for whirlpool %d at %s", id.getNum(), anchorPos().toString()); if(!isProtected(h)) { @@ -772,17 +772,16 @@ void CGArtifact::initObj(vstd::RNG & rand) { if (!storedArtifact) { - auto * a = new CArtifactInstance(); - cb->gameState()->map->addNewArtifactInstance(a); - storedArtifact = a; + storedArtifact = ArtifactUtils::createArtifact(ArtifactID()); + cb->gameState()->map->addNewArtifactInstance(storedArtifact); } - if(!storedArtifact->artType) + if(!storedArtifact->getType()) storedArtifact->setType(getArtifact().toArtifact()); } if(ID == Obj::SPELL_SCROLL) subID = 1; - assert(storedArtifact->artType); + assert(storedArtifact->getType()); assert(!storedArtifact->getParentNodes().empty()); //assert(storedArtifact->artType->id == subID); //this does not stop desync @@ -826,7 +825,7 @@ void CGArtifact::onHeroVisit(const CGHeroInstance * h) const iw.type = EInfoWindowMode::AUTO; iw.player = h->tempOwner; - if(storedArtifact->artType->canBePutAt(h)) + if(storedArtifact->getType()->canBePutAt(h)) { switch (ID.toEnum()) { @@ -901,7 +900,7 @@ void CGArtifact::onHeroVisit(const CGHeroInstance * h) const void CGArtifact::pick(const CGHeroInstance * h) const { - if(cb->putArtifact(ArtifactLocation(h->id, ArtifactPosition::FIRST_AVAILABLE), storedArtifact)) + if(cb->putArtifact(ArtifactLocation(h->id, ArtifactPosition::FIRST_AVAILABLE), storedArtifact->getId())) cb->removeObject(this, h->getOwner()); } @@ -1087,15 +1086,15 @@ void CGMagi::onHeroVisit(const CGHeroInstance * h) const for(const auto & eye : eyes) { - cb->getTilesInRange (fw.tiles, eye->pos, 10, ETileVisibility::HIDDEN, h->tempOwner); - cb->sendAndApply(&fw); - cv.pos = eye->pos; + cb->getTilesInRange (fw.tiles, eye->visitablePos(), 10, ETileVisibility::HIDDEN, h->tempOwner); + cb->sendAndApply(fw); + cv.pos = eye->visitablePos(); - cb->sendAndApply(&cv); + cb->sendAndApply(cv); } cv.pos = h->visitablePos(); cv.focusTime = 0; - cb->sendAndApply(&cv); + cb->sendAndApply(cv); } } else if (ID == Obj::EYE_OF_MAGI) @@ -1153,7 +1152,7 @@ void CGSirens::onHeroVisit( const CGHeroInstance * h ) const if(drown) { cb->changeStackCount(StackLocation(h, i->first), -drown); - xp += drown * i->second->type->getMaxHealth(); + xp += drown * i->second->getType()->getMaxHealth(); } } @@ -1259,7 +1258,7 @@ void CGObelisk::onHeroVisit( const CGHeroInstance * h ) const if(!wasVisited(team)) { iw.text.appendLocalString(EMetaText::ADVOB_TXT, 96); - cb->sendAndApply(&iw); + cb->sendAndApply(iw); // increment general visited obelisks counter cb->setObjPropertyID(id, ObjProperty::OBELISK_VISITED, team); @@ -1274,7 +1273,7 @@ void CGObelisk::onHeroVisit( const CGHeroInstance * h ) const else { iw.text.appendLocalString(EMetaText::ADVOB_TXT, 97); - cb->sendAndApply(&iw); + cb->sendAndApply(iw); } } @@ -1312,75 +1311,6 @@ void CGObelisk::setPropertyDer(ObjProperty what, ObjPropertyID identifier) } } -const IOwnableObject * CGLighthouse::asOwnable() const -{ - return this; -} - -ResourceSet CGLighthouse::dailyIncome() const -{ - return {}; -} - -std::vector CGLighthouse::providedCreatures() const -{ - return {}; -} - -void CGLighthouse::onHeroVisit( const CGHeroInstance * h ) const -{ - if(h->tempOwner != tempOwner) - { - PlayerColor oldOwner = tempOwner; - cb->setOwner(this,h->tempOwner); //not ours? flag it! - h->showInfoDialog(69); - giveBonusTo(h->tempOwner); - - if(oldOwner.isValidPlayer()) //remove bonus from old owner - { - RemoveBonus rb(GiveBonus::ETarget::PLAYER); - rb.whoID = oldOwner; - rb.source = BonusSource::OBJECT_INSTANCE; - rb.id = BonusSourceID(id); - cb->sendAndApply(&rb); - } - } -} - -void CGLighthouse::initObj(vstd::RNG & rand) -{ - if(tempOwner.isValidPlayer()) - { - // FIXME: This is dirty hack - giveBonusTo(tempOwner, true); - } -} - -void CGLighthouse::giveBonusTo(const PlayerColor & player, bool onInit) const -{ - GiveBonus gb(GiveBonus::ETarget::PLAYER); - gb.bonus.type = BonusType::MOVEMENT; - gb.bonus.val = 500; - gb.id = player; - gb.bonus.duration = BonusDuration::PERMANENT; - gb.bonus.source = BonusSource::OBJECT_INSTANCE; - gb.bonus.sid = BonusSourceID(id); - gb.bonus.subtype = BonusCustomSubtype::heroMovementSea; - - // FIXME: This is really dirty hack - // Proper fix would be to make CGLighthouse into bonus system node - // Unfortunately this will cause saves breakage - if(onInit) - gb.applyGs(cb->gameState()); - else - cb->sendAndApply(&gb); -} - -void CGLighthouse::serializeJsonOptions(JsonSerializeFormat& handler) -{ - serializeJsonOwner(handler); -} - void HillFort::onHeroVisit(const CGHeroInstance * h) const { cb->showObjectWindow(this, EOpenWindowMode::HILL_FORT_WINDOW, h, false); @@ -1388,7 +1318,7 @@ void HillFort::onHeroVisit(const CGHeroInstance * h) const void HillFort::fillUpgradeInfo(UpgradeInfo & info, const CStackInstance &stack) const { - int32_t level = stack.type->getLevel(); + int32_t level = stack.getType()->getLevel(); int32_t index = std::clamp(level - 1, 0, upgradeCostPercentage.size() - 1); int costModifier = upgradeCostPercentage[index]; @@ -1396,11 +1326,38 @@ void HillFort::fillUpgradeInfo(UpgradeInfo & info, const CStackInstance &stack) if (costModifier < 0) return; // upgrade not allowed - for(const auto & nid : stack.type->upgrades) + for(const auto & nid : stack.getCreature()->upgrades) { info.newID.push_back(nid); - info.cost.push_back((nid.toCreature()->getFullRecruitCost() - stack.type->getFullRecruitCost()) * costModifier / 100); + info.cost.push_back((nid.toCreature()->getFullRecruitCost() - stack.getType()->getFullRecruitCost()) * costModifier / 100); } } +std::string HillFort::getPopupText(PlayerColor player) const +{ + MetaString message = MetaString::createFromRawString("{%s}\r\n\r\n%s"); + + message.replaceName(ID, subID); + message.replaceTextID(getDescriptionToolTip()); + + return message.toString(); +} + +std::string HillFort::getPopupText(const CGHeroInstance * hero) const +{ + return getPopupText(hero->getOwner()); +} + + +std::string HillFort::getDescriptionToolTip() const +{ + return TextIdentifier(getObjectHandler()->getBaseTextID(), "description").get(); +} + +std::string HillFort::getUnavailableUpgradeMessage() const +{ + assert(getObjectHandler()->getModScope() != "core"); + return TextIdentifier(getObjectHandler()->getBaseTextID(), "unavailableUpgradeMessage").get(); +} + VCMI_LIB_NAMESPACE_END diff --git a/lib/mapObjects/MiscObjects.h b/lib/mapObjects/MiscObjects.h index 862d52596..aa5d2ac46 100644 --- a/lib/mapObjects/MiscObjects.h +++ b/lib/mapObjects/MiscObjects.h @@ -413,28 +413,6 @@ protected: void setPropertyDer(ObjProperty what, ObjPropertyID identifier) override; }; -class DLL_LINKAGE CGLighthouse : public CGObjectInstance, public IOwnableObject -{ -public: - using CGObjectInstance::CGObjectInstance; - - void onHeroVisit(const CGHeroInstance * h) const override; - void initObj(vstd::RNG & rand) override; - - const IOwnableObject * asOwnable() const final; - ResourceSet dailyIncome() const override; - std::vector providedCreatures() const override; - - template void serialize(Handler &h) - { - h & static_cast(*this); - } - void giveBonusTo(const PlayerColor & player, bool onInit = false) const; - -protected: - void serializeJsonOptions(JsonSerializeFormat & handler) override; -}; - class DLL_LINKAGE CGTerrainPatch : public CGObjectInstance { public: @@ -459,6 +437,12 @@ protected: public: using CGObjectInstance::CGObjectInstance; + std::string getPopupText(PlayerColor player) const override; + std::string getPopupText(const CGHeroInstance * hero) const override; + + std::string getDescriptionToolTip() const; + std::string getUnavailableUpgradeMessage() const; + template void serialize(Handler &h) { h & static_cast(*this); diff --git a/lib/mapObjects/ObjectTemplate.cpp b/lib/mapObjects/ObjectTemplate.cpp index 9c32a7566..68c6a6178 100644 --- a/lib/mapObjects/ObjectTemplate.cpp +++ b/lib/mapObjects/ObjectTemplate.cpp @@ -508,6 +508,11 @@ bool ObjectTemplate::canBePlacedAt(TerrainId terrainID) const return vstd::contains(allowedTerrains, terrainID); } +CompoundMapObjectID ObjectTemplate::getCompoundID() const +{ + return CompoundMapObjectID(id, subid); +} + void ObjectTemplate::recalculate() { calculateWidth(); diff --git a/lib/mapObjects/ObjectTemplate.h b/lib/mapObjects/ObjectTemplate.h index 96780fade..584750f48 100644 --- a/lib/mapObjects/ObjectTemplate.h +++ b/lib/mapObjects/ObjectTemplate.h @@ -13,6 +13,7 @@ #include "../int3.h" #include "../filesystem/ResourcePath.h" #include "../serializer/Serializeable.h" +#include "../mapObjects/CompoundMapObjectID.h" VCMI_LIB_NAMESPACE_BEGIN @@ -46,6 +47,7 @@ public: /// H3 ID/subID of this object MapObjectID id; MapObjectSubID subid; + /// print priority, objects with higher priority will be print first, below everything else si32 printPriority; /// animation file that should be used to display object @@ -122,6 +124,8 @@ public: // Checks if object can be placed on specific terrain bool canBePlacedAt(TerrainId terrain) const; + CompoundMapObjectID getCompoundID() const; + ObjectTemplate(); void readTxt(CLegacyConfigParser & parser); diff --git a/lib/mapObjects/ObstacleSetHandler.cpp b/lib/mapObjects/ObstacleSetHandler.cpp index aef9461a8..4da7cac88 100644 --- a/lib/mapObjects/ObstacleSetHandler.cpp +++ b/lib/mapObjects/ObstacleSetHandler.cpp @@ -43,7 +43,7 @@ void ObstacleSet::removeEmptyTemplates() { if (tmpl->getBlockedOffsets().empty()) { - logMod->warn("Obstacle template %s blocks no tiles, removing it", tmpl->stringID); + logMod->debug("Obstacle template %s blocks no tiles, removing it", tmpl->stringID); return true; } return false; @@ -457,7 +457,7 @@ void ObstacleSetHandler::addTemplate(const std::string & scope, const std::strin if (VLC->identifiersHandler->getIdentifier(scope, "obstacleTemplate", strippedName, true)) { - logMod->warn("Duplicate obstacle template: %s", strippedName); + logMod->debug("Duplicate obstacle template: %s", strippedName); return; } else diff --git a/lib/mapObjects/TownBuildingInstance.cpp b/lib/mapObjects/TownBuildingInstance.cpp index a78366f18..8b4a40e5c 100644 --- a/lib/mapObjects/TownBuildingInstance.cpp +++ b/lib/mapObjects/TownBuildingInstance.cpp @@ -12,14 +12,10 @@ #include "TownBuildingInstance.h" #include "CGTownInstance.h" -#include "../texts/CGeneralTextHandler.h" #include "../IGameCallback.h" -#include "../gameState/CGameState.h" #include "../mapObjects/CGHeroInstance.h" -#include "../networkPacks/PacksForClient.h" #include "../entities/building/CBuilding.h" - #include VCMI_LIB_NAMESPACE_BEGIN @@ -60,9 +56,9 @@ int3 TownBuildingInstance::visitablePos() const return town->visitablePos(); } -int3 TownBuildingInstance::getPosition() const +int3 TownBuildingInstance::anchorPos() const { - return town->getPosition(); + return town->anchorPos(); } TownRewardableBuildingInstance::TownRewardableBuildingInstance(IGameCallback *cb) @@ -77,14 +73,14 @@ TownRewardableBuildingInstance::TownRewardableBuildingInstance(CGTownInstance * void TownRewardableBuildingInstance::initObj(vstd::RNG & rand) { - assert(town && town->town); + assert(town && town->getTown()); configuration = generateConfiguration(rand); } Rewardable::Configuration TownRewardableBuildingInstance::generateConfiguration(vstd::RNG & rand) const { Rewardable::Configuration result; - auto building = town->town->buildings.at(getBuildingType()); + auto building = town->getTown()->buildings.at(getBuildingType()); building->rewardableObjectInfo.configureObject(result, rand, cb); for(auto & rewardInfo : result.info) @@ -130,38 +126,22 @@ void TownRewardableBuildingInstance::setProperty(ObjProperty what, ObjPropertyID void TownRewardableBuildingInstance::heroLevelUpDone(const CGHeroInstance *hero) const { - grantRewardAfterLevelup(cb, configuration.info.at(selectedReward), town, hero); + grantRewardAfterLevelup(configuration.info.at(selectedReward), town, hero); } void TownRewardableBuildingInstance::blockingDialogAnswered(const CGHeroInstance *hero, int32_t answer) const { - if(answer == 0) - return; // player refused - - if(visitors.find(hero->id) != visitors.end()) - return; // query not for this building - - if(answer > 0 && answer-1 < configuration.info.size()) - { - auto list = getAvailableRewards(hero, Rewardable::EEventType::EVENT_FIRST_VISIT); - grantReward(list[answer - 1], hero); - } - else - { - throw std::runtime_error("Unhandled choice"); - } + onBlockingDialogAnswered(hero, answer); } void TownRewardableBuildingInstance::grantReward(ui32 rewardID, const CGHeroInstance * hero) const { - town->addHeroToStructureVisitors(hero, getBuildingType()); - - grantRewardBeforeLevelup(cb, configuration.info.at(rewardID), hero); + grantRewardBeforeLevelup(configuration.info.at(rewardID), hero); // hero is not blocked by levelup dialog - grant remainder immediately if(!cb->isVisitCoveredByAnotherQuery(town, hero)) { - grantRewardAfterLevelup(cb, configuration.info.at(rewardID), town, hero); + grantRewardAfterLevelup(configuration.info.at(rewardID), town, hero); } } @@ -196,93 +176,42 @@ bool TownRewardableBuildingInstance::wasVisitedBefore(const CGHeroInstance * con void TownRewardableBuildingInstance::onHeroVisit(const CGHeroInstance *h) const { - auto grantRewardWithMessage = [&](int index) -> void + assert(town->hasBuilt(getBuildingType())); + + if(town->hasBuilt(getBuildingType())) + doHeroVisit(h); +} + +const IObjectInterface * TownRewardableBuildingInstance::getObject() const +{ + return this; +} + +bool TownRewardableBuildingInstance::wasVisited(PlayerColor player) const +{ + switch (configuration.visitMode) { - auto vi = configuration.info.at(index); - logGlobal->debug("Granting reward %d. Message says: %s", index, vi.message.toString()); - - town->addHeroToStructureVisitors(h, getBuildingType()); //adding to visitors - - InfoWindow iw; - iw.player = h->tempOwner; - iw.text = vi.message; - vi.reward.loadComponents(iw.components, h); - iw.type = EInfoWindowMode::MODAL; - if(!iw.components.empty() || !iw.text.toString().empty()) - cb->showInfoDialog(&iw); - - grantReward(index, h); - }; - auto selectRewardsMessage = [&](const std::vector & rewards, const MetaString & dialog) -> void - { - BlockingDialog sd(configuration.canRefuse, rewards.size() > 1); - sd.player = h->tempOwner; - sd.text = dialog; - - if (rewards.size() > 1) - for (auto index : rewards) - sd.components.push_back(configuration.info.at(index).reward.getDisplayedComponent(h)); - - if (rewards.size() == 1) - configuration.info.at(rewards.front()).reward.loadComponents(sd.components, h); - - cb->showBlockingDialog(this, &sd); - }; - - if(!town->hasBuilt(getBuildingType())) - return; - - if(!wasVisitedBefore(h)) - { - auto rewards = getAvailableRewards(h, Rewardable::EEventType::EVENT_FIRST_VISIT); - - logGlobal->debug("Visiting object with %d possible rewards", rewards.size()); - switch (rewards.size()) - { - case 0: // no available rewards, e.g. visiting School of War without gold - { - auto emptyRewards = getAvailableRewards(h, Rewardable::EEventType::EVENT_NOT_AVAILABLE); - if (!emptyRewards.empty()) - grantRewardWithMessage(emptyRewards[0]); - else - logMod->warn("No applicable message for visiting empty object!"); - break; - } - case 1: // one reward. Just give it with message - { - if (configuration.canRefuse) - selectRewardsMessage(rewards, configuration.info.at(rewards.front()).message); - else - grantRewardWithMessage(rewards.front()); - break; - } - default: // multiple rewards. Act according to select mode - { - switch (configuration.selectMode) { - case Rewardable::SELECT_PLAYER: // player must select - selectRewardsMessage(rewards, configuration.onSelect); - break; - case Rewardable::SELECT_FIRST: // give first available - grantRewardWithMessage(rewards.front()); - break; - case Rewardable::SELECT_RANDOM: // give random - grantRewardWithMessage(*RandomGeneratorUtil::nextItem(rewards, cb->gameState()->getRandomGenerator())); - break; - } - break; - } - } + case Rewardable::VISIT_UNLIMITED: + case Rewardable::VISIT_BONUS: + case Rewardable::VISIT_HERO: + case Rewardable::VISIT_LIMITER: + return false; + case Rewardable::VISIT_ONCE: + case Rewardable::VISIT_PLAYER: + return !visitors.empty(); + default: + return false; } - else - { - logGlobal->debug("Revisiting already visited object"); +} - auto visitedRewards = getAvailableRewards(h, Rewardable::EEventType::EVENT_ALREADY_VISITED); - if (!visitedRewards.empty()) - grantRewardWithMessage(visitedRewards[0]); - else - logMod->debug("No applicable message for visiting already visited object!"); - } +void TownRewardableBuildingInstance::markAsVisited(const CGHeroInstance * hero) const +{ + town->addHeroToStructureVisitors(hero, getBuildingType()); +} + +void TownRewardableBuildingInstance::markAsScouted(const CGHeroInstance * hero) const +{ + // no-op - town building is always 'scouted' by owner } diff --git a/lib/mapObjects/TownBuildingInstance.h b/lib/mapObjects/TownBuildingInstance.h index 2315566fa..015421448 100644 --- a/lib/mapObjects/TownBuildingInstance.h +++ b/lib/mapObjects/TownBuildingInstance.h @@ -38,7 +38,7 @@ public: const IOwnableObject * asOwnable() const override; int3 visitablePos() const override; - int3 getPosition() const override; + int3 anchorPos() const override; template void serialize(Handler &h) { @@ -63,10 +63,14 @@ class DLL_LINKAGE TownRewardableBuildingInstance : public TownBuildingInstance, ui16 selectedReward = 0; std::set visitors; - bool wasVisitedBefore(const CGHeroInstance * contextHero) const; - void grantReward(ui32 rewardID, const CGHeroInstance * hero) const; + bool wasVisitedBefore(const CGHeroInstance * contextHero) const override; + void grantReward(ui32 rewardID, const CGHeroInstance * hero) const override; Rewardable::Configuration generateConfiguration(vstd::RNG & rand) const; + const IObjectInterface * getObject() const override; + bool wasVisited(PlayerColor player) const override; + void markAsVisited(const CGHeroInstance * hero) const override; + void markAsScouted(const CGHeroInstance * hero) const override; public: void setProperty(ObjProperty what, ObjPropertyID identifier) override; void onHeroVisit(const CGHeroInstance * h) const override; diff --git a/lib/mapping/CDrawRoadsOperation.cpp b/lib/mapping/CDrawRoadsOperation.cpp index c67684ceb..b3df0efa1 100644 --- a/lib/mapping/CDrawRoadsOperation.cpp +++ b/lib/mapping/CDrawRoadsOperation.cpp @@ -344,12 +344,12 @@ std::string CDrawRiversOperation::getLabel() const void CDrawRoadsOperation::executeTile(TerrainTile & tile) { - tile.roadType = const_cast(VLC->roadTypeHandler->getByIndex(roadType.getNum())); + tile.roadType = roadType; } void CDrawRiversOperation::executeTile(TerrainTile & tile) { - tile.riverType = const_cast(VLC->riverTypeHandler->getByIndex(riverType.getNum())); + tile.riverType = riverType; } bool CDrawRoadsOperation::canApplyPattern(const LinePattern & pattern) const @@ -364,22 +364,22 @@ bool CDrawRiversOperation::canApplyPattern(const LinePattern & pattern) const bool CDrawRoadsOperation::needUpdateTile(const TerrainTile & tile) const { - return tile.roadType->getId() != Road::NO_ROAD; + return tile.hasRoad(); } bool CDrawRiversOperation::needUpdateTile(const TerrainTile & tile) const { - return tile.riverType->getId() != River::NO_RIVER; + return tile.hasRiver(); } bool CDrawRoadsOperation::tileHasSomething(const int3& pos) const { - return map->getTile(pos).roadType->getId() != Road::NO_ROAD; + return map->getTile(pos).hasRoad(); } bool CDrawRiversOperation::tileHasSomething(const int3& pos) const { - return map->getTile(pos).riverType->getId() != River::NO_RIVER; + return map->getTile(pos).hasRiver(); } void CDrawRoadsOperation::updateTile(TerrainTile & tile, const LinePattern & pattern, const int flip) diff --git a/lib/mapping/CMap.cpp b/lib/mapping/CMap.cpp index 8a3c8a469..3760c3bc1 100644 --- a/lib/mapping/CMap.cpp +++ b/lib/mapping/CMap.cpp @@ -13,11 +13,11 @@ #include "../CArtHandler.h" #include "../VCMI_Lib.h" #include "../CCreatureHandler.h" -#include "../CHeroHandler.h" #include "../GameSettings.h" #include "../RiverHandler.h" #include "../RoadHandler.h" #include "../TerrainHandler.h" +#include "../entities/hero/CHeroHandler.h" #include "../mapObjects/CGHeroInstance.h" #include "../mapObjects/CGTownInstance.h" #include "../mapObjects/CQuest.h" @@ -134,32 +134,29 @@ void CCastleEvent::serializeJson(JsonSerializeFormat & handler) } TerrainTile::TerrainTile(): - terType(nullptr), - riverType(VLC->riverTypeHandler->getById(River::NO_RIVER)), - roadType(VLC->roadTypeHandler->getById(Road::NO_ROAD)), + riverType(River::NO_RIVER), + roadType(Road::NO_ROAD), terView(0), riverDir(0), roadDir(0), - extTileFlags(0), - visitable(false), - blocked(false) + extTileFlags(0) { } bool TerrainTile::entrableTerrain(const TerrainTile * from) const { - return entrableTerrain(from ? from->terType->isLand() : true, from ? from->terType->isWater() : true); + return entrableTerrain(from ? from->isLand() : true, from ? from->isWater() : true); } bool TerrainTile::entrableTerrain(bool allowLand, bool allowSea) const { - return terType->isPassable() - && ((allowSea && terType->isWater()) || (allowLand && terType->isLand())); + return getTerrain()->isPassable() + && ((allowSea && isWater()) || (allowLand && isLand())); } bool TerrainTile::isClear(const TerrainTile * from) const { - return entrableTerrain(from) && !blocked; + return entrableTerrain(from) && !blocked(); } Obj TerrainTile::topVisitableId(bool excludeTop) const @@ -180,7 +177,7 @@ CGObjectInstance * TerrainTile::topVisitableObj(bool excludeTop) const EDiggingStatus TerrainTile::getDiggingStatus(const bool excludeTop) const { - if(terType->isWater() || !terType->isPassable()) + if(isWater() || !getTerrain()->isPassable()) return EDiggingStatus::WRONG_TERRAIN; int allowedBlocked = excludeTop ? 1 : 0; @@ -197,9 +194,65 @@ bool TerrainTile::hasFavorableWinds() const bool TerrainTile::isWater() const { - return terType->isWater(); + 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) @@ -235,26 +288,21 @@ CMap::~CMap() void CMap::removeBlockVisTiles(CGObjectInstance * obj, bool total) { - const int zVal = obj->pos.z; + const int zVal = obj->anchorPos().z; for(int fx = 0; fx < obj->getWidth(); ++fx) { - int xVal = obj->pos.x - fx; + int xVal = obj->anchorPos().x - fx; for(int fy = 0; fy < obj->getHeight(); ++fy) { - int yVal = obj->pos.y - fy; + int yVal = obj->anchorPos().y - fy; if(xVal>=0 && xVal < width && yVal>=0 && yVal < height) { TerrainTile & curt = terrain[zVal][xVal][yVal]; - if(total || obj->visitableAt(xVal, yVal)) - { + if(total || obj->visitableAt(int3(xVal, yVal, zVal))) curt.visitableObjects -= obj; - curt.visitable = curt.visitableObjects.size(); - } - if(total || obj->blockingAt(xVal, yVal)) - { + + if(total || obj->blockingAt(int3(xVal, yVal, zVal))) curt.blockingObjects -= obj; - curt.blocked = curt.blockingObjects.size(); - } } } } @@ -262,26 +310,21 @@ void CMap::removeBlockVisTiles(CGObjectInstance * obj, bool total) void CMap::addBlockVisTiles(CGObjectInstance * obj) { - const int zVal = obj->pos.z; + const int zVal = obj->anchorPos().z; for(int fx = 0; fx < obj->getWidth(); ++fx) { - int xVal = obj->pos.x - fx; + int xVal = obj->anchorPos().x - fx; for(int fy = 0; fy < obj->getHeight(); ++fy) { - int yVal = obj->pos.y - fy; + int yVal = obj->anchorPos().y - fy; if(xVal>=0 && xVal < width && yVal >= 0 && yVal < height) { TerrainTile & curt = terrain[zVal][xVal][yVal]; - if(obj->visitableAt(xVal, yVal)) - { + if(obj->visitableAt(int3(xVal, yVal, zVal))) curt.visitableObjects.push_back(obj); - curt.visitable = true; - } - if(obj->blockingAt(xVal, yVal)) - { + + if(obj->blockingAt(int3(xVal, yVal, zVal))) curt.blockingObjects.push_back(obj); - curt.blocked = true; - } } } } @@ -305,7 +348,7 @@ void CMap::calculateGuardingGreaturePositions() CGHeroInstance * CMap::getHero(HeroTypeID heroID) { for(auto & elem : heroesOnMap) - if(elem->getHeroType() == heroID) + if(elem->getHeroTypeID() == heroID) return elem; return nullptr; } @@ -384,7 +427,7 @@ int3 CMap::guardingCreaturePosition (int3 pos) const if (!isInTheMap(pos)) return int3(-1, -1, -1); const TerrainTile &posTile = getTile(pos); - if (posTile.visitable) + if (posTile.visitable()) { for (CGObjectInstance* obj : posTile.visitableObjects) { @@ -404,7 +447,7 @@ int3 CMap::guardingCreaturePosition (int3 pos) const if (isInTheMap(pos)) { const auto & tile = getTile(pos); - if (tile.visitable && (tile.isWater() == water)) + if (tile.visitable() && (tile.isWater() == water)) { for (CGObjectInstance* obj : tile.visitableObjects) { @@ -447,14 +490,14 @@ const CGObjectInstance * CMap::getObjectiveObjectFrom(const int3 & pos, Obj type bestMatch = object; else { - if (object->pos.dist2dSQ(pos) < bestMatch->pos.dist2dSQ(pos)) + if (object->anchorPos().dist2dSQ(pos) < bestMatch->anchorPos().dist2dSQ(pos)) bestMatch = object;// closer than one we already found } } } assert(bestMatch != nullptr); // if this happens - victory conditions or map itself is very, very broken - logGlobal->error("Will use %s from %s", bestMatch->getObjectName(), bestMatch->pos.toString()); + logGlobal->error("Will use %s from %s", bestMatch->getObjectName(), bestMatch->anchorPos().toString()); return bestMatch; } @@ -555,6 +598,34 @@ void CMap::eraseArtifactInstance(CArtifactInstance * art) artInstances[art->getId().getNum()].dellNull(); } +void CMap::moveArtifactInstance( + CArtifactSet & srcSet, const ArtifactPosition & srcSlot, + CArtifactSet & dstSet, const ArtifactPosition & dstSlot) +{ + auto art = srcSet.getArt(srcSlot); + removeArtifactInstance(srcSet, srcSlot); + putArtifactInstance(dstSet, art, dstSlot); +} + +void CMap::putArtifactInstance(CArtifactSet & set, CArtifactInstance * art, const ArtifactPosition & slot) +{ + art->addPlacementMap(set.putArtifact(slot, art)); +} + +void CMap::removeArtifactInstance(CArtifactSet & set, const ArtifactPosition & slot) +{ + auto art = set.getArt(slot); + assert(art); + set.removeArtifact(slot); + CArtifactSet::ArtPlacementMap partsMap; + for(auto & part : art->getPartsInfo()) + { + if(part.slot != ArtifactPosition::PRE_FIRST) + partsMap.try_emplace(part.art, ArtifactPosition::PRE_FIRST); + } + art->addPlacementMap(partsMap); +} + void CMap::addNewQuestInstance(CQuest* quest) { quest->qid = static_cast(quests.size()); @@ -583,7 +654,7 @@ void CMap::setUniqueInstanceName(CGObjectInstance * obj) auto uid = uidCounter++; boost::format fmt("%s_%d"); - fmt % obj->typeName % uid; + fmt % obj->getTypeName() % uid; obj->instanceName = fmt.str(); } @@ -610,7 +681,7 @@ void CMap::addNewObject(CGObjectInstance * obj) void CMap::moveObject(CGObjectInstance * obj, const int3 & pos) { removeBlockVisTiles(obj); - obj->pos = pos; + obj->setAnchorPos(pos); addBlockVisTiles(obj); } @@ -778,7 +849,7 @@ void CMap::reindexObjects() if (lhs->isRemovable() && !rhs->isRemovable()) return false; - return lhs->pos.y < rhs->pos.y; + return lhs->anchorPos().y < rhs->anchorPos().y; }); // instanceNames don't change diff --git a/lib/mapping/CMap.h b/lib/mapping/CMap.h index 46a4fa1e0..5192f7c1a 100644 --- a/lib/mapping/CMap.h +++ b/lib/mapping/CMap.h @@ -110,6 +110,9 @@ public: void addNewArtifactInstance(CArtifactSet & artSet); void addNewArtifactInstance(ConstTransitivePtr art); void eraseArtifactInstance(CArtifactInstance * art); + void moveArtifactInstance(CArtifactSet & srcSet, const ArtifactPosition & srcSlot, CArtifactSet & dstSet, const ArtifactPosition & dstSlot); + void putArtifactInstance(CArtifactSet & set, CArtifactInstance * art, const ArtifactPosition & slot); + void removeArtifactInstance(CArtifactSet & set, const ArtifactPosition & slot); void addNewQuestInstance(CQuest * quest); void removeQuestInstance(CQuest * quest); @@ -177,7 +180,7 @@ public: ui8 obeliskCount = 0; //how many obelisks are on map std::map obelisksVisited; //map: team_id => how many obelisks has been visited - std::vector townMerchantArtifacts; + std::vector townMerchantArtifacts; std::vector townUniversitySkills; void overrideGameSettings(const JsonNode & input); @@ -219,7 +222,25 @@ public: // static members h & obeliskCount; h & obelisksVisited; - h & townMerchantArtifacts; + + if (h.version < Handler::Version::REMOVE_VLC_POINTERS) + { + int32_t size = 0; + h & size; + for (int32_t i = 0; i < size; ++i) + { + bool isNull = false; + ArtifactID artifact; + h & isNull; + if (!isNull) + h & artifact; + townMerchantArtifacts.push_back(artifact); + } + } + else + { + h & townMerchantArtifacts; + } h & townUniversitySkills; h & instanceNames; diff --git a/lib/mapping/CMapDefines.h b/lib/mapping/CMapDefines.h index 17de14e28..bf18fb58d 100644 --- a/lib/mapping/CMapDefines.h +++ b/lib/mapping/CMapDefines.h @@ -43,7 +43,6 @@ public: ui32 firstOccurrence; ui32 nextOccurrence; /// specifies after how many days the event will occur the next time; 0 if event occurs only one time - std::vector deletedObjectsCoordinates; std::vector deletedObjectsInstances; std::vector unused; @@ -73,7 +72,6 @@ public: h & nextOccurrence; if(h.version >= Handler::Version::EVENT_OBJECTS_DELETION) { - h & deletedObjectsCoordinates; h & deletedObjectsInstances; } else @@ -121,20 +119,33 @@ struct DLL_LINKAGE TerrainTile Obj topVisitableId(bool excludeTop = false) const; CGObjectInstance * topVisitableObj(bool excludeTop = false) const; bool isWater() const; - EDiggingStatus getDiggingStatus(const bool excludeTop = true) const; + bool isLand() const; + EDiggingStatus getDiggingStatus(bool excludeTop = true) const; bool hasFavorableWinds() const; - const TerrainType * terType; - const RiverType * riverType; - const RoadType * roadType; + bool visitable() const; + bool blocked() const; + + const TerrainType * getTerrain() const; + const RiverType * getRiver() const; + const RoadType * getRoad() const; + + TerrainId getTerrainID() const; + RiverId getRiverID() const; + RoadId getRoadID() const; + + bool hasRiver() const; + bool hasRoad() const; + + TerrainId terrainType; + RiverId riverType; + RoadId roadType; ui8 terView; ui8 riverDir; ui8 roadDir; /// first two bits - how to rotate terrain graphic (next two - river graphic, next two - road); /// 7th bit - whether tile is coastal (allows disembarking if land or block movement if water); 8th bit - Favorable Winds effect ui8 extTileFlags; - bool visitable; - bool blocked; std::vector visitableObjects; std::vector blockingObjects; @@ -142,15 +153,49 @@ struct DLL_LINKAGE TerrainTile template void serialize(Handler & h) { - h & terType; + if (h.version >= Handler::Version::REMOVE_VLC_POINTERS) + { + h & terrainType; + } + else + { + bool isNull = false; + h & isNull; + if (!isNull) + h & terrainType; + } h & terView; - h & riverType; + if (h.version >= Handler::Version::REMOVE_VLC_POINTERS) + { + h & riverType; + } + else + { + bool isNull = false; + h & isNull; + if (!isNull) + h & riverType; + } h & riverDir; - h & roadType; + if (h.version >= Handler::Version::REMOVE_VLC_POINTERS) + { + h & roadType; + } + else + { + bool isNull = false; + h & isNull; + if (!isNull) + h & roadType; + } h & roadDir; h & extTileFlags; - h & visitable; - h & blocked; + if (h.version < Handler::Version::REMOVE_VLC_POINTERS) + { + bool unused = false; + h & unused; + h & unused; + } h & visitableObjects; h & blockingObjects; } diff --git a/lib/mapping/CMapHeader.cpp b/lib/mapping/CMapHeader.cpp index 61a210b8d..66ed48dbd 100644 --- a/lib/mapping/CMapHeader.cpp +++ b/lib/mapping/CMapHeader.cpp @@ -12,9 +12,9 @@ #include "MapFormat.h" -#include "../CHeroHandler.h" #include "../VCMI_Lib.h" #include "../entities/faction/CTownHandler.h" +#include "../entities/hero/CHeroHandler.h" #include "../json/JsonUtils.h" #include "../modding/CModHandler.h" #include "../texts/CGeneralTextHandler.h" @@ -189,7 +189,7 @@ void CMapHeader::registerMapStrings() JsonUtils::mergeCopy(data, translations[language]); for(auto & s : data.Struct()) - texts.registerString("map", TextIdentifier(s.first), s.second.String(), language); + texts.registerString("map", TextIdentifier(s.first), s.second.String()); } std::string mapRegisterLocalizedString(const std::string & modContext, CMapHeader & mapHeader, const TextIdentifier & UID, const std::string & localized) @@ -199,7 +199,7 @@ std::string mapRegisterLocalizedString(const std::string & modContext, CMapHeade std::string mapRegisterLocalizedString(const std::string & modContext, CMapHeader & mapHeader, const TextIdentifier & UID, const std::string & localized, const std::string & language) { - mapHeader.texts.registerString(modContext, UID, localized, language); + mapHeader.texts.registerString(modContext, UID, localized); mapHeader.translations.Struct()[language].Struct()[UID.get()].String() = localized; return UID.get(); } diff --git a/lib/mapping/CMapInfo.cpp b/lib/mapping/CMapInfo.cpp index 74ee2cd44..a13084ac9 100644 --- a/lib/mapping/CMapInfo.cpp +++ b/lib/mapping/CMapInfo.cpp @@ -25,7 +25,6 @@ #include "../texts/TextOperations.h" #include "../CCreatureHandler.h" #include "../IGameSettings.h" -#include "../CHeroHandler.h" #include "../CConfigHandler.h" VCMI_LIB_NAMESPACE_BEGIN diff --git a/lib/mapping/CMapOperation.cpp b/lib/mapping/CMapOperation.cpp index 7aaf268fc..88a3100e5 100644 --- a/lib/mapping/CMapOperation.cpp +++ b/lib/mapping/CMapOperation.cpp @@ -103,7 +103,7 @@ void CDrawTerrainOperation::execute() for(const auto & pos : terrainSel.getSelectedItems()) { auto & tile = map->getTile(pos); - tile.terType = const_cast(VLC->terrainTypeHandler->getById(terType)); + tile.terrainType = terType; invalidateTerrainViews(pos); } @@ -137,7 +137,7 @@ void CDrawTerrainOperation::updateTerrainTypes() auto tiles = getInvalidTiles(centerPos); auto updateTerrainType = [&](const int3& pos) { - map->getTile(pos).terType = centerTile.terType; + map->getTile(pos).terrainType = centerTile.terrainType; positions.insert(pos); invalidateTerrainViews(pos); //logGlobal->debug("Set additional terrain tile at pos '%s' to type '%s'", pos, centerTile.terType); @@ -161,10 +161,10 @@ void CDrawTerrainOperation::updateTerrainTypes() rect.forEach([&](const int3& posToTest) { auto & terrainTile = map->getTile(posToTest); - if(centerTile.terType->getId() != terrainTile.terType->getId()) + if(centerTile.getTerrain() != terrainTile.getTerrain()) { - const auto * formerTerType = terrainTile.terType; - terrainTile.terType = centerTile.terType; + const auto formerTerType = terrainTile.terrainType; + terrainTile.terrainType = centerTile.terrainType; auto testTile = getInvalidTiles(posToTest); int nativeTilesCntNorm = testTile.nativeTiles.empty() ? std::numeric_limits::max() : static_cast(testTile.nativeTiles.size()); @@ -221,7 +221,7 @@ void CDrawTerrainOperation::updateTerrainTypes() suitableTiles.insert(posToTest); } - terrainTile.terType = formerTerType; + terrainTile.terrainType = formerTerType; } }); @@ -264,7 +264,7 @@ void CDrawTerrainOperation::updateTerrainViews() { for(const auto & pos : invalidatedTerViews) { - const auto & patterns = VLC->terviewh->getTerrainViewPatterns(map->getTile(pos).terType->getId()); + const auto & patterns = VLC->terviewh->getTerrainViewPatterns(map->getTile(pos).getTerrainID()); // Detect a pattern which fits best int bestPattern = -1; @@ -340,7 +340,7 @@ CDrawTerrainOperation::ValidationResult CDrawTerrainOperation::validateTerrainVi CDrawTerrainOperation::ValidationResult CDrawTerrainOperation::validateTerrainViewInner(const int3& pos, const TerrainViewPattern& pattern, int recDepth) const { - const auto * centerTerType = map->getTile(pos).terType; + const auto * centerTerType = map->getTile(pos).getTerrain(); int totalPoints = 0; std::string transitionReplacement; @@ -372,24 +372,24 @@ CDrawTerrainOperation::ValidationResult CDrawTerrainOperation::validateTerrainVi } else if(widthTooHigh) { - terType = map->getTile(int3(currentPos.x - 1, currentPos.y, currentPos.z)).terType; + terType = map->getTile(int3(currentPos.x - 1, currentPos.y, currentPos.z)).getTerrain(); } else if(heightTooHigh) { - terType = map->getTile(int3(currentPos.x, currentPos.y - 1, currentPos.z)).terType; + terType = map->getTile(int3(currentPos.x, currentPos.y - 1, currentPos.z)).getTerrain(); } else if(widthTooLess) { - terType = map->getTile(int3(currentPos.x + 1, currentPos.y, currentPos.z)).terType; + terType = map->getTile(int3(currentPos.x + 1, currentPos.y, currentPos.z)).getTerrain(); } else if(heightTooLess) { - terType = map->getTile(int3(currentPos.x, currentPos.y + 1, currentPos.z)).terType; + terType = map->getTile(int3(currentPos.x, currentPos.y + 1, currentPos.z)).getTerrain(); } } else { - terType = map->getTile(currentPos).terType; + terType = map->getTile(currentPos).getTerrain(); if(terType != centerTerType && (terType->isPassable() || centerTerType->isPassable())) { isAlien = true; @@ -509,13 +509,13 @@ CDrawTerrainOperation::InvalidTiles CDrawTerrainOperation::getInvalidTiles(const { //TODO: this is very expensive function for RMG, needs optimization InvalidTiles tiles; - const auto * centerTerType = map->getTile(centerPos).terType; + const auto * centerTerType = map->getTile(centerPos).getTerrain(); auto rect = extendTileAround(centerPos); rect.forEach([&](const int3& pos) { if(map->isInTheMap(pos)) { - const auto * terType = map->getTile(pos).terType; + const auto * terType = map->getTile(pos).getTerrain(); auto valid = validateTerrainView(pos, VLC->terviewh->getTerrainTypePatternById("n1")).result; // Special validity check for rock & water @@ -615,7 +615,7 @@ std::string CInsertObjectOperation::getLabel() const CMoveObjectOperation::CMoveObjectOperation(CMap* map, CGObjectInstance* obj, const int3& targetPosition) : CMapOperation(map), obj(obj), - initialPos(obj->pos), + initialPos(obj->anchorPos()), targetPos(targetPosition) { } diff --git a/lib/mapping/CMapService.cpp b/lib/mapping/CMapService.cpp index 2b7af0de3..61a1958b0 100644 --- a/lib/mapping/CMapService.cpp +++ b/lib/mapping/CMapService.cpp @@ -17,9 +17,8 @@ #include "../filesystem/CMemoryStream.h" #include "../filesystem/CMemoryBuffer.h" #include "../modding/CModHandler.h" +#include "../modding/ModDescription.h" #include "../modding/ModScope.h" -#include "../modding/CModInfo.h" -#include "../texts/Languages.h" #include "../VCMI_Lib.h" #include "CMap.h" @@ -34,8 +33,7 @@ VCMI_LIB_NAMESPACE_BEGIN std::unique_ptr CMapService::loadMap(const ResourcePath & name, IGameCallback * cb) const { std::string modName = VLC->modh->findResourceOrigin(name); - std::string language = VLC->modh->getModLanguage(modName); - std::string encoding = Languages::getLanguageOptions(language).encoding; + std::string encoding = VLC->modh->findResourceEncoding(name); auto stream = getStreamFromFS(name); return getMapLoader(stream, name.getName(), modName, encoding)->loadMap(cb); @@ -44,8 +42,7 @@ std::unique_ptr CMapService::loadMap(const ResourcePath & name, IGameCallb std::unique_ptr CMapService::loadMapHeader(const ResourcePath & name) const { std::string modName = VLC->modh->findResourceOrigin(name); - std::string language = VLC->modh->getModLanguage(modName); - std::string encoding = Languages::getLanguageOptions(language).encoding; + std::string encoding = VLC->modh->findResourceEncoding(name); auto stream = getStreamFromFS(name); return getMapLoader(stream, name.getName(), modName, encoding)->loadMapHeader(); @@ -102,7 +99,7 @@ ModCompatibilityInfo CMapService::verifyMapHeaderMods(const CMapHeader & map) if(vstd::contains(activeMods, mapMod.first)) { const auto & modInfo = VLC->modh->getModInfo(mapMod.first); - if(modInfo.getVerificationInfo().version.compatible(mapMod.second.version)) + if(modInfo.getVersion().compatible(mapMod.second.version)) continue; } missingMods[mapMod.first] = mapMod.second; diff --git a/lib/mapping/MapEditUtils.cpp b/lib/mapping/MapEditUtils.cpp index cee52194f..9a4588727 100644 --- a/lib/mapping/MapEditUtils.cpp +++ b/lib/mapping/MapEditUtils.cpp @@ -356,7 +356,7 @@ void CTerrainViewPatternUtils::printDebuggingInfoAboutTile(const CMap * map, con { auto debugTile = map->getTile(debugPos); - std::string terType = debugTile.terType->shortIdentifier; + std::string terType = debugTile.getTerrain()->shortIdentifier; line += terType; line.insert(line.end(), PADDED_LENGTH - terType.size(), ' '); } diff --git a/lib/mapping/MapFormatH3M.cpp b/lib/mapping/MapFormatH3M.cpp index 32e0057ac..969e168b2 100644 --- a/lib/mapping/MapFormatH3M.cpp +++ b/lib/mapping/MapFormatH3M.cpp @@ -18,7 +18,6 @@ #include "../ArtifactUtils.h" #include "../CCreatureHandler.h" #include "../texts/CGeneralTextHandler.h" -#include "../CHeroHandler.h" #include "../CSkillHandler.h" #include "../CStopWatch.h" #include "../IGameSettings.h" @@ -27,6 +26,7 @@ #include "../TerrainHandler.h" #include "../VCMI_Lib.h" #include "../constants/StringConstants.h" +#include "../entities/hero/CHeroHandler.h" #include "../filesystem/CBinaryReader.h" #include "../filesystem/Filesystem.h" #include "../mapObjectConstructors/AObjectTypeHandler.h" @@ -208,14 +208,13 @@ void CMapLoaderH3M::readHeader() // optimization - load mappings only once to avoid slow parsing of map headers for map list static const std::map identifierMappers = generateMappings(); + if (!identifierMappers.count(mapHeader->version)) + throw std::runtime_error("Unsupported map format! Format ID " + std::to_string(static_cast(mapHeader->version))); + const MapIdentifiersH3M & identifierMapper = identifierMappers.at(mapHeader->version); reader->setIdentifierRemapper(identifierMapper); - // include basic mod - if(mapHeader->version == EMapFormat::WOG) - mapHeader->mods["wake-of-gods"]; - // Read map name, description, dimensions,... mapHeader->areAnyPlayers = reader->readBool(); mapHeader->height = mapHeader->width = reader->readInt32(); @@ -896,7 +895,7 @@ void CMapLoaderH3M::readPredefinedHeroes() } map->predefinedHeroes.emplace_back(hero); - logGlobal->debug("Map '%s': Hero predefined in map: %s", mapName, VLC->heroh->getById(hero->getHeroType())->getJsonKey()); + logGlobal->debug("Map '%s': Hero predefined in map: %s", mapName, hero->getHeroType()->getJsonKey()); } } @@ -913,11 +912,11 @@ void CMapLoaderH3M::loadArtifactsOfHero(CGHeroInstance * hero) if(!hero->artifactsWorn.empty() || !hero->artifactsInBackpack.empty()) { - logGlobal->debug("Hero %d at %s has set artifacts twice (in map properties and on adventure map instance). Using the latter set...", hero->getHeroType().getNum(), hero->pos.toString()); + logGlobal->debug("Hero %d at %s has set artifacts twice (in map properties and on adventure map instance). Using the latter set...", hero->getHeroTypeID().getNum(), hero->anchorPos().toString()); hero->artifactsInBackpack.clear(); while(!hero->artifactsWorn.empty()) - hero->eraseArtSlot(hero->artifactsWorn.begin()->first); + hero->removeArtifact(hero->artifactsWorn.begin()->first); } for(int i = 0; i < features.artifactSlotsCount; i++) @@ -959,7 +958,7 @@ bool CMapLoaderH3M::loadArtifactToSlot(CGHeroInstance * hero, int slot) if(ArtifactID(artifactID).toArtifact()->canBePutAt(hero, ArtifactPosition(slot))) { auto * artifact = ArtifactUtils::createArtifact(artifactID); - artifact->putAt(*hero, ArtifactPosition(slot)); + map->putArtifactInstance(*hero, artifact, slot); map->addNewArtifactInstance(artifact); } else @@ -985,17 +984,13 @@ void CMapLoaderH3M::readTerrain() for(pos.x = 0; pos.x < map->width; pos.x++) { auto & tile = map->getTile(pos); - tile.terType = VLC->terrainTypeHandler->getById(reader->readTerrain()); + tile.terrainType = reader->readTerrain(); tile.terView = reader->readUInt8(); - tile.riverType = VLC->riverTypeHandler->getById(reader->readRiver()); + tile.riverType = reader->readRiver(); tile.riverDir = reader->readUInt8(); - tile.roadType = VLC->roadTypeHandler->getById(reader->readRoad()); + tile.roadType = reader->readRoad(); tile.roadDir = reader->readUInt8(); tile.extTileFlags = reader->readUInt8(); - tile.blocked = !tile.terType->isPassable(); - tile.visitable = false; - - assert(tile.terType->getId() != ETerrainId::NONE); } } } @@ -1478,9 +1473,9 @@ CGObjectInstance * CMapLoaderH3M::readShipyard(const int3 & mapPosition, std::sh return object; } -CGObjectInstance * CMapLoaderH3M::readLighthouse(const int3 & mapPosition) +CGObjectInstance * CMapLoaderH3M::readLighthouse(const int3 & mapPosition, std::shared_ptr objectTemplate) { - auto * object = new CGLighthouse(map->cb); + auto * object = readGeneric(mapPosition, objectTemplate); setOwnerAndValidate(mapPosition, object, reader->readPlayer32()); return object; } @@ -1618,7 +1613,7 @@ CGObjectInstance * CMapLoaderH3M::readObject(std::shared_ptrpos = mapPosition; + newObject->setAnchorPos(mapPosition); newObject->ID = objectTemplate->id; newObject->id = objectInstanceID; if(newObject->ID != Obj::HERO && newObject->ID != Obj::HERO_PLACEHOLDER && newObject->ID != Obj::PRISON) @@ -1778,7 +1773,7 @@ CGObjectInstance * CMapLoaderH3M::readHero(const int3 & mapPosition, const Objec for(auto & elem : map->disposedHeroes) { - if(elem.heroId == object->getHeroType()) + if(elem.heroId == object->getHeroTypeID()) { object->nameCustomTextId = elem.name; object->customPortraitSource = elem.portrait; @@ -1788,7 +1783,7 @@ CGObjectInstance * CMapLoaderH3M::readHero(const int3 & mapPosition, const Objec bool hasName = reader->readBool(); if(hasName) - object->nameCustomTextId = readLocalizedString(TextIdentifier("heroes", object->getHeroType().getNum(), "name")); + object->nameCustomTextId = readLocalizedString(TextIdentifier("heroes", object->getHeroTypeID().getNum(), "name")); if(features.levelSOD) { @@ -1891,7 +1886,7 @@ CGObjectInstance * CMapLoaderH3M::readHero(const int3 & mapPosition, const Objec auto ps = object->getAllBonuses(Selector::type()(BonusType::PRIMARY_SKILL).And(Selector::sourceType()(BonusSource::HERO_BASE_SKILL)), nullptr); if(ps->size()) { - logGlobal->debug("Hero %s has set primary skills twice (in map properties and on adventure map instance). Using the latter set...", object->getHeroType().getNum() ); + logGlobal->debug("Hero %s has set primary skills twice (in map properties and on adventure map instance). Using the latter set...", object->getHeroTypeID().getNum() ); for(const auto & b : *ps) object->removeBonus(b); } @@ -1904,7 +1899,7 @@ CGObjectInstance * CMapLoaderH3M::readHero(const int3 & mapPosition, const Objec } if (object->subID != MapObjectSubID()) - logGlobal->debug("Map '%s': Hero on map: %s at %s, owned by %s", mapName, VLC->heroh->getById(object->getHeroType())->getJsonKey(), mapPosition.toString(), object->getOwner().toString()); + logGlobal->debug("Map '%s': Hero on map: %s at %s, owned by %s", mapName, object->getHeroType()->getJsonKey(), mapPosition.toString(), object->getOwner().toString()); else logGlobal->debug("Map '%s': Hero on map: (random) at %s, owned by %s", mapName, mapPosition.toString(), object->getOwner().toString()); @@ -2117,7 +2112,7 @@ EQuestMission CMapLoaderH3M::readQuest(IQuestObject * guard, const int3 & positi guard->quest->mission.creatures.resize(typeNumber); for(size_t hh = 0; hh < typeNumber; ++hh) { - guard->quest->mission.creatures[hh].type = reader->readCreature().toCreature(); + guard->quest->mission.creatures[hh].setType(reader->readCreature().toCreature()); guard->quest->mission.creatures[hh].count = reader->readUInt16(); } break; @@ -2235,10 +2230,7 @@ CGObjectInstance * CMapLoaderH3M::readTown(const int3 & position, std::shared_pt } if(features.levelHOTA1) - { - // TODO: HOTA support - [[maybe_unused]] bool spellResearchAvailable = reader->readBool(); - } + object->spellResearchAllowed = reader->readBool(); // Read castle events uint32_t eventsCount = reader->readUInt32(); diff --git a/lib/mapping/MapFormatH3M.h b/lib/mapping/MapFormatH3M.h index 3cc3e93da..7196edbe0 100644 --- a/lib/mapping/MapFormatH3M.h +++ b/lib/mapping/MapFormatH3M.h @@ -208,7 +208,7 @@ private: CGObjectInstance * readPyramid(const int3 & position, std::shared_ptr objTempl); CGObjectInstance * readQuestGuard(const int3 & position); CGObjectInstance * readShipyard(const int3 & mapPosition, std::shared_ptr objectTemplate); - CGObjectInstance * readLighthouse(const int3 & mapPosition); + CGObjectInstance * readLighthouse(const int3 & mapPosition, std::shared_ptr objectTemplate); CGObjectInstance * readGeneric(const int3 & position, std::shared_ptr objectTemplate); CGObjectInstance * readBank(const int3 & position, std::shared_ptr objectTemplate); diff --git a/lib/mapping/MapFormatJson.cpp b/lib/mapping/MapFormatJson.cpp index 753e235c7..2e8868b03 100644 --- a/lib/mapping/MapFormatJson.cpp +++ b/lib/mapping/MapFormatJson.cpp @@ -17,12 +17,12 @@ #include "CMap.h" #include "MapFormat.h" #include "../ArtifactUtils.h" -#include "../CHeroHandler.h" #include "../VCMI_Lib.h" #include "../RiverHandler.h" #include "../RoadHandler.h" #include "../TerrainHandler.h" #include "../entities/faction/CTownHandler.h" +#include "../entities/hero/CHeroHandler.h" #include "../mapObjectConstructors/AObjectTypeHandler.h" #include "../mapObjectConstructors/CObjectClassesHandler.h" #include "../mapObjects/ObjectTemplate.h" @@ -260,34 +260,34 @@ CMapFormatJson::CMapFormatJson(): } -TerrainType * CMapFormatJson::getTerrainByCode(const std::string & code) +TerrainId CMapFormatJson::getTerrainByCode(const std::string & code) { for(const auto & object : VLC->terrainTypeHandler->objects) { if(object->shortIdentifier == code) - return const_cast(object.get()); + return object->getId(); } - return nullptr; + return TerrainId::NONE; } -RiverType * CMapFormatJson::getRiverByCode(const std::string & code) +RiverId CMapFormatJson::getRiverByCode(const std::string & code) { for(const auto & object : VLC->riverTypeHandler->objects) { if (object->shortIdentifier == code) - return const_cast(object.get()); + return object->getId(); } - return nullptr; + return RiverId::NO_RIVER; } -RoadType * CMapFormatJson::getRoadByCode(const std::string & code) +RoadId CMapFormatJson::getRoadByCode(const std::string & code) { for(const auto & object : VLC->roadTypeHandler->objects) { if (object->shortIdentifier == code) - return const_cast(object.get()); + return object->getId(); } - return nullptr; + return RoadId::NO_ROAD; } void CMapFormatJson::serializeAllowedFactions(JsonSerializeFormat & handler, std::set & value) const @@ -436,10 +436,8 @@ void CMapFormatJson::serializePlayerInfo(JsonSerializeFormat & handler) if(hero->ID == Obj::HERO) { std::string temp; - if(hero->type) - temp = hero->type->getJsonKey(); - else - temp = hero->getHeroType().toEntity(VLC)->getJsonKey(); + if(hero->getHeroTypeID().hasValue()) + temp = hero->getHeroType()->getJsonKey(); handler.serializeString("type", temp); } @@ -892,7 +890,7 @@ void CMapLoaderJson::readTerrainTile(const std::string & src, TerrainTile & tile using namespace TerrainDetail; {//terrain type const std::string typeCode = src.substr(0, 2); - tile.terType = getTerrainByCode(typeCode); + tile.terrainType = getTerrainByCode(typeCode); } int startPos = 2; //0+typeCode fixed length {//terrain view @@ -922,7 +920,7 @@ void CMapLoaderJson::readTerrainTile(const std::string & src, TerrainTile & tile tile.roadType = getRoadByCode(typeCode); if(!tile.roadType) //it's not a road, it's a river { - tile.roadType = VLC->roadTypeHandler->getById(Road::NO_ROAD); + tile.roadType = Road::NO_ROAD; tile.riverType = getRiverByCode(typeCode); hasRoad = false; if(!tile.riverType) @@ -1016,8 +1014,6 @@ void CMapLoaderJson::readTerrain() const JsonNode underground = getFromArchive(TERRAIN_FILE_NAMES[1]); readTerrainLevel(underground, 1); } - - map->calculateWaterContent(); } CMapLoaderJson::MapObjectLoader::MapObjectLoader(CMapLoaderJson * _owner, JsonMap::value_type & json): @@ -1072,7 +1068,7 @@ void CMapLoaderJson::MapObjectLoader::construct() instance->id = ObjectInstanceID(static_cast(owner->map->objects.size())); instance->instanceName = jsonKey; - instance->pos = pos; + instance->setAnchorPos(pos); owner->map->addNewObject(instance); } @@ -1154,10 +1150,10 @@ void CMapLoaderJson::readObjects() auto * hero = dynamic_cast(object.get()); - if (debugHeroesOnMap.count(hero->getHeroType())) + if (debugHeroesOnMap.count(hero->getHeroTypeID())) logGlobal->error("Hero is already on the map at %s", hero->visitablePos().toString()); - debugHeroesOnMap.insert(hero->getHeroType()); + debugHeroesOnMap.insert(hero->getHeroTypeID()); } } @@ -1258,13 +1254,13 @@ std::string CMapSaverJson::writeTerrainTile(const TerrainTile & tile) out.setf(std::ios::dec, std::ios::basefield); out.unsetf(std::ios::showbase); - out << tile.terType->shortIdentifier << static_cast(tile.terView) << flipCodes[tile.extTileFlags % 4]; + out << tile.getTerrain()->shortIdentifier << static_cast(tile.terView) << flipCodes[tile.extTileFlags % 4]; - if(tile.roadType->getId() != Road::NO_ROAD) - out << tile.roadType->shortIdentifier << static_cast(tile.roadDir) << flipCodes[(tile.extTileFlags >> 4) % 4]; + if(tile.hasRoad()) + out << tile.getRoad()->shortIdentifier << static_cast(tile.roadDir) << flipCodes[(tile.extTileFlags >> 4) % 4]; - if(tile.riverType->getId() != River::NO_RIVER) - out << tile.riverType->shortIdentifier << static_cast(tile.riverDir) << flipCodes[(tile.extTileFlags >> 2) % 4]; + if(tile.hasRiver()) + out << tile.getRiver()->shortIdentifier << static_cast(tile.riverDir) << flipCodes[(tile.extTileFlags >> 2) % 4]; return out.str(); } diff --git a/lib/mapping/MapFormatJson.h b/lib/mapping/MapFormatJson.h index 117e9f2bb..5688b0ca9 100644 --- a/lib/mapping/MapFormatJson.h +++ b/lib/mapping/MapFormatJson.h @@ -60,9 +60,9 @@ protected: CMapFormatJson(); - static TerrainType * getTerrainByCode(const std::string & code); - static RiverType * getRiverByCode(const std::string & code); - static RoadType * getRoadByCode(const std::string & code); + static TerrainId getTerrainByCode(const std::string & code); + static RiverId getRiverByCode(const std::string & code); + static RoadId getRoadByCode(const std::string & code); void serializeAllowedFactions(JsonSerializeFormat & handler, std::set & value) const; diff --git a/lib/mapping/MapIdentifiersH3M.cpp b/lib/mapping/MapIdentifiersH3M.cpp index 3e093227b..e75e58235 100644 --- a/lib/mapping/MapIdentifiersH3M.cpp +++ b/lib/mapping/MapIdentifiersH3M.cpp @@ -12,7 +12,6 @@ #include "MapIdentifiersH3M.h" #include "../VCMI_Lib.h" -#include "../CHeroHandler.h" #include "../entities/faction/CFaction.h" #include "../entities/faction/CTownHandler.h" #include "../filesystem/Filesystem.h" diff --git a/lib/mapping/MapReaderH3M.cpp b/lib/mapping/MapReaderH3M.cpp index a2fec4e04..21c2a8771 100644 --- a/lib/mapping/MapReaderH3M.cpp +++ b/lib/mapping/MapReaderH3M.cpp @@ -410,9 +410,11 @@ bool MapReaderH3M::readBool() int8_t MapReaderH3M::readInt8Checked(int8_t lowerLimit, int8_t upperLimit) { int8_t result = readInt8(); - assert(result >= lowerLimit); - assert(result <= upperLimit); - return std::clamp(result, lowerLimit, upperLimit); + int8_t resultClamped = std::clamp(result, lowerLimit, upperLimit); + if (result != resultClamped) + logGlobal->warn("Map contains out of range value %d! Expected %d-%d", static_cast(result), static_cast(lowerLimit), static_cast(upperLimit)); + + return resultClamped; } uint8_t MapReaderH3M::readUInt8() diff --git a/lib/modding/ActiveModsInSaveList.cpp b/lib/modding/ActiveModsInSaveList.cpp index 17e8927f0..c4620c170 100644 --- a/lib/modding/ActiveModsInSaveList.cpp +++ b/lib/modding/ActiveModsInSaveList.cpp @@ -11,7 +11,7 @@ #include "ActiveModsInSaveList.h" #include "../VCMI_Lib.h" -#include "CModInfo.h" +#include "ModDescription.h" #include "CModHandler.h" #include "ModIncompatibility.h" @@ -21,13 +21,13 @@ std::vector ActiveModsInSaveList::getActiveGameplayAffectingMods() { std::vector result; for (auto const & entry : VLC->modh->getActiveMods()) - if (VLC->modh->getModInfo(entry).checkModGameplayAffecting()) + if (VLC->modh->getModInfo(entry).affectsGameplay()) result.push_back(entry); return result; } -const ModVerificationInfo & ActiveModsInSaveList::getVerificationInfo(TModID mod) +ModVerificationInfo ActiveModsInSaveList::getVerificationInfo(TModID mod) { return VLC->modh->getModInfo(mod).getVerificationInfo(); } @@ -44,10 +44,10 @@ void ActiveModsInSaveList::verifyActiveMods(const std::mapmodh->getModInfo(compared.first).getVerificationInfo().name); + missingMods.push_back(VLC->modh->getModInfo(compared.first).getName()); if (compared.second == ModVerificationStatus::EXCESSIVE) - excessiveMods.push_back(VLC->modh->getModInfo(compared.first).getVerificationInfo().name); + excessiveMods.push_back(VLC->modh->getModInfo(compared.first).getName()); } if(!missingMods.empty() || !excessiveMods.empty()) diff --git a/lib/modding/ActiveModsInSaveList.h b/lib/modding/ActiveModsInSaveList.h index 0d51ed4f3..d89244788 100644 --- a/lib/modding/ActiveModsInSaveList.h +++ b/lib/modding/ActiveModsInSaveList.h @@ -17,7 +17,7 @@ VCMI_LIB_NAMESPACE_BEGIN class ActiveModsInSaveList { std::vector getActiveGameplayAffectingMods(); - const ModVerificationInfo & getVerificationInfo(TModID mod); + ModVerificationInfo getVerificationInfo(TModID mod); /// Checks whether provided mod list is compatible with current VLC and throws on failure void verifyActiveMods(const std::map & modList); @@ -29,7 +29,10 @@ public: std::vector activeMods = getActiveGameplayAffectingMods(); h & activeMods; for(const auto & m : activeMods) - h & getVerificationInfo(m); + { + ModVerificationInfo info = getVerificationInfo(m); + h & info; + } } else { diff --git a/lib/modding/CModHandler.cpp b/lib/modding/CModHandler.cpp index 8d20639f2..06518e107 100644 --- a/lib/modding/CModHandler.cpp +++ b/lib/modding/CModHandler.cpp @@ -10,266 +10,45 @@ #include "StdInc.h" #include "CModHandler.h" -#include "CModInfo.h" -#include "ModScope.h" #include "ContentTypeHandler.h" #include "IdentifierStorage.h" -#include "ModIncompatibility.h" +#include "ModDescription.h" +#include "ModManager.h" +#include "ModScope.h" +#include "../CConfigHandler.h" #include "../CCreatureHandler.h" -#include "../CStopWatch.h" #include "../GameSettings.h" #include "../ScriptHandler.h" -#include "../constants/StringConstants.h" +#include "../VCMI_Lib.h" #include "../filesystem/Filesystem.h" #include "../json/JsonUtils.h" -#include "../spells/CSpellHandler.h" #include "../texts/CGeneralTextHandler.h" #include "../texts/Languages.h" -#include "../VCMI_Lib.h" VCMI_LIB_NAMESPACE_BEGIN -static JsonNode loadModSettings(const JsonPath & path) -{ - if (CResourceHandler::get("local")->existsResource(ResourcePath(path))) - { - return JsonNode(path); - } - // Probably new install. Create initial configuration - CResourceHandler::get("local")->createResource(path.getOriginalName() + ".json"); - return JsonNode(); -} - CModHandler::CModHandler() : content(std::make_shared()) - , coreMod(std::make_unique()) + , modManager(std::make_unique()) { } CModHandler::~CModHandler() = default; -// currentList is passed by value to get current list of depending mods -bool CModHandler::hasCircularDependency(const TModID & modID, std::set currentList) const -{ - const CModInfo & mod = allMods.at(modID); - - // Mod already present? We found a loop - if (vstd::contains(currentList, modID)) - { - logMod->error("Error: Circular dependency detected! Printing dependency list:"); - logMod->error("\t%s -> ", mod.getVerificationInfo().name); - return true; - } - - currentList.insert(modID); - - // recursively check every dependency of this mod - for(const TModID & dependency : mod.dependencies) - { - if (hasCircularDependency(dependency, currentList)) - { - logMod->error("\t%s ->\n", mod.getVerificationInfo().name); // conflict detected, print dependency list - return true; - } - } - return false; -} - -// Returned vector affects the resource loaders call order (see CFilesystemList::load). -// The loaders call order matters when dependent mod overrides resources in its dependencies. -std::vector CModHandler::validateAndSortDependencies(std::vector modsToResolve) const -{ - // Topological sort algorithm. - // TODO: Investigate possible ways to improve performance. - boost::range::sort(modsToResolve); // Sort mods per name - std::vector sortedValidMods; // Vector keeps order of elements (LIFO) - sortedValidMods.reserve(modsToResolve.size()); // push_back calls won't cause memory reallocation - std::set resolvedModIDs; // Use a set for validation for performance reason, but set does not keep order of elements - - // Mod is resolved if it has not dependencies or all its dependencies are already resolved - auto isResolved = [&](const CModInfo & mod) -> bool - { - if(mod.dependencies.size() > resolvedModIDs.size()) - return false; - - for(const TModID & dependency : mod.dependencies) - { - if(!vstd::contains(resolvedModIDs, dependency)) - return false; - } - - for(const TModID & conflict : mod.conflicts) - { - if(vstd::contains(resolvedModIDs, conflict)) - return false; - } - for(const TModID & reverseConflict : resolvedModIDs) - { - if (vstd::contains(allMods.at(reverseConflict).conflicts, mod.identifier)) - return false; - } - return true; - }; - - while(true) - { - std::set resolvedOnCurrentTreeLevel; - for(auto it = modsToResolve.begin(); it != modsToResolve.end();) // One iteration - one level of mods tree - { - if(isResolved(allMods.at(*it))) - { - resolvedOnCurrentTreeLevel.insert(*it); // Not to the resolvedModIDs, so current node children will be resolved on the next iteration - sortedValidMods.push_back(*it); - it = modsToResolve.erase(it); - continue; - } - it++; - } - if(!resolvedOnCurrentTreeLevel.empty()) - { - resolvedModIDs.insert(resolvedOnCurrentTreeLevel.begin(), resolvedOnCurrentTreeLevel.end()); - continue; - } - // If there're no valid mods on the current mods tree level, no more mod can be resolved, should be end. - break; - } - - modLoadErrors = std::make_unique(); - - auto addErrorMessage = [this](const std::string & textID, const std::string & brokenModID, const std::string & missingModID) - { - modLoadErrors->appendTextID(textID); - - if (allMods.count(brokenModID)) - modLoadErrors->replaceRawString(allMods.at(brokenModID).getVerificationInfo().name); - else - modLoadErrors->replaceRawString(brokenModID); - - if (allMods.count(missingModID)) - modLoadErrors->replaceRawString(allMods.at(missingModID).getVerificationInfo().name); - else - modLoadErrors->replaceRawString(missingModID); - - }; - - // Left mods have unresolved dependencies, output all to log. - for(const auto & brokenModID : modsToResolve) - { - const CModInfo & brokenMod = allMods.at(brokenModID); - for(const TModID & dependency : brokenMod.dependencies) - { - if(!vstd::contains(resolvedModIDs, dependency) && brokenMod.config["modType"].String() != "Compatibility") - addErrorMessage("vcmi.server.errors.modNoDependency", brokenModID, dependency); - } - for(const TModID & conflict : brokenMod.conflicts) - { - if(vstd::contains(resolvedModIDs, conflict)) - addErrorMessage("vcmi.server.errors.modConflict", brokenModID, conflict); - } - for(const TModID & reverseConflict : resolvedModIDs) - { - if (vstd::contains(allMods.at(reverseConflict).conflicts, brokenModID)) - addErrorMessage("vcmi.server.errors.modConflict", brokenModID, reverseConflict); - } - } - return sortedValidMods; -} - -std::vector CModHandler::getModList(const std::string & path) const -{ - std::string modDir = boost::to_upper_copy(path + "MODS/"); - size_t depth = boost::range::count(modDir, '/'); - - auto list = CResourceHandler::get("initial")->getFilteredFiles([&](const ResourcePath & id) -> bool - { - if (id.getType() != EResType::DIRECTORY) - return false; - if (!boost::algorithm::starts_with(id.getName(), modDir)) - return false; - if (boost::range::count(id.getName(), '/') != depth ) - return false; - return true; - }); - - //storage for found mods - std::vector foundMods; - for(const auto & entry : list) - { - std::string name = entry.getName(); - name.erase(0, modDir.size()); //Remove path prefix - - if (!name.empty()) - foundMods.push_back(name); - } - return foundMods; -} - - - -void CModHandler::loadMods(const std::string & path, const std::string & parent, const JsonNode & modSettings, bool enableMods) -{ - for(const std::string & modName : getModList(path)) - loadOneMod(modName, parent, modSettings, enableMods); -} - -void CModHandler::loadOneMod(std::string modName, const std::string & parent, const JsonNode & modSettings, bool enableMods) -{ - boost::to_lower(modName); - std::string modFullName = parent.empty() ? modName : parent + '.' + modName; - - if ( ModScope::isScopeReserved(modFullName)) - { - logMod->error("Can not load mod %s - this name is reserved for internal use!", modFullName); - return; - } - - if(CResourceHandler::get("initial")->existsResource(CModInfo::getModFile(modFullName))) - { - CModInfo mod(modFullName, modSettings[modName], JsonNode(CModInfo::getModFile(modFullName))); - if (!parent.empty()) // this is submod, add parent to dependencies - mod.dependencies.insert(parent); - - allMods[modFullName] = mod; - if (mod.isEnabled() && enableMods) - activeMods.push_back(modFullName); - - loadMods(CModInfo::getModDir(modFullName) + '/', modFullName, modSettings[modName]["mods"], enableMods && mod.isEnabled()); - } -} - -void CModHandler::loadMods() -{ - JsonNode modConfig; - - modConfig = loadModSettings(JsonPath::builtin("config/modSettings.json")); - loadMods("", "", modConfig["activeMods"], true); - - coreMod = std::make_unique(ModScope::scopeBuiltin(), modConfig[ModScope::scopeBuiltin()], JsonNode(JsonPath::builtin("config/gameConfig.json"))); -} - std::vector CModHandler::getAllMods() const { - std::vector modlist; - modlist.reserve(allMods.size()); - for (auto & entry : allMods) - modlist.push_back(entry.first); - return modlist; + return modManager->getAllMods(); } -std::vector CModHandler::getActiveMods() const +const std::vector & CModHandler::getActiveMods() const { - return activeMods; + return modManager->getActiveMods(); } -std::string CModHandler::getModLoadErrors() const +const ModDescription & CModHandler::getModInfo(const TModID & modId) const { - return modLoadErrors->toString(); -} - -const CModInfo & CModHandler::getModInfo(const TModID & modId) const -{ - return allMods.at(modId); + return modManager->getModDescription(modId); } static JsonNode genDefaultFS() @@ -284,57 +63,69 @@ static JsonNode genDefaultFS() return defaultFS; } +static std::string getModDirectory(const TModID & modName) +{ + std::string result = modName; + boost::to_upper(result); + boost::algorithm::replace_all(result, ".", "/MODS/"); + return "MODS/" + result; +} + static ISimpleResourceLoader * genModFilesystem(const std::string & modName, const JsonNode & conf) { static const JsonNode defaultFS = genDefaultFS(); - if (!conf["filesystem"].isNull()) - return CResourceHandler::createFileSystem(CModInfo::getModDir(modName), conf["filesystem"]); + if (!conf.isNull()) + return CResourceHandler::createFileSystem(getModDirectory(modName), conf); else - return CResourceHandler::createFileSystem(CModInfo::getModDir(modName), defaultFS); -} - -static ui32 calculateModChecksum(const std::string & modName, ISimpleResourceLoader * filesystem) -{ - boost::crc_32_type modChecksum; - // first - add current VCMI version into checksum to force re-validation on VCMI updates - modChecksum.process_bytes(reinterpret_cast(GameConstants::VCMI_VERSION.data()), GameConstants::VCMI_VERSION.size()); - - // second - add mod.json into checksum because filesystem does not contains this file - // FIXME: remove workaround for core mod - if (modName != ModScope::scopeBuiltin()) - { - auto modConfFile = CModInfo::getModFile(modName); - ui32 configChecksum = CResourceHandler::get("initial")->load(modConfFile)->calculateCRC32(); - modChecksum.process_bytes(reinterpret_cast(&configChecksum), sizeof(configChecksum)); - } - // third - add all detected text files from this mod into checksum - auto files = filesystem->getFilteredFiles([](const ResourcePath & resID) - { - return (resID.getType() == EResType::TEXT || resID.getType() == EResType::JSON) && - ( boost::starts_with(resID.getName(), "DATA") || boost::starts_with(resID.getName(), "CONFIG")); - }); - - for (const ResourcePath & file : files) - { - ui32 fileChecksum = filesystem->load(file)->calculateCRC32(); - modChecksum.process_bytes(reinterpret_cast(&fileChecksum), sizeof(fileChecksum)); - } - return modChecksum.checksum(); + return CResourceHandler::createFileSystem(getModDirectory(modName), defaultFS); } void CModHandler::loadModFilesystems() { CGeneralTextHandler::detectInstallParameters(); - activeMods = validateAndSortDependencies(activeMods); + const auto & activeMods = modManager->getActiveMods(); - coreMod->updateChecksum(calculateModChecksum(ModScope::scopeBuiltin(), CResourceHandler::get(ModScope::scopeBuiltin()))); + std::map modFilesystems; - for(std::string & modName : activeMods) + for(const TModID & modName : activeMods) + modFilesystems[modName] = genModFilesystem(modName, getModInfo(modName).getFilesystemConfig()); + + for(const TModID & modName : activeMods) + if (modName != "core") // virtual mod 'core' has no filesystem on its own - shared with base install + CResourceHandler::addFilesystem("data", modName, modFilesystems[modName]); + + if (settings["mods"]["validation"].String() == "full") + checkModFilesystemsConflicts(modFilesystems); +} + +void CModHandler::checkModFilesystemsConflicts(const std::map & modFilesystems) +{ + for(const auto & [leftName, leftFilesystem] : modFilesystems) { - CModInfo & mod = allMods[modName]; - CResourceHandler::addFilesystem("data", modName, genModFilesystem(modName, mod.config)); + for(const auto & [rightName, rightFilesystem] : modFilesystems) + { + if (leftName == rightName) + continue; + + if (getModDependencies(leftName).count(rightName) || getModDependencies(rightName).count(leftName)) + continue; + + if (getModSoftDependencies(leftName).count(rightName) || getModSoftDependencies(rightName).count(leftName)) + continue; + + const auto & filter = [](const ResourcePath &path){return path.getType() != EResType::DIRECTORY && path.getType() != EResType::JSON;}; + + std::unordered_set leftResources = leftFilesystem->getFilteredFiles(filter); + std::unordered_set rightResources = rightFilesystem->getFilteredFiles(filter); + + for (auto const & leftFile : leftResources) + { + if (rightResources.count(leftFile)) + logMod->warn("Potential confict detected between '%s' and '%s': both mods add file '%s'", leftName, rightName, leftFile.getOriginalName()); + } + } } } @@ -342,7 +133,8 @@ TModID CModHandler::findResourceOrigin(const ResourcePath & name) const { try { - for(const auto & modID : boost::adaptors::reverse(activeMods)) + auto activeMode = modManager->getActiveMods(); + for(const auto & modID : boost::adaptors::reverse(activeMode)) { if(CResourceHandler::get(modID)->existsResource(name)) return modID; @@ -361,115 +153,140 @@ TModID CModHandler::findResourceOrigin(const ResourcePath & name) const throw std::runtime_error("Resource with name " + name.getName() + " and type " + EResTypeHelper::getEResTypeAsString(name.getType()) + " wasn't found."); } +std::string CModHandler::findResourceLanguage(const ResourcePath & name) const +{ + std::string modName = findResourceOrigin(name); + std::string modLanguage = getModLanguage(modName); + return modLanguage; +} + +std::string CModHandler::findResourceEncoding(const ResourcePath & resource) const +{ + std::string modName = findResourceOrigin(resource); + std::string modLanguage = findResourceLanguage(resource); + + bool potentiallyUserMadeContent = resource.getType() == EResType::MAP || resource.getType() == EResType::CAMPAIGN; + if (potentiallyUserMadeContent && modName == ModScope::scopeBuiltin() && modLanguage == "english") + { + // this might be a map or campaign that player downloaded manually and placed in Maps/ directory + // in this case, this file may be in user-preferred language, and not in same language as the rest of H3 data + // however at the moment we have no way to detect that for sure - file can be either in English or in user-preferred language + // but since all known H3 encodings (Win125X or GBK) are supersets of ASCII, we can safely load English data using encoding of user-preferred language + std::string preferredLanguage = VLC->generaltexth->getPreferredLanguage(); + std::string fileEncoding = Languages::getLanguageOptions(preferredLanguage).encoding; + return fileEncoding; + } + else + { + std::string fileEncoding = Languages::getLanguageOptions(modLanguage).encoding; + return fileEncoding; + } +} + std::string CModHandler::getModLanguage(const TModID& modId) const { if(modId == "core") return VLC->generaltexth->getInstalledLanguage(); if(modId == "map") return VLC->generaltexth->getPreferredLanguage(); - return allMods.at(modId).baseLanguage; + return getModInfo(modId).getBaseLanguage(); +} + +std::set CModHandler::getModDependencies(const TModID & modId) const +{ + bool isModFound; + return getModDependencies(modId, isModFound); } std::set CModHandler::getModDependencies(const TModID & modId, bool & isModFound) const { - auto it = allMods.find(modId); - isModFound = (it != allMods.end()); - - if(isModFound) - return it->second.dependencies; + isModFound = modManager->isModActive(modId); + if (isModFound) + return modManager->getModDescription(modId).getDependencies(); logMod->error("Mod not found: '%s'", modId); return {}; } +std::set CModHandler::getModSoftDependencies(const TModID & modId) const +{ + return modManager->getModDescription(modId).getSoftDependencies(); +} + +std::set CModHandler::getModEnabledSoftDependencies(const TModID & modId) const +{ + std::set softDependencies = getModSoftDependencies(modId); + + vstd::erase_if(softDependencies, [this](const TModID & dependency){ return !modManager->isModActive(dependency);}); + + return softDependencies; +} + void CModHandler::initializeConfig() { - VLC->settingsHandler->loadBase(coreMod->config["settings"]); - - for(const TModID & modName : activeMods) + for(const TModID & modName : getActiveMods()) { - const auto & mod = allMods[modName]; - if (!mod.config["settings"].isNull()) - VLC->settingsHandler->loadBase(mod.config["settings"]); + const auto & mod = getModInfo(modName); + if (!mod.getLocalConfig()["settings"].isNull()) + VLC->settingsHandler->loadBase(mod.getLocalConfig()["settings"]); } } -CModVersion CModHandler::getModVersion(TModID modName) const -{ - if (allMods.count(modName)) - return allMods.at(modName).getVerificationInfo().version; - return {}; -} - -bool CModHandler::validateTranslations(TModID modName) const -{ - bool result = true; - const auto & mod = allMods.at(modName); - - { - auto fileList = mod.config["translations"].convertTo >(); - JsonNode json = JsonUtils::assembleFromFiles(fileList); - result |= VLC->generaltexth->validateTranslation(mod.baseLanguage, modName, json); - } - - for(const auto & language : Languages::getLanguageList()) - { - if (mod.config[language.identifier].isNull()) - continue; - - if (mod.config[language.identifier]["skipValidation"].Bool()) - continue; - - auto fileList = mod.config[language.identifier]["translations"].convertTo >(); - JsonNode json = JsonUtils::assembleFromFiles(fileList); - result |= VLC->generaltexth->validateTranslation(language.identifier, modName, json); - } - - return result; -} - void CModHandler::loadTranslation(const TModID & modName) { - const auto & mod = allMods[modName]; + const auto & mod = getModInfo(modName); std::string preferredLanguage = VLC->generaltexth->getPreferredLanguage(); - std::string modBaseLanguage = allMods[modName].baseLanguage; + std::string modBaseLanguage = getModInfo(modName).getBaseLanguage(); - auto baseTranslationList = mod.config["translations"].convertTo >(); - auto extraTranslationList = mod.config[preferredLanguage]["translations"].convertTo >(); + JsonNode baseTranslation = JsonUtils::assembleFromFiles(mod.getLocalConfig()["translations"]); + JsonNode extraTranslation = JsonUtils::assembleFromFiles(mod.getLocalConfig()[preferredLanguage]["translations"]); - JsonNode baseTranslation = JsonUtils::assembleFromFiles(baseTranslationList); - JsonNode extraTranslation = JsonUtils::assembleFromFiles(extraTranslationList); - - VLC->generaltexth->loadTranslationOverrides(modBaseLanguage, modName, baseTranslation); - VLC->generaltexth->loadTranslationOverrides(preferredLanguage, modName, extraTranslation); + VLC->generaltexth->loadTranslationOverrides(modName, modBaseLanguage, baseTranslation); + VLC->generaltexth->loadTranslationOverrides(modName, preferredLanguage, extraTranslation); } void CModHandler::load() { - CStopWatch totalTime; - CStopWatch timer; - - logMod->info("\tInitializing content handler: %d ms", timer.getDiff()); + logMod->info("\tInitializing content handler"); content->init(); + const auto & activeMods = getActiveMods(); + + validationPassed.insert(activeMods.begin(), activeMods.end()); + for(const TModID & modName : activeMods) { - logMod->trace("Generating checksum for %s", modName); - allMods[modName].updateChecksum(calculateModChecksum(modName, CResourceHandler::get(modName))); + modChecksums[modName] = this->modManager->computeChecksum(modName); } - // first - load virtual builtin mod that contains all data - // TODO? move all data into real mods? RoE, AB, SoD, WoG - content->preloadData(*coreMod); for(const TModID & modName : activeMods) - content->preloadData(allMods[modName]); - logMod->info("\tParsing mod data: %d ms", timer.getDiff()); + { + const auto & modInfo = getModInfo(modName); + bool isValid = content->preloadData(modInfo, isModValidationNeeded(modInfo)); + if (isValid) + logGlobal->info("\t\tParsing mod: OK (%s)", modInfo.getID()); + else + logGlobal->warn("\t\tParsing mod: Issues found! (%s)", modInfo.getID()); + + if (!isValid) + validationPassed.erase(modName); + } + logMod->info("\tParsing mod data"); - content->load(*coreMod); for(const TModID & modName : activeMods) - content->load(allMods[modName]); + { + const auto & modInfo = getModInfo(modName); + bool isValid = content->load(getModInfo(modName), isModValidationNeeded(getModInfo(modName))); + if (isValid) + logGlobal->info("\t\tLoading mod: OK (%s)", modInfo.getID()); + else + logGlobal->warn("\t\tLoading mod: Issues found! (%s)", modInfo.getID()); + + if (!isValid) + validationPassed.erase(modName); + } #if SCRIPTING_ENABLED VLC->scriptHandler->performRegistration(VLC);//todo: this should be done before any other handlers load @@ -480,39 +297,42 @@ void CModHandler::load() for(const TModID & modName : activeMods) loadTranslation(modName); -#if 0 - for(const TModID & modName : activeMods) - if (!validateTranslations(modName)) - allMods[modName].validation = CModInfo::FAILED; -#endif - - logMod->info("\tLoading mod data: %d ms", timer.getDiff()); + logMod->info("\tLoading mod data"); VLC->creh->loadCrExpMod(); VLC->identifiersHandler->finalize(); - logMod->info("\tResolving identifiers: %d ms", timer.getDiff()); + logMod->info("\tResolving identifiers"); content->afterLoadFinalization(); - logMod->info("\tHandlers post-load finalization: %d ms ", timer.getDiff()); - logMod->info("\tAll game content loaded in %d ms", totalTime.getDiff()); + logMod->info("\tHandlers post-load finalization"); + logMod->info("\tAll game content loaded"); } void CModHandler::afterLoad(bool onlyEssential) { JsonNode modSettings; - for (auto & modEntry : allMods) + for (const auto & modEntry : getActiveMods()) { - std::string pointer = "/" + boost::algorithm::replace_all_copy(modEntry.first, ".", "/mods/"); - - modSettings["activeMods"].resolvePointer(pointer) = modEntry.second.saveLocalData(); + if (validationPassed.count(modEntry)) + modManager->setValidatedChecksum(modEntry, modChecksums.at(modEntry)); + else + modManager->setValidatedChecksum(modEntry, std::nullopt); } - modSettings[ModScope::scopeBuiltin()] = coreMod->saveLocalData(); - modSettings[ModScope::scopeBuiltin()]["name"].String() = "Original game files"; - if(!onlyEssential) - { - std::fstream file(CResourceHandler::get()->getResourceName(ResourcePath("config/modSettings.json"))->c_str(), std::ofstream::out | std::ofstream::trunc); - file << modSettings.toString(); - } + modManager->saveConfigurationState(); +} + +bool CModHandler::isModValidationNeeded(const ModDescription & mod) const +{ + if (settings["mods"]["validation"].String() == "full") + return true; + + if (modManager->getValidatedChecksum(mod.getID()) == modChecksums.at(mod.getID())) + return false; + + if (settings["mods"]["validation"].String() == "off") + return false; + + return true; } VCMI_LIB_NAMESPACE_END diff --git a/lib/modding/CModHandler.h b/lib/modding/CModHandler.h index f88e1fe26..e991459c8 100644 --- a/lib/modding/CModHandler.h +++ b/lib/modding/CModHandler.h @@ -12,70 +12,53 @@ VCMI_LIB_NAMESPACE_BEGIN class CModHandler; -class CModIdentifier; -class CModInfo; -struct CModVersion; -class JsonNode; -class IHandlerBase; -class CIdentifierStorage; +class ModDescription; class CContentHandler; -struct ModVerificationInfo; class ResourcePath; -class MetaString; +class ModManager; +class ISimpleResourceLoader; using TModID = std::string; class DLL_LINKAGE CModHandler final : boost::noncopyable { - std::map allMods; - std::vector activeMods;//active mods, in order in which they were loaded - std::unique_ptr coreMod; - mutable std::unique_ptr modLoadErrors; + std::unique_ptr modManager; + std::map modChecksums; + std::set validationPassed; - bool hasCircularDependency(const TModID & mod, std::set currentList = std::set()) const; - - /** - * 1. Set apart mods with resolved dependencies from mods which have unresolved dependencies - * 2. Sort resolved mods using topological algorithm - * 3. Log all problem mods and their unresolved dependencies - * - * @param modsToResolve list of valid mod IDs (checkDependencies returned true - TODO: Clarify it.) - * @return a vector of the topologically sorted resolved mods: child nodes (dependent mods) have greater index than parents - */ - std::vector validateAndSortDependencies(std::vector modsToResolve) const; - - std::vector getModList(const std::string & path) const; - void loadMods(const std::string & path, const std::string & parent, const JsonNode & modSettings, bool enableMods); - void loadOneMod(std::string modName, const std::string & parent, const JsonNode & modSettings, bool enableMods); void loadTranslation(const TModID & modName); + void checkModFilesystemsConflicts(const std::map & modFilesystems); - bool validateTranslations(TModID modName) const; - - CModVersion getModVersion(TModID modName) const; + bool isModValidationNeeded(const ModDescription & mod) const; public: - std::shared_ptr content; //(!)Do not serialize FIXME: make private + std::shared_ptr content; /// receives list of available mods and trying to load mod.json from all of them void initializeConfig(); - void loadMods(); void loadModFilesystems(); /// returns ID of mod that provides selected file resource TModID findResourceOrigin(const ResourcePath & name) const; + /// Returns assumed language ID of mod that provides selected file resource + std::string findResourceLanguage(const ResourcePath & name) const; + + /// Returns assumed encoding of language of mod that provides selected file resource + std::string findResourceEncoding(const ResourcePath & name) const; + std::string getModLanguage(const TModID & modId) const; + std::set getModDependencies(const TModID & modId) const; std::set getModDependencies(const TModID & modId, bool & isModFound) const; + std::set getModSoftDependencies(const TModID & modId) const; + std::set getModEnabledSoftDependencies(const TModID & modId) const; /// returns list of all (active) mods std::vector getAllMods() const; - std::vector getActiveMods() const; + const std::vector & getActiveMods() const; - /// Returns human-readable string that describes errors encounter during mod loading, such as missing dependencies - std::string getModLoadErrors() const; - - const CModInfo & getModInfo(const TModID & modId) const; + const ModDescription & getModInfo(const TModID & modId) const; /// load content from all available mods void load(); diff --git a/lib/modding/CModInfo.cpp b/lib/modding/CModInfo.cpp deleted file mode 100644 index 66e7421c7..000000000 --- a/lib/modding/CModInfo.cpp +++ /dev/null @@ -1,205 +0,0 @@ -/* - * CModInfo.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 "CModInfo.h" - -#include "../texts/CGeneralTextHandler.h" -#include "../VCMI_Lib.h" -#include "../filesystem/Filesystem.h" - -VCMI_LIB_NAMESPACE_BEGIN - -static JsonNode addMeta(JsonNode config, const std::string & meta) -{ - config.setModScope(meta); - return config; -} - -std::set CModInfo::readModList(const JsonNode & input) -{ - std::set result; - - for (auto const & string : input.convertTo>()) - result.insert(boost::to_lower_copy(string)); - - return result; -} - -CModInfo::CModInfo(): - explicitlyEnabled(false), - implicitlyEnabled(true), - validation(PENDING) -{ - -} - -CModInfo::CModInfo(const std::string & identifier, const JsonNode & local, const JsonNode & config): - identifier(identifier), - dependencies(readModList(config["depends"])), - conflicts(readModList(config["conflicts"])), - explicitlyEnabled(false), - implicitlyEnabled(true), - validation(PENDING), - config(addMeta(config, identifier)) -{ - if (!config["name"].String().empty()) - verificationInfo.name = config["name"].String(); - else - verificationInfo.name = identifier; - - verificationInfo.version = CModVersion::fromString(config["version"].String()); - verificationInfo.parent = identifier.substr(0, identifier.find_last_of('.')); - if(verificationInfo.parent == identifier) - verificationInfo.parent.clear(); - - if(!config["compatibility"].isNull()) - { - vcmiCompatibleMin = CModVersion::fromString(config["compatibility"]["min"].String()); - vcmiCompatibleMax = CModVersion::fromString(config["compatibility"]["max"].String()); - } - - if (!config["language"].isNull()) - baseLanguage = config["language"].String(); - else - baseLanguage = "english"; - - loadLocalData(local); -} - -JsonNode CModInfo::saveLocalData() const -{ - std::ostringstream stream; - stream << std::noshowbase << std::hex << std::setw(8) << std::setfill('0') << verificationInfo.checksum; - - JsonNode conf; - conf["active"].Bool() = explicitlyEnabled; - conf["validated"].Bool() = validation != FAILED; - conf["checksum"].String() = stream.str(); - return conf; -} - -std::string CModInfo::getModDir(const std::string & name) -{ - return "MODS/" + boost::algorithm::replace_all_copy(name, ".", "/MODS/"); -} - -JsonPath CModInfo::getModFile(const std::string & name) -{ - return JsonPath::builtinTODO(getModDir(name) + "/mod.json"); -} - -void CModInfo::updateChecksum(ui32 newChecksum) -{ - // comment-out next line to force validation of all mods ignoring checksum - if (newChecksum != verificationInfo.checksum) - { - verificationInfo.checksum = newChecksum; - validation = PENDING; - } -} - -void CModInfo::loadLocalData(const JsonNode & data) -{ - bool validated = false; - implicitlyEnabled = true; - explicitlyEnabled = !config["keepDisabled"].Bool(); - verificationInfo.checksum = 0; - if (data.isStruct()) - { - explicitlyEnabled = data["active"].Bool(); - validated = data["validated"].Bool(); - updateChecksum(strtol(data["checksum"].String().c_str(), nullptr, 16)); - } - - //check compatibility - implicitlyEnabled &= (vcmiCompatibleMin.isNull() || CModVersion::GameVersion().compatible(vcmiCompatibleMin, true, true)); - implicitlyEnabled &= (vcmiCompatibleMax.isNull() || vcmiCompatibleMax.compatible(CModVersion::GameVersion(), true, true)); - - if(!implicitlyEnabled) - logGlobal->warn("Mod %s is incompatible with current version of VCMI and cannot be enabled", verificationInfo.name); - - if (config["modType"].String() == "Translation") - { - if (baseLanguage != CGeneralTextHandler::getPreferredLanguage()) - { - if (identifier.find_last_of('.') == std::string::npos) - logGlobal->warn("Translation mod %s was not loaded: language mismatch!", verificationInfo.name); - implicitlyEnabled = false; - } - } - if (config["modType"].String() == "Compatibility") - { - // compatibility mods are always explicitly enabled - // however they may be implicitly disabled - if one of their dependencies is missing - explicitlyEnabled = true; - } - - if (isEnabled()) - validation = validated ? PASSED : PENDING; - else - validation = validated ? PASSED : FAILED; - - verificationInfo.impactsGameplay = checkModGameplayAffecting(); -} - -bool CModInfo::checkModGameplayAffecting() const -{ - if (modGameplayAffecting.has_value()) - return *modGameplayAffecting; - - static const std::vector keysToTest = { - "heroClasses", - "artifacts", - "creatures", - "factions", - "objects", - "heroes", - "spells", - "skills", - "templates", - "scripts", - "battlefields", - "terrains", - "rivers", - "roads", - "obstacles" - }; - - JsonPath modFileResource(CModInfo::getModFile(identifier)); - - if(CResourceHandler::get("initial")->existsResource(modFileResource)) - { - const JsonNode modConfig(modFileResource); - - for(const auto & key : keysToTest) - { - if (!modConfig[key].isNull()) - { - modGameplayAffecting = true; - return *modGameplayAffecting; - } - } - } - modGameplayAffecting = false; - return *modGameplayAffecting; -} - -const ModVerificationInfo & CModInfo::getVerificationInfo() const -{ - assert(!verificationInfo.name.empty()); - return verificationInfo; -} - -bool CModInfo::isEnabled() const -{ - return implicitlyEnabled && explicitlyEnabled; -} - -VCMI_LIB_NAMESPACE_END diff --git a/lib/modding/CModInfo.h b/lib/modding/CModInfo.h deleted file mode 100644 index 3d6b40320..000000000 --- a/lib/modding/CModInfo.h +++ /dev/null @@ -1,82 +0,0 @@ -/* - * CModInfo.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 "../json/JsonNode.h" -#include "ModVerificationInfo.h" - -VCMI_LIB_NAMESPACE_BEGIN - -class DLL_LINKAGE CModInfo -{ - /// cached result of checkModGameplayAffecting() call - /// Do not serialize - depends on local mod version, not server/save mod version - mutable std::optional modGameplayAffecting; - - static std::set readModList(const JsonNode & input); -public: - enum EValidationStatus - { - PENDING, - FAILED, - PASSED - }; - - /// identifier, identical to name of folder with mod - std::string identifier; - - /// detailed mod description - std::string description; - - /// Base language of mod, all mod strings are assumed to be in this language - std::string baseLanguage; - - /// vcmi versions compatible with the mod - CModVersion vcmiCompatibleMin, vcmiCompatibleMax; - - /// list of mods that should be loaded before this one - std::set dependencies; - - /// list of mods that can't be used in the same time as this one - std::set conflicts; - - EValidationStatus validation; - - JsonNode config; - - CModInfo(); - CModInfo(const std::string & identifier, const JsonNode & local, const JsonNode & config); - - JsonNode saveLocalData() const; - void updateChecksum(ui32 newChecksum); - - bool isEnabled() const; - - static std::string getModDir(const std::string & name); - static JsonPath getModFile(const std::string & name); - - /// return true if this mod can affect gameplay, e.g. adds or modifies any game objects - bool checkModGameplayAffecting() const; - - const ModVerificationInfo & getVerificationInfo() const; - -private: - /// true if mod is enabled by user, e.g. in Launcher UI - bool explicitlyEnabled; - - /// true if mod can be loaded - compatible and has no missing deps - bool implicitlyEnabled; - - ModVerificationInfo verificationInfo; - - void loadLocalData(const JsonNode & data); -}; - -VCMI_LIB_NAMESPACE_END diff --git a/lib/modding/ContentTypeHandler.cpp b/lib/modding/ContentTypeHandler.cpp index 83ae46c1b..380e98d18 100644 --- a/lib/modding/ContentTypeHandler.cpp +++ b/lib/modding/ContentTypeHandler.cpp @@ -11,15 +11,18 @@ #include "ContentTypeHandler.h" #include "CModHandler.h" -#include "CModInfo.h" +#include "ModDescription.h" +#include "ModManager.h" #include "ModScope.h" #include "../BattleFieldHandler.h" #include "../CArtHandler.h" #include "../CCreatureHandler.h" +#include "../CConfigHandler.h" #include "../entities/faction/CTownHandler.h" +#include "../entities/hero/CHeroClassHandler.h" +#include "../entities/hero/CHeroHandler.h" #include "../texts/CGeneralTextHandler.h" -#include "../CHeroHandler.h" #include "../CSkillHandler.h" #include "../CStopWatch.h" #include "../IGameSettings.h" @@ -39,9 +42,9 @@ VCMI_LIB_NAMESPACE_BEGIN -ContentTypeHandler::ContentTypeHandler(IHandlerBase * handler, const std::string & objectName): +ContentTypeHandler::ContentTypeHandler(IHandlerBase * handler, const std::string & entityName): handler(handler), - objectName(objectName), + entityName(entityName), originalData(handler->loadLegacyData()) { for(auto & node : originalData) @@ -50,9 +53,9 @@ ContentTypeHandler::ContentTypeHandler(IHandlerBase * handler, const std::string } } -bool ContentTypeHandler::preloadModData(const std::string & modName, const std::vector & fileList, bool validate) +bool ContentTypeHandler::preloadModData(const std::string & modName, const JsonNode & fileList, bool validate) { - bool result = false; + bool result = true; JsonNode data = JsonUtils::assembleFromFiles(fileList, result); data.setModScope(modName); @@ -79,6 +82,9 @@ bool ContentTypeHandler::preloadModData(const std::string & modName, const std:: logMod->trace("Patching object %s (%s) from %s", objectName, remoteName, modName); JsonNode & remoteConf = modData[remoteName].patches[objectName]; + if (!remoteConf.isNull() && settings["mods"]["validation"].String() != "off") + JsonUtils::detectConflicts(conflictList, remoteConf, entry.second, objectName); + JsonUtils::merge(remoteConf, entry.second); } } @@ -93,7 +99,7 @@ bool ContentTypeHandler::loadMod(const std::string & modName, bool validate) auto performValidate = [&,this](JsonNode & data, const std::string & name){ handler->beforeValidate(data); if (validate) - result &= JsonUtils::validate(data, "vcmi:" + objectName, name); + result &= JsonUtils::validate(data, "vcmi:" + entityName, name); }; // apply patches @@ -113,7 +119,7 @@ bool ContentTypeHandler::loadMod(const std::string & modName, bool validate) // - another mod attempts to add object into this mod (technically can be supported, but might lead to weird edge cases) // - another mod attempts to edit object from this mod that no longer exist - DANGER since such patch likely has very incomplete data // so emit warning and skip such case - logMod->warn("Mod '%s' attempts to edit object '%s' of type '%s' from mod '%s' but no such object exist!", data.getModScope(), name, objectName, modName); + logMod->warn("Mod '%s' attempts to edit object '%s' of type '%s' from mod '%s' but no such object exist!", data.getModScope(), name, entityName, modName); continue; } @@ -159,31 +165,73 @@ void ContentTypeHandler::loadCustom() void ContentTypeHandler::afterLoadFinalization() { - for (auto const & data : modData) + if (settings["mods"]["validation"].String() != "off") { - if (data.second.modData.isNull()) + for (auto const & data : modData) { - for (auto node : data.second.patches.Struct()) - logMod->warn("Mod '%s' have added patch for object '%s' from mod '%s', but this mod was not loaded or has no new objects.", node.second.getModScope(), node.first, data.first); - } - - for(auto & otherMod : modData) - { - if (otherMod.first == data.first) - continue; - - if (otherMod.second.modData.isNull()) - continue; - - for(auto & otherObject : otherMod.second.modData.Struct()) + if (data.second.modData.isNull()) { - if (data.second.modData.Struct().count(otherObject.first)) + for (const auto & node : data.second.patches.Struct()) + logMod->warn("Mod '%s' have added patch for object '%s' from mod '%s', but this mod was not loaded or has no new objects.", node.second.getModScope(), node.first, data.first); + } + + for(auto & otherMod : modData) + { + if (otherMod.first == data.first) + continue; + + if (otherMod.second.modData.isNull()) + continue; + + for(auto & otherObject : otherMod.second.modData.Struct()) { - logMod->warn("Mod '%s' have added object with name '%s' that is also available in mod '%s'", data.first, otherObject.first, otherMod.first); - logMod->warn("Two objects with same name were loaded. Please use form '%s:%s' if mod '%s' needs to modify this object instead", otherMod.first, otherObject.first, data.first); + if (data.second.modData.Struct().count(otherObject.first)) + { + logMod->warn("Mod '%s' have added object with name '%s' that is also available in mod '%s'", data.first, otherObject.first, otherMod.first); + logMod->warn("Two objects with same name were loaded. Please use form '%s:%s' if mod '%s' needs to modify this object instead", otherMod.first, otherObject.first, data.first); + } } } } + + for (const auto& [conflictPath, conflictModData] : conflictList.Struct()) + { + std::set conflictingMods; + std::set resolvedConflicts; + + for (auto const & conflictModEntry: conflictModData.Struct()) + conflictingMods.insert(conflictModEntry.first); + + for (auto const & modID : conflictingMods) + { + resolvedConflicts.merge(VLC->modh->getModDependencies(modID)); + resolvedConflicts.merge(VLC->modh->getModEnabledSoftDependencies(modID)); + } + + vstd::erase_if(conflictingMods, [&resolvedConflicts](const std::string & entry){ return resolvedConflicts.count(entry);}); + + if (conflictingMods.size() < 2) + continue; // all conflicts were resolved - either via compatibility patch (mod that depends on 2 conflicting mods) or simple mod that depends on another one + + bool allEqual = true; + + for (auto const & modID : conflictingMods) + { + if (conflictModData[modID] != conflictModData[*conflictingMods.begin()]) + { + allEqual = false; + break; + } + } + + if (allEqual) + continue; // conflict still present, but all mods use the same value for conflicting entry - permit it + + logMod->warn("Potential confict in '%s'", conflictPath); + + for (auto const & modID : conflictingMods) + logMod->warn("Mod '%s' - value set to %s", modID, conflictModData[modID].toCompactString()); + } } handler->afterLoadFinalization(); @@ -211,22 +259,26 @@ void CContentHandler::init() handlers.insert(std::make_pair("biomes", ContentTypeHandler(VLC->biomeHandler.get(), "biome"))); } -bool CContentHandler::preloadModData(const std::string & modName, JsonNode modConfig, bool validate) +bool CContentHandler::preloadData(const ModDescription & mod, bool validate) { bool result = true; + + if (!JsonUtils::validate(mod.getLocalConfig(), "vcmi:mod", mod.getID())) + result = false; + for(auto & handler : handlers) { - result &= handler.second.preloadModData(modName, modConfig[handler.first].convertTo >(), validate); + result &= handler.second.preloadModData(mod.getID(), mod.getLocalValue(handler.first), validate); } return result; } -bool CContentHandler::loadMod(const std::string & modName, bool validate) +bool CContentHandler::load(const ModDescription & mod, bool validate) { bool result = true; for(auto & handler : handlers) { - result &= handler.second.loadMod(modName, validate); + result &= handler.second.loadMod(mod.getID(), validate); } return result; } @@ -247,41 +299,6 @@ void CContentHandler::afterLoadFinalization() } } -void CContentHandler::preloadData(CModInfo & mod) -{ - bool validate = (mod.validation != CModInfo::PASSED); - - // print message in format [<8-symbols checksum>] - auto & info = mod.getVerificationInfo(); - logMod->info("\t\t[%08x]%s", info.checksum, info.name); - - if (validate && mod.identifier != ModScope::scopeBuiltin()) - { - if (!JsonUtils::validate(mod.config, "vcmi:mod", mod.identifier)) - mod.validation = CModInfo::FAILED; - } - if (!preloadModData(mod.identifier, mod.config, validate)) - mod.validation = CModInfo::FAILED; -} - -void CContentHandler::load(CModInfo & mod) -{ - bool validate = (mod.validation != CModInfo::PASSED); - - if (!loadMod(mod.identifier, validate)) - mod.validation = CModInfo::FAILED; - - if (validate) - { - if (mod.validation != CModInfo::FAILED) - logMod->info("\t\t[DONE] %s", mod.getVerificationInfo().name); - else - logMod->error("\t\t[FAIL] %s", mod.getVerificationInfo().name); - } - else - logMod->info("\t\t[SKIP] %s", mod.getVerificationInfo().name); -} - const ContentTypeHandler & CContentHandler::operator[](const std::string & name) const { return handlers.at(name); diff --git a/lib/modding/ContentTypeHandler.h b/lib/modding/ContentTypeHandler.h index 7093c12d5..abcf7321e 100644 --- a/lib/modding/ContentTypeHandler.h +++ b/lib/modding/ContentTypeHandler.h @@ -14,11 +14,13 @@ VCMI_LIB_NAMESPACE_BEGIN class IHandlerBase; -class CModInfo; +class ModDescription; /// internal type to handle loading of one data type (e.g. artifacts, creatures) class DLL_LINKAGE ContentTypeHandler { + JsonNode conflictList; + public: struct ModInfo { @@ -29,7 +31,7 @@ public: }; /// handler to which all data will be loaded IHandlerBase * handler; - std::string objectName; + std::string entityName; /// contains all loaded H3 data std::vector originalData; @@ -39,7 +41,7 @@ public: /// local version of methods in ContentHandler /// returns true if loading was successful - bool preloadModData(const std::string & modName, const std::vector & fileList, bool validate); + bool preloadModData(const std::string & modName, const JsonNode & fileList, bool validate); bool loadMod(const std::string & modName, bool validate); void loadCustom(); void afterLoadFinalization(); @@ -48,22 +50,16 @@ public: /// class used to load all game data into handlers. Used only during loading class DLL_LINKAGE CContentHandler { - /// preloads all data from fileList as data from modName. - bool preloadModData(const std::string & modName, JsonNode modConfig, bool validate); - - /// actually loads data in mod - bool loadMod(const std::string & modName, bool validate); - std::map handlers; public: void init(); /// preloads all data from fileList as data from modName. - void preloadData(CModInfo & mod); + bool preloadData(const ModDescription & mod, bool validateMod); /// actually loads data in mod - void load(CModInfo & mod); + bool load(const ModDescription & mod, bool validateMod); void loadCustom(); diff --git a/lib/modding/ModDescription.cpp b/lib/modding/ModDescription.cpp new file mode 100644 index 000000000..d27253ea3 --- /dev/null +++ b/lib/modding/ModDescription.cpp @@ -0,0 +1,233 @@ +/* + * ModDescription.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 "ModDescription.h" + +#include "CModVersion.h" +#include "ModVerificationInfo.h" + +#include "../json/JsonNode.h" +#include "../texts/CGeneralTextHandler.h" + +VCMI_LIB_NAMESPACE_BEGIN + +ModDescription::ModDescription(const TModID & fullID, const JsonNode & localConfig, const JsonNode & repositoryConfig) + : identifier(fullID) + , localConfig(std::make_unique(localConfig)) + , repositoryConfig(std::make_unique(repositoryConfig)) + , dependencies(loadModList(getValue("depends"))) + , softDependencies(loadModList(getValue("softDepends"))) + , conflicts(loadModList(getValue("conflicts"))) +{ + if(getID() != "core") + dependencies.emplace("core"); + + if (!getParentID().empty()) + dependencies.emplace(getParentID()); +} + +ModDescription::~ModDescription() = default; + +TModSet ModDescription::loadModList(const JsonNode & configNode) const +{ + TModSet result; + for(const auto & entry : configNode.Vector()) + result.insert(boost::algorithm::to_lower_copy(entry.String())); + return result; +} + +const TModID & ModDescription::getID() const +{ + return identifier; +} + +TModID ModDescription::getParentID() const +{ + size_t dotPos = identifier.find_last_of('.'); + + if(dotPos == std::string::npos) + return {}; + + return identifier.substr(0, dotPos); +} + +TModID ModDescription::getTopParentID() const +{ + size_t dotPos = identifier.find('.'); + + if(dotPos == std::string::npos) + return {}; + + return identifier.substr(0, dotPos); +} + +const TModSet & ModDescription::getDependencies() const +{ + return dependencies; +} + +const TModSet & ModDescription::getSoftDependencies() const +{ + return softDependencies; +} + +const TModSet & ModDescription::getConflicts() const +{ + return conflicts; +} + +const std::string & ModDescription::getBaseLanguage() const +{ + static const std::string defaultLanguage = "english"; + + return getValue("language").isString() ? getValue("language").String() : defaultLanguage; +} + +const std::string & ModDescription::getName() const +{ + return getLocalizedValue("name").String(); +} + +const JsonNode & ModDescription::getFilesystemConfig() const +{ + return getLocalValue("filesystem"); +} + +const JsonNode & ModDescription::getLocalConfig() const +{ + return *localConfig; +} + +const JsonNode & ModDescription::getLocalizedValue(const std::string & keyName) const +{ + const std::string language = CGeneralTextHandler::getPreferredLanguage(); + const JsonNode & languageNode = getValue(language); + const JsonNode & baseValue = getValue(keyName); + const JsonNode & localizedValue = languageNode[keyName]; + + if (localizedValue.isNull()) + return baseValue; + else + return localizedValue; +} + +const JsonNode & ModDescription::getValue(const std::string & keyName) const +{ + const JsonNode & localValue = getLocalValue(keyName); + if (localValue.isNull()) + return getRepositoryValue(keyName); + else + return getLocalValue(keyName); +} + +const JsonNode & ModDescription::getLocalValue(const std::string & keyName) const +{ + return getLocalConfig()[keyName]; +} + +const JsonNode & ModDescription::getRepositoryValue(const std::string & keyName) const +{ + return (*repositoryConfig)[keyName]; +} + +CModVersion ModDescription::getVersion() const +{ + return CModVersion::fromString(getValue("version").String()); +} + +ModVerificationInfo ModDescription::getVerificationInfo() const +{ + ModVerificationInfo result; + result.name = getName(); + result.version = getVersion(); + result.impactsGameplay = affectsGameplay(); + result.parent = getParentID(); + + return result; +} + +bool ModDescription::isCompatible() const +{ + const JsonNode & compatibility = getValue("compatibility"); + + if (compatibility.isNull()) + return true; + + auto vcmiCompatibleMin = CModVersion::fromString(compatibility["min"].String()); + auto vcmiCompatibleMax = CModVersion::fromString(compatibility["max"].String()); + + bool compatible = true; + compatible &= (vcmiCompatibleMin.isNull() || CModVersion::GameVersion().compatible(vcmiCompatibleMin, true, true)); + compatible &= (vcmiCompatibleMax.isNull() || vcmiCompatibleMax.compatible(CModVersion::GameVersion(), true, true)); + + return compatible; +} + +bool ModDescription::isCompatibility() const +{ + return getValue("modType").String() == "Compatibility"; +} + +bool ModDescription::isTranslation() const +{ + return getValue("modType").String() == "Translation"; +} + +bool ModDescription::keepDisabled() const +{ + return getValue("keepDisabled").Bool(); +} + +bool ModDescription::isInstalled() const +{ + return !localConfig->isNull(); +} + +bool ModDescription::affectsGameplay() const +{ + static const std::array keysToTest = { + "artifacts", + "battlefields", + "creatures", + "factions", + "heroClasses", + "heroes", + "objects", + "obstacles", + "rivers", + "roads", + "settings", + "skills", + "spells", + "terrains", + }; + + for(const auto & key : keysToTest) + if (!getLocalValue(key).isNull()) + return true; + + return false; +} + +bool ModDescription::isUpdateAvailable() const +{ + if (getRepositoryValue("version").isNull()) + return false; + + if (getLocalValue("version").isNull()) + return false; + + auto localVersion = CModVersion::fromString(getLocalValue("version").String()); + auto repositoryVersion = CModVersion::fromString(getRepositoryValue("version").String()); + + return localVersion < repositoryVersion; +} + +VCMI_LIB_NAMESPACE_END diff --git a/lib/modding/ModDescription.h b/lib/modding/ModDescription.h new file mode 100644 index 000000000..225cf953e --- /dev/null +++ b/lib/modding/ModDescription.h @@ -0,0 +1,70 @@ +/* + * ModDescription.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 + +VCMI_LIB_NAMESPACE_BEGIN + +struct CModVersion; +struct ModVerificationInfo; +class JsonNode; + +using TModID = std::string; +using TModList = std::vector; +using TModSet = std::set; + +class DLL_LINKAGE ModDescription : boost::noncopyable +{ + TModID identifier; + + std::unique_ptr localConfig; + std::unique_ptr repositoryConfig; + + TModSet dependencies; + TModSet softDependencies; + TModSet conflicts; + + TModSet loadModList(const JsonNode & configNode) const; + +public: + ModDescription(const TModID & fullID, const JsonNode & localConfig, const JsonNode & repositoryConfig); + ~ModDescription(); + + const TModID & getID() const; + TModID getParentID() const; + TModID getTopParentID() const; + + const TModSet & getDependencies() const; + const TModSet & getSoftDependencies() const; + const TModSet & getConflicts() const; + + const std::string & getBaseLanguage() const; + const std::string & getName() const; + + const JsonNode & getFilesystemConfig() const; + const JsonNode & getLocalConfig() const; + const JsonNode & getValue(const std::string & keyName) const; + const JsonNode & getLocalizedValue(const std::string & keyName) const; + const JsonNode & getLocalValue(const std::string & keyName) const; + const JsonNode & getRepositoryValue(const std::string & keyName) const; + + CModVersion getVersion() const; + ModVerificationInfo getVerificationInfo() const; + + bool isCompatible() const; + bool isUpdateAvailable() const; + + bool affectsGameplay() const; + bool isCompatibility() const; + bool isTranslation() const; + bool keepDisabled() const; + bool isInstalled() const; +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/modding/ModManager.cpp b/lib/modding/ModManager.cpp new file mode 100644 index 000000000..f18452def --- /dev/null +++ b/lib/modding/ModManager.cpp @@ -0,0 +1,706 @@ +/* + * ModManager.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 "ModManager.h" + +#include "ModDescription.h" +#include "ModScope.h" + +#include "../constants/StringConstants.h" +#include "../filesystem/Filesystem.h" +#include "../json/JsonNode.h" +#include "../texts/CGeneralTextHandler.h" + +VCMI_LIB_NAMESPACE_BEGIN + +static std::string getModDirectory(const TModID & modName) +{ + std::string result = modName; + boost::to_upper(result); + boost::algorithm::replace_all(result, ".", "/MODS/"); + return "MODS/" + result; +} + +static std::string getModSettingsDirectory(const TModID & modName) +{ + return getModDirectory(modName) + "/MODS/"; +} + +static JsonPath getModDescriptionFile(const TModID & modName) +{ + return JsonPath::builtin(getModDirectory(modName) + "/mod"); +} + +ModsState::ModsState() +{ + modList.push_back(ModScope::scopeBuiltin()); + + std::vector testLocations = scanModsDirectory("MODS/"); + + while(!testLocations.empty()) + { + std::string target = testLocations.back(); + testLocations.pop_back(); + modList.push_back(boost::algorithm::to_lower_copy(target)); + + for(const auto & submod : scanModsDirectory(getModSettingsDirectory(target))) + testLocations.push_back(target + '.' + submod); + } +} + +TModList ModsState::getInstalledMods() const +{ + return modList; +} + +uint32_t ModsState::computeChecksum(const TModID & modName) const +{ + boost::crc_32_type modChecksum; + // first - add current VCMI version into checksum to force re-validation on VCMI updates + modChecksum.process_bytes(static_cast(GameConstants::VCMI_VERSION.data()), GameConstants::VCMI_VERSION.size()); + + // second - add mod.json into checksum because filesystem does not contains this file + if (modName != ModScope::scopeBuiltin()) + { + auto modConfFile = getModDescriptionFile(modName); + ui32 configChecksum = CResourceHandler::get("initial")->load(modConfFile)->calculateCRC32(); + modChecksum.process_bytes(static_cast(&configChecksum), sizeof(configChecksum)); + } + + // third - add all detected text files from this mod into checksum + const auto & filesystem = CResourceHandler::get(modName); + + auto files = filesystem->getFilteredFiles([](const ResourcePath & resID) + { + return resID.getType() == EResType::JSON && boost::starts_with(resID.getName(), "CONFIG"); + }); + + for (const ResourcePath & file : files) + { + ui32 fileChecksum = filesystem->load(file)->calculateCRC32(); + modChecksum.process_bytes(static_cast(&fileChecksum), sizeof(fileChecksum)); + } + return modChecksum.checksum(); +} + +double ModsState::getInstalledModSizeMegabytes(const TModID & modName) const +{ + ResourcePath resDir(getModDirectory(modName), EResType::DIRECTORY); + std::string path = CResourceHandler::get()->getResourceName(resDir)->string(); + + size_t sizeBytes = 0; + for(boost::filesystem::recursive_directory_iterator it(path); it != boost::filesystem::recursive_directory_iterator(); ++it) + { + if(!boost::filesystem::is_directory(*it)) + sizeBytes += boost::filesystem::file_size(*it); + } + + double sizeMegabytes = sizeBytes / static_cast(1024*1024); + return sizeMegabytes; +} + +std::vector ModsState::scanModsDirectory(const std::string & modDir) const +{ + size_t depth = boost::range::count(modDir, '/'); + + const auto & modScanFilter = [&](const ResourcePath & id) -> bool + { + if(id.getType() != EResType::DIRECTORY) + return false; + if(!boost::algorithm::starts_with(id.getName(), modDir)) + return false; + if(boost::range::count(id.getName(), '/') != depth) + return false; + return true; + }; + + auto list = CResourceHandler::get("initial")->getFilteredFiles(modScanFilter); + + //storage for found mods + std::vector foundMods; + for(const auto & entry : list) + { + std::string name = entry.getName(); + name.erase(0, modDir.size()); //Remove path prefix + + if(name.empty()) + continue; + + if(name.find('.') != std::string::npos) + continue; + + if (ModScope::isScopeReserved(boost::to_lower_copy(name))) + continue; + + if(!CResourceHandler::get("initial")->existsResource(JsonPath::builtin(entry.getName() + "/MOD"))) + continue; + + foundMods.push_back(name); + } + return foundMods; +} + +/////////////////////////////////////////////////////////////////////////////// + +ModsPresetState::ModsPresetState() +{ + static const JsonPath settingsPath = JsonPath::builtin("config/modSettings.json"); + + if(CResourceHandler::get("local")->existsResource(ResourcePath(settingsPath))) + { + modConfig = JsonNode(settingsPath); + } + else + { + // Probably new install. Create initial configuration + CResourceHandler::get("local")->createResource(settingsPath.getOriginalName() + ".json"); + } + + if(modConfig["presets"].isNull()) + { + modConfig["activePreset"] = JsonNode("default"); + if(modConfig["activeMods"].isNull()) + createInitialPreset(); // new install + else + importInitialPreset(); // 1.5 format import + } +} + +void ModsPresetState::createInitialPreset() +{ + // TODO: scan mods directory for all its content? Probably unnecessary since this looks like new install, but who knows? + modConfig["presets"]["default"]["mods"].Vector().emplace_back("vcmi"); +} + +void ModsPresetState::importInitialPreset() +{ + JsonNode preset; + + for(const auto & mod : modConfig["activeMods"].Struct()) + { + if(mod.second["active"].Bool()) + preset["mods"].Vector().emplace_back(mod.first); + + for(const auto & submod : mod.second["mods"].Struct()) + preset["settings"][mod.first][submod.first] = submod.second["active"]; + } + modConfig["presets"]["default"] = preset; +} + +const JsonNode & ModsPresetState::getActivePresetConfig() const +{ + const std::string & currentPresetName = modConfig["activePreset"].String(); + const JsonNode & currentPreset = modConfig["presets"][currentPresetName]; + return currentPreset; +} + +TModList ModsPresetState::getActiveRootMods() const +{ + const JsonNode & modsToActivateJson = getActivePresetConfig()["mods"]; + auto modsToActivate = modsToActivateJson.convertTo>(); + if (!vstd::contains(modsToActivate, ModScope::scopeBuiltin())) + modsToActivate.push_back(ModScope::scopeBuiltin()); + return modsToActivate; +} + +std::map ModsPresetState::getModSettings(const TModID & modID) const +{ + const JsonNode & modSettingsJson = getActivePresetConfig()["settings"][modID]; + auto modSettings = modSettingsJson.convertTo>(); + return modSettings; +} + +std::optional ModsPresetState::getValidatedChecksum(const TModID & modName) const +{ + const JsonNode & node = modConfig["validatedMods"][modName]; + if (node.isNull()) + return std::nullopt; + else + return node.Integer(); +} + +void ModsPresetState::setModActive(const TModID & modID, bool isActive) +{ + size_t dotPos = modID.find('.'); + + if(dotPos != std::string::npos) + { + std::string rootMod = modID.substr(0, dotPos); + std::string settingID = modID.substr(dotPos + 1); + setSettingActive(rootMod, settingID, isActive); + } + else + { + if (isActive) + addRootMod(modID); + else + eraseRootMod(modID); + } +} + +void ModsPresetState::addRootMod(const TModID & modName) +{ + const std::string & currentPresetName = modConfig["activePreset"].String(); + JsonNode & currentPreset = modConfig["presets"][currentPresetName]; + + if (!vstd::contains(currentPreset["mods"].Vector(), JsonNode(modName))) + currentPreset["mods"].Vector().emplace_back(modName); +} + +void ModsPresetState::setSettingActive(const TModID & modName, const TModID & settingName, bool isActive) +{ + const std::string & currentPresetName = modConfig["activePreset"].String(); + JsonNode & currentPreset = modConfig["presets"][currentPresetName]; + + currentPreset["settings"][modName][settingName].Bool() = isActive; +} + +void ModsPresetState::removeOldMods(const TModList & modsToKeep) +{ + const std::string & currentPresetName = modConfig["activePreset"].String(); + JsonNode & currentPreset = modConfig["presets"][currentPresetName]; + + vstd::erase_if(currentPreset["mods"].Vector(), [&](const JsonNode & entry){ + return !vstd::contains(modsToKeep, entry.String()); + }); + + vstd::erase_if(currentPreset["settings"].Struct(), [&](const auto & entry){ + return !vstd::contains(modsToKeep, entry.first); + }); +} + +void ModsPresetState::eraseRootMod(const TModID & modName) +{ + const std::string & currentPresetName = modConfig["activePreset"].String(); + JsonNode & currentPreset = modConfig["presets"][currentPresetName]; + vstd::erase(currentPreset["mods"].Vector(), JsonNode(modName)); +} + +void ModsPresetState::eraseModSetting(const TModID & modName, const TModID & settingName) +{ + const std::string & currentPresetName = modConfig["activePreset"].String(); + JsonNode & currentPreset = modConfig["presets"][currentPresetName]; + currentPreset["settings"][modName].Struct().erase(settingName); +} + +std::vector ModsPresetState::getActiveMods() const +{ + TModList activeRootMods = getActiveRootMods(); + TModList allActiveMods; + + for(const auto & activeMod : activeRootMods) + { + assert(!vstd::contains(allActiveMods, activeMod)); + allActiveMods.push_back(activeMod); + + for(const auto & submod : getModSettings(activeMod)) + { + if(submod.second) + { + assert(!vstd::contains(allActiveMods, activeMod + '.' + submod.first)); + allActiveMods.push_back(activeMod + '.' + submod.first); + } + } + } + return allActiveMods; +} + +void ModsPresetState::setValidatedChecksum(const TModID & modName, std::optional value) +{ + if (value.has_value()) + modConfig["validatedMods"][modName].Integer() = *value; + else + modConfig["validatedMods"].Struct().erase(modName); +} + +void ModsPresetState::saveConfigurationState() const +{ + std::fstream file(CResourceHandler::get()->getResourceName(ResourcePath("config/modSettings.json"))->c_str(), std::ofstream::out | std::ofstream::trunc); + file << modConfig.toCompactString(); +} + +ModsStorage::ModsStorage(const std::vector & modsToLoad, const JsonNode & repositoryList) +{ + JsonNode coreModConfig(JsonPath::builtin("config/gameConfig.json")); + coreModConfig.setModScope(ModScope::scopeBuiltin()); + mods.try_emplace(ModScope::scopeBuiltin(), ModScope::scopeBuiltin(), coreModConfig, JsonNode()); + + for(auto modID : modsToLoad) + { + if(ModScope::isScopeReserved(modID)) + continue; + + JsonNode modConfig(getModDescriptionFile(modID)); + modConfig.setModScope(modID); + + if(modConfig["modType"].isNull()) + { + logMod->error("Can not load mod %s - invalid mod config file!", modID); + continue; + } + + mods.try_emplace(modID, modID, modConfig, repositoryList[modID]); + } + + for(const auto & mod : repositoryList.Struct()) + { + if (vstd::contains(modsToLoad, mod.first)) + continue; + + if (mod.second["modType"].isNull() || mod.second["name"].isNull()) + continue; + + mods.try_emplace(mod.first, mod.first, JsonNode(), mod.second); + } +} + +const ModDescription & ModsStorage::getMod(const TModID & fullID) const +{ + return mods.at(fullID); +} + +TModList ModsStorage::getAllMods() const +{ + TModList result; + for (const auto & mod : mods) + result.push_back(mod.first); + + return result; +} + +ModManager::ModManager() + :ModManager(JsonNode()) +{ +} + +ModManager::ModManager(const JsonNode & repositoryList) + : modsState(std::make_unique()) + , modsPreset(std::make_unique()) +{ + modsStorage = std::make_unique(modsState->getInstalledMods(), repositoryList); + + eraseMissingModsFromPreset(); + addNewModsToPreset(); + + std::vector desiredModList = modsPreset->getActiveMods(); + ModDependenciesResolver newResolver(desiredModList, *modsStorage); + updatePreset(newResolver); +} + +ModManager::~ModManager() = default; + +const ModDescription & ModManager::getModDescription(const TModID & modID) const +{ + assert(boost::to_lower_copy(modID) == modID); + return modsStorage->getMod(modID); +} + +bool ModManager::isModSettingActive(const TModID & rootModID, const TModID & modSettingID) const +{ + return modsPreset->getModSettings(rootModID).at(modSettingID); +} + +bool ModManager::isModActive(const TModID & modID) const +{ + return vstd::contains(getActiveMods(), modID); +} + +const TModList & ModManager::getActiveMods() const +{ + return depedencyResolver->getActiveMods(); +} + +uint32_t ModManager::computeChecksum(const TModID & modName) const +{ + return modsState->computeChecksum(modName); +} + +std::optional ModManager::getValidatedChecksum(const TModID & modName) const +{ + return modsPreset->getValidatedChecksum(modName); +} + +void ModManager::setValidatedChecksum(const TModID & modName, std::optional value) +{ + modsPreset->setValidatedChecksum(modName, value); +} + +void ModManager::saveConfigurationState() const +{ + modsPreset->saveConfigurationState(); +} + +TModList ModManager::getAllMods() const +{ + return modsStorage->getAllMods(); +} + +double ModManager::getInstalledModSizeMegabytes(const TModID & modName) const +{ + return modsState->getInstalledModSizeMegabytes(modName); +} + +void ModManager::eraseMissingModsFromPreset() +{ + const TModList & installedMods = modsState->getInstalledMods(); + const TModList & rootMods = modsPreset->getActiveRootMods(); + + modsPreset->removeOldMods(installedMods); + + for(const auto & rootMod : rootMods) + { + const auto & modSettings = modsPreset->getModSettings(rootMod); + + for(const auto & modSetting : modSettings) + { + TModID fullModID = rootMod + '.' + modSetting.first; + if(!vstd::contains(installedMods, fullModID)) + { + modsPreset->eraseModSetting(rootMod, modSetting.first); + continue; + } + } + } +} + +void ModManager::addNewModsToPreset() +{ + const TModList & installedMods = modsState->getInstalledMods(); + + for(const auto & modID : installedMods) + { + size_t dotPos = modID.find('.'); + + if(dotPos == std::string::npos) + continue; // only look up submods aka mod settings + + std::string rootMod = modID.substr(0, dotPos); + std::string settingID = modID.substr(dotPos + 1); + + const auto & modSettings = modsPreset->getModSettings(rootMod); + + if (!modSettings.count(settingID)) + modsPreset->setSettingActive(rootMod, settingID, !modsStorage->getMod(modID).keepDisabled()); + } +} + +TModList ModManager::collectDependenciesRecursive(const TModID & modID) const +{ + TModList result; + TModList toTest; + + toTest.push_back(modID); + while (!toTest.empty()) + { + TModID currentModID = toTest.back(); + const auto & currentMod = getModDescription(currentModID); + toTest.pop_back(); + result.push_back(currentModID); + + if (!currentMod.isInstalled()) + throw std::runtime_error("Unable to enable mod " + modID + "! Dependency " + currentModID + " is not installed!"); + + for (const auto & dependency : currentMod.getDependencies()) + { + if (!vstd::contains(result, dependency)) + toTest.push_back(dependency); + } + } + + return result; +} + +void ModManager::tryEnableMods(const TModList & modList) +{ + TModList requiredActiveMods; + TModList additionalActiveMods = getActiveMods(); + + for (const auto & modName : modList) + { + for (const auto & dependency : collectDependenciesRecursive(modName)) + { + if (!vstd::contains(requiredActiveMods, dependency)) + { + requiredActiveMods.push_back(dependency); + vstd::erase(additionalActiveMods, dependency); + } + } + + assert(!vstd::contains(additionalActiveMods, modName)); + assert(vstd::contains(requiredActiveMods, modName));// FIXME: fails on attempt to enable broken mod / translation to other language + } + + ModDependenciesResolver testResolver(requiredActiveMods, *modsStorage); + + testResolver.tryAddMods(additionalActiveMods, *modsStorage); + + TModList additionalActiveSubmods; + for (const auto & modName : modList) + { + if (modName.find('.') != std::string::npos) + continue; + + auto modSettings = modsPreset->getModSettings(modName); + for (const auto & entry : modSettings) + { + TModID fullModID = modName + '.' + entry.first; + if (entry.second && !vstd::contains(requiredActiveMods, fullModID)) + additionalActiveSubmods.push_back(fullModID); + } + } + + testResolver.tryAddMods(additionalActiveSubmods, *modsStorage); + + for (const auto & modName : modList) + if (!vstd::contains(testResolver.getActiveMods(), modName)) + throw std::runtime_error("Failed to enable mod! Mod " + modName + " remains disabled!"); + + updatePreset(testResolver); +} + +void ModManager::tryDisableMod(const TModID & modName) +{ + auto desiredActiveMods = getActiveMods(); + assert(vstd::contains(desiredActiveMods, modName)); + + vstd::erase(desiredActiveMods, modName); + + ModDependenciesResolver testResolver(desiredActiveMods, *modsStorage); + + if (vstd::contains(testResolver.getActiveMods(), modName)) + throw std::runtime_error("Failed to disable mod! Mod " + modName + " remains enabled!"); + + modsPreset->setModActive(modName, false); + updatePreset(testResolver); +} + +void ModManager::updatePreset(const ModDependenciesResolver & testResolver) +{ + const auto & newActiveMods = testResolver.getActiveMods(); + const auto & newBrokenMods = testResolver.getBrokenMods(); + + for (const auto & modID : newActiveMods) + { + assert(vstd::contains(modsState->getInstalledMods(), modID)); + modsPreset->setModActive(modID, true); + } + + for (const auto & modID : newBrokenMods) + { + const auto & mod = getModDescription(modID); + if (vstd::contains(newActiveMods, mod.getTopParentID())) + modsPreset->setModActive(modID, false); + } + + std::vector desiredModList = modsPreset->getActiveMods(); + + // Try to enable all existing compatibility patches. Ignore on failure + for (const auto & rootMod : modsPreset->getActiveRootMods()) + { + for (const auto & modSetting : modsPreset->getModSettings(rootMod)) + { + if (modSetting.second) + continue; + + TModID fullModID = rootMod + '.' + modSetting.first; + const auto & modDescription = modsStorage->getMod(fullModID); + + if (modDescription.isCompatibility()) + desiredModList.push_back(fullModID); + } + } + + depedencyResolver = std::make_unique(desiredModList, *modsStorage); + modsPreset->saveConfigurationState(); +} + +ModDependenciesResolver::ModDependenciesResolver(const TModList & modsToResolve, const ModsStorage & storage) +{ + tryAddMods(modsToResolve, storage); +} + +const TModList & ModDependenciesResolver::getActiveMods() const +{ + return activeMods; +} + +const TModList & ModDependenciesResolver::getBrokenMods() const +{ + return brokenMods; +} + +void ModDependenciesResolver::tryAddMods(TModList modsToResolve, const ModsStorage & storage) +{ + // Topological sort algorithm. + boost::range::sort(modsToResolve); // Sort mods per name + std::vector sortedValidMods(activeMods.begin(), activeMods.end()); // Vector keeps order of elements (LIFO) + std::set resolvedModIDs(activeMods.begin(), activeMods.end()); // Use a set for validation for performance reason, but set does not keep order of elements + std::set notResolvedModIDs(modsToResolve.begin(), modsToResolve.end()); // Use a set for validation for performance reason + + // Mod is resolved if it has no dependencies or all its dependencies are already resolved + auto isResolved = [&](const ModDescription & mod) -> bool + { + if (mod.isTranslation() && CGeneralTextHandler::getPreferredLanguage() != mod.getBaseLanguage()) + return false; + + if(mod.getDependencies().size() > resolvedModIDs.size()) + return false; + + for(const TModID & dependency : mod.getDependencies()) + if(!vstd::contains(resolvedModIDs, dependency)) + return false; + + for(const TModID & softDependency : mod.getSoftDependencies()) + if(vstd::contains(notResolvedModIDs, softDependency)) + return false; + + for(const TModID & conflict : mod.getConflicts()) + if(vstd::contains(resolvedModIDs, conflict)) + return false; + + for(const TModID & reverseConflict : resolvedModIDs) + if(vstd::contains(storage.getMod(reverseConflict).getConflicts(), mod.getID())) + return false; + + return true; + }; + + while(true) + { + std::set resolvedOnCurrentTreeLevel; + for(auto it = modsToResolve.begin(); it != modsToResolve.end();) // One iteration - one level of mods tree + { + if(isResolved(storage.getMod(*it))) + { + resolvedOnCurrentTreeLevel.insert(*it); // Not to the resolvedModIDs, so current node children will be resolved on the next iteration + assert(!vstd::contains(sortedValidMods, *it)); + sortedValidMods.push_back(*it); + it = modsToResolve.erase(it); + continue; + } + it++; + } + if(!resolvedOnCurrentTreeLevel.empty()) + { + resolvedModIDs.insert(resolvedOnCurrentTreeLevel.begin(), resolvedOnCurrentTreeLevel.end()); + for(const auto & it : resolvedOnCurrentTreeLevel) + notResolvedModIDs.erase(it); + continue; + } + // If there are no valid mods on the current mods tree level, no more mod can be resolved, should be ended. + break; + } + + assert(!sortedValidMods.empty()); + activeMods = sortedValidMods; + brokenMods.insert(brokenMods.end(), modsToResolve.begin(), modsToResolve.end()); +} + +VCMI_LIB_NAMESPACE_END diff --git a/lib/modding/ModManager.h b/lib/modding/ModManager.h new file mode 100644 index 000000000..2acdb90a6 --- /dev/null +++ b/lib/modding/ModManager.h @@ -0,0 +1,144 @@ +/* + * ModManager.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 "../json/JsonNode.h" + +VCMI_LIB_NAMESPACE_BEGIN + +class JsonNode; +class ModDescription; +struct CModVersion; + +using TModID = std::string; +using TModList = std::vector; +using TModSet = std::set; + +/// Provides interface to access list of locally installed mods +class ModsState : boost::noncopyable +{ + TModList modList; + + TModList scanModsDirectory(const std::string & modDir) const; + +public: + ModsState(); + + TModList getInstalledMods() const; + double getInstalledModSizeMegabytes(const TModID & modName) const; + + uint32_t computeChecksum(const TModID & modName) const; +}; + +/// Provides interface to access or change current mod preset +class ModsPresetState : boost::noncopyable +{ + JsonNode modConfig; + + void createInitialPreset(); + void importInitialPreset(); + + const JsonNode & getActivePresetConfig() const; + +public: + ModsPresetState(); + + void setModActive(const TModID & modName, bool isActive); + + void addRootMod(const TModID & modName); + void eraseRootMod(const TModID & modName); + void removeOldMods(const TModList & modsToKeep); + + void setSettingActive(const TModID & modName, const TModID & settingName, bool isActive); + void eraseModSetting(const TModID & modName, const TModID & settingName); + + /// Returns list of all mods active in current preset. Mod order is unspecified + TModList getActiveMods() const; + + /// Returns list of currently active root mods (non-submod) + TModList getActiveRootMods() const; + + /// Returns list of all known settings (submods) for a specified mod + std::map getModSettings(const TModID & modID) const; + std::optional getValidatedChecksum(const TModID & modName) const; + void setValidatedChecksum(const TModID & modName, std::optional value); + + void saveConfigurationState() const; +}; + +/// Provides access to mod properties +class ModsStorage : boost::noncopyable +{ + std::map mods; + +public: + ModsStorage(const TModList & modsToLoad, const JsonNode & repositoryList); + + const ModDescription & getMod(const TModID & fullID) const; + + TModList getAllMods() const; +}; + +class ModDependenciesResolver : boost::noncopyable +{ + /// all currently active mods, in their load order + TModList activeMods; + + /// Mods from current preset that failed to load due to invalid dependencies + TModList brokenMods; + +public: + ModDependenciesResolver(const TModList & modsToResolve, const ModsStorage & storage); + + void tryAddMods(TModList modsToResolve, const ModsStorage & storage); + + const TModList & getActiveMods() const; + const TModList & getBrokenMods() const; +}; + +/// Provides public interface to access mod state +class DLL_LINKAGE ModManager : boost::noncopyable +{ + std::unique_ptr modsState; + std::unique_ptr modsPreset; + std::unique_ptr modsStorage; + std::unique_ptr depedencyResolver; + + void generateLoadOrder(TModList desiredModList); + void eraseMissingModsFromPreset(); + void addNewModsToPreset(); + void updatePreset(const ModDependenciesResolver & newData); + + TModList collectDependenciesRecursive(const TModID & modID) const; + + void tryEnableMod(const TModID & modList); + +public: + ModManager(const JsonNode & repositoryList); + ModManager(); + ~ModManager(); + + const ModDescription & getModDescription(const TModID & modID) const; + const TModList & getActiveMods() const; + TModList getAllMods() const; + + bool isModSettingActive(const TModID & rootModID, const TModID & modSettingID) const; + bool isModActive(const TModID & modID) const; + uint32_t computeChecksum(const TModID & modName) const; + std::optional getValidatedChecksum(const TModID & modName) const; + void setValidatedChecksum(const TModID & modName, std::optional value); + void saveConfigurationState() const; + double getInstalledModSizeMegabytes(const TModID & modName) const; + + void tryEnableMods(const TModList & modList); + void tryDisableMod(const TModID & modName); +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/modding/ModVerificationInfo.cpp b/lib/modding/ModVerificationInfo.cpp index db2c7370a..72104462b 100644 --- a/lib/modding/ModVerificationInfo.cpp +++ b/lib/modding/ModVerificationInfo.cpp @@ -10,9 +10,10 @@ #include "StdInc.h" #include "ModVerificationInfo.h" -#include "CModInfo.h" #include "CModHandler.h" +#include "ModDescription.h" #include "ModIncompatibility.h" +#include "ModScope.h" #include "../json/JsonNode.h" #include "../VCMI_Lib.h" @@ -68,7 +69,10 @@ ModListVerificationStatus ModVerificationInfo::verifyListAgainstLocalMods(const if(modList.count(m)) continue; - if(VLC->modh->getModInfo(m).checkModGameplayAffecting()) + if (m == ModScope::scopeBuiltin()) + continue; + + if(VLC->modh->getModInfo(m).affectsGameplay()) result[m] = ModVerificationStatus::EXCESSIVE; } @@ -77,6 +81,9 @@ ModListVerificationStatus ModVerificationInfo::verifyListAgainstLocalMods(const auto & remoteModId = infoPair.first; auto & remoteModInfo = infoPair.second; + if (remoteModId == ModScope::scopeBuiltin()) + continue; + bool modAffectsGameplay = remoteModInfo.impactsGameplay; //parent mod affects gameplay if child affects too for(const auto & subInfoPair : modList) @@ -88,8 +95,8 @@ ModListVerificationStatus ModVerificationInfo::verifyListAgainstLocalMods(const continue; } - auto & localModInfo = VLC->modh->getModInfo(remoteModId).getVerificationInfo(); - modAffectsGameplay |= VLC->modh->getModInfo(remoteModId).checkModGameplayAffecting(); + const auto & localVersion = VLC->modh->getModInfo(remoteModId).getVersion(); + modAffectsGameplay |= VLC->modh->getModInfo(remoteModId).affectsGameplay(); // skip it. Such mods should only be present in old saves or if mod changed and no longer affects gameplay if (!modAffectsGameplay) @@ -101,7 +108,7 @@ ModListVerificationStatus ModVerificationInfo::verifyListAgainstLocalMods(const continue; } - if(remoteModInfo.version != localModInfo.version) + if(remoteModInfo.version != localVersion) { result[remoteModId] = ModVerificationStatus::VERSION_MISMATCH; continue; diff --git a/lib/networkPacks/NetPackVisitor.h b/lib/networkPacks/NetPackVisitor.h index 475b9b5db..335f7caa1 100644 --- a/lib/networkPacks/NetPackVisitor.h +++ b/lib/networkPacks/NetPackVisitor.h @@ -13,6 +13,7 @@ #include "PacksForClientBattle.h" #include "PacksForServer.h" #include "PacksForLobby.h" +#include "SaveLocalState.h" #include "SetRewardableConfiguration.h" #include "SetStackEffect.h" @@ -42,6 +43,7 @@ public: virtual void visitSetSecSkill(SetSecSkill & pack) {} virtual void visitHeroVisitCastle(HeroVisitCastle & pack) {} virtual void visitChangeSpells(ChangeSpells & pack) {} + virtual void visitSetResearchedSpells(SetResearchedSpells & pack) {} virtual void visitSetMana(SetMana & pack) {} virtual void visitSetMovePoints(SetMovePoints & pack) {} virtual void visitFoWChange(FoWChange & pack) {} @@ -128,6 +130,7 @@ public: virtual void visitBuildStructure(BuildStructure & pack) {} virtual void visitVisitTownBuilding(VisitTownBuilding & pack) {} virtual void visitRazeStructure(RazeStructure & pack) {} + virtual void visitSpellResearch(SpellResearch & pack) {} virtual void visitRecruitCreatures(RecruitCreatures & pack) {} virtual void visitUpgradeCreature(UpgradeCreature & pack) {} virtual void visitGarrisonHeroSwap(GarrisonHeroSwap & pack) {} @@ -175,6 +178,8 @@ public: virtual void visitLobbyForceSetPlayer(LobbyForceSetPlayer & pack) {} virtual void visitLobbyShowMessage(LobbyShowMessage & pack) {} virtual void visitLobbyPvPAction(LobbyPvPAction & pack) {} + virtual void visitLobbyDelete(LobbyDelete & pack) {} + virtual void visitSaveLocalState(SaveLocalState & pack) {} }; VCMI_LIB_NAMESPACE_END diff --git a/lib/networkPacks/NetPacksLib.cpp b/lib/networkPacks/NetPacksLib.cpp index 0524a3953..14080d6ef 100644 --- a/lib/networkPacks/NetPacksLib.cpp +++ b/lib/networkPacks/NetPacksLib.cpp @@ -12,6 +12,7 @@ #include "PacksForClient.h" #include "PacksForClientBattle.h" #include "PacksForServer.h" +#include "SaveLocalState.h" #include "SetRewardableConfiguration.h" #include "StackLocation.h" #include "PacksForLobby.h" @@ -19,7 +20,6 @@ #include "NetPackVisitor.h" #include "texts/CGeneralTextHandler.h" #include "CArtHandler.h" -#include "CHeroHandler.h" #include "VCMI_Lib.h" #include "mapping/CMap.h" #include "spells/CSpellHandler.h" @@ -92,6 +92,12 @@ bool CLobbyPackToServer::isForServer() const return true; } +void SaveLocalState::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitSaveLocalState(*this); +} + + void PackageApplied::visitTyped(ICPackVisitor & visitor) { visitor.visitPackageApplied(*this); @@ -162,6 +168,10 @@ void ChangeSpells::visitTyped(ICPackVisitor & visitor) visitor.visitChangeSpells(*this); } +void SetResearchedSpells::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitSetResearchedSpells(*this); +} void SetMana::visitTyped(ICPackVisitor & visitor) { visitor.visitSetMana(*this); @@ -592,6 +602,11 @@ void RazeStructure::visitTyped(ICPackVisitor & visitor) visitor.visitRazeStructure(*this); } +void SpellResearch::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitSpellResearch(*this); +} + void RecruitCreatures::visitTyped(ICPackVisitor & visitor) { visitor.visitRecruitCreatures(*this); @@ -827,6 +842,11 @@ void LobbyPvPAction::visitTyped(ICPackVisitor & visitor) visitor.visitLobbyPvPAction(*this); } +void LobbyDelete::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitLobbyDelete(*this); +} + void SetResources::applyGs(CGameState *gs) { assert(player.isValidPlayer()); @@ -930,6 +950,16 @@ void ChangeSpells::applyGs(CGameState *gs) hero->removeSpellFromSpellbook(sid); } +void SetResearchedSpells::applyGs(CGameState *gs) +{ + CGTownInstance *town = gs->getTown(tid); + + town->spells[level] = spells; + town->spellResearchCounterDay++; + if(accepted) + town->spellResearchAcceptedCounter++; +} + void SetMana::applyGs(CGameState *gs) { CGHeroInstance * hero = gs->getHero(hid); @@ -1028,7 +1058,7 @@ void ChangeObjPos::applyGs(CGameState *gs) return; } gs->map->removeBlockVisTiles(obj); - obj->pos = nPos + obj->getVisitableOffset(); + obj->setAnchorPos(nPos + obj->getVisitableOffset()); gs->map->addBlockVisTiles(obj); } @@ -1183,7 +1213,7 @@ void RemoveObject::applyGs(CGameState *gs) beatenHero->tempOwner = PlayerColor::NEUTRAL; //no one owns beaten hero vstd::erase_if(beatenHero->artifactsInBackpack, [](const ArtSlotInfo& asi) { - return asi.artifact->artType->getId() == ArtifactID::GRAIL; + return asi.artifact->getTypeId() == ArtifactID::GRAIL; }); if(beatenHero->visitedTown) @@ -1332,7 +1362,7 @@ void NewStructures::applyGs(CGameState *gs) for(const auto & id : bid) { - assert(t->town->buildings.at(id) != nullptr); + assert(t->getTown()->buildings.at(id) != nullptr); t->addBuilding(id); } t->updateAppearance(); @@ -1408,6 +1438,7 @@ void HeroRecruited::applyGs(CGameState *gs) h->setOwner(player); h->pos = tile; + h->updateAppearance(); if(h->id == ObjectInstanceID()) { @@ -1447,11 +1478,11 @@ void GiveHero::applyGs(CGameState *gs) auto oldVisitablePos = h->visitablePos(); gs->map->removeBlockVisTiles(h,true); - h->appearance = VLC->objtypeh->getHandlerFor(Obj::HERO, h->type->heroClass->getIndex())->getTemplates().front(); + h->updateAppearance(); h->setOwner(player); h->setMovementPoints(h->movementPointsLimit(true)); - h->pos = h->convertFromVisitablePos(oldVisitablePos); + h->setAnchorPos(h->convertFromVisitablePos(oldVisitablePos)); gs->map->heroesOnMap.emplace_back(h); gs->getPlayerState(h->getOwner())->addOwnedObject(h); @@ -1474,8 +1505,7 @@ void NewArtifact::applyGs(CGameState *gs) { auto art = ArtifactUtils::createArtifact(artId, spellId); gs->map->addNewArtifactInstance(art); - PutArtifact pa(ArtifactLocation(artHolder, pos), false); - pa.art = art; + PutArtifact pa(art->getId(), ArtifactLocation(artHolder, pos), false); pa.applyGs(gs); } @@ -1594,14 +1624,14 @@ void RebalanceStacks::applyGs(CGameState *gs) const auto dstHero = dynamic_cast(dst.army.get()); auto srcStack = const_cast(src.getStack()); auto dstStack = const_cast(dst.getStack()); - if(auto srcArt = srcStack->getArt(ArtifactPosition::CREATURE_SLOT)) + if(srcStack->getArt(ArtifactPosition::CREATURE_SLOT)) { if(auto dstArt = dstStack->getArt(ArtifactPosition::CREATURE_SLOT)) { auto dstSlot = ArtifactUtils::getArtBackpackPosition(srcHero, dstArt->getTypeId()); if(srcHero && dstSlot != ArtifactPosition::PRE_FIRST) { - dstArt->move(*dstStack, ArtifactPosition::CREATURE_SLOT, *srcHero, dstSlot); + gs->map->moveArtifactInstance(*dstStack, ArtifactPosition::CREATURE_SLOT, *srcHero, dstSlot); } //else - artifact can be lost :/ else @@ -1613,12 +1643,12 @@ void RebalanceStacks::applyGs(CGameState *gs) ea.applyGs(gs); logNetwork->warn("Cannot move artifact! No free slots"); } - srcArt->move(*srcStack, ArtifactPosition::CREATURE_SLOT, *dstStack, ArtifactPosition::CREATURE_SLOT); + gs->map->moveArtifactInstance(*srcStack, ArtifactPosition::CREATURE_SLOT, *dstStack, ArtifactPosition::CREATURE_SLOT); //TODO: choose from dialog } else //just move to the other slot before stack gets erased { - srcArt->move(*srcStack, ArtifactPosition::CREATURE_SLOT, *dstStack, ArtifactPosition::CREATURE_SLOT); + gs->map->moveArtifactInstance(*srcStack, ArtifactPosition::CREATURE_SLOT, *dstStack, ArtifactPosition::CREATURE_SLOT); } } if (stackExp) @@ -1688,14 +1718,13 @@ void BulkSmartRebalanceStacks::applyGs(CGameState *gs) void PutArtifact::applyGs(CGameState *gs) { - // Ensure that artifact has been correctly added via NewArtifact pack - assert(vstd::contains(gs->map->artInstances, art)); + auto art = gs->getArtInstance(id); assert(!art->getParentNodes().empty()); auto hero = gs->getHero(al.artHolder); assert(hero); assert(art && art->canBePutAt(hero, al.slot)); assert(ArtifactUtils::checkIfSlotValid(*hero, al.slot)); - art->putAt(*hero, al.slot); + gs->map->putArtifactInstance(*hero, art, al.slot); } void BulkEraseArtifacts::applyGs(CGameState *gs) @@ -1713,36 +1742,34 @@ void BulkEraseArtifacts::applyGs(CGameState *gs) const auto slotInfo = artSet->getSlot(slot); if(slotInfo->locked) { - logGlobal->debug("Erasing locked artifact: %s", slotInfo->artifact->artType->getNameTranslated()); + logGlobal->debug("Erasing locked artifact: %s", slotInfo->artifact->getType()->getNameTranslated()); DisassembledArtifact dis; dis.al.artHolder = artHolder; for(auto & slotInfoWorn : artSet->artifactsWorn) { auto art = slotInfoWorn.second.artifact; - if(art->isCombined() && art->isPart(slotInfo->getArt())) + if(art->isCombined() && art->isPart(slotInfo->artifact)) { dis.al.slot = artSet->getArtPos(art); break; } } assert((dis.al.slot != ArtifactPosition::PRE_FIRST) && "Failed to determine the assembly this locked artifact belongs to"); - logGlobal->debug("Found the corresponding assembly: %s", artSet->getArt(dis.al.slot)->artType->getNameTranslated()); + logGlobal->debug("Found the corresponding assembly: %s", artSet->getArt(dis.al.slot)->getType()->getNameTranslated()); dis.applyGs(gs); } else { - logGlobal->debug("Erasing artifact %s", slotInfo->artifact->artType->getNameTranslated()); + logGlobal->debug("Erasing artifact %s", slotInfo->artifact->getType()->getNameTranslated()); } - auto art = artSet->getArt(slot); - assert(art); - art->removeFrom(*artSet, slot); + gs->map->removeArtifactInstance(*artSet, slot); } } void BulkMoveArtifacts::applyGs(CGameState *gs) { - const auto bulkArtsRemove = [](std::vector & artsPack, CArtifactSet & artSet) + const auto bulkArtsRemove = [gs](std::vector & artsPack, CArtifactSet & artSet) { std::vector packToRemove; for(const auto & slotsPair : artsPack) @@ -1753,20 +1780,16 @@ void BulkMoveArtifacts::applyGs(CGameState *gs) }); for(const auto & slot : packToRemove) - { - auto * art = artSet.getArt(slot); - assert(art); - art->removeFrom(artSet, slot); - } + gs->map->removeArtifactInstance(artSet, slot); }; - const auto bulkArtsPut = [](std::vector & artsPack, CArtifactSet & initArtSet, CArtifactSet & dstArtSet) + const auto bulkArtsPut = [gs](std::vector & artsPack, CArtifactSet & initArtSet, CArtifactSet & dstArtSet) { for(const auto & slotsPair : artsPack) { auto * art = initArtSet.getArt(slotsPair.srcPos); assert(art); - art->putAt(dstArtSet, slotsPair.dstPos); + gs->map->putArtifactInstance(dstArtSet, art, slotsPair.dstPos); } }; @@ -1787,72 +1810,71 @@ void BulkMoveArtifacts::applyGs(CGameState *gs) void AssembledArtifact::applyGs(CGameState *gs) { - auto hero = gs->getHero(al.artHolder); - assert(hero); - const auto transformedArt = hero->getArt(al.slot); + auto artSet = gs->getArtSet(al.artHolder); + assert(artSet); + const auto transformedArt = artSet->getArt(al.slot); assert(transformedArt); - assert(vstd::contains_if(ArtifactUtils::assemblyPossibilities(hero, transformedArt->getTypeId()), [=](const CArtifact * art)->bool + const auto builtArt = artId.toArtifact(); + assert(vstd::contains_if(ArtifactUtils::assemblyPossibilities(artSet, transformedArt->getTypeId()), [=](const CArtifact * art)->bool { return art->getId() == builtArt->getId(); })); - const auto transformedArtSlot = hero->getArtPos(transformedArt); auto * combinedArt = new CArtifactInstance(builtArt); gs->map->addNewArtifactInstance(combinedArt); // Find slots for all involved artifacts - std::vector slotsInvolved; - for(const auto constituent : builtArt->getConstituents()) + std::set> slotsInvolved = { al.slot }; + CArtifactFittingSet fittingSet(*artSet); + auto parts = builtArt->getConstituents(); + parts.erase(std::find(parts.begin(), parts.end(), transformedArt->getType())); + for(const auto constituent : parts) { - ArtifactPosition slot; - if(transformedArt->getTypeId() == constituent->getId()) - slot = transformedArtSlot; - else - slot = hero->getArtPos(constituent->getId(), false, false); - + const auto slot = fittingSet.getArtPos(constituent->getId(), false, false); + fittingSet.lockSlot(slot); assert(slot != ArtifactPosition::PRE_FIRST); - slotsInvolved.emplace_back(slot); + slotsInvolved.insert(slot); } - std::sort(slotsInvolved.begin(), slotsInvolved.end(), std::greater<>()); // Find a slot for combined artifact - al.slot = transformedArtSlot; - for(const auto slot : slotsInvolved) + if(ArtifactUtils::isSlotEquipment(al.slot) && ArtifactUtils::isSlotBackpack(*slotsInvolved.begin())) { - if(ArtifactUtils::isSlotEquipment(transformedArtSlot)) - { - + al.slot = ArtifactPosition::BACKPACK_START; + } + else if(ArtifactUtils::isSlotBackpack(al.slot)) + { + for(const auto & slot : slotsInvolved) if(ArtifactUtils::isSlotBackpack(slot)) + al.slot = slot; + } + else + { + for(const auto & slot : slotsInvolved) + if(!vstd::contains(builtArt->getPossibleSlots().at(artSet->bearerType()), al.slot) + && vstd::contains(builtArt->getPossibleSlots().at(artSet->bearerType()), slot)) { - al.slot = ArtifactPosition::BACKPACK_START; + al.slot = slot; break; } - - if(!vstd::contains(combinedArt->artType->getPossibleSlots().at(hero->bearerType()), al.slot) - && vstd::contains(combinedArt->artType->getPossibleSlots().at(hero->bearerType()), slot)) - al.slot = slot; - } - else - { - if(ArtifactUtils::isSlotBackpack(slot)) - al.slot = std::min(al.slot, slot); - } } // Delete parts from hero - for(const auto slot : slotsInvolved) + for(const auto & slot : slotsInvolved) { - const auto constituentInstance = hero->getArt(slot); - constituentInstance->removeFrom(*hero, slot); + const auto constituentInstance = artSet->getArt(slot); + gs->map->removeArtifactInstance(*artSet, slot); - if(ArtifactUtils::isSlotEquipment(al.slot) && slot != al.slot) - combinedArt->addPart(constituentInstance, slot); - else - combinedArt->addPart(constituentInstance, ArtifactPosition::PRE_FIRST); + if(!combinedArt->getType()->isFused()) + { + if(ArtifactUtils::isSlotEquipment(al.slot) && slot != al.slot) + combinedArt->addPart(constituentInstance, slot); + else + combinedArt->addPart(constituentInstance, ArtifactPosition::PRE_FIRST); + } } // Put new combined artifacts - combinedArt->putAt(*hero, al.slot); + gs->map->putArtifactInstance(*artSet, combinedArt, al.slot); } void DisassembledArtifact::applyGs(CGameState *gs) @@ -1862,14 +1884,14 @@ void DisassembledArtifact::applyGs(CGameState *gs) auto disassembledArt = hero->getArt(al.slot); assert(disassembledArt); - auto parts = disassembledArt->getPartsInfo(); - disassembledArt->removeFrom(*hero, al.slot); + const auto parts = disassembledArt->getPartsInfo(); + gs->map->removeArtifactInstance(*hero, al.slot); for(auto & part : parts) { // ArtifactPosition::PRE_FIRST is value of main part slot -> it'll replace combined artifact in its pos auto slot = (ArtifactUtils::isSlotEquipment(part.slot) ? part.slot : al.slot); disassembledArt->detachFrom(*part.art); - part.art->putAt(*hero, slot); + gs->map->putArtifactInstance(*hero, part.art, slot); } gs->map->eraseArtifactInstance(disassembledArt); } @@ -1925,7 +1947,10 @@ void NewTurn::applyGs(CGameState *gs) creatureSet.applyGs(gs); for(CGTownInstance* t : gs->map->towns) + { t->built = 0; + t->spellResearchCounterDay = 0; + } if(newRumor) gs->currentRumor = *newRumor; @@ -2465,10 +2490,7 @@ void SetBankConfiguration::applyGs(CGameState *gs) const CArtifactInstance * ArtSlotInfo::getArt() const { if(locked) - { - logNetwork->warn("ArtifactLocation::getArt: This location is locked!"); return nullptr; - } return artifact; } diff --git a/lib/networkPacks/PacksForClient.h b/lib/networkPacks/PacksForClient.h index 5a81ec806..9a0b20f45 100644 --- a/lib/networkPacks/PacksForClient.h +++ b/lib/networkPacks/PacksForClient.h @@ -288,6 +288,26 @@ struct DLL_LINKAGE ChangeSpells : public CPackForClient } }; +struct DLL_LINKAGE SetResearchedSpells : public CPackForClient +{ + void applyGs(CGameState * gs) override; + + void visitTyped(ICPackVisitor & visitor) override; + + ui8 level = 0; + ObjectInstanceID tid; + std::vector spells; + bool accepted; + + template void serialize(Handler & h) + { + h & level; + h & tid; + h & spells; + h & accepted; + } +}; + struct DLL_LINKAGE SetMana : public CPackForClient { void applyGs(CGameState * gs) override; @@ -799,7 +819,7 @@ struct DLL_LINKAGE SetAvailableArtifacts : public CPackForClient //two variants: id < 0: set artifact pool for Artifact Merchants in towns; id >= 0: set pool for adv. map Black Market (id is the id of Black Market instance then) ObjectInstanceID id; - std::vector arts; + std::vector arts; void visitTyped(ICPackVisitor & visitor) override; @@ -965,14 +985,14 @@ struct DLL_LINKAGE CArtifactOperationPack : CPackForClient struct DLL_LINKAGE PutArtifact : CArtifactOperationPack { PutArtifact() = default; - explicit PutArtifact(const ArtifactLocation & dst, bool askAssemble = true) - : al(dst), askAssemble(askAssemble) + explicit PutArtifact(const ArtifactInstanceID & id, const ArtifactLocation & dst, bool askAssemble = true) + : al(dst), askAssemble(askAssemble), id(id) { } ArtifactLocation al; bool askAssemble; - ConstTransitivePtr art; + ArtifactInstanceID id; void applyGs(CGameState * gs) override; void visitTyped(ICPackVisitor & visitor) override; @@ -981,7 +1001,7 @@ struct DLL_LINKAGE PutArtifact : CArtifactOperationPack { h & al; h & askAssemble; - h & art; + h & id; } }; @@ -1088,8 +1108,8 @@ struct DLL_LINKAGE BulkMoveArtifacts : CArtifactOperationPack struct DLL_LINKAGE AssembledArtifact : CArtifactOperationPack { - ArtifactLocation al; //where assembly will be put - const CArtifact * builtArt; + ArtifactLocation al; + ArtifactID artId; void applyGs(CGameState * gs) override; @@ -1098,7 +1118,7 @@ struct DLL_LINKAGE AssembledArtifact : CArtifactOperationPack template void serialize(Handler & h) { h & al; - h & builtArt; + h & artId; } }; diff --git a/lib/networkPacks/PacksForLobby.h b/lib/networkPacks/PacksForLobby.h index 1477bc98e..c702935c2 100644 --- a/lib/networkPacks/PacksForLobby.h +++ b/lib/networkPacks/PacksForLobby.h @@ -170,12 +170,14 @@ struct DLL_LINKAGE LobbyUpdateState : public CLobbyPackToPropagate { LobbyState state; bool hostChanged = false; // Used on client-side only + bool refreshList = false; void visitTyped(ICPackVisitor & visitor) override; template void serialize(Handler &h) { h & state; + h & refreshList; } }; @@ -381,4 +383,22 @@ struct DLL_LINKAGE LobbyPvPAction : public CLobbyPackToServer } }; +struct DLL_LINKAGE LobbyDelete : public CLobbyPackToServer +{ + enum class EType : ui8 { + SAVEGAME, SAVEGAME_FOLDER, RANDOMMAP + }; + + EType type = EType::SAVEGAME; + std::string name; + + void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler &h) + { + h & type; + h & name; + } +}; + VCMI_LIB_NAMESPACE_END diff --git a/lib/networkPacks/PacksForServer.h b/lib/networkPacks/PacksForServer.h index a909cf652..d72be5265 100644 --- a/lib/networkPacks/PacksForServer.h +++ b/lib/networkPacks/PacksForServer.h @@ -306,6 +306,28 @@ struct DLL_LINKAGE RazeStructure : public BuildStructure void visitTyped(ICPackVisitor & visitor) override; }; +struct DLL_LINKAGE SpellResearch : public CPackForServer +{ + SpellResearch() = default; + SpellResearch(const ObjectInstanceID & TID, SpellID spellAtSlot, bool accepted) + : tid(TID), spellAtSlot(spellAtSlot), accepted(accepted) + { + } + ObjectInstanceID tid; + SpellID spellAtSlot; + bool accepted; + + void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h) + { + h & static_cast(*this); + h & tid; + h & spellAtSlot; + h & accepted; + } +}; + struct DLL_LINKAGE RecruitCreatures : public CPackForServer { RecruitCreatures() = default; diff --git a/lib/networkPacks/SaveLocalState.h b/lib/networkPacks/SaveLocalState.h new file mode 100644 index 000000000..b6106fa47 --- /dev/null +++ b/lib/networkPacks/SaveLocalState.h @@ -0,0 +1,31 @@ +/* + * SaveLocalState.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 "NetPacksBase.h" + +#include "../json/JsonNode.h" + +VCMI_LIB_NAMESPACE_BEGIN + +struct DLL_LINKAGE SaveLocalState : public CPackForServer +{ + JsonNode data; + + void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h) + { + h & static_cast(*this); + h & data; + } +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/pathfinder/CPathfinder.cpp b/lib/pathfinder/CPathfinder.cpp index 41b7e7ee5..7a7fc1af6 100644 --- a/lib/pathfinder/CPathfinder.cpp +++ b/lib/pathfinder/CPathfinder.cpp @@ -292,7 +292,7 @@ TeleporterTilesVector CPathfinderHelper::getTeleportExits(const PathNodeInfo & s { auto * town = dynamic_cast(source.nodeObject); assert(town); - if (town && town->getFaction() == FactionID::INFERNO) + if (town && town->getFactionID() == FactionID::INFERNO) { /// TODO: Find way to reuse CPlayerSpecificInfoCallback::getTownsInfo /// This may be handy if we allow to use teleportation to friendly towns @@ -596,25 +596,19 @@ void CPathfinderHelper::getNeighbours( continue; const TerrainTile & destTile = map->getTile(destCoord); - if(!destTile.terType->isPassable()) + if(!destTile.getTerrain()->isPassable()) continue; -// //we cannot visit things from blocked tiles -// if(srcTile.blocked && !srcTile.visitable && destTile.visitable && srcTile.blockingObjects.front()->ID != HEROI_TYPE) -// { -// continue; -// } - /// Following condition let us avoid diagonal movement over coast when sailing - if(srcTile.terType->isWater() && limitCoastSailing && destTile.terType->isWater() && dir.x && dir.y) //diagonal move through water + if(srcTile.isWater() && limitCoastSailing && destTile.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}; - if(map->getTile(horizontalNeighbour).terType->isLand() || map->getTile(verticalNeighbour).terType->isLand()) + if(map->getTile(horizontalNeighbour).isLand() || map->getTile(verticalNeighbour).isLand()) continue; } - if(indeterminate(onLand) || onLand == destTile.terType->isLand()) + if(indeterminate(onLand) || onLand == destTile.isLand()) { vec.push_back(destCoord); } @@ -662,13 +656,13 @@ int CPathfinderHelper::getMovementCost( bool isSailLayer; if(indeterminate(isDstSailLayer)) - isSailLayer = hero->boat && hero->boat->layer == EPathfindingLayer::SAIL && dt->terType->isWater(); + isSailLayer = hero->boat && hero->boat->layer == EPathfindingLayer::SAIL && dt->isWater(); else isSailLayer = static_cast(isDstSailLayer); bool isWaterLayer; if(indeterminate(isDstWaterLayer)) - isWaterLayer = ((hero->boat && hero->boat->layer == EPathfindingLayer::WATER) || ti->hasBonusOfType(BonusType::WATER_WALKING)) && dt->terType->isWater(); + isWaterLayer = ((hero->boat && hero->boat->layer == EPathfindingLayer::WATER) || ti->hasBonusOfType(BonusType::WATER_WALKING)) && dt->isWater(); else isWaterLayer = static_cast(isDstWaterLayer); @@ -703,7 +697,7 @@ int CPathfinderHelper::getMovementCost( { NeighbourTilesVector vec; - getNeighbours(*dt, dst, vec, ct->terType->isLand(), true); + getNeighbours(*dt, dst, vec, ct->isLand(), true); for(const auto & elem : vec) { int fcost = getMovementCost(dst, elem, nullptr, nullptr, left, false); diff --git a/lib/pathfinder/NodeStorage.cpp b/lib/pathfinder/NodeStorage.cpp index a2ca9a0db..dd9c7a198 100644 --- a/lib/pathfinder/NodeStorage.cpp +++ b/lib/pathfinder/NodeStorage.cpp @@ -41,7 +41,7 @@ void NodeStorage::initialize(const PathfinderOptions & options, const CGameState for(pos.y=0; pos.y < sizes.y; ++pos.y) { const TerrainTile & tile = gs->map->getTile(pos); - if(tile.terType->isWater()) + if(tile.isWater()) { resetTile(pos, ELayer::SAIL, PathfinderUtil::evaluateAccessibility(pos, tile, fow, player, gs)); if(useFlying) @@ -49,7 +49,7 @@ void NodeStorage::initialize(const PathfinderOptions & options, const CGameState if(useWaterWalking) resetTile(pos, ELayer::WATER, PathfinderUtil::evaluateAccessibility(pos, tile, fow, player, gs)); } - if(tile.terType->isLand()) + if(tile.isLand()) { resetTile(pos, ELayer::LAND, PathfinderUtil::evaluateAccessibility(pos, tile, fow, player, gs)); if(useFlying) diff --git a/lib/pathfinder/PathfinderUtil.h b/lib/pathfinder/PathfinderUtil.h index bef955e68..3e1f4cbaf 100644 --- a/lib/pathfinder/PathfinderUtil.h +++ b/lib/pathfinder/PathfinderUtil.h @@ -32,7 +32,7 @@ namespace PathfinderUtil { case ELayer::LAND: case ELayer::SAIL: - if(tinfo.visitable) + if(tinfo.visitable()) { if(tinfo.visitableObjects.front()->ID == Obj::SANCTUARY && tinfo.visitableObjects.back()->ID == Obj::HERO && tinfo.visitableObjects.back()->tempOwner != player) //non-owned hero stands on Sanctuary { @@ -51,7 +51,7 @@ namespace PathfinderUtil } } } - else if(tinfo.blocked) + else if(tinfo.blocked()) { return EPathAccessibility::BLOCKED; } @@ -64,7 +64,7 @@ namespace PathfinderUtil break; case ELayer::WATER: - if(tinfo.blocked || tinfo.terType->isLand()) + if(tinfo.blocked() || tinfo.isLand()) return EPathAccessibility::BLOCKED; break; diff --git a/lib/pathfinder/PathfindingRules.cpp b/lib/pathfinder/PathfindingRules.cpp index 500b04665..8f90a3fca 100644 --- a/lib/pathfinder/PathfindingRules.cpp +++ b/lib/pathfinder/PathfindingRules.cpp @@ -380,7 +380,7 @@ void LayerTransitionRule::process( case EPathfindingLayer::SAIL: // have to disembark first before visiting objects on land - if (destination.tile->visitable) + if (destination.tile->visitable()) destination.blocked = true; //can disembark only on accessible tiles or tiles guarded by nearby monster @@ -397,7 +397,7 @@ void LayerTransitionRule::process( if (destination.node->accessible == EPathAccessibility::BLOCKVIS) { // Can't visit 'blockvisit' objects on coast if hero will end up on water terrain - if (source.tile->blocked || !destination.tile->entrableTerrain(source.tile)) + if (source.tile->blocked() || !destination.tile->entrableTerrain(source.tile)) destination.blocked = true; } } diff --git a/lib/rewardable/Info.cpp b/lib/rewardable/Info.cpp index 6008b3e53..8c8d321de 100644 --- a/lib/rewardable/Info.cpp +++ b/lib/rewardable/Info.cpp @@ -76,7 +76,7 @@ void Rewardable::Info::init(const JsonNode & objectConfig, const std::string & o auto loadString = [&](const JsonNode & entry, const TextIdentifier & textID){ if (entry.isString() && !entry.String().empty() && entry.String()[0] != '@') - VLC->generaltexth->registerString(entry.getModScope(), textID, entry.String()); + VLC->generaltexth->registerString(entry.getModScope(), textID, entry); }; parameters = objectConfig; @@ -526,6 +526,11 @@ bool Rewardable::Info::givesBonuses() const return testForKey(parameters, "bonuses"); } +bool Rewardable::Info::hasGuards() const +{ + return testForKey(parameters, "guards"); +} + const JsonNode & Rewardable::Info::getParameters() const { return parameters; diff --git a/lib/rewardable/Info.h b/lib/rewardable/Info.h index bad4f5031..ac89bdc8d 100644 --- a/lib/rewardable/Info.h +++ b/lib/rewardable/Info.h @@ -68,6 +68,8 @@ public: bool givesBonuses() const override; + bool hasGuards() const override; + void configureObject(Rewardable::Configuration & object, vstd::RNG & rng, IGameCallback * cb) const; void init(const JsonNode & objectConfig, const std::string & objectTextID); diff --git a/lib/rewardable/Interface.cpp b/lib/rewardable/Interface.cpp index d86047f9c..5544a0ba2 100644 --- a/lib/rewardable/Interface.cpp +++ b/lib/rewardable/Interface.cpp @@ -11,10 +11,10 @@ #include "StdInc.h" #include "Interface.h" -#include "../CHeroHandler.h" #include "../TerrainHandler.h" #include "../CPlayerState.h" #include "../CSoundBase.h" +#include "../entities/hero/CHeroHandler.h" #include "../gameState/CGameState.h" #include "../spells/CSpellHandler.h" #include "../spells/ISpellMechanics.h" @@ -25,6 +25,8 @@ #include "../networkPacks/PacksForClient.h" #include "../IGameCallback.h" +#include + VCMI_LIB_NAMESPACE_BEGIN std::vector Rewardable::Interface::getAvailableRewards(const CGHeroInstance * hero, Rewardable::EEventType event) const @@ -44,8 +46,10 @@ std::vector Rewardable::Interface::getAvailableRewards(const CGHeroInstanc return ret; } -void Rewardable::Interface::grantRewardBeforeLevelup(IGameCallback * cb, const Rewardable::VisitInfo & info, const CGHeroInstance * hero) const +void Rewardable::Interface::grantRewardBeforeLevelup(const Rewardable::VisitInfo & info, const CGHeroInstance * hero) const { + auto cb = getObject()->cb; + assert(hero); assert(hero->tempOwner.isValidPlayer()); assert(info.reward.creatures.size() <= GameConstants::ARMY_SIZE); @@ -59,16 +63,16 @@ void Rewardable::Interface::grantRewardBeforeLevelup(IGameCallback * cb, const R const auto functor = [&props](const TerrainTile * tile) { int score = 0; - if (tile->terType->isSurface()) + if (tile->getTerrain()->isSurface()) score += props.scoreSurface; - if (tile->terType->isUnderground()) + if (tile->getTerrain()->isUnderground()) score += props.scoreSubterra; - if (tile->terType->isWater()) + if (tile->getTerrain()->isWater()) score += props.scoreWater; - if (tile->terType->isRock()) + if (tile->getTerrain()->isRock()) score += props.scoreRock; return score > 0; @@ -129,8 +133,10 @@ void Rewardable::Interface::grantRewardBeforeLevelup(IGameCallback * cb, const R cb->giveExperience(hero, expToGive); } -void Rewardable::Interface::grantRewardAfterLevelup(IGameCallback * cb, const Rewardable::VisitInfo & info, const CArmedInstance * army, const CGHeroInstance * hero) const +void Rewardable::Interface::grantRewardAfterLevelup(const Rewardable::VisitInfo & info, const CArmedInstance * army, const CGHeroInstance * hero) const { + auto cb = getObject()->cb; + if(info.reward.manaDiff || info.reward.manaPercentage >= 0) cb->setManaPoints(hero->id, info.reward.calculateManaPoints(hero)); @@ -179,7 +185,7 @@ void Rewardable::Interface::grantRewardAfterLevelup(IGameCallback * cb, const Re for(const auto & change : info.reward.creaturesChange) { - if (heroStack->type->getId() == change.first) + if (heroStack->getId() == change.first) { StackLocation location(hero, slot.first); cb->changeStackType(location, change.second.toCreature()); @@ -193,7 +199,7 @@ void Rewardable::Interface::grantRewardAfterLevelup(IGameCallback * cb, const Re { CCreatureSet creatures; for(const auto & crea : info.reward.creatures) - creatures.addToSlot(creatures.getFreeSlot(), new CStackInstance(crea.type, crea.count)); + creatures.addToSlot(creatures.getFreeSlot(), new CStackInstance(crea.getCreature(), crea.count)); if(auto * army = dynamic_cast(this)) //TODO: to fix that, CArmedInstance must be split on map instance part and interface part cb->giveCreatures(army, hero, creatures, false); @@ -216,4 +222,165 @@ void Rewardable::Interface::serializeJson(JsonSerializeFormat & handler) configuration.serializeJson(handler); } +void Rewardable::Interface::grantRewardWithMessage(const CGHeroInstance * contextHero, int index, bool markAsVisit) const +{ + auto vi = configuration.info.at(index); + logGlobal->debug("Granting reward %d. Message says: %s", index, vi.message.toString()); + // show message only if it is not empty or in infobox + if (configuration.infoWindowType != EInfoWindowMode::MODAL || !vi.message.toString().empty()) + { + InfoWindow iw; + iw.player = contextHero->tempOwner; + iw.text = vi.message; + vi.reward.loadComponents(iw.components, contextHero); + iw.type = configuration.infoWindowType; + if(!iw.components.empty() || !iw.text.toString().empty()) + getObject()->cb->showInfoDialog(&iw); + } + // grant reward afterwards. Note that it may remove object + if(markAsVisit) + markAsVisited(contextHero); + grantReward(index, contextHero); +} + +void Rewardable::Interface::selectRewardWithMessage(const CGHeroInstance * contextHero, const std::vector & rewardIndices, const MetaString & dialog) const +{ + BlockingDialog sd(configuration.canRefuse, rewardIndices.size() > 1); + sd.player = contextHero->tempOwner; + sd.text = dialog; + sd.components = loadComponents(contextHero, rewardIndices); + getObject()->cb->showBlockingDialog(getObject(), &sd); +} + +std::vector Rewardable::Interface::loadComponents(const CGHeroInstance * contextHero, const std::vector & rewardIndices) const +{ + std::vector result; + + if (rewardIndices.empty()) + return result; + + if (configuration.selectMode != Rewardable::SELECT_FIRST && rewardIndices.size() > 1) + { + for (auto index : rewardIndices) + result.push_back(configuration.info.at(index).reward.getDisplayedComponent(contextHero)); + } + else + { + configuration.info.at(rewardIndices.front()).reward.loadComponents(result, contextHero); + } + + return result; +} + +void Rewardable::Interface::grantAllRewardsWithMessage(const CGHeroInstance * contextHero, const std::vector & rewardIndices, bool markAsVisit) const +{ + if (rewardIndices.empty()) + return; + + for (auto index : rewardIndices) + { + // TODO: Merge all rewards of same type, with single message? + grantRewardWithMessage(contextHero, index, false); + } + // Mark visited only after all rewards were processed + if(markAsVisit) + markAsVisited(contextHero); +} + +void Rewardable::Interface::doHeroVisit(const CGHeroInstance *h) const +{ + if(!wasVisitedBefore(h)) + { + auto rewards = getAvailableRewards(h, Rewardable::EEventType::EVENT_FIRST_VISIT); + bool objectRemovalPossible = false; + for(auto index : rewards) + { + if(configuration.info.at(index).reward.removeObject) + objectRemovalPossible = true; + } + + logGlobal->debug("Visiting object with %d possible rewards", rewards.size()); + switch (rewards.size()) + { + case 0: // no available rewards, e.g. visiting School of War without gold + { + auto emptyRewards = getAvailableRewards(h, Rewardable::EEventType::EVENT_NOT_AVAILABLE); + if (!emptyRewards.empty()) + grantRewardWithMessage(h, emptyRewards[0], false); + else + logMod->warn("No applicable message for visiting empty object!"); + break; + } + case 1: // one reward. Just give it with message + { + if (configuration.canRefuse) + selectRewardWithMessage(h, rewards, configuration.info.at(rewards.front()).message); + else + grantRewardWithMessage(h, rewards.front(), true); + break; + } + default: // multiple rewards. Act according to select mode + { + switch (configuration.selectMode) { + case Rewardable::SELECT_PLAYER: // player must select + selectRewardWithMessage(h, rewards, configuration.onSelect); + break; + case Rewardable::SELECT_FIRST: // give first available + if (configuration.canRefuse) + selectRewardWithMessage(h, { rewards.front() }, configuration.info.at(rewards.front()).message); + else + grantRewardWithMessage(h, rewards.front(), true); + break; + case Rewardable::SELECT_RANDOM: // give random + { + ui32 rewardIndex = *RandomGeneratorUtil::nextItem(rewards, getObject()->cb->getRandomGenerator()); + if (configuration.canRefuse) + selectRewardWithMessage(h, { rewardIndex }, configuration.info.at(rewardIndex).message); + else + grantRewardWithMessage(h, rewardIndex, true); + break; + } + case Rewardable::SELECT_ALL: // grant all possible + grantAllRewardsWithMessage(h, rewards, true); + break; + } + break; + } + } + + if(!objectRemovalPossible && getAvailableRewards(h, Rewardable::EEventType::EVENT_FIRST_VISIT).empty()) + markAsScouted(h); + } + else + { + logGlobal->debug("Revisiting already visited object"); + + if (!wasVisited(h->getOwner())) + markAsScouted(h); + + auto visitedRewards = getAvailableRewards(h, Rewardable::EEventType::EVENT_ALREADY_VISITED); + if (!visitedRewards.empty()) + grantRewardWithMessage(h, visitedRewards[0], false); + else + logMod->warn("No applicable message for visiting already visited object!"); + } +} + +void Rewardable::Interface::onBlockingDialogAnswered(const CGHeroInstance * hero, int32_t answer) const +{ + if (answer == 0) + return; //Player refused + + if(answer > 0 && answer - 1 < configuration.info.size()) + { + auto list = getAvailableRewards(hero, Rewardable::EEventType::EVENT_FIRST_VISIT); + markAsVisited(hero); + grantReward(list[answer - 1], hero); + } + else + { + throw std::runtime_error("Unhandled choice"); + } +} + VCMI_LIB_NAMESPACE_END diff --git a/lib/rewardable/Interface.h b/lib/rewardable/Interface.h index 7fd6fb690..3fe764637 100644 --- a/lib/rewardable/Interface.h +++ b/lib/rewardable/Interface.h @@ -15,7 +15,7 @@ VCMI_LIB_NAMESPACE_BEGIN -class IGameCallback; +class IObjectInterface; namespace Rewardable { @@ -30,11 +30,26 @@ private: protected: /// function that must be called if hero got level-up during grantReward call - virtual void grantRewardAfterLevelup(IGameCallback * cb, const Rewardable::VisitInfo & reward, const CArmedInstance * army, const CGHeroInstance * hero) const; + void grantRewardAfterLevelup(const Rewardable::VisitInfo & reward, const CArmedInstance * army, const CGHeroInstance * hero) const; /// grants reward to hero - virtual void grantRewardBeforeLevelup(IGameCallback * cb, const Rewardable::VisitInfo & reward, const CGHeroInstance * hero) const; + void grantRewardBeforeLevelup(const Rewardable::VisitInfo & reward, const CGHeroInstance * hero) const; + virtual void grantRewardWithMessage(const CGHeroInstance * contextHero, int rewardIndex, bool markAsVisit) const; + void selectRewardWithMessage(const CGHeroInstance * contextHero, const std::vector & rewardIndices, const MetaString & dialog) const; + void grantAllRewardsWithMessage(const CGHeroInstance * contextHero, const std::vector& rewardIndices, bool markAsVisit) const; + std::vector loadComponents(const CGHeroInstance * contextHero, const std::vector & rewardIndices) const; + + void doHeroVisit(const CGHeroInstance *h) const; + + virtual const IObjectInterface * getObject() const = 0; + virtual bool wasVisitedBefore(const CGHeroInstance * hero) const = 0; + virtual bool wasVisited(PlayerColor player) const = 0; + virtual void markAsVisited(const CGHeroInstance * hero) const = 0; + virtual void markAsScouted(const CGHeroInstance * hero) const = 0; + virtual void grantReward(ui32 rewardID, const CGHeroInstance * hero) const = 0; + + void onBlockingDialogAnswered(const CGHeroInstance * hero, int32_t answer) const; public: /// filters list of visit info and returns rewards that can be granted to current hero diff --git a/lib/rewardable/Limiter.cpp b/lib/rewardable/Limiter.cpp index a5b522e4b..58b66c66c 100644 --- a/lib/rewardable/Limiter.cpp +++ b/lib/rewardable/Limiter.cpp @@ -17,7 +17,6 @@ #include "../networkPacks/Component.h" #include "../serializer/JsonSerializeFormat.h" #include "../constants/StringConstants.h" -#include "../CHeroHandler.h" #include "../CSkillHandler.h" #include "../ArtifactUtils.h" @@ -85,7 +84,7 @@ bool Rewardable::Limiter::heroAllowed(const CGHeroInstance * hero) const for(const auto & slot : hero->Slots()) { const CStackInstance * heroStack = slot.second; - if (heroStack->type == reqStack.type) + if (heroStack->getType() == reqStack.getType()) count += heroStack->count; } if (count < reqStack.count) //not enough creatures of this kind @@ -143,10 +142,28 @@ bool Rewardable::Limiter::heroAllowed(const CGHeroInstance * hero) const for(const auto & elem : artifactsRequirements) { // check required amount of artifacts - if(hero->getArtPosCount(elem.first, false, true, true) < elem.second) + size_t artCnt = 0; + for(const auto & [slot, slotInfo] : hero->artifactsWorn) + if(slotInfo.artifact->getTypeId() == elem.first) + artCnt++; + + for(auto & slotInfo : hero->artifactsInBackpack) + if(slotInfo.artifact->getTypeId() == elem.first) + { + artCnt++; + } + else if(slotInfo.artifact->isCombined()) + { + for(const auto & partInfo : slotInfo.artifact->getPartsInfo()) + if(partInfo.art->getTypeId() == elem.first) + artCnt++; + } + + if(artCnt < elem.second) return false; - if(!hero->hasArt(elem.first)) - reqSlots += hero->getAssemblyByConstituent(elem.first)->getPartsInfo().size() - 2; + // Check if art has no own slot. (As part of combined in backpack) + if(hero->getArtPos(elem.first, false) == ArtifactPosition::PRE_FIRST) + reqSlots += hero->getCombinedArtWithPart(elem.first)->getPartsInfo().size() - 2; } if(!ArtifactUtils::isBackpackFreeSlots(hero, reqSlots)) return false; @@ -155,10 +172,10 @@ bool Rewardable::Limiter::heroAllowed(const CGHeroInstance * hero) const if(!players.empty() && !vstd::contains(players, hero->getOwner())) return false; - if(!heroes.empty() && !vstd::contains(heroes, hero->type->getId())) + if(!heroes.empty() && !vstd::contains(heroes, hero->getHeroTypeID())) return false; - if(!heroClasses.empty() && !vstd::contains(heroClasses, hero->type->heroClass->getId())) + if(!heroClasses.empty() && !vstd::contains(heroClasses, hero->getHeroClassID())) return false; @@ -216,7 +233,7 @@ void Rewardable::Limiter::loadComponents(std::vector & comps, comps.emplace_back(ComponentType::SPELL, entry); for(const auto & entry : creatures) - comps.emplace_back(ComponentType::CREATURE, entry.type->getId(), entry.count); + comps.emplace_back(ComponentType::CREATURE, entry.getId(), entry.count); for(const auto & entry : players) comps.emplace_back(ComponentType::FLAG, entry); diff --git a/lib/rewardable/Reward.cpp b/lib/rewardable/Reward.cpp index c52501a6b..22240a737 100644 --- a/lib/rewardable/Reward.cpp +++ b/lib/rewardable/Reward.cpp @@ -121,7 +121,7 @@ void Rewardable::Reward::loadComponents(std::vector & comps, const CG } for(const auto & entry : creatures) - comps.emplace_back(ComponentType::CREATURE, entry.type->getId(), entry.count); + comps.emplace_back(ComponentType::CREATURE, entry.getId(), entry.count); for (size_t i=0; i= Handler::Version::REWARDABLE_GUARDS) + h & guards; h & heroExperience; h & heroLevel; h & manaDiff; diff --git a/lib/rmg/CMapGenerator.cpp b/lib/rmg/CMapGenerator.cpp index 0e02212b9..f789b78c6 100644 --- a/lib/rmg/CMapGenerator.cpp +++ b/lib/rmg/CMapGenerator.cpp @@ -17,11 +17,11 @@ #include "../CRandomGenerator.h" #include "../entities/faction/CTownHandler.h" #include "../entities/faction/CFaction.h" +#include "../entities/hero/CHero.h" #include "../mapObjectConstructors/AObjectTypeHandler.h" #include "../mapObjectConstructors/CObjectClassesHandler.h" #include "../mapping/CMapEditManager.h" #include "../CArtHandler.h" -#include "../CHeroHandler.h" #include "../constants/StringConstants.h" #include "../filesystem/Filesystem.h" #include "CZonePlacer.h" @@ -35,6 +35,7 @@ #include "modificators/RoadPlacer.h" #include +#include VCMI_LIB_NAMESPACE_BEGIN @@ -151,41 +152,55 @@ std::unique_ptr CMapGenerator::generate() return std::move(map->mapInstance); } -std::string CMapGenerator::getMapDescription() const +MetaString CMapGenerator::getMapDescription() const { - assert(map); + const TextIdentifier mainPattern("vcmi", "randomMap", "description"); + const TextIdentifier isHuman("vcmi", "randomMap", "description", "isHuman"); + const TextIdentifier townChoiceIs("vcmi", "randomMap", "description", "townChoice"); + const std::array waterContent = { + TextIdentifier("vcmi", "randomMap", "description", "water", "none"), + TextIdentifier("vcmi", "randomMap", "description", "water", "normal"), + TextIdentifier("vcmi", "randomMap", "description", "water", "islands") + }; + const std::array monsterStrength = { + TextIdentifier("vcmi", "randomMap", "description", "monster", "weak"), + TextIdentifier("vcmi", "randomMap", "description", "monster", "normal"), + TextIdentifier("vcmi", "randomMap", "description", "monster", "strong") + }; - const std::string waterContentStr[3] = { "none", "normal", "islands" }; - const std::string monsterStrengthStr[3] = { "weak", "normal", "strong" }; - - int monsterStrengthIndex = mapGenOptions.getMonsterStrength() - EMonsterStrength::GLOBAL_WEAK; //does not start from 0 const auto * mapTemplate = mapGenOptions.getMapTemplate(); + int monsterStrengthIndex = mapGenOptions.getMonsterStrength() - EMonsterStrength::GLOBAL_WEAK; //does not start from 0 - if(!mapTemplate) - throw rmgException("Map template for Random Map Generator is not found. Could not start the game."); + MetaString result = MetaString::createFromTextID(mainPattern.get()); - std::stringstream ss; - ss << boost::str(boost::format(std::string("Map created by the Random Map Generator.\nTemplate was %s, size %dx%d") + - ", levels %d, players %d, computers %d, water %s, monster %s, VCMI map") % mapTemplate->getName() % - map->width() % map->height() % static_cast(map->levels()) % static_cast(mapGenOptions.getHumanOrCpuPlayerCount()) % - static_cast(mapGenOptions.getCompOnlyPlayerCount()) % waterContentStr[mapGenOptions.getWaterContent()] % - monsterStrengthStr[monsterStrengthIndex]); + result.replaceRawString(mapTemplate->getName()); + result.replaceNumber(map->width()); + result.replaceNumber(map->height()); + result.replaceNumber(map->levels()); + result.replaceNumber(mapGenOptions.getHumanOrCpuPlayerCount()); + result.replaceNumber(mapGenOptions.getCompOnlyPlayerCount()); + result.replaceTextID(waterContent.at(mapGenOptions.getWaterContent()).get()); + result.replaceTextID(monsterStrength.at(monsterStrengthIndex).get()); for(const auto & pair : mapGenOptions.getPlayersSettings()) { const auto & pSettings = pair.second; + if(pSettings.getPlayerType() == EPlayerType::HUMAN) { - ss << ", " << GameConstants::PLAYER_COLOR_NAMES[pSettings.getColor().getNum()] << " is human"; + result.appendTextID(isHuman.get()); + result.replaceName(pSettings.getColor()); } + if(pSettings.getStartingTown() != FactionID::RANDOM) { - ss << ", " << GameConstants::PLAYER_COLOR_NAMES[pSettings.getColor().getNum()] - << " town choice is " << (*VLC->townh)[pSettings.getStartingTown()]->getNameTranslated(); + result.appendTextID(townChoiceIs.get()); + result.replaceName(pSettings.getColor()); + result.replaceName(pSettings.getStartingTown()); } } - return ss.str(); + return result; } void CMapGenerator::addPlayerInfo() @@ -450,7 +465,7 @@ void CMapGenerator::addHeaderInfo() m.height = mapGenOptions.getHeight(); m.twoLevel = mapGenOptions.getHasTwoLevels(); m.name.appendLocalString(EMetaText::GENERAL_TXT, 740); - m.description.appendRawString(getMapDescription()); + m.description = getMapDescription(); m.difficulty = EMapDifficulty::NORMAL; addPlayerInfo(); m.waterMap = (mapGenOptions.getWaterContent() != EWaterContent::EWaterContent::NONE); diff --git a/lib/rmg/CMapGenerator.h b/lib/rmg/CMapGenerator.h index c1971bdba..a635534b6 100644 --- a/lib/rmg/CMapGenerator.h +++ b/lib/rmg/CMapGenerator.h @@ -10,14 +10,12 @@ #pragma once -#include "../GameConstants.h" #include "CMapGenOptions.h" -#include "../int3.h" -#include "CRmgTemplate.h" #include "../LoadProgress.h" VCMI_LIB_NAMESPACE_BEGIN +class MetaString; class CRmgTemplate; class CMapGenOptions; class JsonNode; @@ -93,7 +91,7 @@ private: /// Generation methods void loadConfig(); - std::string getMapDescription() const; + MetaString getMapDescription() const; void initPrisonsRemaining(); void initQuestArtsRemaining(); diff --git a/lib/rmg/CRmgTemplate.cpp b/lib/rmg/CRmgTemplate.cpp index c448499b1..9abaaed34 100644 --- a/lib/rmg/CRmgTemplate.cpp +++ b/lib/rmg/CRmgTemplate.cpp @@ -102,6 +102,7 @@ void ZoneOptions::CTownInfo::serializeJson(JsonSerializeFormat & handler) handler.serializeInt("castles", castleCount, 0); handler.serializeInt("townDensity", townDensity, 0); handler.serializeInt("castleDensity", castleDensity, 0); + handler.serializeInt("sourceZone", sourceZone, NO_ZONE); } ZoneOptions::ZoneOptions(): @@ -156,7 +157,7 @@ std::optional ZoneOptions::getOwner() const return owner; } -const std::set ZoneOptions::getTerrainTypes() const +std::set ZoneOptions::getTerrainTypes() const { if (terrainTypes.empty()) { @@ -191,7 +192,7 @@ std::set ZoneOptions::getDefaultTownTypes() const return VLC->townh->getDefaultAllowed(); } -const std::set ZoneOptions::getTownTypes() const +std::set ZoneOptions::getTownTypes() const { if (townTypes.empty()) { @@ -214,7 +215,7 @@ void ZoneOptions::setMonsterTypes(const std::set & value) monsterTypes = value; } -const std::set ZoneOptions::getMonsterTypes() const +std::set ZoneOptions::getMonsterTypes() const { return vstd::difference(monsterTypes, bannedMonsters); } @@ -250,7 +251,7 @@ void ZoneOptions::addTreasureInfo(const CTreasureInfo & value) vstd::amax(maxTreasureValue, value.max); } -const std::vector & ZoneOptions::getTreasureInfo() const +std::vector ZoneOptions::getTreasureInfo() const { return treasureInfo; } @@ -272,7 +273,22 @@ TRmgTemplateZoneId ZoneOptions::getTerrainTypeLikeZone() const TRmgTemplateZoneId ZoneOptions::getTreasureLikeZone() const { - return treasureLikeZone; + return treasureLikeZone; +} + +ObjectConfig ZoneOptions::getCustomObjects() const +{ + return objectConfig; +} + +void ZoneOptions::setCustomObjects(const ObjectConfig & value) +{ + objectConfig = value; +} + +TRmgTemplateZoneId ZoneOptions::getCustomObjectsLikeZone() const +{ + return customObjectsLikeZone; } void ZoneOptions::addConnection(const ZoneConnection & connection) @@ -319,7 +335,8 @@ void ZoneOptions::serializeJson(JsonSerializeFormat & handler) "cpuStart", "treasure", "junction", - "water" + "water", + "sealed" }; handler.serializeEnum("type", type, zoneTypes); @@ -334,6 +351,7 @@ void ZoneOptions::serializeJson(JsonSerializeFormat & handler) SERIALIZE_ZONE_LINK(minesLikeZone); SERIALIZE_ZONE_LINK(terrainTypeLikeZone); SERIALIZE_ZONE_LINK(treasureLikeZone); + SERIALIZE_ZONE_LINK(customObjectsLikeZone); #undef SERIALIZE_ZONE_LINK @@ -398,9 +416,12 @@ void ZoneOptions::serializeJson(JsonSerializeFormat & handler) handler.serializeInt(GameConstants::RESOURCE_NAMES[idx], mines[idx], 0); } } + + handler.serializeStruct("customObjects", objectConfig); } ZoneConnection::ZoneConnection(): + id(-1), zoneA(-1), zoneB(-1), guardStrength(0), @@ -410,6 +431,16 @@ ZoneConnection::ZoneConnection(): } +int ZoneConnection::getId() const +{ + return id; +} + +void ZoneConnection::setId(int id) +{ + this->id = id; +} + TRmgTemplateZoneId ZoneConnection::getZoneA() const { return zoneA; @@ -453,7 +484,7 @@ rmg::ERoadOption ZoneConnection::getRoadOption() const bool operator==(const ZoneConnection & l, const ZoneConnection & r) { - return l.zoneA == r.zoneA && l.zoneB == r.zoneB && l.guardStrength == r.guardStrength; + return l.id == r.id; } void ZoneConnection::serializeJson(JsonSerializeFormat & handler) @@ -572,7 +603,7 @@ const CRmgTemplate::Zones & CRmgTemplate::getZones() const const std::vector & CRmgTemplate::getConnectedZoneIds() const { - return connectedZoneIds; + return connections; } void CRmgTemplate::validate() const @@ -701,7 +732,14 @@ void CRmgTemplate::serializeJson(JsonSerializeFormat & handler) { auto connectionsData = handler.enterArray("connections"); - connectionsData.serializeStruct(connectedZoneIds); + connectionsData.serializeStruct(connections); + if(!handler.saving) + { + for(size_t i = 0; i < connections.size(); ++i) + { + connections[i].setId(i); + } + } } { @@ -759,53 +797,29 @@ const JsonNode & CRmgTemplate::getMapSettings() const return *mapSettings; } -std::set CRmgTemplate::inheritTerrainType(std::shared_ptr zone, uint32_t iteration /* = 0 */) +template +T CRmgTemplate::inheritZoneProperty(std::shared_ptr zone, + T (rmg::ZoneOptions::*getter)() const, + void (rmg::ZoneOptions::*setter)(const T&), + TRmgTemplateZoneId (rmg::ZoneOptions::*inheritFrom)() const, + const std::string& propertyString, + uint32_t iteration) { if (iteration >= 50) { - logGlobal->error("Infinite recursion for terrain types detected in template %s", name); - return std::set(); + logGlobal->error("Infinite recursion for %s detected in template %s", propertyString, name); + return T(); } - if (zone->getTerrainTypeLikeZone() != ZoneOptions::NO_ZONE) + + if (((*zone).*inheritFrom)() != rmg::ZoneOptions::NO_ZONE) { iteration++; - const auto otherZone = zones.at(zone->getTerrainTypeLikeZone()); - zone->setTerrainTypes(inheritTerrainType(otherZone, iteration)); + const auto otherZone = zones.at(((*zone).*inheritFrom)()); + T inheritedValue = inheritZoneProperty(otherZone, getter, setter, inheritFrom, propertyString, iteration); + ((*zone).*setter)(inheritedValue); } - //This implicitly excludes banned terrains - return zone->getTerrainTypes(); -} - -std::map CRmgTemplate::inheritMineTypes(std::shared_ptr zone, uint32_t iteration /* = 0 */) -{ - if (iteration >= 50) - { - logGlobal->error("Infinite recursion for mine types detected in template %s", name); - return std::map(); - } - if (zone->getMinesLikeZone() != ZoneOptions::NO_ZONE) - { - iteration++; - const auto otherZone = zones.at(zone->getMinesLikeZone()); - zone->setMinesInfo(inheritMineTypes(otherZone, iteration)); - } - return zone->getMinesInfo(); -} - -std::vector CRmgTemplate::inheritTreasureInfo(std::shared_ptr zone, uint32_t iteration /* = 0 */) -{ - if (iteration >= 50) - { - logGlobal->error("Infinite recursion for treasures detected in template %s", name); - return std::vector(); - } - if (zone->getTreasureLikeZone() != ZoneOptions::NO_ZONE) - { - iteration++; - const auto otherZone = zones.at(zone->getTreasureLikeZone()); - zone->setTreasureInfo(inheritTreasureInfo(otherZone, iteration)); - } - return zone->getTreasureInfo(); + + return ((*zone).*getter)(); } void CRmgTemplate::afterLoad() @@ -814,12 +828,32 @@ void CRmgTemplate::afterLoad() { auto zone = idAndZone.second; - //Inherit properties recursively. - inheritTerrainType(zone); - inheritMineTypes(zone); - inheritTreasureInfo(zone); + // Inherit properties recursively + inheritZoneProperty(zone, + &rmg::ZoneOptions::getTerrainTypes, + &rmg::ZoneOptions::setTerrainTypes, + &rmg::ZoneOptions::getTerrainTypeLikeZone, + "terrain types"); + + inheritZoneProperty(zone, + &rmg::ZoneOptions::getMinesInfo, + &rmg::ZoneOptions::setMinesInfo, + &rmg::ZoneOptions::getMinesLikeZone, + "mine types"); + + inheritZoneProperty(zone, + &rmg::ZoneOptions::getTreasureInfo, + &rmg::ZoneOptions::setTreasureInfo, + &rmg::ZoneOptions::getTreasureLikeZone, + "treasure info"); - //TODO: Inherit monster types as well + inheritZoneProperty(zone, + &rmg::ZoneOptions::getCustomObjects, + &rmg::ZoneOptions::setCustomObjects, + &rmg::ZoneOptions::getCustomObjectsLikeZone, + "custom objects"); + + //TODO: Inherit monster types as well auto monsterTypes = zone->getMonsterTypes(); if (monsterTypes.empty()) { @@ -827,7 +861,7 @@ void CRmgTemplate::afterLoad() } } - for(const auto & connection : connectedZoneIds) + for(const auto & connection : connections) { auto id1 = connection.getZoneA(); auto id2 = connection.getZoneB(); @@ -848,6 +882,7 @@ void CRmgTemplate::afterLoad() allowedWaterContent.erase(EWaterContent::RANDOM); } +// TODO: Allow any integer size which does not match enum, as well void CRmgTemplate::serializeSize(JsonSerializeFormat & handler, int3 & value, const std::string & fieldName) { static const std::map sizeMapping = @@ -916,5 +951,19 @@ void CRmgTemplate::serializePlayers(JsonSerializeFormat & handler, CPlayerCountR value.fromString(encodedValue); } +const std::vector & ZoneOptions::getBannedObjects() const +{ + return objectConfig.getBannedObjects(); +} + +const std::vector & ZoneOptions::getBannedObjectCategories() const +{ + return objectConfig.getBannedObjectCategories(); +} + +const std::vector & ZoneOptions::getConfiguredObjects() const +{ + return objectConfig.getConfiguredObjects(); +} VCMI_LIB_NAMESPACE_END diff --git a/lib/rmg/CRmgTemplate.h b/lib/rmg/CRmgTemplate.h index 13aafce64..649f4878d 100644 --- a/lib/rmg/CRmgTemplate.h +++ b/lib/rmg/CRmgTemplate.h @@ -13,10 +13,14 @@ #include "../int3.h" #include "../GameConstants.h" #include "../ResourceSet.h" +#include "ObjectInfo.h" +#include "ObjectConfig.h" +#include "../mapObjectConstructors/CObjectClassesHandler.h" VCMI_LIB_NAMESPACE_BEGIN class JsonSerializeFormat; +struct CompoundMapObjectID; enum class ETemplateZoneType { @@ -24,7 +28,8 @@ enum class ETemplateZoneType CPU_START, TREASURE, JUNCTION, - WATER + WATER, + SEALED }; namespace EWaterContent // Not enum class, because it's used in method RandomMapTab::setMapGenOptions @@ -92,6 +97,8 @@ public: ZoneConnection(); + int getId() const; + void setId(int id); TRmgTemplateZoneId getZoneA() const; TRmgTemplateZoneId getZoneB() const; TRmgTemplateZoneId getOtherZoneId(TRmgTemplateZoneId id) const; @@ -103,6 +110,7 @@ public: friend bool operator==(const ZoneConnection &, const ZoneConnection &); private: + int id; TRmgTemplateZoneId zoneA; TRmgTemplateZoneId zoneB; int guardStrength; @@ -132,6 +140,9 @@ public: int castleCount; int townDensity; int castleDensity; + + // TODO: Copy from another zone once its randomized + TRmgTemplateZoneId sourceZone = NO_ZONE; }; ZoneOptions(); @@ -146,15 +157,15 @@ public: void setSize(int value); std::optional getOwner() const; - const std::set getTerrainTypes() const; + std::set getTerrainTypes() const; void setTerrainTypes(const std::set & value); std::set getDefaultTerrainTypes() const; const CTownInfo & getPlayerTowns() const; const CTownInfo & getNeutralTowns() const; std::set getDefaultTownTypes() const; - const std::set getTownTypes() const; - const std::set getMonsterTypes() const; + std::set getTownTypes() const; + std::set getMonsterTypes() const; void setTownTypes(const std::set & value); void setMonsterTypes(const std::set & value); @@ -164,7 +175,7 @@ public: void setTreasureInfo(const std::vector & value); void addTreasureInfo(const CTreasureInfo & value); - const std::vector & getTreasureInfo() const; + std::vector getTreasureInfo() const; ui32 getMaxTreasureValue() const; void recalculateMaxTreasureValue(); @@ -183,12 +194,24 @@ public: bool areTownsSameType() const; bool isMatchTerrainToTown() const; + // Get a group of configured objects + const std::vector & getBannedObjects() const; + const std::vector & getBannedObjectCategories() const; + const std::vector & getConfiguredObjects() const; + + // Copy whole custom object config from another zone + ObjectConfig getCustomObjects() const; + void setCustomObjects(const ObjectConfig & value); + TRmgTemplateZoneId getCustomObjectsLikeZone() const; + protected: TRmgTemplateZoneId id; ETemplateZoneType type; int size; ui32 maxTreasureValue; std::optional owner; + + ObjectConfig objectConfig; CTownInfo playerTowns; CTownInfo neutralTowns; bool matchTerrainToTown; @@ -211,6 +234,7 @@ protected: TRmgTemplateZoneId minesLikeZone; TRmgTemplateZoneId terrainTypeLikeZone; TRmgTemplateZoneId treasureLikeZone; + TRmgTemplateZoneId customObjectsLikeZone; }; } @@ -273,15 +297,28 @@ private: CPlayerCountRange players; CPlayerCountRange humanPlayers; Zones zones; - std::vector connectedZoneIds; + std::vector connections; std::set allowedWaterContent; std::unique_ptr mapSettings; std::set inheritTerrainType(std::shared_ptr zone, uint32_t iteration = 0); std::map inheritMineTypes(std::shared_ptr zone, uint32_t iteration = 0); std::vector inheritTreasureInfo(std::shared_ptr zone, uint32_t iteration = 0); + + // TODO: Copy custom object settings + // TODO: Copy town type after source town is actually randomized + void serializeSize(JsonSerializeFormat & handler, int3 & value, const std::string & fieldName); void serializePlayers(JsonSerializeFormat & handler, CPlayerCountRange & value, const std::string & fieldName); + + template + T inheritZoneProperty(std::shared_ptr zone, + T (rmg::ZoneOptions::*getter)() const, + void (rmg::ZoneOptions::*setter)(const T&), + TRmgTemplateZoneId (rmg::ZoneOptions::*inheritFrom)() const, + const std::string& propertyString, + uint32_t iteration = 0); + }; -VCMI_LIB_NAMESPACE_END +VCMI_LIB_NAMESPACE_END \ No newline at end of file diff --git a/lib/rmg/ObjectConfig.cpp b/lib/rmg/ObjectConfig.cpp new file mode 100644 index 000000000..9ea127edf --- /dev/null +++ b/lib/rmg/ObjectConfig.cpp @@ -0,0 +1,189 @@ +/* + * ObjectConfig.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 +#include +#include "ObjectInfo.h" +#include "ObjectConfig.h" + +#include "../VCMI_Lib.h" +#include "../mapObjectConstructors/CObjectClassesHandler.h" +#include "../mapObjectConstructors/AObjectTypeHandler.h" +#include "../serializer/JsonSerializeFormat.h" + +VCMI_LIB_NAMESPACE_BEGIN + +void ObjectConfig::addBannedObject(const CompoundMapObjectID & objid) +{ + // FIXME: We do not need to store the object info, just the id + + bannedObjects.push_back(objid); + + logGlobal->info("Banned object of type %d.%d", objid.primaryID, objid.secondaryID); +} + +void ObjectConfig::addCustomObject(const ObjectInfo & object, const CompoundMapObjectID & objid) +{ + customObjects.push_back(object); + auto & lastObject = customObjects.back(); + lastObject.setAllTemplates(objid.primaryID, objid.secondaryID); + + assert(lastObject.templates.size() > 0); + logGlobal->info("Added custom object of type %d.%d", objid.primaryID, objid.secondaryID); +} + +void ObjectConfig::serializeJson(JsonSerializeFormat & handler) +{ + // TODO: We need serializer utility for list of enum values + + static const boost::bimap OBJECT_CATEGORY_STRINGS = boost::assign::list_of::relation> + (EObjectCategory::OTHER, "other") + (EObjectCategory::ALL, "all") + (EObjectCategory::NONE, "none") + (EObjectCategory::CREATURE_BANK, "creatureBank") + (EObjectCategory::BONUS, "bonus") + (EObjectCategory::DWELLING, "dwelling") + (EObjectCategory::RESOURCE, "resource") + (EObjectCategory::RESOURCE_GENERATOR, "resourceGenerator") + (EObjectCategory::SPELL_SCROLL, "spellScroll") + (EObjectCategory::RANDOM_ARTIFACT, "randomArtifact") + (EObjectCategory::PANDORAS_BOX, "pandorasBox") + (EObjectCategory::QUEST_ARTIFACT, "questArtifact") + (EObjectCategory::SEER_HUT, "seerHut"); + + + // TODO: Separate into individual methods to enforce RAII destruction? + { + auto categories = handler.enterArray("bannedCategories"); + if (handler.saving) + { + for (const auto& category : bannedObjectCategories) + { + auto str = OBJECT_CATEGORY_STRINGS.left.at(category); + categories.serializeString(categories.size(), str); + } + } + else + { + std::vector categoryNames; + categories.serializeArray(categoryNames); + + for (const auto & categoryName : categoryNames) + { + auto it = OBJECT_CATEGORY_STRINGS.right.find(categoryName); + if (it != OBJECT_CATEGORY_STRINGS.right.end()) + { + bannedObjectCategories.push_back(it->second); + } + } + } + } + + // FIXME: Doesn't seem to use this field at all + + { + auto bannedObjectData = handler.enterArray("bannedObjects"); + if (handler.saving) + { + + // FIXME: Do we even need to serialize / store banned objects? + /* + for (const auto & object : bannedObjects) + { + // TODO: Translate id back to string? + + + JsonNode node; + node.String() = VLC->objtypeh->getHandlerFor(object.primaryID, object.secondaryID); + // TODO: Check if AI-generated code is right + + + } + // handler.serializeRaw("bannedObjects", node, std::nullopt); + + */ + } + else + { + std::vector objectNames; + bannedObjectData.serializeArray(objectNames); + + for (const auto & objectName : objectNames) + { + VLC->objtypeh->resolveObjectCompoundId(objectName, + [this](CompoundMapObjectID objid) + { + addBannedObject(objid); + } + ); + + } + } + } + + auto commonObjectData = handler.getCurrent()["commonObjects"].Vector(); + if (handler.saving) + { + + //TODO? + } + else + { + for (const auto & objectConfig : commonObjectData) + { + auto objectName = objectConfig["id"].String(); + auto rmg = objectConfig["rmg"].Struct(); + + // TODO: Use common code with default rmg config + auto objectValue = rmg["value"].Integer(); + auto objectProbability = rmg["rarity"].Integer(); + + auto objectMaxPerZone = rmg["zoneLimit"].Integer(); + if (objectMaxPerZone == 0) + { + objectMaxPerZone = std::numeric_limits::max(); + } + + VLC->objtypeh->resolveObjectCompoundId(objectName, + + [this, objectValue, objectProbability, objectMaxPerZone](CompoundMapObjectID objid) + { + ObjectInfo object(objid.primaryID, objid.secondaryID); + + // TODO: Configure basic generateObject function + + object.value = objectValue; + object.probability = objectProbability; + object.maxPerZone = objectMaxPerZone; + addCustomObject(object, objid); + } + ); + + } + } +} + +const std::vector & ObjectConfig::getConfiguredObjects() const +{ + return customObjects; +} + +const std::vector & ObjectConfig::getBannedObjects() const +{ + return bannedObjects; +} + +const std::vector & ObjectConfig::getBannedObjectCategories() const +{ + return bannedObjectCategories; +} + +VCMI_LIB_NAMESPACE_END \ No newline at end of file diff --git a/lib/rmg/ObjectConfig.h b/lib/rmg/ObjectConfig.h new file mode 100644 index 000000000..998052944 --- /dev/null +++ b/lib/rmg/ObjectConfig.h @@ -0,0 +1,57 @@ +/* + * ObjectInfo.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 "../mapObjects/CompoundMapObjectID.h" + +VCMI_LIB_NAMESPACE_BEGIN + +class DLL_LINKAGE ObjectConfig +{ +public: + + enum class EObjectCategory + { + OTHER = -2, + ALL = -1, + NONE = 0, + CREATURE_BANK = 1, + BONUS, + DWELLING, + RESOURCE, + RESOURCE_GENERATOR, + SPELL_SCROLL, + RANDOM_ARTIFACT, + PANDORAS_BOX, + QUEST_ARTIFACT, + SEER_HUT + }; + + void addBannedObject(const CompoundMapObjectID & objid); + void addCustomObject(const ObjectInfo & object, const CompoundMapObjectID & objid); + void clearBannedObjects(); + void clearCustomObjects(); + const std::vector & getBannedObjects() const; + const std::vector & getBannedObjectCategories() const; + const std::vector & getConfiguredObjects() const; + + void serializeJson(JsonSerializeFormat & handler); +private: + // TODO: Add convenience method for banning objects by name + std::vector bannedObjects; + std::vector bannedObjectCategories; + + // TODO: In what format should I store custom objects? + // Need to convert map serialization format to ObjectInfo + std::vector customObjects; +}; + +VCMI_LIB_NAMESPACE_END \ No newline at end of file diff --git a/lib/rmg/ObjectInfo.cpp b/lib/rmg/ObjectInfo.cpp new file mode 100644 index 000000000..254ff79b4 --- /dev/null +++ b/lib/rmg/ObjectInfo.cpp @@ -0,0 +1,85 @@ +/* + * ObjectInfo.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 "ObjectInfo.h" + +#include "../VCMI_Lib.h" +#include "../mapObjectConstructors/CObjectClassesHandler.h" +#include "../mapObjectConstructors/AObjectTypeHandler.h" +#include "../serializer/JsonSerializeFormat.h" + +VCMI_LIB_NAMESPACE_BEGIN + +ObjectInfo::ObjectInfo(si32 ID, si32 subID): + primaryID(ID), + secondaryID(subID), + destroyObject([](CGObjectInstance * obj){}), + maxPerZone(std::numeric_limits::max()) +{ +} + +ObjectInfo::ObjectInfo(CompoundMapObjectID id): + ObjectInfo(id.primaryID, id.secondaryID) +{ +} + +ObjectInfo::ObjectInfo(const ObjectInfo & other) +{ + templates = other.templates; + primaryID = other.primaryID; + secondaryID = other.secondaryID; + value = other.value; + probability = other.probability; + maxPerZone = other.maxPerZone; + generateObject = other.generateObject; + destroyObject = other.destroyObject; +} + +ObjectInfo & ObjectInfo::operator=(const ObjectInfo & other) +{ + if (this == &other) + return *this; + + templates = other.templates; + primaryID = other.primaryID; + secondaryID = other.secondaryID; + value = other.value; + probability = other.probability; + maxPerZone = other.maxPerZone; + generateObject = other.generateObject; + destroyObject = other.destroyObject; + return *this; +} + +void ObjectInfo::setAllTemplates(MapObjectID type, MapObjectSubID subtype) +{ + auto templHandler = VLC->objtypeh->getHandlerFor(type, subtype); + if(!templHandler) + return; + + templates = templHandler->getTemplates(); +} + +void ObjectInfo::setTemplates(MapObjectID type, MapObjectSubID subtype, TerrainId terrainType) +{ + auto templHandler = VLC->objtypeh->getHandlerFor(type, subtype); + if(!templHandler) + return; + + templates = templHandler->getTemplates(terrainType); +} + +CompoundMapObjectID ObjectInfo::getCompoundID() const +{ + return CompoundMapObjectID(primaryID, secondaryID); +} + +VCMI_LIB_NAMESPACE_END \ No newline at end of file diff --git a/lib/rmg/ObjectInfo.h b/lib/rmg/ObjectInfo.h new file mode 100644 index 000000000..6e414e516 --- /dev/null +++ b/lib/rmg/ObjectInfo.h @@ -0,0 +1,45 @@ +/* + * ObjectInfo.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 "../mapObjects/ObjectTemplate.h" +#include "../mapObjects/CompoundMapObjectID.h" + +VCMI_LIB_NAMESPACE_BEGIN + +struct CompoundMapObjectID; +class CGObjectInstance; + +struct DLL_LINKAGE ObjectInfo +{ + ObjectInfo(si32 ID, si32 subID); + ObjectInfo(CompoundMapObjectID id); + ObjectInfo(const ObjectInfo & other); + ObjectInfo & operator=(const ObjectInfo & other); + + std::vector> templates; + si32 primaryID; + si32 secondaryID; + ui32 value = 0; + ui16 probability = 0; + ui32 maxPerZone = 1; + //ui32 maxPerMap; //unused + std::function generateObject; + std::function destroyObject; + + void setAllTemplates(MapObjectID type, MapObjectSubID subtype); + void setTemplates(MapObjectID type, MapObjectSubID subtype, TerrainId terrain); + + CompoundMapObjectID getCompoundID() const; + //bool matchesId(const CompoundMapObjectID & id) const; +}; + +VCMI_LIB_NAMESPACE_END \ No newline at end of file diff --git a/lib/rmg/RmgObject.cpp b/lib/rmg/RmgObject.cpp index 1004072cb..08c5d1096 100644 --- a/lib/rmg/RmgObject.cpp +++ b/lib/rmg/RmgObject.cpp @@ -87,7 +87,7 @@ const rmg::Area & Object::Instance::getAccessibleArea() const void Object::Instance::setPosition(const int3 & position) { dPosition = position; - dObject.pos = dPosition + dParent.getPosition(); + dObject.setAnchorPos(dPosition + dParent.getPosition()); dBlockedAreaCache.clear(); dAccessibleAreaCache.clear(); @@ -96,21 +96,21 @@ void Object::Instance::setPosition(const int3 & position) void Object::Instance::setPositionRaw(const int3 & position) { - if(!dObject.pos.valid()) + if(!dObject.anchorPos().valid()) { - dObject.pos = dPosition + dParent.getPosition(); + dObject.setAnchorPos(dPosition + dParent.getPosition()); dBlockedAreaCache.clear(); dAccessibleAreaCache.clear(); dParent.clearCachedArea(); } - auto shift = position + dParent.getPosition() - dObject.pos; + auto shift = position + dParent.getPosition() - dObject.anchorPos(); dAccessibleAreaCache.translate(shift); dBlockedAreaCache.translate(shift); dPosition = position; - dObject.pos = dPosition + dParent.getPosition(); + dObject.setAnchorPos(dPosition + dParent.getPosition()); } void Object::Instance::setAnyTemplate(vstd::RNG & rng) @@ -484,7 +484,7 @@ void Object::Instance::finalize(RmgMap & map, vstd::RNG & rng) //If no specific template was defined for this object, select any matching if (!dObject.appearance) { - const auto * terrainType = map.getTile(getPosition(true)).terType; + const auto * terrainType = map.getTile(getPosition(true)).getTerrain(); auto templates = dObject.getObjectHandler()->getTemplates(terrainType->getId()); if (templates.empty()) { @@ -497,12 +497,12 @@ void Object::Instance::finalize(RmgMap & map, vstd::RNG & rng) } if (dObject.isVisitable() && !map.isOnMap(dObject.visitablePos())) - throw rmgException(boost::str(boost::format("Visitable tile %s of object %d at %s is outside the map") % dObject.visitablePos().toString() % dObject.id % dObject.pos.toString())); + throw rmgException(boost::str(boost::format("Visitable tile %s of object %d at %s is outside the map") % dObject.visitablePos().toString() % dObject.id % dObject.anchorPos().toString())); for(const auto & tile : dObject.getBlockedPos()) { if(!map.isOnMap(tile)) - throw rmgException(boost::str(boost::format("Tile %s of object %d at %s is outside the map") % tile.toString() % dObject.id % dObject.pos.toString())); + throw rmgException(boost::str(boost::format("Tile %s of object %d at %s is outside the map") % tile.toString() % dObject.id % dObject.anchorPos().toString())); } for(const auto & tile : getBlockedArea().getTilesVector()) diff --git a/lib/rmg/Zone.cpp b/lib/rmg/Zone.cpp index 273dc226e..37db277db 100644 --- a/lib/rmg/Zone.cpp +++ b/lib/rmg/Zone.cpp @@ -138,7 +138,7 @@ void Zone::initFreeTiles() }); dAreaPossible.assign(possibleTiles); - if(dAreaFree.empty()) + if(dAreaFree.empty() && getType() != ETemplateZoneType::SEALED) { // Fixme: This might fail fot water zone, which doesn't need to have a tile in its center of the mass dAreaPossible.erase(pos); @@ -348,6 +348,16 @@ void Zone::fractalize() tilesToIgnore.clear(); } } + else if (type == ETemplateZoneType::SEALED) + { + //Completely block all the tiles in the zone + auto tiles = areaPossible()->getTiles(); + for(const auto & t : tiles) + map.setOccupied(t, ETileType::BLOCKED); + possibleTiles.clear(); + dAreaFree.clear(); + return; + } else { // Handle special case - place Monoliths at the edge of a zone diff --git a/lib/rmg/modificators/ConnectionsPlacer.cpp b/lib/rmg/modificators/ConnectionsPlacer.cpp index 79c4ff438..37f158855 100644 --- a/lib/rmg/modificators/ConnectionsPlacer.cpp +++ b/lib/rmg/modificators/ConnectionsPlacer.cpp @@ -143,7 +143,7 @@ void ConnectionsPlacer::forcePortalConnection(const rmg::ZoneConnection & connec void ConnectionsPlacer::selfSideDirectConnection(const rmg::ZoneConnection & connection) { bool success = false; - auto otherZoneId = (connection.getZoneA() == zone.getId() ? connection.getZoneB() : connection.getZoneA()); + auto otherZoneId = connection.getOtherZoneId(zone.getId()); auto & otherZone = map.getZones().at(otherZoneId); bool createRoad = shouldGenerateRoad(connection); @@ -327,10 +327,9 @@ void ConnectionsPlacer::selfSideDirectConnection(const rmg::ZoneConnection & con assert(otherZone->getModificator()); otherZone->getModificator()->addRoadNode(roadNode); - - assert(otherZone->getModificator()); - otherZone->getModificator()->otherSideConnection(connection); } + assert(otherZone->getModificator()); + otherZone->getModificator()->otherSideConnection(connection); success = true; } diff --git a/lib/rmg/modificators/ObjectDistributor.cpp b/lib/rmg/modificators/ObjectDistributor.cpp index ae6c42f65..76bae9e41 100644 --- a/lib/rmg/modificators/ObjectDistributor.cpp +++ b/lib/rmg/modificators/ObjectDistributor.cpp @@ -45,7 +45,6 @@ void ObjectDistributor::init() void ObjectDistributor::distributeLimitedObjects() { - ObjectInfo oi; auto zones = map.getZones(); for (auto primaryID : VLC->objtypeh->knownObjects()) @@ -81,6 +80,8 @@ void ObjectDistributor::distributeLimitedObjects() RandomGeneratorUtil::randomShuffle(matchingZones, zone.getRand()); for (auto& zone : matchingZones) { + ObjectInfo oi(primaryID, secondaryID); + oi.generateObject = [cb=map.mapInstance->cb, primaryID, secondaryID]() -> CGObjectInstance * { return VLC->objtypeh->getHandlerFor(primaryID, secondaryID)->create(cb, nullptr); diff --git a/lib/rmg/modificators/ObjectManager.cpp b/lib/rmg/modificators/ObjectManager.cpp index a15ef0937..c256a85c1 100644 --- a/lib/rmg/modificators/ObjectManager.cpp +++ b/lib/rmg/modificators/ObjectManager.cpp @@ -733,7 +733,7 @@ CGCreature * ObjectManager::chooseGuard(si32 strength, bool zoneGuard) continue; if(!cre->getAIValue()) //bug #2681 continue; - if(!vstd::contains(zone.getMonsterTypes(), cre->getFaction())) + if(!vstd::contains(zone.getMonsterTypes(), cre->getFactionID())) continue; if((static_cast(cre->getAIValue() * (cre->ammMin + cre->ammMax) / 2) < strength) && (strength < static_cast(cre->getAIValue()) * 100)) //at least one full monster. size between average size of given stack and 100 { diff --git a/lib/rmg/modificators/ObstaclePlacer.cpp b/lib/rmg/modificators/ObstaclePlacer.cpp index 456ddbeb0..5447a256e 100644 --- a/lib/rmg/modificators/ObstaclePlacer.cpp +++ b/lib/rmg/modificators/ObstaclePlacer.cpp @@ -153,7 +153,7 @@ void ObstaclePlacer::postProcess(const rmg::Object & object) riverManager = zone.getModificator(); if(riverManager) { - const auto objTypeName = object.instances().front()->object().typeName; + const auto objTypeName = object.instances().front()->object().getTypeName(); if(objTypeName == "mountain") riverManager->riverSource().unite(object.getArea()); else if(objTypeName == "lake") diff --git a/lib/rmg/modificators/QuestArtifactPlacer.cpp b/lib/rmg/modificators/QuestArtifactPlacer.cpp index 9117ab34c..31d9e5c05 100644 --- a/lib/rmg/modificators/QuestArtifactPlacer.cpp +++ b/lib/rmg/modificators/QuestArtifactPlacer.cpp @@ -112,7 +112,7 @@ void QuestArtifactPlacer::placeQuestArtifacts(vstd::RNG & rand) logGlobal->trace("Replacing %s at %s with the quest artifact %s", objectToReplace->getObjectName(), - objectToReplace->getPosition().toString(), + objectToReplace->anchorPos().toString(), VLC->artifacts()->getById(artifactToPlace)->getNameTranslated()); //Update appearance. Terrain is irrelevant. @@ -121,7 +121,7 @@ void QuestArtifactPlacer::placeQuestArtifacts(vstd::RNG & rand) auto templates = handler->getTemplates(); //artifactToReplace->appearance = templates.front(); newObj->appearance = templates.front(); - newObj->pos = objectToReplace->pos; + newObj->setAnchorPos(objectToReplace->anchorPos()); mapProxy->insertObject(newObj); mapProxy->removeObject(objectToReplace); break; diff --git a/lib/rmg/modificators/RoadPlacer.cpp b/lib/rmg/modificators/RoadPlacer.cpp index 61b934659..a2a404766 100644 --- a/lib/rmg/modificators/RoadPlacer.cpp +++ b/lib/rmg/modificators/RoadPlacer.cpp @@ -149,7 +149,7 @@ void RoadPlacer::drawRoads(bool secondary) //Do not draw roads on underground rock or water roads.erase_if([this](const int3& pos) -> bool { - const auto* terrain = map.getTile(pos).terType; + const auto* terrain = map.getTile(pos).getTerrain(); return !terrain->isPassable() || !terrain->isLand(); }); diff --git a/lib/rmg/modificators/RockFiller.cpp b/lib/rmg/modificators/RockFiller.cpp index 116885706..e7cf8848b 100644 --- a/lib/rmg/modificators/RockFiller.cpp +++ b/lib/rmg/modificators/RockFiller.cpp @@ -72,7 +72,7 @@ void RockFiller::init() char RockFiller::dump(const int3 & t) { - if(!map.getTile(t).terType->isPassable()) + if(!map.getTile(t).getTerrain()->isPassable()) { return zone.area()->contains(t) ? 'R' : 'E'; } diff --git a/lib/rmg/modificators/RockPlacer.cpp b/lib/rmg/modificators/RockPlacer.cpp index 3fefee83b..bf51be7e8 100644 --- a/lib/rmg/modificators/RockPlacer.cpp +++ b/lib/rmg/modificators/RockPlacer.cpp @@ -60,7 +60,7 @@ void RockPlacer::postProcess() //Finally mark rock tiles as occupied, spawn no obstacles there rockArea = zone.area()->getSubarea([this](const int3 & t) { - return !map.getTile(t).terType->isPassable(); + return !map.getTile(t).getTerrain()->isPassable(); }); // Do not place rock on roads @@ -96,7 +96,7 @@ void RockPlacer::init() char RockPlacer::dump(const int3 & t) { - if(!map.getTile(t).terType->isPassable()) + if(!map.getTile(t).getTerrain()->isPassable()) { return zone.area()->contains(t) ? 'R' : 'E'; } diff --git a/lib/rmg/modificators/TownPlacer.cpp b/lib/rmg/modificators/TownPlacer.cpp index 0a3e412c4..1ab68779d 100644 --- a/lib/rmg/modificators/TownPlacer.cpp +++ b/lib/rmg/modificators/TownPlacer.cpp @@ -90,7 +90,7 @@ void TownPlacer::placeTowns(ObjectManager & manager) totalTowns++; //register MAIN town of zone only - map.registerZone(town->getFaction()); + map.registerZone(town->getFactionID()); if(player.isValidPlayer()) //configure info for owning player { @@ -213,7 +213,7 @@ void TownPlacer::addNewTowns(int count, bool hasFort, const PlayerColor & player { //FIXME: discovered bug with small zones - getPos is close to map boarder and we have outOfMap exception //register MAIN town of zone - map.registerZone(town->getFaction()); + map.registerZone(town->getFactionID()); //first town in zone goes in the middle placeMainTown(manager, *town); } diff --git a/lib/rmg/modificators/TreasurePlacer.cpp b/lib/rmg/modificators/TreasurePlacer.cpp index 13de65b39..781458d43 100644 --- a/lib/rmg/modificators/TreasurePlacer.cpp +++ b/lib/rmg/modificators/TreasurePlacer.cpp @@ -10,6 +10,7 @@ #include "StdInc.h" #include "TreasurePlacer.h" +#include "../CRmgTemplate.h" #include "../CMapGenerator.h" #include "../Functions.h" #include "ObjectManager.h" @@ -24,6 +25,7 @@ #include "../../mapObjectConstructors/AObjectTypeHandler.h" #include "../../mapObjectConstructors/CObjectClassesHandler.h" #include "../../mapObjectConstructors/DwellingInstanceConstructor.h" +#include "../../rewardable/Info.h" #include "../../mapObjects/CGHeroInstance.h" #include "../../mapObjects/CGPandoraBox.h" #include "../../mapObjects/CQuest.h" @@ -37,15 +39,29 @@ VCMI_LIB_NAMESPACE_BEGIN -ObjectInfo::ObjectInfo(): - destroyObject([](CGObjectInstance * obj){}) -{ - -} - void TreasurePlacer::process() { + if (zone.getMaxTreasureValue() == 0) + { + //No treasures at all + return; + } + + tierValues = generator.getConfig().pandoraCreatureValues; + // Add all native creatures + for(auto const & cre : VLC->creh->objects) + { + if(!cre->special && cre->getFactionID() == zone.getTownType()) + { + creatures.push_back(cre.get()); + } + } + + // Get default objects addAllPossibleObjects(); + // Override with custom objects + objects.patchWithZoneConfig(zone, this); + auto * m = zone.getModificator(); if(m) createTreasures(*m); @@ -62,14 +78,37 @@ void TreasurePlacer::init() void TreasurePlacer::addObjectToRandomPool(const ObjectInfo& oi) { + if (oi.templates.empty()) + { + logGlobal->error("Attempt to add ObjectInfo with no templates! Value: %d", oi.value); + return; + } + if (!oi.generateObject) + { + logGlobal->error("Attempt to add ObjectInfo with no generateObject function! Value: %d", oi.value); + return; + } + if (!oi.maxPerZone) + { + logGlobal->warn("Attempt to add ObjectInfo with 0 maxPerZone! Value: %d", oi.value); + return; + } RecursiveLock lock(externalAccessMutex); - possibleObjects.push_back(oi); + objects.addObject(oi); } void TreasurePlacer::addAllPossibleObjects() { - ObjectInfo oi; - + addCommonObjects(); + addDwellings(); + addPandoraBoxes(); + addSeerHuts(); + addPrisons(); + addScrolls(); +} + +void TreasurePlacer::addCommonObjects() +{ for(auto primaryID : VLC->objtypeh->knownObjects()) { for(auto secondaryID : VLC->objtypeh->knownSubObjects(primaryID)) @@ -83,21 +122,31 @@ void TreasurePlacer::addAllPossibleObjects() //Skip objects with per-map limit here continue; } + ObjectInfo oi(primaryID, secondaryID); + setBasicProperties(oi, CompoundMapObjectID(primaryID, secondaryID)); - oi.generateObject = [this, primaryID, secondaryID]() -> CGObjectInstance * - { - return VLC->objtypeh->getHandlerFor(primaryID, secondaryID)->create(map.mapInstance->cb, nullptr); - }; oi.value = rmgInfo.value; oi.probability = rmgInfo.rarity; - oi.setTemplates(primaryID, secondaryID, zone.getTerrainType()); oi.maxPerZone = rmgInfo.zoneLimit; + if(!oi.templates.empty()) addObjectToRandomPool(oi); } } } +} +void TreasurePlacer::setBasicProperties(ObjectInfo & oi, CompoundMapObjectID objid) const +{ + oi.generateObject = [this, objid]() -> CGObjectInstance * + { + return VLC->objtypeh->getHandlerFor(objid)->create(map.mapInstance->cb, nullptr); + }; + oi.setTemplates(objid.primaryID, objid.secondaryID, zone.getTerrainType()); +} + +void TreasurePlacer::addPrisons() +{ //Generate Prison on water only if it has a template auto prisonTemplates = VLC->objtypeh->getHandlerFor(Obj::PRISON, 0)->getTemplates(zone.getTerrainType()); if (!prisonTemplates.empty()) @@ -119,7 +168,7 @@ void TreasurePlacer::addAllPossibleObjects() size_t prisonsLeft = getMaxPrisons(); for (int i = prisonsLevels - 1; i >= 0; i--) { - ObjectInfo oi; // Create new instance which will hold destructor operation + ObjectInfo oi(Obj::PRISON, 0); // Create new instance which will hold destructor operation oi.value = generator.getConfig().prisonValues[i]; if (oi.value > zone.getMaxTreasureValue()) @@ -143,7 +192,7 @@ void TreasurePlacer::addAllPossibleObjects() { // Hero can be used again auto* hero = dynamic_cast(obj); - prisonHeroPlacer->restoreDrawnHero(hero->getHeroType()); + prisonHeroPlacer->restoreDrawnHero(hero->getHeroTypeID()); }; oi.setTemplates(Obj::PRISON, 0, zone.getTerrainType()); @@ -157,22 +206,13 @@ void TreasurePlacer::addAllPossibleObjects() addObjectToRandomPool(oi); } } +} +void TreasurePlacer::addDwellings() +{ if(zone.getType() == ETemplateZoneType::WATER) return; - - //all following objects are unlimited - oi.maxPerZone = std::numeric_limits::max(); - std::vector creatures; //native creatures for this zone - for(auto const & cre : VLC->creh->objects) - { - if(!cre->special && cre->getFaction() == zone.getTownType()) - { - creatures.push_back(cre.get()); - } - } - //dwellings auto dwellingTypes = {Obj::CREATURE_GENERATOR1, Obj::CREATURE_GENERATOR4}; @@ -196,9 +236,12 @@ void TreasurePlacer::addAllPossibleObjects() continue; const auto * cre = creatures.front(); - if(cre->getFaction() == zone.getTownType()) + if(cre->getFactionID() == zone.getTownType()) { - auto nativeZonesCount = static_cast(map.getZoneCount(cre->getFaction())); + auto nativeZonesCount = static_cast(map.getZoneCount(cre->getFactionID())); + ObjectInfo oi(dwellingType, secondaryID); + setBasicProperties(oi, CompoundMapObjectID(dwellingType, secondaryID)); + oi.value = static_cast(cre->getAIValue() * cre->getGrowth() * (1 + (nativeZonesCount / map.getTotalZoneCount()) + (nativeZonesCount / 2))); oi.probability = 40; @@ -208,13 +251,20 @@ void TreasurePlacer::addAllPossibleObjects() obj->tempOwner = PlayerColor::NEUTRAL; return obj; }; - oi.setTemplates(dwellingType, secondaryID, zone.getTerrainType()); if(!oi.templates.empty()) addObjectToRandomPool(oi); } } } - +} + +void TreasurePlacer::addScrolls() +{ + if(zone.getType() == ETemplateZoneType::WATER) + return; + + ObjectInfo oi(Obj::SPELL_SCROLL, 0); + for(int i = 0; i < generator.getConfig().scrollValues.size(); i++) { oi.generateObject = [i, this]() -> CGObjectInstance * @@ -239,7 +289,22 @@ void TreasurePlacer::addAllPossibleObjects() addObjectToRandomPool(oi); } - //pandora box with gold +} + +void TreasurePlacer::addPandoraBoxes() +{ + if(zone.getType() == ETemplateZoneType::WATER) + return; + + addPandoraBoxesWithGold(); + addPandoraBoxesWithExperience(); + addPandoraBoxesWithCreatures(); + addPandoraBoxesWithSpells(); +} + +void TreasurePlacer::addPandoraBoxesWithGold() +{ + ObjectInfo oi(Obj::PANDORAS_BOX, 0); for(int i = 1; i < 5; i++) { oi.generateObject = [this, i]() -> CGObjectInstance * @@ -260,8 +325,11 @@ void TreasurePlacer::addAllPossibleObjects() if(!oi.templates.empty()) addObjectToRandomPool(oi); } - - //pandora box with experience +} + +void TreasurePlacer::addPandoraBoxesWithExperience() +{ + ObjectInfo oi(Obj::PANDORAS_BOX, 0); for(int i = 1; i < 5; i++) { oi.generateObject = [this, i]() -> CGObjectInstance * @@ -282,49 +350,17 @@ void TreasurePlacer::addAllPossibleObjects() if(!oi.templates.empty()) addObjectToRandomPool(oi); } - - //pandora box with creatures - const std::vector & tierValues = generator.getConfig().pandoraCreatureValues; - - auto creatureToCount = [tierValues](const CCreature * creature) -> int - { - if(!creature->getAIValue() || tierValues.empty()) //bug #2681 - return 0; //this box won't be generated - - //Follow the rules from https://heroes.thelazy.net/index.php/Pandora%27s_Box - - int actualTier = creature->getLevel() > tierValues.size() ? - tierValues.size() - 1 : - creature->getLevel() - 1; - float creaturesAmount = std::floor((static_cast(tierValues[actualTier])) / creature->getAIValue()); - if (creaturesAmount < 1) - { - return 0; - } - else if(creaturesAmount <= 5) - { - //No change - } - else if(creaturesAmount <= 12) - { - creaturesAmount = std::ceil(creaturesAmount / 2) * 2; - } - else if(creaturesAmount <= 50) - { - creaturesAmount = std::round(creaturesAmount / 5) * 5; - } - else - { - creaturesAmount = std::round(creaturesAmount / 10) * 10; - } - return static_cast(creaturesAmount); - }; +} +void TreasurePlacer::addPandoraBoxesWithCreatures() +{ for(auto * creature : creatures) { int creaturesAmount = creatureToCount(creature); if(!creaturesAmount) continue; + + ObjectInfo oi(Obj::PANDORAS_BOX, 0); oi.generateObject = [this, creature, creaturesAmount]() -> CGObjectInstance * { @@ -339,12 +375,16 @@ void TreasurePlacer::addAllPossibleObjects() return obj; }; oi.setTemplates(Obj::PANDORAS_BOX, 0, zone.getTerrainType()); - oi.value = static_cast((2 * (creature->getAIValue()) * creaturesAmount * (1 + static_cast(map.getZoneCount(creature->getFaction())) / map.getTotalZoneCount())) / 3); + oi.value = static_cast(creature->getAIValue() * creaturesAmount * (1 + static_cast(map.getZoneCount(creature->getFactionID())) / map.getTotalZoneCount())); oi.probability = 3; if(!oi.templates.empty()) addObjectToRandomPool(oi); } - +} + +void TreasurePlacer::addPandoraBoxesWithSpells() +{ + ObjectInfo oi(Obj::PANDORAS_BOX, 0); //Pandora with 12 spells of certain level for(int i = 1; i <= GameConstants::SPELL_LEVELS; i++) { @@ -441,9 +481,14 @@ void TreasurePlacer::addAllPossibleObjects() oi.probability = 2; if(!oi.templates.empty()) addObjectToRandomPool(oi); - +} + +void TreasurePlacer::addSeerHuts() +{ //Seer huts with creatures or generic rewards + ObjectInfo oi(Obj::SEER_HUT, 0); + if(zone.getConnectedZoneIds().size()) //Unlikely, but... { auto * qap = zone.getModificator(); @@ -510,7 +555,7 @@ void TreasurePlacer::addAllPossibleObjects() oi.destroyObject = destroyObject; oi.probability = 3; oi.setTemplates(Obj::SEER_HUT, randomAppearance, zone.getTerrainType()); - oi.value = static_cast(((2 * (creature->getAIValue()) * creaturesAmount * (1 + static_cast(map.getZoneCount(creature->getFaction())) / map.getTotalZoneCount())) - 4000) / 3); + oi.value = static_cast(((2 * (creature->getAIValue()) * creaturesAmount * (1 + static_cast(map.getZoneCount(creature->getFactionID())) / map.getTotalZoneCount())) - 4000) / 3); if (oi.value > zone.getMaxTreasureValue()) { continue; @@ -588,12 +633,6 @@ void TreasurePlacer::addAllPossibleObjects() } } -size_t TreasurePlacer::getPossibleObjectsSize() const -{ - RecursiveLock lock(externalAccessMutex); - return possibleObjects.size(); -} - void TreasurePlacer::setMaxPrisons(size_t count) { RecursiveLock lock(externalAccessMutex); @@ -606,6 +645,40 @@ size_t TreasurePlacer::getMaxPrisons() const return maxPrisons; } +int TreasurePlacer::creatureToCount(const CCreature * creature) const +{ + if(!creature->getAIValue() || tierValues.empty()) //bug #2681 + return 0; //this box won't be generated + + //Follow the rules from https://heroes.thelazy.net/index.php/Pandora%27s_Box + + int actualTier = creature->getLevel() > tierValues.size() ? + tierValues.size() - 1 : + creature->getLevel() - 1; + float creaturesAmount = std::floor((static_cast(tierValues[actualTier])) / creature->getAIValue()); + if (creaturesAmount < 1) + { + return 0; + } + else if(creaturesAmount <= 5) + { + //No change + } + else if(creaturesAmount <= 12) + { + creaturesAmount = std::ceil(creaturesAmount / 2) * 2; + } + else if(creaturesAmount <= 50) + { + creaturesAmount = std::round(creaturesAmount / 5) * 5; + } + else + { + creaturesAmount = std::round(creaturesAmount / 10) * 10; + } + return static_cast(creaturesAmount); +}; + bool TreasurePlacer::isGuardNeededForTreasure(int value) {// no guard in a zone with "monsters: none" and for small treasures; water zones cen get monster strength ZONE_NONE elsewhere if needed return zone.monsterStrength != EMonsterStrength::ZONE_NONE && value > minGuardedValue; @@ -623,6 +696,7 @@ std::vector TreasurePlacer::prepareTreasurePile(const CTreasureInfo bool hasLargeObject = false; while(currentValue <= static_cast(desiredValue) - 100) //no objects with value below 100 are available { + // FIXME: Pointer might be invalidated after this auto * oi = getRandomObject(desiredValue, currentValue, !hasLargeObject); if(!oi) //fail break; @@ -674,12 +748,21 @@ rmg::Object TreasurePlacer::constructTreasurePile(const std::vector accessibleArea.add(int3()); } - auto * object = oi->generateObject(); - if(oi->templates.empty()) + CGObjectInstance * object = nullptr; + if (oi->generateObject) { - logGlobal->warn("Deleting randomized object with no templates: %s", object->getObjectName()); - oi->destroyObject(object); - delete object; + object = oi->generateObject(); + if(oi->templates.empty()) + { + logGlobal->warn("Deleting randomized object with no templates: %s", object->getObjectName()); + oi->destroyObject(object); + delete object; + continue; + } + } + else + { + logGlobal->error("ObjectInfo has no generateObject function! Templates: %d", oi->templates.size()); continue; } @@ -785,7 +868,7 @@ ObjectInfo * TreasurePlacer::getRandomObject(ui32 desiredValue, ui32 currentValu ui32 maxVal = desiredValue - currentValue; ui32 minValue = static_cast(0.25f * (desiredValue - currentValue)); - for(ObjectInfo & oi : possibleObjects) //copy constructor turned out to be costly + for(ObjectInfo & oi : objects.getPossibleObjects()) //copy constructor turned out to be costly { if(oi.value > maxVal) break; //this assumes values are sorted in ascending order @@ -859,24 +942,19 @@ void TreasurePlacer::createTreasures(ObjectManager& manager) boost::sort(treasureInfo, valueComparator); //sort treasures by ascending value so we can stop checking treasures with too high value - boost::sort(possibleObjects, [](const ObjectInfo& oi1, const ObjectInfo& oi2) -> bool - { - return oi1.value < oi2.value; - }); + objects.sortPossibleObjects(); const size_t size = zone.area()->getTilesVector().size(); int totalDensity = 0; + // FIXME: No need to use iterator here for (auto t = treasureInfo.begin(); t != treasureInfo.end(); t++) { std::vector treasures; //discard objects with too high value to be ever placed - vstd::erase_if(possibleObjects, [t](const ObjectInfo& oi) -> bool - { - return oi.value > t->max; - }); + objects.discardObjectsAboveValue(t->max); totalDensity += t->density; @@ -895,7 +973,11 @@ void TreasurePlacer::createTreasures(ObjectManager& manager) continue; } - int value = std::accumulate(treasurePileInfos.begin(), treasurePileInfos.end(), 0, [](int v, const ObjectInfo* oi) {return v + oi->value; }); + int value = std::accumulate(treasurePileInfos.begin(), treasurePileInfos.end(), 0, + [](int v, const ObjectInfo* oi) + { + return v + oi->value; + }); const ui32 maxPileGenerationAttempts = 2; for (ui32 attempt = 0; attempt < maxPileGenerationAttempts; attempt++) @@ -1016,13 +1098,222 @@ char TreasurePlacer::dump(const int3 & t) return Modificator::dump(t); } -void ObjectInfo::setTemplates(MapObjectID type, MapObjectSubID subtype, TerrainId terrainType) +void TreasurePlacer::ObjectPool::addObject(const ObjectInfo & info) { - auto templHandler = VLC->objtypeh->getHandlerFor(type, subtype); - if(!templHandler) - return; - - templates = templHandler->getTemplates(terrainType); + possibleObjects.push_back(info); +} + +void TreasurePlacer::ObjectPool::updateObject(MapObjectID id, MapObjectSubID subid, ObjectInfo info) +{ + /* + Handle separately: + - Dwellings + - Prisons + - Seer huts (quests) + - Pandora Boxes + */ + // FIXME: This will drop all templates + customObjects.insert(std::make_pair(CompoundMapObjectID(id, subid), info)); +} + +void TreasurePlacer::ObjectPool::patchWithZoneConfig(const Zone & zone, TreasurePlacer * tp) +{ + // FIXME: Wycina wszystkie obiekty poza pandorami i dwellami :? + + // Copy standard objects if they are not already modified + /* + for (const auto & object : possibleObjects) + { + for (const auto & templ : object.templates) + { + // FIXME: Objects with same temmplates (Pandora boxes) are not added + CompoundMapObjectID key(templ->id, templ->subid); + if (!vstd::contains(customObjects, key)) + { + customObjects[key] = object; + } + } + } + */ + auto bannedObjectCategories = zone.getBannedObjectCategories(); + auto categoriesSet = std::unordered_set(bannedObjectCategories.begin(), bannedObjectCategories.end()); + + if (categoriesSet.count(ObjectConfig::EObjectCategory::ALL)) + { + possibleObjects.clear(); + } + else + { + vstd::erase_if(possibleObjects, [this, &categoriesSet](const ObjectInfo & oi) -> bool + + { + auto category = getObjectCategory(oi.getCompoundID()); + if (categoriesSet.count(category)) + { + logGlobal->info("Removing object %s from possible objects", oi.templates.front()->stringID); + return true; + } + return false; + }); + + auto bannedObjects = zone.getBannedObjects(); + auto bannedObjectsSet = std::set(bannedObjects.begin(), bannedObjects.end()); + vstd::erase_if(possibleObjects, [&bannedObjectsSet](const ObjectInfo & object) + { + for (const auto & templ : object.templates) + { + CompoundMapObjectID key = object.getCompoundID(); + if (bannedObjectsSet.count(key)) + { + // FIXME: Stopped working, nothing is banned + logGlobal->info("Banning object %s from possible objects", templ->stringID); + return true; + } + } + return false; + }); + } + + auto configuredObjects = zone.getConfiguredObjects(); + + // FIXME: Access TreasurePlacer from ObjectPool + for (auto & object : configuredObjects) + { + tp->setBasicProperties(object, object.getCompoundID()); + addObject(object); + logGlobal->info("Added custom object of type %d.%d", object.primaryID, object.secondaryID); + } + // TODO: Overwrite or add to possibleObjects + + // FIXME: Protect with mutex as well? + /* + for (const auto & customObject : customObjects) + { + addObject(customObject.second); + } + */ + // TODO: Consider adding custom Pandora boxes with arbitrary content +} + +std::vector & TreasurePlacer::ObjectPool::getPossibleObjects() +{ + return possibleObjects; +} + +void TreasurePlacer::ObjectPool::sortPossibleObjects() +{ + boost::sort(possibleObjects, [](const ObjectInfo& oi1, const ObjectInfo& oi2) -> bool + { + return oi1.value < oi2.value; + }); +} + +void TreasurePlacer::ObjectPool::discardObjectsAboveValue(ui32 value) +{ + vstd::erase_if(possibleObjects, [value](const ObjectInfo& oi) -> bool + { + return oi.value > value; + }); +} + +ObjectConfig::EObjectCategory TreasurePlacer::ObjectPool::getObjectCategory(CompoundMapObjectID id) +{ + auto name = VLC->objtypeh->getObjectHandlerName(id.primaryID); + + if (name == "configurable") + { + auto handler = VLC->objtypeh->getHandlerFor(id.primaryID, id.secondaryID); + if (!handler) + { + return ObjectConfig::EObjectCategory::NONE; + } + + auto temp = handler->getTemplates().front(); + auto info = handler->getObjectInfo(temp); + + if (info->hasGuards()) + { + return ObjectConfig::EObjectCategory::CREATURE_BANK; + } + else if (info->givesResources()) + { + return ObjectConfig::EObjectCategory::RESOURCE; + } + else if (info->givesArtifacts()) + { + return ObjectConfig::EObjectCategory::RANDOM_ARTIFACT; + } + else if (info->givesBonuses()) + { + return ObjectConfig::EObjectCategory::BONUS; + } + + return ObjectConfig::EObjectCategory::OTHER; + } + else if (name == "dwelling" || name == "randomDwelling") + { + // TODO: Special handling for different tiers + return ObjectConfig::EObjectCategory::DWELLING; + } + else if (name == "bank") + return ObjectConfig::EObjectCategory::CREATURE_BANK; + else if (name == "market") + return ObjectConfig::EObjectCategory::OTHER; + else if (name == "hillFort") + return ObjectConfig::EObjectCategory::OTHER; + else if (name == "resource" || name == "randomResource") + return ObjectConfig::EObjectCategory::RESOURCE; + else if (name == "randomArtifact") //"artifact" + return ObjectConfig::EObjectCategory::RANDOM_ARTIFACT; + else if (name == "artifact") + { + if (id.primaryID == Obj::SPELL_SCROLL ) // randomArtifactTreasure + { + return ObjectConfig::EObjectCategory::SPELL_SCROLL; + } + else + { + return ObjectConfig::EObjectCategory::QUEST_ARTIFACT; + } + } + else if (name == "denOfThieves") + return ObjectConfig::EObjectCategory::OTHER; + else if (name == "lighthouse") + { + return ObjectConfig::EObjectCategory::BONUS; + } + else if (name == "magi") + { + // TODO: By default, both eye and hut are banned in every zone + return ObjectConfig::EObjectCategory::OTHER; + } + else if (name == "mine") + return ObjectConfig::EObjectCategory::RESOURCE_GENERATOR; + else if (name == "pandora") + return ObjectConfig::EObjectCategory::PANDORAS_BOX; + else if (name == "prison") + { + // TODO: Prisons should be configurable + return ObjectConfig::EObjectCategory::OTHER; + } + else if (name == "questArtifact") + { + // TODO: There are no dedicated quest artifacts, needs extra logic + return ObjectConfig::EObjectCategory::QUEST_ARTIFACT; + } + else if (name == "seerHut") + { + return ObjectConfig::EObjectCategory::SEER_HUT; + } + else if (name == "siren") + return ObjectConfig::EObjectCategory::BONUS; + else if (name == "obelisk") + return ObjectConfig::EObjectCategory::OTHER; + + // TODO: ObjectConfig::EObjectCategory::SPELL_SCROLL + + // Not interesting for us + return ObjectConfig::EObjectCategory::NONE; } VCMI_LIB_NAMESPACE_END diff --git a/lib/rmg/modificators/TreasurePlacer.h b/lib/rmg/modificators/TreasurePlacer.h index 450c812b7..f82873cd1 100644 --- a/lib/rmg/modificators/TreasurePlacer.h +++ b/lib/rmg/modificators/TreasurePlacer.h @@ -9,6 +9,8 @@ */ #pragma once + +#include "../ObjectInfo.h" #include "../Zone.h" #include "../../mapObjects/ObjectTemplate.h" @@ -18,21 +20,7 @@ class CGObjectInstance; class ObjectManager; class RmgMap; class CMapGenerator; - -struct ObjectInfo -{ - ObjectInfo(); - - std::vector> templates; - ui32 value = 0; - ui16 probability = 0; - ui32 maxPerZone = 1; - //ui32 maxPerMap; //unused - std::function generateObject; - std::function destroyObject; - - void setTemplates(MapObjectID type, MapObjectSubID subtype, TerrainId terrain); -}; +class ObjectConfig; class TreasurePlacer: public Modificator { @@ -45,11 +33,27 @@ public: void createTreasures(ObjectManager & manager); void addObjectToRandomPool(const ObjectInfo& oi); - void addAllPossibleObjects(); //add objects, including zone-specific, to possibleObjects + void setBasicProperties(ObjectInfo & oi, CompoundMapObjectID objid) const; + + // TODO: Can be defaulted to addAllPossibleObjects, but then each object will need to be configured + void addCommonObjects(); + void addDwellings(); + void addPandoraBoxes(); + void addPandoraBoxesWithGold(); + void addPandoraBoxesWithExperience(); + void addPandoraBoxesWithCreatures(); + void addPandoraBoxesWithSpells(); + void addSeerHuts(); + void addPrisons(); + void addScrolls(); + void addAllPossibleObjects(); //add objects, including zone-specific, to possibleObjects + // TODO: Read custom object config from zone file + + /// Get all objects for this terrain - size_t getPossibleObjectsSize() const; void setMaxPrisons(size_t count); size_t getMaxPrisons() const; + int creatureToCount(const CCreature * creature) const; protected: bool isGuardNeededForTreasure(int value); @@ -59,7 +63,26 @@ protected: rmg::Object constructTreasurePile(const std::vector & treasureInfos, bool densePlacement = false); protected: - std::vector possibleObjects; + class ObjectPool + { + public: + void addObject(const ObjectInfo & info); + void updateObject(MapObjectID id, MapObjectSubID subid, ObjectInfo info); + std::vector & getPossibleObjects(); + void patchWithZoneConfig(const Zone & zone, TreasurePlacer * tp); + void sortPossibleObjects(); + void discardObjectsAboveValue(ui32 value); + + ObjectConfig::EObjectCategory getObjectCategory(CompoundMapObjectID id); + + private: + + std::vector possibleObjects; + std::map customObjects; + + } objects; + // TODO: Need to nagivate and update these + int minGuardedValue = 0; rmg::Area treasureArea; @@ -67,6 +90,9 @@ protected: rmg::Area guards; size_t maxPrisons; + + std::vector creatures; //native creatures for this zone + std::vector tierValues; }; VCMI_LIB_NAMESPACE_END diff --git a/lib/rmg/modificators/WaterProxy.cpp b/lib/rmg/modificators/WaterProxy.cpp index f50c1c555..544c6f197 100644 --- a/lib/rmg/modificators/WaterProxy.cpp +++ b/lib/rmg/modificators/WaterProxy.cpp @@ -51,7 +51,7 @@ void WaterProxy::process() for([[maybe_unused]] const auto & t : area->getTilesVector()) { assert(map.isOnMap(t)); - assert(map.getTile(t).terType->getId() == zone.getTerrainType()); + assert(map.getTile(t).getTerrainID() == zone.getTerrainType()); } // FIXME: Possible deadlock for 2 zones @@ -66,7 +66,7 @@ void WaterProxy::process() auto secondAreaPossible = z.second->areaPossible(); for(const auto & t : secondArea->getTilesVector()) { - if(map.getTile(t).terType->getId() == zone.getTerrainType()) + if(map.getTile(t).getTerrainID() == zone.getTerrainType()) { secondArea->erase(t); secondAreaPossible->erase(t); diff --git a/lib/serializer/BinaryDeserializer.h b/lib/serializer/BinaryDeserializer.h index edcbb634f..9db041b2e 100644 --- a/lib/serializer/BinaryDeserializer.h +++ b/lib/serializer/BinaryDeserializer.h @@ -131,12 +131,12 @@ public: if ((byteValue & 0x80) != 0) { - valueUnsigned |= (byteValue & 0x7f) << offset; + valueUnsigned |= static_cast(byteValue & 0x7f) << offset; offset += 7; } else { - valueUnsigned |= (byteValue & 0x3f) << offset; + valueUnsigned |= static_cast(byteValue & 0x3f) << offset; bool isNegative = (byteValue & 0x40) != 0; if (isNegative) return -static_cast(valueUnsigned); @@ -235,25 +235,6 @@ public: return; } - loadPointerImpl(data); - } - - template < typename T, typename std::enable_if_t < std::is_base_of_v>, int > = 0 > - void loadPointerImpl(T &data) - { - using DataType = std::remove_pointer_t; - - typename DataType::IdentifierType index; - load(index); - - auto * constEntity = index.toEntity(VLC); - auto * constData = dynamic_cast(constEntity); - data = const_cast(constData); - } - - template < typename T, typename std::enable_if_t < !std::is_base_of_v>, int > = 0 > - void loadPointerImpl(T &data) - { if(reader->smartVectorMembersSerialization) { typedef typename std::remove_const_t> TObjectType; //eg: const CGHeroInstance * => CGHeroInstance diff --git a/lib/serializer/BinarySerializer.h b/lib/serializer/BinarySerializer.h index 71d985de9..851d53836 100644 --- a/lib/serializer/BinarySerializer.h +++ b/lib/serializer/BinarySerializer.h @@ -186,19 +186,6 @@ public: if(data == nullptr) return; - savePointerImpl(data); - } - - template < typename T, typename std::enable_if_t < std::is_base_of_v>, int > = 0 > - void savePointerImpl(const T &data) - { - auto index = data->getId(); - save(index); - } - - template < typename T, typename std::enable_if_t < !std::is_base_of_v>, int > = 0 > - void savePointerImpl(const T &data) - { typedef typename std::remove_const_t> TObjectType; if(writer->smartVectorMembersSerialization) diff --git a/lib/serializer/CSerializer.cpp b/lib/serializer/CSerializer.cpp index 23de9965e..3e7e84a6f 100644 --- a/lib/serializer/CSerializer.cpp +++ b/lib/serializer/CSerializer.cpp @@ -10,9 +10,9 @@ #include "StdInc.h" #include "CSerializer.h" +#include "../entities/hero/CHero.h" #include "../gameState/CGameState.h" #include "../mapping/CMap.h" -#include "../CHeroHandler.h" #include "../mapObjects/CGHeroInstance.h" #include "../mapObjects/CQuest.h" @@ -26,7 +26,7 @@ void CSerializer::addStdVecItems(CGameState *gs, LibClasses *lib) registerVectoredType(&gs->map->objects, [](const CGObjectInstance &obj){ return obj.id; }); registerVectoredType(&gs->map->allHeroes, - [](const CGHeroInstance &h){ return h.type->getId(); }); + [](const CGHeroInstance &h){ return h.getHeroType()->getId(); }); registerVectoredType(&gs->map->artInstances, [](const CArtifactInstance &artInst){ return artInst.getId(); }); registerVectoredType(&gs->map->quests, diff --git a/lib/serializer/Connection.cpp b/lib/serializer/Connection.cpp index 3560dff95..823fe721c 100644 --- a/lib/serializer/Connection.cpp +++ b/lib/serializer/Connection.cpp @@ -68,7 +68,7 @@ CConnection::CConnection(std::weak_ptr networkConnection) CConnection::~CConnection() = default; -void CConnection::sendPack(const CPack * pack) +void CConnection::sendPack(const CPack & pack) { boost::mutex::scoped_lock lock(writeMutex); @@ -78,18 +78,18 @@ void CConnection::sendPack(const CPack * pack) throw std::runtime_error("Attempt to send packet on a closed connection!"); packWriter->buffer.clear(); - *serializer & pack; + (*serializer) & (&pack); - logNetwork->trace("Sending a pack of type %s", typeid(*pack).name()); + logNetwork->trace("Sending a pack of type %s", typeid(pack).name()); connectionPtr->sendPacket(packWriter->buffer); packWriter->buffer.clear(); serializer->savedPointers.clear(); } -CPack * CConnection::retrievePack(const std::vector & data) +std::unique_ptr CConnection::retrievePack(const std::vector & data) { - CPack * result; + std::unique_ptr result; packReader->buffer = &data; packReader->position = 0; @@ -102,7 +102,7 @@ CPack * CConnection::retrievePack(const std::vector & data) if (packReader->position != data.size()) throw std::runtime_error("Failed to retrieve pack! Not all data has been read!"); - logNetwork->trace("Received CPack of type %s", typeid(*result).name()); + logNetwork->trace("Received CPack of type %s", typeid(result.get()).name()); deserializer->loadedPointers.clear(); deserializer->loadedSharedPointers.clear(); return result; diff --git a/lib/serializer/Connection.h b/lib/serializer/Connection.h index 838bcba53..b62849894 100644 --- a/lib/serializer/Connection.h +++ b/lib/serializer/Connection.h @@ -51,8 +51,8 @@ public: explicit CConnection(std::weak_ptr networkConnection); ~CConnection(); - void sendPack(const CPack * pack); - CPack * retrievePack(const std::vector & data); + void sendPack(const CPack & pack); + std::unique_ptr retrievePack(const std::vector & data); void enterLobbyConnectionMode(); void setCallback(IGameCallback * cb); diff --git a/lib/serializer/ESerializationVersion.h b/lib/serializer/ESerializationVersion.h index 3fcc844fb..a5b374f9b 100644 --- a/lib/serializer/ESerializationVersion.h +++ b/lib/serializer/ESerializationVersion.h @@ -61,7 +61,15 @@ enum class ESerializationVersion : int32_t CAMPAIGN_OUTRO_SUPPORT, // 862 - support for campaign outro video REWARDABLE_BANKS, // 863 - team state contains list of scouted objects, coast visitable rewardable objects REGION_LABEL, // 864 - labels for campaign regions - EVENT_OBJECTS_DELETION, //865 - allow events to remove map objects - + SPELL_RESEARCH, // 865 - spell research + LOCAL_PLAYER_STATE_DATA, // 866 - player state contains arbitrary client-side data + REMOVE_TOWN_PTR, // 867 - removed pointer to CTown from CGTownInstance + REMOVE_OBJECT_TYPENAME, // 868 - remove typename from CGObjectInstance + REMOVE_VLC_POINTERS, // 869 removed remaining pointers to VLC entities + FOLDER_NAME_REWORK, // 870 - rework foldername + REWARDABLE_GUARDS, // 871 - fix missing serialization of guards in rewardable objects + MARKET_TRANSLATION_FIX, // 872 - remove serialization of markets translateable strings + EVENT_OBJECTS_DELETION, //873 - allow events to remove map objects + CURRENT = EVENT_OBJECTS_DELETION }; diff --git a/lib/serializer/RegisterTypes.h b/lib/serializer/RegisterTypes.h index 94663357e..c3dab0559 100644 --- a/lib/serializer/RegisterTypes.h +++ b/lib/serializer/RegisterTypes.h @@ -9,7 +9,6 @@ */ #pragma once -#include "../CHeroHandler.h" #include "../CPlayerState.h" #include "../CStack.h" #include "../battle/BattleInfo.h" @@ -21,19 +20,23 @@ #include "../gameState/CGameState.h" #include "../gameState/CGameStateCampaign.h" #include "../gameState/TavernHeroesPool.h" + #include "../mapObjects/CGCreature.h" #include "../mapObjects/CGDwelling.h" #include "../mapObjects/CGMarket.h" #include "../mapObjects/CGPandoraBox.h" #include "../mapObjects/CGTownInstance.h" #include "../mapObjects/CQuest.h" +#include "../mapObjects/FlaggableMapObject.h" #include "../mapObjects/MiscObjects.h" #include "../mapObjects/TownBuildingInstance.h" + #include "../mapping/CMap.h" #include "../networkPacks/PacksForClient.h" #include "../networkPacks/PacksForClientBattle.h" #include "../networkPacks/PacksForLobby.h" #include "../networkPacks/PacksForServer.h" +#include "../networkPacks/SaveLocalState.h" #include "../networkPacks/SetRewardableConfiguration.h" #include "../networkPacks/SetStackEffect.h" @@ -73,7 +76,7 @@ void registerTypes(Serializer &s) s.template registerType(15); s.template registerType(16); s.template registerType(17); - s.template registerType(18); + s.template registerType(18); s.template registerType(19); s.template registerType(20); s.template registerType(21); @@ -288,6 +291,10 @@ void registerTypes(Serializer &s) s.template registerType(238); s.template registerType(239); s.template registerType(240); + s.template registerType(241); + s.template registerType(242); + s.template registerType(243); + s.template registerType(244); } VCMI_LIB_NAMESPACE_END diff --git a/lib/serializer/SerializerReflection.cpp b/lib/serializer/SerializerReflection.cpp index 4862c62b7..ecb3427b8 100644 --- a/lib/serializer/SerializerReflection.cpp +++ b/lib/serializer/SerializerReflection.cpp @@ -19,6 +19,7 @@ #include "../RiverHandler.h" #include "../RoadHandler.h" #include "../TerrainHandler.h" +#include "../entities/hero/CHero.h" #include "../mapObjects/ObjectTemplate.h" #include "../mapping/CMapInfo.h" #include "../rmg/CMapGenOptions.h" diff --git a/lib/spells/AdventureSpellMechanics.cpp b/lib/spells/AdventureSpellMechanics.cpp index c7785d522..e197e2cd5 100644 --- a/lib/spells/AdventureSpellMechanics.cpp +++ b/lib/spells/AdventureSpellMechanics.cpp @@ -105,7 +105,7 @@ ESpellCastResult AdventureSpellMechanics::applyAdventureEffects(SpellCastEnviron GiveBonus gb; gb.id = ObjectInstanceID(parameters.caster->getCasterUnitId()); gb.bonus = b; - env->apply(&gb); + env->apply(gb); } return ESpellCastResult::OK; @@ -136,7 +136,7 @@ void AdventureSpellMechanics::performCast(SpellCastEnvironment * env, const Adve AdvmapSpellCast asc; asc.casterID = ObjectInstanceID(parameters.caster->getCasterUnitId()); asc.spellID = owner->id; - env->apply(&asc); + env->apply(asc); ESpellCastResult result = applyAdventureEffects(env, parameters); @@ -194,7 +194,7 @@ ESpellCastResult SummonBoatMechanics::applyAdventureEffects(SpellCastEnvironment iw.player = parameters.caster->getCasterOwner(); iw.text.appendLocalString(EMetaText::GENERAL_TXT, 336); //%s tried to summon a boat, but failed. parameters.caster->getCasterName(iw.text); - env->apply(&iw); + env->apply(iw); return ESpellCastResult::OK; } @@ -209,7 +209,7 @@ ESpellCastResult SummonBoatMechanics::applyAdventureEffects(SpellCastEnvironment if(b->hero || b->layer != EPathfindingLayer::SAIL) continue; //we're looking for unoccupied boat - double nDist = b->pos.dist2d(parameters.caster->getHeroCaster()->visitablePos()); + double nDist = b->visitablePos().dist2d(parameters.caster->getHeroCaster()->visitablePos()); if(!nearest || nDist < dist) //it's first boat or closer than previous { nearest = b; @@ -226,14 +226,14 @@ ESpellCastResult SummonBoatMechanics::applyAdventureEffects(SpellCastEnvironment cop.objid = nearest->id; cop.nPos = summonPos; cop.initiator = parameters.caster->getCasterOwner(); - env->apply(&cop); + env->apply(cop); } else if(schoolLevel < 2) //none or basic level -> cannot create boat :( { InfoWindow iw; iw.player = parameters.caster->getCasterOwner(); iw.text.appendLocalString(EMetaText::GENERAL_TXT, 335); //There are no boats to summon. - env->apply(&iw); + env->apply(iw); return ESpellCastResult::ERROR; } else //create boat @@ -282,7 +282,7 @@ ESpellCastResult ScuttleBoatMechanics::applyAdventureEffects(SpellCastEnvironmen iw.player = parameters.caster->getCasterOwner(); iw.text.appendLocalString(EMetaText::GENERAL_TXT, 337); //%s tried to scuttle the boat, but failed parameters.caster->getCasterName(iw.text); - env->apply(&iw); + env->apply(iw); return ESpellCastResult::OK; } @@ -291,7 +291,7 @@ ESpellCastResult ScuttleBoatMechanics::applyAdventureEffects(SpellCastEnvironmen RemoveObject ro; ro.initiator = parameters.caster->getCasterOwner(); ro.objectID = t.visitableObjects.back()->id; - env->apply(&ro); + env->apply(ro); return ESpellCastResult::OK; } @@ -374,7 +374,7 @@ bool DimensionDoorMechanics::canBeCastAtImpl(spells::Problem & problem, const CG } else { - if (dest->blocked) + if (dest->blocked()) return false; } @@ -400,14 +400,14 @@ ESpellCastResult DimensionDoorMechanics::applyAdventureEffects(SpellCastEnvironm { // SOD: DD to such "wrong" terrain results in mana and move points spending, but fails to move hero iw.text = MetaString::createFromTextID("core.genrltxt.70"); // Dimension Door failed! - env->apply(&iw); + env->apply(iw); // no return - resources will be spent } else { // HotA: game will show error message without taking mana or move points, even when DD into terra incognita iw.text = MetaString::createFromTextID("vcmi.dimensionDoor.seaToLandError"); - env->apply(&iw); + env->apply(iw); return ESpellCastResult::CANCEL; } } @@ -415,7 +415,7 @@ ESpellCastResult DimensionDoorMechanics::applyAdventureEffects(SpellCastEnvironm GiveBonus gb; gb.id = ObjectInstanceID(parameters.caster->getCasterUnitId()); gb.bonus = Bonus(BonusDuration::ONE_DAY, BonusType::NONE, BonusSource::SPELL_EFFECT, 0, BonusSourceID(owner->id)); - env->apply(&gb); + env->apply(gb); SetMovePoints smp; smp.hid = ObjectInstanceID(parameters.caster->getCasterUnitId()); @@ -423,7 +423,7 @@ ESpellCastResult DimensionDoorMechanics::applyAdventureEffects(SpellCastEnvironm smp.val = parameters.caster->getHeroCaster()->movementPointsRemaining() - movementCost; else smp.val = 0; - env->apply(&smp); + env->apply(smp); return ESpellCastResult::OK; } @@ -471,7 +471,7 @@ ESpellCastResult TownPortalMechanics::applyAdventureEffects(SpellCastEnvironment InfoWindow iw; iw.player = parameters.caster->getCasterOwner(); iw.text.appendLocalString(EMetaText::GENERAL_TXT, 123); - env->apply(&iw); + env->apply(iw); return ESpellCastResult::CANCEL; } } @@ -539,7 +539,7 @@ ESpellCastResult TownPortalMechanics::applyAdventureEffects(SpellCastEnvironment InfoWindow iw; iw.player = parameters.caster->getCasterOwner(); iw.text.appendLocalString(EMetaText::GENERAL_TXT, 135); - env->apply(&iw); + env->apply(iw); return ESpellCastResult::ERROR; } @@ -568,7 +568,7 @@ void TownPortalMechanics::endCast(SpellCastEnvironment * env, const AdventureSpe SetMovePoints smp; smp.hid = ObjectInstanceID(parameters.caster->getCasterUnitId()); smp.val = std::max(0, parameters.caster->getHeroCaster()->movementPointsRemaining() - moveCost); - env->apply(&smp); + env->apply(smp); } } @@ -587,7 +587,7 @@ ESpellCastResult TownPortalMechanics::beginCast(SpellCastEnvironment * env, cons InfoWindow iw; iw.player = parameters.caster->getCasterOwner(); iw.text.appendLocalString(EMetaText::GENERAL_TXT, 124); - env->apply(&iw); + env->apply(iw); return ESpellCastResult::CANCEL; } @@ -598,7 +598,7 @@ ESpellCastResult TownPortalMechanics::beginCast(SpellCastEnvironment * env, cons InfoWindow iw; iw.player = parameters.caster->getCasterOwner(); iw.text.appendLocalString(EMetaText::GENERAL_TXT, 125); - env->apply(&iw); + env->apply(iw); return ESpellCastResult::CANCEL; } @@ -643,7 +643,7 @@ ESpellCastResult TownPortalMechanics::beginCast(SpellCastEnvironment * env, cons InfoWindow iw; iw.player = parameters.caster->getCasterOwner(); iw.text.appendLocalString(EMetaText::GENERAL_TXT, 124); - env->apply(&iw); + env->apply(iw); return ESpellCastResult::CANCEL; } @@ -669,11 +669,11 @@ const CGTownInstance * TownPortalMechanics::findNearestTown(SpellCastEnvironment return nullptr; auto nearest = pool.cbegin(); //nearest town's iterator - si32 dist = (*nearest)->pos.dist2dSQ(parameters.caster->getHeroCaster()->pos); + si32 dist = (*nearest)->visitablePos().dist2dSQ(parameters.caster->getHeroCaster()->visitablePos()); for(auto i = nearest + 1; i != pool.cend(); ++i) { - si32 curDist = (*i)->pos.dist2dSQ(parameters.caster->getHeroCaster()->pos); + si32 curDist = (*i)->visitablePos().dist2dSQ(parameters.caster->getHeroCaster()->visitablePos()); if(curDist < dist) { @@ -737,7 +737,7 @@ ESpellCastResult ViewMechanics::applyAdventureEffects(SpellCastEnvironment * env } pack.showTerrain = showTerrain(spellLevel); - env->apply(&pack); + env->apply(pack); return ESpellCastResult::OK; } diff --git a/lib/spells/BattleSpellMechanics.cpp b/lib/spells/BattleSpellMechanics.cpp index 4d79fd0e3..81a7f1a1b 100644 --- a/lib/spells/BattleSpellMechanics.cpp +++ b/lib/spells/BattleSpellMechanics.cpp @@ -217,23 +217,28 @@ bool BattleSpellMechanics::canBeCastAt(const Target & target, Problem & problem) const battle::Unit * mainTarget = nullptr; - if (!getSpell()->canCastOnSelf()) + if(spellTarget.front().unitValue) { - if(spellTarget.front().unitValue) - { - mainTarget = target.front().unitValue; - } - else if(spellTarget.front().hexValue.isValid()) - { - mainTarget = battle()->battleGetUnitByPos(target.front().hexValue, true); - } + mainTarget = target.front().unitValue; + } + else if(spellTarget.front().hexValue.isValid()) + { + mainTarget = battle()->battleGetUnitByPos(target.front().hexValue, true); + } + if (!getSpell()->canCastOnSelf() && !getSpell()->canCastOnlyOnSelf()) + { if(mainTarget && mainTarget == caster) return false; // can't cast on self if(mainTarget && mainTarget->hasBonusOfType(BonusType::INVINCIBLE) && !getSpell()->getPositiveness()) return false; } + else if(getSpell()->canCastOnlyOnSelf()) + { + if(mainTarget && mainTarget != caster) + return false; // can't cast on others + } return effects->applicable(problem, this, target, spellTarget); } @@ -254,7 +259,7 @@ std::vector BattleSpellMechanics::getAffectedStacks(const Target for(const Destination & dest : all) { - if(dest.unitValue) + if(dest.unitValue && !dest.unitValue->hasBonusOfType(BonusType::INVINCIBLE)) { //FIXME: remove and return battle::Unit stacks.insert(battle()->battleGetStackByID(dest.unitValue->unitId(), false)); @@ -348,9 +353,9 @@ void BattleSpellMechanics::cast(ServerCallback * server, const Target & target) sc.affectedCres.insert(unit->unitId()); if(!castDescription.lines.empty()) - server->apply(&castDescription); + server->apply(castDescription); - server->apply(&sc); + server->apply(sc); for(auto & p : effectsToApply) p.first->apply(server, this, p.second); @@ -370,7 +375,7 @@ void BattleSpellMechanics::cast(ServerCallback * server, const Target & target) // temporary(?) workaround to force animations to trigger StacksInjured fakeEvent; fakeEvent.battleID = battle()->getBattle()->getBattleID(); - server->apply(&fakeEvent); + server->apply(fakeEvent); } void BattleSpellMechanics::beforeCast(BattleSpellCast & sc, vstd::RNG & rng, const Target & target) @@ -486,7 +491,7 @@ void BattleSpellMechanics::doRemoveEffects(ServerCallback * server, const std::v } if(!sse.toRemove.empty()) - server->apply(&sse); + server->apply(sse); } bool BattleSpellMechanics::counteringSelector(const Bonus * bonus) const diff --git a/lib/spells/BonusCaster.cpp b/lib/spells/BonusCaster.cpp index deb119a89..a2d8523b1 100644 --- a/lib/spells/BonusCaster.cpp +++ b/lib/spells/BonusCaster.cpp @@ -12,12 +12,12 @@ #include "BonusCaster.h" #include +#include #include "../battle/Unit.h" #include "../bonuses/Bonus.h" #include "../VCMI_Lib.h" #include "../CSkillHandler.h" -#include "../CHeroHandler.h" VCMI_LIB_NAMESPACE_BEGIN diff --git a/lib/spells/CSpellHandler.cpp b/lib/spells/CSpellHandler.cpp index 845c2cbf9..79619ac51 100644 --- a/lib/spells/CSpellHandler.cpp +++ b/lib/spells/CSpellHandler.cpp @@ -79,6 +79,8 @@ CSpell::CSpell(): combat(false), creatureAbility(false), castOnSelf(false), + castOnlyOnSelf(false), + castWithoutSkip(false), positiveness(ESpellPositiveness::NEUTRAL), defaultProbability(0), rising(false), @@ -298,6 +300,11 @@ bool CSpell::canCastOnSelf() const return castOnSelf; } +bool CSpell::canCastOnlyOnSelf() const +{ + return castOnlyOnSelf; +} + bool CSpell::canCastWithoutSkip() const { return castWithoutSkip; @@ -537,6 +544,7 @@ void CSpell::serializeJson(JsonSerializeFormat & handler) ///CSpell::AnimationInfo CSpell::AnimationItem::AnimationItem() : verticalPosition(VerticalPosition::TOP), + transparency(1), pause(0) { @@ -776,7 +784,7 @@ std::shared_ptr CSpellHandler::loadFromJson(const std::string & scope, c spell->combat = type == "combat"; } - VLC->generaltexth->registerString(scope, spell->getNameTextID(), json["name"].String()); + VLC->generaltexth->registerString(scope, spell->getNameTextID(), json["name"]); logMod->trace("%s: loading spell %s", __FUNCTION__, spell->getNameTranslated()); @@ -788,6 +796,7 @@ std::shared_ptr CSpellHandler::loadFromJson(const std::string & scope, c } spell->castOnSelf = json["canCastOnSelf"].Bool(); + spell->castOnlyOnSelf = json["canCastOnlyOnSelf"].Bool(); spell->castWithoutSkip = json["canCastWithoutSkip"].Bool(); spell->level = static_cast(json["level"].Integer()); spell->power = static_cast(json["power"].Integer()); @@ -957,10 +966,15 @@ std::shared_ptr CSpellHandler::loadFromJson(const std::string & scope, c auto vPosStr = item["verticalPosition"].String(); if("bottom" == vPosStr) newItem.verticalPosition = VerticalPosition::BOTTOM; + + if (item["transparency"].isNumber()) + newItem.transparency = item["transparency"].Float(); + else + newItem.transparency = 1.0; } else if(item.isNumber()) { - newItem.pause = static_cast(item.Float()); + newItem.pause = item.Integer(); } q.push_back(newItem); @@ -997,7 +1011,7 @@ std::shared_ptr CSpellHandler::loadFromJson(const std::string & scope, c const si32 levelPower = levelObject.power = static_cast(levelNode["power"].Integer()); if (!spell->isCreatureAbility()) - VLC->generaltexth->registerString(scope, spell->getDescriptionTextID(levelIndex), levelNode["description"].String()); + VLC->generaltexth->registerString(scope, spell->getDescriptionTextID(levelIndex), levelNode["description"]); levelObject.cost = static_cast(levelNode["cost"].Integer()); levelObject.AIValue = static_cast(levelNode["aiValue"].Integer()); diff --git a/lib/spells/CSpellHandler.h b/lib/spells/CSpellHandler.h index c16e10ceb..77e69ff47 100644 --- a/lib/spells/CSpellHandler.h +++ b/lib/spells/CSpellHandler.h @@ -74,6 +74,7 @@ public: AnimationPath resourceName; std::string effectName; VerticalPosition verticalPosition; + float transparency; int pause; AnimationItem(); @@ -167,6 +168,7 @@ public: bool hasSchool(SpellSchool school) const override; bool canCastOnSelf() const override; + bool canCastOnlyOnSelf() const override; bool canCastWithoutSkip() const override; /** @@ -297,6 +299,7 @@ private: bool combat; //is this spell combat (true) or adventure (false) bool creatureAbility; //if true, only creatures can use this spell bool castOnSelf; // if set, creature caster can cast this spell on itself + bool castOnlyOnSelf; // if set, creature caster can cast this spell on itself bool castWithoutSkip; // if set the creature will not skip the turn after casting a spell si8 positiveness; //1 if spell is positive for influenced stacks, 0 if it is indifferent, -1 if it's negative diff --git a/lib/spells/ISpellMechanics.cpp b/lib/spells/ISpellMechanics.cpp index a1488e1e8..24f40b067 100644 --- a/lib/spells/ISpellMechanics.cpp +++ b/lib/spells/ISpellMechanics.cpp @@ -36,7 +36,6 @@ #include "CSpellHandler.h" -#include "../CHeroHandler.h"//todo: remove #include "../IGameCallback.h"//todo: remove #include "../BattleFieldHandler.h" diff --git a/lib/spells/effects/Catapult.cpp b/lib/spells/effects/Catapult.cpp index 49737dc78..28ec7998e 100644 --- a/lib/spells/effects/Catapult.cpp +++ b/lib/spells/effects/Catapult.cpp @@ -104,7 +104,7 @@ void Catapult::applyMassive(ServerCallback * server, const Mechanics * m) const attackInfo->damageDealt += getRandomDamage(server); } } - server->apply(&ca); + server->apply(ca); removeTowerShooters(server, m); } @@ -144,7 +144,7 @@ void Catapult::applyTargeted(ServerCallback * server, const Mechanics * m, const ca.battleID = m->battle()->getBattle()->getBattleID(); ca.attacker = m->caster->getHeroCaster() ? -1 : m->caster->getCasterUnitId(); ca.attackedParts.push_back(attack); - server->apply(&ca); + server->apply(ca); removeTowerShooters(server, m); } } @@ -228,7 +228,7 @@ void Catapult::removeTowerShooters(ServerCallback * server, const Mechanics * m) } if(!removeUnits.changedStacks.empty()) - server->apply(&removeUnits); + server->apply(removeUnits); } std::vector Catapult::getPotentialTargets(const Mechanics * m, bool bypassGateCheck, bool bypassTowerCheck) const diff --git a/lib/spells/effects/Clone.cpp b/lib/spells/effects/Clone.cpp index 86e3085d0..03f35ff9d 100644 --- a/lib/spells/effects/Clone.cpp +++ b/lib/spells/effects/Clone.cpp @@ -65,7 +65,7 @@ void Clone::apply(ServerCallback * server, const Mechanics * m, const EffectTarg pack.battleID = m->battle()->getBattle()->getBattleID(); pack.changedStacks.emplace_back(info.id, UnitChanges::EOperation::ADD); info.save(pack.changedStacks.back().data); - server->apply(&pack); + server->apply(pack); //TODO: use BattleUnitsChanged with UPDATE operation @@ -90,7 +90,7 @@ void Clone::apply(ServerCallback * server, const Mechanics * m, const EffectTarg cloneFlags.changedStacks.emplace_back(originalState->unitId(), UnitChanges::EOperation::RESET_STATE); originalState->save(cloneFlags.changedStacks.back().data); - server->apply(&cloneFlags); + server->apply(cloneFlags); SetStackEffect sse; sse.battleID = m->battle()->getBattle()->getBattleID(); @@ -100,7 +100,7 @@ void Clone::apply(ServerCallback * server, const Mechanics * m, const EffectTarg std::vector buffer; buffer.push_back(lifeTimeMarker); sse.toAdd.emplace_back(unitId, buffer); - server->apply(&sse); + server->apply(sse); } } diff --git a/lib/spells/effects/Damage.cpp b/lib/spells/effects/Damage.cpp index 59d19658f..0e9297329 100644 --- a/lib/spells/effects/Damage.cpp +++ b/lib/spells/effects/Damage.cpp @@ -80,10 +80,10 @@ void Damage::apply(ServerCallback * server, const Mechanics * m, const EffectTar } if(!stacksInjured.stacks.empty()) - server->apply(&stacksInjured); + server->apply(stacksInjured); if(!blm.lines.empty()) - server->apply(&blm); + server->apply(blm); } bool Damage::isReceptive(const Mechanics * m, const battle::Unit * unit) const diff --git a/lib/spells/effects/DemonSummon.cpp b/lib/spells/effects/DemonSummon.cpp index d55f24a57..b86a87d1a 100644 --- a/lib/spells/effects/DemonSummon.cpp +++ b/lib/spells/effects/DemonSummon.cpp @@ -98,7 +98,7 @@ void DemonSummon::apply(ServerCallback * server, const Mechanics * m, const Effe } if(!pack.changedStacks.empty()) - server->apply(&pack); + server->apply(pack); } bool DemonSummon::isValidTarget(const Mechanics * m, const battle::Unit * unit) const diff --git a/lib/spells/effects/Dispel.cpp b/lib/spells/effects/Dispel.cpp index 8ee66b75f..41fedab42 100644 --- a/lib/spells/effects/Dispel.cpp +++ b/lib/spells/effects/Dispel.cpp @@ -65,10 +65,10 @@ void Dispel::apply(ServerCallback * server, const Mechanics * m, const EffectTar } if(!sse.toRemove.empty()) - server->apply(&sse); + server->apply(sse); if(describe && !blm.lines.empty()) - server->apply(&blm); + server->apply(blm); } bool Dispel::isValidTarget(const Mechanics * m, const battle::Unit * unit) const diff --git a/lib/spells/effects/Heal.cpp b/lib/spells/effects/Heal.cpp index c0aedb3a1..ce5f13cf5 100644 --- a/lib/spells/effects/Heal.cpp +++ b/lib/spells/effects/Heal.cpp @@ -42,9 +42,9 @@ void Heal::apply(int64_t value, ServerCallback * server, const Mechanics * m, co prepareHealEffect(value, pack, logMessage, *server->getRNG(), m, target); if(!pack.changedStacks.empty()) - server->apply(&pack); + server->apply(pack); if(!logMessage.lines.empty()) - server->apply(&logMessage); + server->apply(logMessage); } bool Heal::isValidTarget(const Mechanics * m, const battle::Unit * unit) const diff --git a/lib/spells/effects/Moat.cpp b/lib/spells/effects/Moat.cpp index 6cb29016e..889644b88 100644 --- a/lib/spells/effects/Moat.cpp +++ b/lib/spells/effects/Moat.cpp @@ -88,7 +88,7 @@ void Moat::convertBonus(const Mechanics * m, std::vector & converted) con if(m->battle()->battleGetDefendedTown() && m->battle()->battleGetFortifications().hasMoat) { - nb.sid = BonusSourceID(m->battle()->battleGetDefendedTown()->town->buildings.at(BuildingID::CITADEL)->getUniqueTypeID()); + nb.sid = BonusSourceID(m->battle()->battleGetDefendedTown()->getTown()->buildings.at(BuildingID::CITADEL)->getUniqueTypeID()); nb.source = BonusSource::TOWN_STRUCTURE; } else @@ -122,7 +122,7 @@ void Moat::apply(ServerCallback * server, const Mechanics * m, const EffectTarge GiveBonus gb(GiveBonus::ETarget::BATTLE); gb.id = m->battle()->getBattle()->getBattleID(); gb.bonus = b; - server->apply(&gb); + server->apply(gb); } } } @@ -171,7 +171,7 @@ void Moat::placeObstacles(ServerCallback * server, const Mechanics * m, const Ef } if(!pack.changes.empty()) - server->apply(&pack); + server->apply(pack); } } diff --git a/lib/spells/effects/Obstacle.cpp b/lib/spells/effects/Obstacle.cpp index 918c89c3e..40c22a088 100644 --- a/lib/spells/effects/Obstacle.cpp +++ b/lib/spells/effects/Obstacle.cpp @@ -326,7 +326,7 @@ void Obstacle::placeObstacles(ServerCallback * server, const Mechanics * m, cons } if(!pack.changes.empty()) - server->apply(&pack); + server->apply(pack); } } diff --git a/lib/spells/effects/RemoveObstacle.cpp b/lib/spells/effects/RemoveObstacle.cpp index 58e462c69..c8d9840cb 100644 --- a/lib/spells/effects/RemoveObstacle.cpp +++ b/lib/spells/effects/RemoveObstacle.cpp @@ -54,7 +54,7 @@ void RemoveObstacle::apply(ServerCallback * server, const Mechanics * m, const E } if(!pack.changes.empty()) - server->apply(&pack); + server->apply(pack); } void RemoveObstacle::serializeJsonEffect(JsonSerializeFormat & handler) diff --git a/lib/spells/effects/Sacrifice.cpp b/lib/spells/effects/Sacrifice.cpp index 978d0f4d4..e51715e87 100644 --- a/lib/spells/effects/Sacrifice.cpp +++ b/lib/spells/effects/Sacrifice.cpp @@ -125,7 +125,7 @@ void Sacrifice::apply(ServerCallback * server, const Mechanics * m, const Effect BattleUnitsChanged removeUnits; removeUnits.battleID = m->battle()->getBattle()->getBattleID(); removeUnits.changedStacks.emplace_back(victim->unitId(), UnitChanges::EOperation::REMOVE); - server->apply(&removeUnits); + server->apply(removeUnits); } bool Sacrifice::isValidTarget(const Mechanics * m, const battle::Unit * unit) const diff --git a/lib/spells/effects/Summon.cpp b/lib/spells/effects/Summon.cpp index 65eb5dafb..e4d51bd5b 100644 --- a/lib/spells/effects/Summon.cpp +++ b/lib/spells/effects/Summon.cpp @@ -18,7 +18,6 @@ #include "../../battle/Unit.h" #include "../../serializer/JsonSerializeFormat.h" #include "../../CCreatureHandler.h" -#include "../../CHeroHandler.h" #include "../../mapObjects/CGHeroInstance.h" #include "../../networkPacks/PacksForClientBattle.h" @@ -79,7 +78,7 @@ bool Summon::applicable(Problem & problem, const Mechanics * m) const text.replaceNamePlural(elemental->creatureId()); - if(caster->type->gender == EHeroGender::FEMALE) + if(caster->gender == EHeroGender::FEMALE) text.replaceLocalString(EMetaText::GENERAL_TXT, 540); else text.replaceLocalString(EMetaText::GENERAL_TXT, 539); @@ -159,7 +158,7 @@ void Summon::apply(ServerCallback * server, const Mechanics * m, const EffectTar } if(!pack.changedStacks.empty()) - server->apply(&pack); + server->apply(pack); } EffectTarget Summon::filterTarget(const Mechanics * m, const EffectTarget & target) const diff --git a/lib/spells/effects/Teleport.cpp b/lib/spells/effects/Teleport.cpp index c0dce4d80..cffe3c2f1 100644 --- a/lib/spells/effects/Teleport.cpp +++ b/lib/spells/effects/Teleport.cpp @@ -85,7 +85,7 @@ void Teleport::apply(ServerCallback * server, const Mechanics * m, const EffectT tiles.push_back(destination); pack.tilesToMove = tiles; pack.teleporting = true; - server->apply(&pack); + server->apply(pack); if(triggerObstacles) { diff --git a/lib/spells/effects/Timed.cpp b/lib/spells/effects/Timed.cpp index 545505f20..694f5628d 100644 --- a/lib/spells/effects/Timed.cpp +++ b/lib/spells/effects/Timed.cpp @@ -205,10 +205,10 @@ void Timed::apply(ServerCallback * server, const Mechanics * m, const EffectTarg } if(!(sse.toAdd.empty() && sse.toUpdate.empty())) - server->apply(&sse); + server->apply(sse); if(describe && !blm.lines.empty()) - server->apply(&blm); + server->apply(blm); } void Timed::convertBonus(const Mechanics * m, int32_t & duration, std::vector & converted) const diff --git a/lib/spells/effects/UnitEffect.cpp b/lib/spells/effects/UnitEffect.cpp index 657f98e9f..3598fbbb8 100644 --- a/lib/spells/effects/UnitEffect.cpp +++ b/lib/spells/effects/UnitEffect.cpp @@ -197,7 +197,7 @@ EffectTarget UnitEffect::transformTargetByChain(const Mechanics * m, const Targe auto possibleTargets = m->battle()->battleGetUnitsIf([&](const battle::Unit * unit) -> bool { - return isValidTarget(m, unit); + return isReceptive(m, unit) && isValidTarget(m, unit); }); for(const auto *unit : possibleTargets) diff --git a/lib/texts/CGeneralTextHandler.cpp b/lib/texts/CGeneralTextHandler.cpp index 9cc3724c2..2f3cff5e8 100644 --- a/lib/texts/CGeneralTextHandler.cpp +++ b/lib/texts/CGeneralTextHandler.cpp @@ -120,21 +120,16 @@ void CGeneralTextHandler::readToVector(const std::string & sourceID, const std:: } CGeneralTextHandler::CGeneralTextHandler(): - victoryConditions(*this, "core.vcdesc" ), - lossConditions (*this, "core.lcdesc" ), - colors (*this, "core.plcolors" ), tcommands (*this, "core.tcommand" ), hcommands (*this, "core.hallinfo" ), fcommands (*this, "core.castinfo" ), advobtxt (*this, "core.advevent" ), restypes (*this, "core.restypes" ), - randsign (*this, "core.randsign" ), overview (*this, "core.overview" ), arraytxt (*this, "core.arraytxt" ), primarySkillNames(*this, "core.priskill" ), jktexts (*this, "core.jktext" ), tavernInfo (*this, "core.tvrninfo" ), - tavernRumors (*this, "core.randtvrn" ), turnDurations (*this, "core.turndur" ), heroscrn (*this, "core.heroscrn" ), tentColors (*this, "core.tentcolr" ), @@ -144,9 +139,7 @@ CGeneralTextHandler::CGeneralTextHandler(): // pseudo-array, that don't have H3 file with same name seerEmpty (*this, "core.seerhut.empty" ), seerNames (*this, "core.seerhut.names" ), - capColors (*this, "vcmi.capitalColors" ), - znpc00 (*this, "vcmi.znpc00" ), // technically - wog - qeModCommands (*this, "vcmi.quickExchange" ) + capColors (*this, "vcmi.capitalColors" ) { readToVector("core.vcdesc", "DATA/VCDESC.TXT" ); readToVector("core.lcdesc", "DATA/LCDESC.TXT" ); @@ -171,10 +164,6 @@ CGeneralTextHandler::CGeneralTextHandler(): readToVector("core.mineevnt", "DATA/MINEEVNT.TXT" ); readToVector("core.xtrainfo", "DATA/XTRAINFO.TXT" ); - static const std::string QE_MOD_COMMANDS = "DATA/QECOMMANDS.TXT"; - if (CResourceHandler::get()->existsResource(TextPath::builtin(QE_MOD_COMMANDS))) - readToVector("vcmi.quickExchange", QE_MOD_COMMANDS); - { CLegacyConfigParser parser(TextPath::builtin("DATA/RANDTVRN.TXT")); parser.endLine(); @@ -303,11 +292,6 @@ CGeneralTextHandler::CGeneralTextHandler(): scenariosCountPerCampaign.push_back(region); } } - if (VLC->engineSettings()->getBoolean(EGameSettings::MODULE_COMMANDERS)) - { - if(CResourceHandler::get()->existsResource(TextPath::builtin("DATA/ZNPC00.TXT"))) - readToVector("vcmi.znpc00", "DATA/ZNPC00.TXT" ); - } } int32_t CGeneralTextHandler::pluralText(const int32_t textIndex, const int32_t count) const diff --git a/lib/texts/CGeneralTextHandler.h b/lib/texts/CGeneralTextHandler.h index ce5263ced..428eea1b0 100644 --- a/lib/texts/CGeneralTextHandler.h +++ b/lib/texts/CGeneralTextHandler.h @@ -53,7 +53,6 @@ public: LegacyTextContainer jktexts; LegacyTextContainer heroscrn; LegacyTextContainer overview;//text for Kingdom Overview window - LegacyTextContainer colors; //names of player colors ("red",...) LegacyTextContainer capColors; //names of player colors with first letter capitalized ("Red",...) LegacyTextContainer turnDurations; //turn durations for pregame (1 Minute ... Unlimited) @@ -62,26 +61,18 @@ public: LegacyTextContainer hcommands; // town hall screen LegacyTextContainer fcommands; // fort screen LegacyTextContainer tavernInfo; - LegacyTextContainer tavernRumors; - - LegacyTextContainer qeModCommands; LegacyHelpContainer zelp; - LegacyTextContainer lossConditions; - LegacyTextContainer victoryConditions; //objects LegacyTextContainer advobtxt; LegacyTextContainer restypes; //names of resources - LegacyTextContainer randsign; LegacyTextContainer seerEmpty; LegacyTextContainer seerNames; LegacyTextContainer tentColors; //sec skills LegacyTextContainer levels; - //commanders - LegacyTextContainer znpc00; //more or less useful content of that file std::vector findStringsWithPrefix(const std::string & prefix); diff --git a/lib/texts/CLegacyConfigParser.cpp b/lib/texts/CLegacyConfigParser.cpp index 15ea93c74..90eb9a206 100644 --- a/lib/texts/CLegacyConfigParser.cpp +++ b/lib/texts/CLegacyConfigParser.cpp @@ -32,9 +32,7 @@ protected: CLegacyConfigParser::CLegacyConfigParser(const TextPath & resource) { auto input = CResourceHandler::get()->load(resource); - std::string modName = VLC->modh->findResourceOrigin(resource); - std::string language = VLC->modh->getModLanguage(modName); - fileEncoding = Languages::getLanguageOptions(language).encoding; + fileEncoding = VLC->modh->findResourceEncoding(resource); data.reset(new char[input->getSize()]); input->read(reinterpret_cast(data.get()), input->getSize()); diff --git a/lib/texts/Languages.h b/lib/texts/Languages.h index a454f1dfa..9e0f87585 100644 --- a/lib/texts/Languages.h +++ b/lib/texts/Languages.h @@ -77,23 +77,23 @@ inline const auto & getLanguageList() { static const std::array languages { { - { "czech", "Czech", "Čeština", "CP1250", "cs", "cze", "%d.%m.%Y %T", EPluralForms::CZ_3 }, - { "chinese", "Chinese", "简体中文", "GBK", "zh", "chi", "%F %T", EPluralForms::VI_1 }, // Note: actually Simplified Chinese - { "english", "English", "English", "CP1252", "en", "eng", "%F %T", EPluralForms::EN_2 }, // English uses international date/time format here - { "finnish", "Finnish", "Suomi", "CP1252", "fi", "fin", "%d.%m.%Y %T", EPluralForms::EN_2, }, - { "french", "French", "Français", "CP1252", "fr", "fre", "%d/%m/%Y %T", EPluralForms::FR_2, }, - { "german", "German", "Deutsch", "CP1252", "de", "ger", "%d.%m.%Y %T", EPluralForms::EN_2, }, - { "hungarian", "Hungarian", "Magyar", "CP1250", "hu", "hun", "%Y. %m. %d. %T", EPluralForms::EN_2 }, - { "italian", "Italian", "Italiano", "CP1250", "it", "ita", "%d/%m/%Y %T", EPluralForms::EN_2 }, - { "korean", "Korean", "한국어", "CP949", "ko", "kor", "%F %T", EPluralForms::VI_1 }, - { "polish", "Polish", "Polski", "CP1250", "pl", "pol", "%d.%m.%Y %T", EPluralForms::PL_3 }, - { "portuguese", "Portuguese", "Português", "CP1252", "pt", "por", "%d/%m/%Y %T", EPluralForms::EN_2 }, // Note: actually Brazilian Portuguese - { "russian", "Russian", "Русский", "CP1251", "ru", "rus", "%d.%m.%Y %T", EPluralForms::UK_3 }, - { "spanish", "Spanish", "Español", "CP1252", "es", "spa", "%d/%m/%Y %T", EPluralForms::EN_2 }, - { "swedish", "Swedish", "Svenska", "CP1252", "sv", "swe", "%F %T", EPluralForms::EN_2 }, - { "turkish", "Turkish", "Türkçe", "CP1254", "tr", "tur", "%d.%m.%Y %T", EPluralForms::EN_2 }, - { "ukrainian", "Ukrainian", "Українська", "CP1251", "uk", "ukr", "%d.%m.%Y %T", EPluralForms::UK_3 }, - { "vietnamese", "Vietnamese", "Tiếng Việt", "UTF-8", "vi", "vie", "%d/%m/%Y %T", EPluralForms::VI_1 }, // Fan translation uses special encoding + { "czech", "Czech", "Čeština", "CP1250", "cs", "cze", "%d.%m.%Y %H:%M:%S", EPluralForms::CZ_3 }, + { "chinese", "Chinese", "简体中文", "GBK", "zh", "chi", "%Y-%m-%d %H:%M:%S", EPluralForms::VI_1 }, // Note: actually Simplified Chinese + { "english", "English", "English", "CP1252", "en", "eng", "%Y-%m-%d %H:%M:%S", EPluralForms::EN_2 }, // English uses international date/time format here + { "finnish", "Finnish", "Suomi", "CP1252", "fi", "fin", "%d.%m.%Y %H:%M:%S", EPluralForms::EN_2, }, + { "french", "French", "Français", "CP1252", "fr", "fre", "%d/%m/%Y %H:%M:%S", EPluralForms::FR_2, }, + { "german", "German", "Deutsch", "CP1252", "de", "ger", "%d.%m.%Y %H:%M:%S", EPluralForms::EN_2, }, + { "hungarian", "Hungarian", "Magyar", "CP1250", "hu", "hun", "%Y. %m. %d. %H:%M:%S", EPluralForms::EN_2 }, + { "italian", "Italian", "Italiano", "CP1250", "it", "ita", "%d/%m/%Y %H:%M:%S", EPluralForms::EN_2 }, + { "korean", "Korean", "한국어", "CP949", "ko", "kor", "%Y-%m-%d %H:%M:%S", EPluralForms::VI_1 }, + { "polish", "Polish", "Polski", "CP1250", "pl", "pol", "%d.%m.%Y %H:%M:%S", EPluralForms::PL_3 }, + { "portuguese", "Portuguese", "Português", "CP1252", "pt", "por", "%d/%m/%Y %H:%M:%S", EPluralForms::EN_2 }, // Note: actually Brazilian Portuguese + { "russian", "Russian", "Русский", "CP1251", "ru", "rus", "%d.%m.%Y %H:%M:%S", EPluralForms::UK_3 }, + { "spanish", "Spanish", "Español", "CP1252", "es", "spa", "%d/%m/%Y %H:%M:%S", EPluralForms::EN_2 }, + { "swedish", "Swedish", "Svenska", "CP1252", "sv", "swe", "%Y-%m-%d %H:%M:%S", EPluralForms::EN_2 }, + { "turkish", "Turkish", "Türkçe", "CP1254", "tr", "tur", "%d.%m.%Y %H:%M:%S", EPluralForms::EN_2 }, + { "ukrainian", "Ukrainian", "Українська", "CP1251", "uk", "ukr", "%d.%m.%Y %H:%M:%S", EPluralForms::UK_3 }, + { "vietnamese", "Vietnamese", "Tiếng Việt", "UTF-8", "vi", "vie", "%d/%m/%Y %H:%M:%S", EPluralForms::VI_1 }, // Fan translation uses special encoding } }; static_assert(languages.size() == static_cast(ELanguages::COUNT), "Languages array is missing a value!"); diff --git a/lib/texts/MetaString.cpp b/lib/texts/MetaString.cpp index 4ed57ce22..0bb7304ec 100644 --- a/lib/texts/MetaString.cpp +++ b/lib/texts/MetaString.cpp @@ -13,6 +13,7 @@ #include "CArtHandler.h" #include "CCreatureHandler.h" #include "CCreatureSet.h" +#include "entities/faction/CFaction.h" #include "texts/CGeneralTextHandler.h" #include "CSkillHandler.h" #include "GameConstants.h" @@ -387,11 +388,21 @@ void MetaString::replaceName(const ArtifactID & id) replaceTextID(id.toEntity(VLC)->getNameTextID()); } -void MetaString::replaceName(const MapObjectID& id) +void MetaString::replaceName(const FactionID & id) +{ + replaceTextID(id.toEntity(VLC)->getNameTextID()); +} + +void MetaString::replaceName(const MapObjectID & id) { replaceTextID(VLC->objtypeh->getObjectName(id, 0)); } +void MetaString::replaceName(const MapObjectID & id, const MapObjectSubID & subId) +{ + replaceTextID(VLC->objtypeh->getObjectName(id, subId)); +} + void MetaString::replaceName(const PlayerColor & id) { replaceTextID(TextIdentifier("vcmi.capitalColors", id.getNum()).get()); @@ -432,7 +443,7 @@ void MetaString::replaceName(const CreatureID & id, TQuantity count) //adds sing void MetaString::replaceName(const CStackBasicDescriptor & stack) { - replaceName(stack.type->getId(), stack.count); + replaceName(stack.getId(), stack.count); } VCMI_LIB_NAMESPACE_END diff --git a/lib/texts/MetaString.h b/lib/texts/MetaString.h index e6ae5fd3f..e13b55709 100644 --- a/lib/texts/MetaString.h +++ b/lib/texts/MetaString.h @@ -21,6 +21,7 @@ class MapObjectSubID; class PlayerColor; class SecondarySkill; class SpellID; +class FactionID; class GameResID; using TQuantity = si32; @@ -97,7 +98,9 @@ public: void replacePositiveNumber(int64_t txt); void replaceName(const ArtifactID & id); - void replaceName(const MapObjectID& id); + void replaceName(const FactionID& id); + void replaceName(const MapObjectID & id); + void replaceName(const MapObjectID & id, const MapObjectSubID & subId); void replaceName(const PlayerColor& id); void replaceName(const SecondarySkill& id); void replaceName(const SpellID& id); diff --git a/lib/texts/TextLocalizationContainer.cpp b/lib/texts/TextLocalizationContainer.cpp index 4a210ede3..e22e7d3c4 100644 --- a/lib/texts/TextLocalizationContainer.cpp +++ b/lib/texts/TextLocalizationContainer.cpp @@ -22,20 +22,36 @@ VCMI_LIB_NAMESPACE_BEGIN std::recursive_mutex TextLocalizationContainer::globalTextMutex; -void TextLocalizationContainer::registerStringOverride(const std::string & modContext, const std::string & language, const TextIdentifier & UID, const std::string & localized) +void TextLocalizationContainer::registerStringOverride(const std::string & modContext, const TextIdentifier & UID, const std::string & localized, const std::string & language) { std::lock_guard globalLock(globalTextMutex); assert(!modContext.empty()); - assert(!language.empty()); // NOTE: implicitly creates entry, intended - strings added by maps, campaigns, vcmi and potentially - UI mods are not registered anywhere at the moment auto & entry = stringsLocalizations[UID.get()]; - entry.overrideLanguage = language; - entry.overrideValue = localized; - if (entry.modContext.empty()) - entry.modContext = modContext; + // load string override only in following cases: + // a) string was not modified in another mod (e.g. rebalance mod gave skill new description) + // b) this string override is defined in the same mod as one that provided modified version of this string + if (entry.identifierModContext == entry.baseStringModContext || modContext == entry.baseStringModContext) + { + entry.translatedText = localized; + if (entry.identifierModContext.empty()) + { + entry.identifierModContext = modContext; + entry.baseStringModContext = modContext; + } + else + { + if (language == VLC->generaltexth->getPreferredLanguage()) + entry.overriden = true; + } + } + else + { + logGlobal->debug("Skipping translation override for string %s: changed in a different mod", UID.get()); + } } void TextLocalizationContainer::addSubContainer(const TextLocalizationContainer & container) @@ -55,7 +71,7 @@ void TextLocalizationContainer::removeSubContainer(const TextLocalizationContain subContainers.erase(std::remove(subContainers.begin(), subContainers.end(), &container), subContainers.end()); } -const std::string & TextLocalizationContainer::deserialize(const TextIdentifier & identifier) const +const std::string & TextLocalizationContainer::translateString(const TextIdentifier & identifier) const { std::lock_guard globalLock(globalTextMutex); @@ -63,108 +79,63 @@ const std::string & TextLocalizationContainer::deserialize(const TextIdentifier { for(auto containerIter = subContainers.rbegin(); containerIter != subContainers.rend(); ++containerIter) if((*containerIter)->identifierExists(identifier)) - return (*containerIter)->deserialize(identifier); + return (*containerIter)->translateString(identifier); logGlobal->error("Unable to find localization for string '%s'", identifier.get()); return identifier.get(); } const auto & entry = stringsLocalizations.at(identifier.get()); - - if (!entry.overrideValue.empty()) - return entry.overrideValue; - return entry.baseValue; + return entry.translatedText; } -void TextLocalizationContainer::registerString(const std::string & modContext, const TextIdentifier & UID, const std::string & localized, const std::string & language) +void TextLocalizationContainer::registerString(const std::string & modContext, const TextIdentifier & inputUID, const JsonNode & localized) +{ + assert(localized.isNull() || !localized.getModScope().empty()); + assert(localized.isNull() || !getModLanguage(localized.getModScope()).empty()); + + if (localized.isNull()) + registerString(modContext, modContext, inputUID, localized.String()); + else + registerString(modContext, localized.getModScope(), inputUID, localized.String()); +} + +void TextLocalizationContainer::registerString(const std::string & modContext, const TextIdentifier & UID, const std::string & localized) +{ + registerString(modContext, modContext, UID, localized); +} + +void TextLocalizationContainer::registerString(const std::string & identifierModContext, const std::string & localizedStringModContext, const TextIdentifier & UID, const std::string & localized) { std::lock_guard globalLock(globalTextMutex); - assert(!modContext.empty()); - assert(!Languages::getLanguageOptions(language).identifier.empty()); + assert(!identifierModContext.empty()); + assert(!localizedStringModContext.empty()); assert(UID.get().find("..") == std::string::npos); // invalid identifier - there is section that was evaluated to empty string - //assert(stringsLocalizations.count(UID.get()) == 0); // registering already registered string? + assert(stringsLocalizations.count(UID.get()) == 0 || boost::algorithm::starts_with(UID.get(), "map") || boost::algorithm::starts_with(UID.get(), "header")); // registering already registered string? FIXME: "header" is a workaround. VMAP needs proper integration in translation system if(stringsLocalizations.count(UID.get()) > 0) { auto & value = stringsLocalizations[UID.get()]; - value.baseLanguage = language; - value.baseValue = localized; + value.translatedText = localized; + value.identifierModContext = identifierModContext; + value.baseStringModContext = localizedStringModContext; } else { StringState value; - value.baseLanguage = language; - value.baseValue = localized; - value.modContext = modContext; + value.translatedText = localized; + value.identifierModContext = identifierModContext; + value.baseStringModContext = localizedStringModContext; stringsLocalizations[UID.get()] = value; } } -void TextLocalizationContainer::registerString(const std::string & modContext, const TextIdentifier & UID, const std::string & localized) -{ - assert(!getModLanguage(modContext).empty()); - registerString(modContext, UID, localized, getModLanguage(modContext)); -} - -bool TextLocalizationContainer::validateTranslation(const std::string & language, const std::string & modContext, const JsonNode & config) const -{ - std::lock_guard globalLock(globalTextMutex); - - bool allPresent = true; - - for(const auto & string : stringsLocalizations) - { - if (string.second.modContext != modContext) - continue; // Not our mod - - if (string.second.overrideLanguage == language) - continue; // Already translated - - if (string.second.baseLanguage == language && !string.second.baseValue.empty()) - continue; // Base string already uses our language - - if (string.second.baseLanguage.empty()) - continue; // String added in localization, not present in base language (e.g. maps/campaigns) - - if (config.Struct().count(string.first) > 0) - continue; - - if (allPresent) - logMod->warn("Translation into language '%s' in mod '%s' is incomplete! Missing lines:", language, modContext); - - std::string currentText; - if (string.second.overrideValue.empty()) - currentText = string.second.baseValue; - else - currentText = string.second.overrideValue; - - logMod->warn(R"( "%s" : "%s",)", string.first, TextOperations::escapeString(currentText)); - allPresent = false; - } - - bool allFound = true; - - // for(const auto & string : config.Struct()) - // { - // if (stringsLocalizations.count(string.first) > 0) - // continue; - // - // if (allFound) - // logMod->warn("Translation into language '%s' in mod '%s' has unused lines:", language, modContext); - // - // logMod->warn(R"( "%s" : "%s",)", string.first, TextOperations::escapeString(string.second.String())); - // allFound = false; - // } - - return allPresent && allFound; -} - -void TextLocalizationContainer::loadTranslationOverrides(const std::string & language, const std::string & modContext, const JsonNode & config) +void TextLocalizationContainer::loadTranslationOverrides(const std::string & modContext, const std::string & language, const JsonNode & config) { for(const auto & node : config.Struct()) - registerStringOverride(modContext, language, node.first, node.second.String()); + registerStringOverride(modContext, node.first, node.second.String(), language); } bool TextLocalizationContainer::identifierExists(const TextIdentifier & UID) const @@ -174,27 +145,30 @@ bool TextLocalizationContainer::identifierExists(const TextIdentifier & UID) con return stringsLocalizations.count(UID.get()); } -void TextLocalizationContainer::exportAllTexts(std::map> & storage) const +void TextLocalizationContainer::exportAllTexts(std::map> & storage, bool onlyMissing) const { std::lock_guard globalLock(globalTextMutex); for (auto const & subContainer : subContainers) - subContainer->exportAllTexts(storage); + subContainer->exportAllTexts(storage, onlyMissing); for (auto const & entry : stringsLocalizations) { - std::string textToWrite; - std::string modName = entry.second.modContext; + if (onlyMissing && entry.second.overriden) + continue; - if (modName.find('.') != std::string::npos) + std::string textToWrite; + std::string modName = entry.second.baseStringModContext; + + if (entry.second.baseStringModContext == entry.second.identifierModContext && modName.find('.') != std::string::npos) modName = modName.substr(0, modName.find('.')); - if (!entry.second.overrideValue.empty()) - textToWrite = entry.second.overrideValue; - else - textToWrite = entry.second.baseValue; + boost::range::replace(modName, '.', '_'); - storage[modName][entry.first] = textToWrite; + textToWrite = entry.second.translatedText; + + if (!textToWrite.empty()) + storage[modName][entry.first] = textToWrite; } } @@ -210,11 +184,7 @@ void TextLocalizationContainer::jsonSerialize(JsonNode & dest) const std::lock_guard globalLock(globalTextMutex); for(auto & s : stringsLocalizations) - { - dest.Struct()[s.first].String() = s.second.baseValue; - if(!s.second.overrideValue.empty()) - dest.Struct()[s.first].String() = s.second.overrideValue; - } + dest.Struct()[s.first].String() = s.second.translatedText; } TextContainerRegistrable::TextContainerRegistrable() diff --git a/lib/texts/TextLocalizationContainer.h b/lib/texts/TextLocalizationContainer.h index 28aabd640..523335c57 100644 --- a/lib/texts/TextLocalizationContainer.h +++ b/lib/texts/TextLocalizationContainer.h @@ -23,26 +23,23 @@ protected: struct StringState { /// Human-readable string that was added on registration - std::string baseValue; - - /// Language of base string - std::string baseLanguage; - - /// Translated human-readable string - std::string overrideValue; - - /// Language of the override string - std::string overrideLanguage; + std::string translatedText; /// ID of mod that created this string - std::string modContext; + std::string identifierModContext; + + /// ID of mod that provides original, untranslated version of this string + /// Different from identifierModContext if mod has modified object from another mod (e.g. rebalance mods) + std::string baseStringModContext; + + bool overriden = false; template void serialize(Handler & h) { - h & baseValue; - h & baseLanguage; - h & modContext; + h & translatedText; + h & identifierModContext; + h & baseStringModContext; } }; @@ -52,7 +49,7 @@ protected: std::vector subContainers; /// add selected string to internal storage as high-priority strings - void registerStringOverride(const std::string & modContext, const std::string & language, const TextIdentifier & UID, const std::string & localized); + void registerStringOverride(const std::string & modContext, const TextIdentifier & UID, const std::string & localized, const std::string & language); std::string getModLanguage(const std::string & modContext); @@ -60,33 +57,29 @@ protected: bool identifierExists(const TextIdentifier & UID) const; public: - /// validates translation of specified language for specified mod - /// returns true if localization is valid and complete - /// any error messages will be written to log file - bool validateTranslation(const std::string & language, const std::string & modContext, JsonNode const & file) const; - /// Loads translation from provided json /// Any entries loaded by this will have priority over texts registered normally - void loadTranslationOverrides(const std::string & language, const std::string & modContext, JsonNode const & file); + void loadTranslationOverrides(const std::string & modContext, const std::string & language, JsonNode const & file); /// add selected string to internal storage + void registerString(const std::string & modContext, const TextIdentifier & UID, const JsonNode & localized); void registerString(const std::string & modContext, const TextIdentifier & UID, const std::string & localized); - void registerString(const std::string & modContext, const TextIdentifier & UID, const std::string & localized, const std::string & language); + void registerString(const std::string & identifierModContext, const std::string & localizedStringModContext, const TextIdentifier & UID, const std::string & localized); /// returns translated version of a string that can be displayed to user template std::string translate(std::string arg1, Args ... args) const { TextIdentifier id(arg1, args ...); - return deserialize(id); + return translateString(id); } /// converts identifier into user-readable string - const std::string & deserialize(const TextIdentifier & identifier) const; + const std::string & translateString(const TextIdentifier & identifier) const; /// Debug method, returns all currently stored texts /// Format: [mod ID][string ID] -> human-readable text - void exportAllTexts(std::map> & storage) const; + void exportAllTexts(std::map> & storage, bool onlyMissing) const; /// Add or override subcontainer which can store identifiers void addSubContainer(const TextLocalizationContainer & container); diff --git a/mapeditor/Animation.cpp b/mapeditor/Animation.cpp index 0b1c89161..265dbf394 100644 --- a/mapeditor/Animation.cpp +++ b/mapeditor/Animation.cpp @@ -46,7 +46,7 @@ private: std::map > offset; std::unique_ptr data; - std::unique_ptr> palette; + QVector palette; public: DefFile(std::string Name); @@ -138,6 +138,44 @@ enum class DefType : uint32_t static FileCache animationCache; +//First 8 colors in def palette used for transparency +static constexpr std::array TARGET_PALETTE = +{ + qRgba(0, 0, 0, 0 ), // transparency ( used in most images ) + qRgba(0, 0, 0, 64 ), // shadow border ( used in battle, adventure map def's ) + qRgba(0, 0, 0, 64 ), // shadow border ( used in fog-of-war def's ) + qRgba(0, 0, 0, 128), // shadow body ( used in fog-of-war def's ) + qRgba(0, 0, 0, 128), // shadow body ( used in battle, adventure map def's ) + qRgba(0, 0, 0, 0 ), // selection / owner flag ( used in battle, adventure map def's ) + qRgba(0, 0, 0, 128), // shadow body below selection ( used in battle def's ) + qRgba(0, 0, 0, 64 ) // shadow border below selection ( used in battle def's ) +}; + +static constexpr std::array SOURCE_PALETTE = +{ + qRgba(0, 255, 255, 255), + qRgba(255, 150, 255, 255), + qRgba(255, 100, 255, 255), + qRgba(255, 50, 255, 255), + qRgba(255, 0, 255, 255), + qRgba(255, 255, 0, 255), + qRgba(180, 0, 255, 255), + qRgba(0, 255, 0, 255) +}; + +static bool colorsSimilar(const QRgb & lhs, const QRgb & rhs) +{ + // it seems that H3 does not requires exact match to replace colors -> (255, 103, 255) gets interpreted as shadow + // exact logic is not clear and requires extensive testing with image editing + // potential reason is that H3 uses 16-bit color format (565 RGB bits), meaning that 3 least significant bits are lost in red and blue component + static const int threshold = 8; + int diffR = qRed(lhs) - qRed(rhs); + int diffG = qGreen(lhs) - qGreen(rhs); + int diffB = qBlue(lhs) - qBlue(rhs); + int diffA = qAlpha(lhs) - qAlpha(rhs); + return std::abs(diffR) < threshold && std::abs(diffG) < threshold && std::abs(diffB) < threshold && std::abs(diffA) < threshold; +}; + /************************************************************************* * DefFile, class used for def loading * *************************************************************************/ @@ -160,24 +198,12 @@ DefFile::DefFile(std::string Name): }; #endif // 0 - //First 8 colors in def palette used for transparency - constexpr std::array H3Palette = - { - qRgba(0, 0, 0, 0), // 100% - transparency - qRgba(0, 0, 0, 32), // 75% - shadow border, - qRgba(0, 0, 0, 64), // TODO: find exact value - qRgba(0, 0, 0, 128), // TODO: for transparency - qRgba(0, 0, 0, 128), // 50% - shadow body - qRgba(0, 0, 0, 0), // 100% - selection highlight - qRgba(0, 0, 0, 128), // 50% - shadow body below selection - qRgba(0, 0, 0, 64) // 75% - shadow border below selection - }; data = animationCache.getCachedFile(AnimationPath::builtin("SPRITES/" + Name)); - palette = std::make_unique>(256); + palette = QVector(256); int it = 0; - ui32 type = read_le_u32(data.get() + it); + //ui32 type = read_le_u32(data.get() + it); it += 4; //int width = read_le_u32(data + it); it+=4;//not used //int height = read_le_u32(data + it); it+=4; @@ -191,59 +217,19 @@ DefFile::DefFile(std::string Name): c[0] = data[it++]; c[1] = data[it++]; c[2] = data[it++]; - (*palette)[i] = qRgba(c[0], c[1], c[2], 255); + palette[i] = qRgba(c[0], c[1], c[2], 255); } - switch(static_cast(type)) + // these colors seems to be used unconditionally + palette[0] = TARGET_PALETTE[0]; + palette[1] = TARGET_PALETTE[1]; + palette[4] = TARGET_PALETTE[4]; + + // rest of special colors are used only if their RGB values are close to H3 + for (uint32_t i = 0; i < 8; ++i) { - case DefType::SPELL: - (*palette)[0] = H3Palette[0]; - break; - case DefType::SPRITE: - case DefType::SPRITE_FRAME: - for(ui32 i= 0; i<8; i++) - (*palette)[i] = H3Palette[i]; - break; - case DefType::CREATURE: - (*palette)[0] = H3Palette[0]; - (*palette)[1] = H3Palette[1]; - (*palette)[4] = H3Palette[4]; - (*palette)[5] = H3Palette[5]; - (*palette)[6] = H3Palette[6]; - (*palette)[7] = H3Palette[7]; - break; - case DefType::MAP: - case DefType::MAP_HERO: - (*palette)[0] = H3Palette[0]; - (*palette)[1] = H3Palette[1]; - (*palette)[4] = H3Palette[4]; - //5 = owner flag, handled separately - break; - case DefType::TERRAIN: - (*palette)[0] = H3Palette[0]; - (*palette)[1] = H3Palette[1]; - (*palette)[2] = H3Palette[2]; - (*palette)[3] = H3Palette[3]; - (*palette)[4] = H3Palette[4]; - break; - case DefType::CURSOR: - (*palette)[0] = H3Palette[0]; - break; - case DefType::INTERFACE: - (*palette)[0] = H3Palette[0]; - (*palette)[1] = H3Palette[1]; - (*palette)[4] = H3Palette[4]; - //player colors handled separately - //TODO: disallow colorizing other def types - break; - case DefType::BATTLE_HERO: - (*palette)[0] = H3Palette[0]; - (*palette)[1] = H3Palette[1]; - (*palette)[4] = H3Palette[4]; - break; - default: - logAnim->error("Unknown def type %d in %s", type, Name); - break; + if (colorsSimilar(SOURCE_PALETTE[i], palette[i])) + palette[i] = TARGET_PALETTE[i]; } @@ -421,7 +407,7 @@ std::shared_ptr DefFile::loadFrame(size_t frame, size_t group) const } - img->setColorTable(*palette); + img->setColorTable(palette); return img; } diff --git a/mapeditor/CMakeLists.txt b/mapeditor/CMakeLists.txt index dc69ed286..e9f7ad6cf 100644 --- a/mapeditor/CMakeLists.txt +++ b/mapeditor/CMakeLists.txt @@ -1,8 +1,6 @@ set(editor_SRCS StdInc.cpp main.cpp - launcherdirs.cpp - jsonutils.cpp mainwindow.cpp BitmapHandler.cpp maphandler.cpp @@ -45,8 +43,6 @@ set(editor_SRCS set(editor_HEADERS StdInc.h - launcherdirs.h - jsonutils.h mainwindow.h BitmapHandler.h maphandler.h @@ -221,7 +217,11 @@ if(APPLE) set_property(GLOBAL PROPERTY AUTOGEN_TARGETS_FOLDER vcmieditor) endif() -target_link_libraries(vcmieditor vcmi Qt${QT_VERSION_MAJOR}::Widgets Qt${QT_VERSION_MAJOR}::Network) +if(ENABLE_STATIC_LIBS OR NOT (ENABLE_EDITOR AND ENABLE_LAUNCHER)) + target_compile_definitions(vcmieditor PRIVATE VCMIQT_STATIC) +endif() + +target_link_libraries(vcmieditor vcmi vcmiqt Qt${QT_VERSION_MAJOR}::Widgets Qt${QT_VERSION_MAJOR}::Network) target_include_directories(vcmieditor PUBLIC ${CMAKE_CURRENT_SOURCE_DIR} ) diff --git a/mapeditor/StdInc.h b/mapeditor/StdInc.h index 9f51e8a29..4ff74c6b0 100644 --- a/mapeditor/StdInc.h +++ b/mapeditor/StdInc.h @@ -11,7 +11,6 @@ #include "../Global.h" -#define VCMI_EDITOR_VERSION "0.2" #define VCMI_EDITOR_NAME "VCMI Map Editor" #include @@ -22,6 +21,8 @@ #include #include +#include "../vcmiqt/convpathqstring.h" + VCMI_LIB_USING_NAMESPACE using NumericPointer = typename std::conditional_t(_numeric); } - -inline QString pathToQString(const boost::filesystem::path & path) -{ -#ifdef VCMI_WINDOWS - return QString::fromStdWString(path.wstring()); -#else - return QString::fromStdString(path.string()); -#endif -} - -inline boost::filesystem::path qstringToPath(const QString & path) -{ -#ifdef VCMI_WINDOWS - return boost::filesystem::path(path.toStdWString()); -#else - return boost::filesystem::path(path.toUtf8().data()); -#endif -} diff --git a/mapeditor/graphics.cpp b/mapeditor/graphics.cpp index 3ca8bd4a9..33dfb8b4d 100644 --- a/mapeditor/graphics.cpp +++ b/mapeditor/graphics.cpp @@ -29,11 +29,11 @@ #include "../lib/texts/CGeneralTextHandler.h" #include "BitmapHandler.h" #include "../lib/CStopWatch.h" +#include "../lib/entities/hero/CHeroClassHandler.h" #include "../lib/mapObjectConstructors/AObjectTypeHandler.h" #include "../lib/mapObjectConstructors/CObjectClassesHandler.h" #include "../lib/mapObjects/CGObjectInstance.h" #include "../lib/mapObjects/ObjectTemplate.h" -#include "../lib/CHeroHandler.h" Graphics * graphics = nullptr; diff --git a/mapeditor/inspector/inspector.cpp b/mapeditor/inspector/inspector.cpp index 30aa744a3..b8dceed99 100644 --- a/mapeditor/inspector/inspector.cpp +++ b/mapeditor/inspector/inspector.cpp @@ -11,8 +11,9 @@ #include "inspector.h" #include "../lib/ArtifactUtils.h" #include "../lib/CArtHandler.h" +#include "../lib/entities/hero/CHeroClass.h" +#include "../lib/entities/hero/CHeroHandler.h" #include "../lib/spells/CSpellHandler.h" -#include "../lib/CHeroHandler.h" #include "../lib/CRandomGenerator.h" #include "../lib/mapObjectConstructors/AObjectTypeHandler.h" #include "../lib/mapObjectConstructors/CObjectClassesHandler.h" @@ -59,7 +60,7 @@ Initializer::Initializer(CGObjectInstance * o, const PlayerColor & pl) : default INIT_OBJ_TYPE(CGHeroPlaceholder); INIT_OBJ_TYPE(CGHeroInstance); INIT_OBJ_TYPE(CGSignBottle); - INIT_OBJ_TYPE(CGLighthouse); + INIT_OBJ_TYPE(FlaggableMapObject); //INIT_OBJ_TYPE(CRewardableObject); //INIT_OBJ_TYPE(CGPandoraBox); //INIT_OBJ_TYPE(CGEvent); @@ -90,6 +91,20 @@ void Initializer::initialize(CGDwelling * o) if(!o) return; o->tempOwner = defaultPlayer; + + if(o->ID == Obj::RANDOM_DWELLING || o->ID == Obj::RANDOM_DWELLING_LVL || o->ID == Obj::RANDOM_DWELLING_FACTION) + { + o->randomizationInfo = CGDwellingRandomizationInfo(); + if(o->ID == Obj::RANDOM_DWELLING_LVL) + { + o->randomizationInfo->minLevel = o->subID; + o->randomizationInfo->maxLevel = o->subID; + } + if(o->ID == Obj::RANDOM_DWELLING_FACTION) + { + o->randomizationInfo->allowedFactions.insert(FactionID(o->subID)); + } + } } void Initializer::initialize(CGGarrison * o) @@ -107,7 +122,7 @@ void Initializer::initialize(CGShipyard * o) o->tempOwner = defaultPlayer; } -void Initializer::initialize(CGLighthouse * o) +void Initializer::initialize(FlaggableMapObject * o) { if(!o) return; @@ -138,25 +153,23 @@ void Initializer::initialize(CGHeroInstance * o) o->subID = 0; o->tempOwner = PlayerColor::NEUTRAL; } - + if(o->ID == Obj::HERO) { for(auto const & t : VLC->heroh->objects) { if(t->heroClass->getId() == HeroClassID(o->subID)) { - o->type = t.get(); + o->subID = t->getId(); break; } } } - - if(o->type) + + if(o->getHeroTypeID().hasValue()) { - // o->type = VLC->heroh->objects.at(o->subID); - - o->gender = o->type->gender; - o->randomizeArmy(o->type->heroClass->faction); + o->gender = o->getHeroType()->gender; + o->randomizeArmy(o->getFactionID()); } } @@ -173,10 +186,12 @@ void Initializer::initialize(CGTownInstance * o) if(lvl > 2) o->addBuilding(BuildingID::CASTLE); if(lvl > 3) o->addBuilding(BuildingID::CAPITOL); - for(auto const & spell : VLC->spellh->objects) //add all regular spells to town + if(o->possibleSpells.empty()) { - if(!spell->isSpecial() && !spell->isCreatureAbility()) - o->possibleSpells.push_back(spell->id); + for(auto const & spellId : VLC->spellh->getDefaultAllowed()) //add all regular spells to town + { + o->possibleSpells.push_back(spellId); + } } } @@ -245,7 +260,7 @@ void Inspector::updateProperties(CGDwelling * o) } } -void Inspector::updateProperties(CGLighthouse * o) +void Inspector::updateProperties(FlaggableMapObject * o) { if(!o) return; @@ -304,7 +319,7 @@ void Inspector::updateProperties(CGHeroInstance * o) 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->type ? o->type->heroClass->getNameTranslated() : "", true); + addProperty("Hero class", o->getHeroClassID().hasValue() ? o->getHeroClass()->getNameTranslated() : "", true); { //Gender auto * delegate = new InspectorDelegate; @@ -319,20 +334,29 @@ void Inspector::updateProperties(CGHeroInstance * o) addProperty("Skills", PropertyEditorPlaceholder(), delegate, false); addProperty("Spells", PropertyEditorPlaceholder(), new HeroSpellDelegate(*o), false); - if(o->type || o->ID == Obj::PRISON) + if(o->getHeroTypeID().hasValue() || o->ID == Obj::PRISON) { //Hero type auto * delegate = new InspectorDelegate; - for(int i = 0; i < VLC->heroh->objects.size(); ++i) + for(const auto & heroPtr : VLC->heroh->objects) { - if(controller.map()->allowedHeroes.count(HeroTypeID(i)) != 0) + if(controller.map()->allowedHeroes.count(heroPtr->getId()) != 0) { - if(o->ID == Obj::PRISON || (o->type && VLC->heroh->objects[i]->heroClass->getIndex() == o->type->heroClass->getIndex())) + if(o->ID == Obj::PRISON || heroPtr->heroClass->getIndex() == o->getHeroClassID()) { - delegate->options.push_back({QObject::tr(VLC->heroh->objects[i]->getNameTranslated().c_str()), QVariant::fromValue(VLC->heroh->objects[i]->getId().getNum())}); + delegate->options.push_back({QObject::tr(heroPtr->getNameTranslated().c_str()), QVariant::fromValue(heroPtr->getIndex())}); } } } - addProperty("Hero type", o->type ? o->type->getNameTranslated() : "", delegate, false); + addProperty("Hero type", o->getHeroTypeID().hasValue() ? o->getHeroType()->getNameTranslated() : "", delegate, false); + } + { + const int maxRadius = 60; + auto * patrolDelegate = new InspectorDelegate; + patrolDelegate->options = { {QObject::tr("No patrol"), QVariant::fromValue(CGHeroInstance::NO_PATROLLING)} }; + 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); } } @@ -472,8 +496,6 @@ void Inspector::updateProperties() addProperty("ID", obj->ID.getNum()); addProperty("SubID", obj->subID); addProperty("InstanceName", obj->instanceName); - addProperty("TypeName", obj->typeName); - addProperty("SubTypeName", obj->subTypeName); if(obj->ID != Obj::HERO_PLACEHOLDER && !dynamic_cast(obj)) { @@ -495,7 +517,7 @@ void Inspector::updateProperties() UPDATE_OBJ_PROPERTIES(CGHeroPlaceholder); UPDATE_OBJ_PROPERTIES(CGHeroInstance); UPDATE_OBJ_PROPERTIES(CGSignBottle); - UPDATE_OBJ_PROPERTIES(CGLighthouse); + UPDATE_OBJ_PROPERTIES(FlaggableMapObject); UPDATE_OBJ_PROPERTIES(CRewardableObject); UPDATE_OBJ_PROPERTIES(CGPandoraBox); UPDATE_OBJ_PROPERTIES(CGEvent); @@ -543,7 +565,7 @@ void Inspector::setProperty(const QString & key, const QVariant & value) SET_PROPERTIES(CGHeroInstance); SET_PROPERTIES(CGShipyard); SET_PROPERTIES(CGSignBottle); - SET_PROPERTIES(CGLighthouse); + SET_PROPERTIES(FlaggableMapObject); SET_PROPERTIES(CRewardableObject); SET_PROPERTIES(CGPandoraBox); SET_PROPERTIES(CGEvent); @@ -556,7 +578,7 @@ void Inspector::setProperty(CArmedInstance * o, const QString & key, const QVari if(!o) return; } -void Inspector::setProperty(CGLighthouse * o, const QString & key, const QVariant & value) +void Inspector::setProperty(FlaggableMapObject * o, const QString & key, const QVariant & value) { if(!o) return; } @@ -706,15 +728,19 @@ void Inspector::setProperty(CGHeroInstance * o, const QString & key, const QVari for(auto const & t : VLC->heroh->objects) { if(t->getId() == value.toInt()) - { o->subID = value.toInt(); - o->type = t.get(); - } } - o->gender = o->type->gender; - o->randomizeArmy(o->type->heroClass->faction); + o->gender = o->getHeroType()->gender; + o->randomizeArmy(o->getHeroType()->heroClass->faction); updateProperties(); //updating other properties after change } + + if(key == "Patrol radius") + { + auto radius = value.toInt(); + o->patrol.patrolRadius = radius; + o->patrol.patrolling = radius != CGHeroInstance::NO_PATROLLING; + } } void Inspector::setProperty(CGShipyard * o, const QString & key, const QVariant & value) diff --git a/mapeditor/inspector/inspector.h b/mapeditor/inspector/inspector.h index 50d631185..1f03d5601 100644 --- a/mapeditor/inspector/inspector.h +++ b/mapeditor/inspector/inspector.h @@ -17,6 +17,7 @@ #include "../lib/GameConstants.h" #include "../lib/mapObjects/CGCreature.h" #include "../lib/mapObjects/MapObjects.h" +#include "../lib/mapObjects/FlaggableMapObject.h" #include "../lib/mapObjects/CRewardableObject.h" #include "../lib/texts/CGeneralTextHandler.h" #include "../lib/ResourceSet.h" @@ -48,7 +49,7 @@ public: DECLARE_OBJ_TYPE(CGHeroInstance); DECLARE_OBJ_TYPE(CGCreature); DECLARE_OBJ_TYPE(CGSignBottle); - DECLARE_OBJ_TYPE(CGLighthouse); + DECLARE_OBJ_TYPE(FlaggableMapObject); //DECLARE_OBJ_TYPE(CRewardableObject); //DECLARE_OBJ_TYPE(CGEvent); //DECLARE_OBJ_TYPE(CGPandoraBox); @@ -78,7 +79,7 @@ protected: DECLARE_OBJ_PROPERTY_METHODS(CGHeroInstance); DECLARE_OBJ_PROPERTY_METHODS(CGCreature); DECLARE_OBJ_PROPERTY_METHODS(CGSignBottle); - DECLARE_OBJ_PROPERTY_METHODS(CGLighthouse); + DECLARE_OBJ_PROPERTY_METHODS(FlaggableMapObject); DECLARE_OBJ_PROPERTY_METHODS(CRewardableObject); DECLARE_OBJ_PROPERTY_METHODS(CGPandoraBox); DECLARE_OBJ_PROPERTY_METHODS(CGEvent); diff --git a/mapeditor/inspector/portraitwidget.cpp b/mapeditor/inspector/portraitwidget.cpp index 669bb880c..af113be2e 100644 --- a/mapeditor/inspector/portraitwidget.cpp +++ b/mapeditor/inspector/portraitwidget.cpp @@ -10,8 +10,8 @@ #include "StdInc.h" #include "portraitwidget.h" #include "ui_portraitwidget.h" -#include "../../lib/CHeroHandler.h" #include "../Animation.h" +#include "../lib/entities/hero/CHeroHandler.h" PortraitWidget::PortraitWidget(CGHeroInstance & h, QWidget *parent): QDialog(parent), diff --git a/mapeditor/inspector/questwidget.cpp b/mapeditor/inspector/questwidget.cpp index df706c19d..395d729e6 100644 --- a/mapeditor/inspector/questwidget.cpp +++ b/mapeditor/inspector/questwidget.cpp @@ -16,12 +16,16 @@ #include "../lib/spells/CSpellHandler.h" #include "../lib/CArtHandler.h" #include "../lib/CCreatureHandler.h" -#include "../lib/CHeroHandler.h" #include "../lib/constants/StringConstants.h" #include "../lib/mapping/CMap.h" #include "../lib/mapObjects/CGHeroInstance.h" #include "../lib/mapObjects/CGCreature.h" +#include +#include +#include +#include + QuestWidget::QuestWidget(MapController & _controller, CQuest & _sh, QWidget *parent) : QDialog(parent), controller(_controller), @@ -165,7 +169,7 @@ void QuestWidget::obtainData() } for(auto & i : quest.mission.creatures) { - int index = i.type->getIndex(); + int index = i.getType()->getIndex(); ui->lCreatureId->setCurrentIndex(index); ui->lCreatureAmount->setValue(i.count); onCreatureAdd(ui->lCreatures, ui->lCreatureId, ui->lCreatureAmount); diff --git a/mapeditor/inspector/rewardswidget.cpp b/mapeditor/inspector/rewardswidget.cpp index 4a8d7ab4c..614846d2b 100644 --- a/mapeditor/inspector/rewardswidget.cpp +++ b/mapeditor/inspector/rewardswidget.cpp @@ -13,7 +13,6 @@ #include "../lib/VCMI_Lib.h" #include "../lib/CSkillHandler.h" #include "../lib/spells/CSpellHandler.h" -#include "../lib/CHeroHandler.h" #include "../lib/CArtHandler.h" #include "../lib/CCreatureHandler.h" #include "../lib/constants/StringConstants.h" @@ -24,6 +23,11 @@ #include "../lib/mapObjects/CGPandoraBox.h" #include "../lib/mapObjects/CQuest.h" +#include +#include +#include +#include + RewardsWidget::RewardsWidget(CMap & m, CRewardableObject & p, QWidget *parent) : QDialog(parent), map(m), @@ -455,7 +459,7 @@ void RewardsWidget::loadCurrentVisitInfo(int index) } for(auto & i : vinfo.reward.creatures) { - int index = i.type->getIndex(); + int index = i.getType()->getIndex(); ui->rCreatureId->setCurrentIndex(index); ui->rCreatureAmount->setValue(i.count); onCreatureAdd(ui->rCreatures, ui->rCreatureId, ui->rCreatureAmount); @@ -523,7 +527,7 @@ void RewardsWidget::loadCurrentVisitInfo(int index) } for(auto & i : vinfo.limiter.creatures) { - int index = i.type->getIndex(); + int index = i.getType()->getIndex(); ui->lCreatureId->setCurrentIndex(index); ui->lCreatureAmount->setValue(i.count); onCreatureAdd(ui->lCreatures, ui->lCreatureId, ui->lCreatureAmount); diff --git a/mapeditor/inspector/townbuildingswidget.cpp b/mapeditor/inspector/townbuildingswidget.cpp index a90bd654a..4bf95c5c8 100644 --- a/mapeditor/inspector/townbuildingswidget.cpp +++ b/mapeditor/inspector/townbuildingswidget.cpp @@ -331,7 +331,7 @@ void TownBuildingsDelegate::setEditorData(QWidget *editor, const QModelIndex &in { if(auto * ed = qobject_cast(editor)) { - auto * ctown = town.town; + auto * ctown = town.getTown(); if(!ctown) ctown = VLC->townh->randomTown; if(!ctown) diff --git a/mapeditor/inspector/towneventdialog.cpp b/mapeditor/inspector/towneventdialog.cpp index e56af191e..475c42edc 100644 --- a/mapeditor/inspector/towneventdialog.cpp +++ b/mapeditor/inspector/towneventdialog.cpp @@ -100,7 +100,7 @@ void TownEventDialog::initResources() void TownEventDialog::initBuildings() { - auto * ctown = town.town; + auto * ctown = town.getTown(); if (!ctown) ctown = VLC->townh->randomTown; if (!ctown) @@ -156,7 +156,7 @@ QStandardItem * TownEventDialog::addBuilding(const CTown& ctown, BuildingID buil void TownEventDialog::initCreatures() { auto creatures = params.value("creatures").toList(); - auto * ctown = town.town; + auto * ctown = town.getTown(); if (!ctown) ui->creaturesTable->setRowCount(GameConstants::CREATURES_PER_TOWN); else diff --git a/mapeditor/jsonutils.cpp b/mapeditor/jsonutils.cpp deleted file mode 100644 index e4eb884d6..000000000 --- a/mapeditor/jsonutils.cpp +++ /dev/null @@ -1,124 +0,0 @@ -/* - * jsonutils.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 "jsonutils.h" - -#include "../lib/json/JsonNode.h" - -static QVariantMap JsonToMap(const JsonMap & json) -{ - QVariantMap map; - for(auto & entry : json) - { - map.insert(QString::fromStdString(entry.first), JsonUtils::toVariant(entry.second)); - } - return map; -} - -static QVariantList JsonToList(const JsonVector & json) -{ - QVariantList list; - for(auto & entry : json) - { - list.push_back(JsonUtils::toVariant(entry)); - } - return list; -} - -static JsonVector VariantToList(QVariantList variant) -{ - JsonVector vector; - for(auto & entry : variant) - { - vector.push_back(JsonUtils::toJson(entry)); - } - return vector; -} - -static JsonMap VariantToMap(QVariantMap variant) -{ - JsonMap map; - for(auto & entry : variant.toStdMap()) - { - map[entry.first.toUtf8().data()] = JsonUtils::toJson(entry.second); - } - return map; -} - -VCMI_LIB_NAMESPACE_BEGIN - -namespace JsonUtils -{ - -QVariant toVariant(const JsonNode & node) -{ - switch(node.getType()) - { - case JsonNode::JsonType::DATA_NULL: - return QVariant(); - case JsonNode::JsonType::DATA_BOOL: - return QVariant(node.Bool()); - case JsonNode::JsonType::DATA_FLOAT: - return QVariant(node.Float()); - case JsonNode::JsonType::DATA_INTEGER: - return QVariant{static_cast(node.Integer())}; - case JsonNode::JsonType::DATA_STRING: - return QVariant(QString::fromStdString(node.String())); - case JsonNode::JsonType::DATA_VECTOR: - return JsonToList(node.Vector()); - case JsonNode::JsonType::DATA_STRUCT: - return JsonToMap(node.Struct()); - } - return QVariant(); -} - -QVariant JsonFromFile(QString filename) -{ - QFile file(filename); - if(!file.open(QFile::ReadOnly)) - { - logGlobal->error("Failed to open file %s. Reason: %s", qUtf8Printable(filename), qUtf8Printable(file.errorString())); - return {}; - } - - const auto data = file.readAll(); - JsonNode node(reinterpret_cast(data.data()), data.size(), filename.toStdString()); - return toVariant(node); -} - -JsonNode toJson(QVariant object) -{ - JsonNode ret; - - if(object.userType() == QMetaType::QString) - ret.String() = object.toString().toUtf8().data(); - else if(object.userType() == QMetaType::Bool) - ret.Bool() = object.toBool(); - else if(object.canConvert()) - ret.Struct() = VariantToMap(object.toMap()); - else if(object.canConvert()) - ret.Vector() = VariantToList(object.toList()); - else if(object.canConvert()) - ret.Integer() = object.toInt(); - else if(object.canConvert()) - ret.Float() = object.toFloat(); - - return ret; -} - -void JsonToFile(QString filename, QVariant object) -{ - std::fstream file(qstringToPath(filename).c_str(), std::ios::out | std::ios_base::binary); - file << toJson(object).toString(); -} - -} - -VCMI_LIB_NAMESPACE_END diff --git a/mapeditor/jsonutils.h b/mapeditor/jsonutils.h deleted file mode 100644 index 791711eb0..000000000 --- a/mapeditor/jsonutils.h +++ /dev/null @@ -1,27 +0,0 @@ -/* - * jsonutils.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 - -VCMI_LIB_NAMESPACE_BEGIN - -class JsonNode; - -namespace JsonUtils -{ -QVariant toVariant(const JsonNode & node); -QVariant JsonFromFile(QString filename); - -JsonNode toJson(QVariant object); -void JsonToFile(QString filename, QVariant object); -} - -VCMI_LIB_NAMESPACE_END diff --git a/mapeditor/launcherdirs.cpp b/mapeditor/launcherdirs.cpp deleted file mode 100644 index 97d456bb5..000000000 --- a/mapeditor/launcherdirs.cpp +++ /dev/null @@ -1,36 +0,0 @@ -/* - * launcherdirs.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 "launcherdirs.h" - -#include "../lib/VCMIDirs.h" - -static CLauncherDirs launcherDirsGlobal; - -CLauncherDirs::CLauncherDirs() -{ - QDir().mkdir(downloadsPath()); - QDir().mkdir(modsPath()); -} - -CLauncherDirs & CLauncherDirs::get() -{ - return launcherDirsGlobal; -} - -QString CLauncherDirs::downloadsPath() -{ - return pathToQString(VCMIDirs::get().userCachePath() / "downloads"); -} - -QString CLauncherDirs::modsPath() -{ - return pathToQString(VCMIDirs::get().userDataPath() / "Mods"); -} diff --git a/mapeditor/launcherdirs.h b/mapeditor/launcherdirs.h deleted file mode 100644 index 9117bd9fb..000000000 --- a/mapeditor/launcherdirs.h +++ /dev/null @@ -1,22 +0,0 @@ -/* - * launcherdirs.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 - -/// similar to lib/VCMIDirs, controls where all launcher-related data will be stored -class CLauncherDirs -{ -public: - CLauncherDirs(); - - static CLauncherDirs & get(); - - QString downloadsPath(); - QString modsPath(); -}; diff --git a/mapeditor/mainwindow.cpp b/mapeditor/mainwindow.cpp index eeaa8fd31..14c0cf5d3 100644 --- a/mapeditor/mainwindow.cpp +++ b/mapeditor/mainwindow.cpp @@ -182,6 +182,7 @@ MainWindow::MainWindow(QWidget* parent) : console = new CConsoleHandler(); logConfig = new CBasicLogConfigurator(logPath, console); logConfig->configureDefault(); + logGlobal->info("Starting map editor of '%s'", GameConstants::VCMI_VERSION); logGlobal->info("The log file will be saved to %s", logPath); //init @@ -317,7 +318,7 @@ void MainWindow::setStatusMessage(const QString & status) void MainWindow::setTitle() { - QString title = QString("%1%2 - %3 (v%4)").arg(filename, unsaved ? "*" : "", VCMI_EDITOR_NAME, VCMI_EDITOR_VERSION); + QString title = QString("%1%2 - %3 (%4)").arg(filename, unsaved ? "*" : "", VCMI_EDITOR_NAME, GameConstants::VCMI_VERSION.c_str()); setWindowTitle(title); } @@ -1122,7 +1123,7 @@ void MainWindow::on_actionUpdate_appearance_triggered() continue; } - auto * terrain = controller.map()->getTile(obj->visitablePos()).terType; + auto * terrain = controller.map()->getTile(obj->visitablePos()).getTerrain(); if(handler->isStaticObject()) { diff --git a/mapeditor/mapcontroller.cpp b/mapeditor/mapcontroller.cpp index 5fc0c25ec..bc10d558d 100644 --- a/mapeditor/mapcontroller.cpp +++ b/mapeditor/mapcontroller.cpp @@ -13,6 +13,8 @@ #include "../lib/ArtifactUtils.h" #include "../lib/GameConstants.h" +#include "../lib/entities/hero/CHeroClass.h" +#include "../lib/entities/hero/CHeroHandler.h" #include "../lib/mapObjectConstructors/AObjectTypeHandler.h" #include "../lib/mapObjectConstructors/CObjectClassesHandler.h" #include "../lib/mapObjects/ObjectTemplate.h" @@ -21,11 +23,10 @@ #include "../lib/mapping/CMapEditManager.h" #include "../lib/mapping/ObstacleProxy.h" #include "../lib/modding/CModHandler.h" -#include "../lib/modding/CModInfo.h" +#include "../lib/modding/ModDescription.h" #include "../lib/TerrainHandler.h" #include "../lib/CSkillHandler.h" #include "../lib/spells/CSpellHandler.h" -#include "../lib/CHeroHandler.h" #include "../lib/CRandomGenerator.h" #include "../lib/serializer/CMemorySerializer.h" #include "mapview.h" @@ -112,13 +113,6 @@ void MapController::repairMap(CMap * map) const allImpactedObjects.insert(allImpactedObjects.end(), map->predefinedHeroes.begin(), map->predefinedHeroes.end()); for(auto obj : allImpactedObjects) { - //setup proper names (hero name will be fixed later - if(obj->ID != Obj::HERO && obj->ID != Obj::PRISON && (obj->typeName.empty() || obj->subTypeName.empty())) - { - auto handler = VLC->objtypeh->getHandlerFor(obj->ID, obj->subID); - obj->typeName = handler->getTypeName(); - obj->subTypeName = handler->getSubTypeName(); - } //fix flags if(obj->getOwner() == PlayerColor::UNFLAGGABLE) { @@ -127,7 +121,7 @@ void MapController::repairMap(CMap * map) const dynamic_cast(obj.get()) || dynamic_cast(obj.get()) || dynamic_cast(obj.get()) || - dynamic_cast(obj.get()) || + dynamic_cast(obj.get()) || dynamic_cast(obj.get())) obj->tempOwner = PlayerColor::NEUTRAL; } @@ -138,26 +132,11 @@ void MapController::repairMap(CMap * map) const // FIXME: How about custom scenarios where defeated hero cannot be hired again? - map->allowedHeroes.insert(nih->getHeroType()); + map->allowedHeroes.insert(nih->getHeroTypeID()); auto const & type = VLC->heroh->objects[nih->subID]; assert(type->heroClass); - //TODO: find a way to get proper type name - if(obj->ID == Obj::HERO) - { - nih->typeName = "hero"; - nih->subTypeName = type->heroClass->getJsonKey(); - } - if(obj->ID == Obj::PRISON) - { - nih->typeName = "prison"; - nih->subTypeName = "prison"; - nih->subID = 0; - } - - if(obj->ID != Obj::RANDOM_HERO) - nih->type = type.get(); - + if(nih->ID == Obj::HERO) //not prison nih->appearance = VLC->objtypeh->getHandlerFor(Obj::HERO, type->heroClass->getIndex())->getTemplates().front(); //fix spellbook @@ -390,6 +369,7 @@ void MapController::pasteFromClipboard(int level) if (!canPlaceObject(level, obj, errorMsg)) { errors.push_back(std::move(errorMsg)); + continue; } auto newPos = objUniquePtr->pos + shift; if(_map->isInTheMap(newPos)) @@ -401,8 +381,8 @@ void MapController::pasteFromClipboard(int level) _scenes[level]->selectionObjectsView.selectObject(obj); _mapHandler->invalidate(obj); } - - QMessageBox::warning(main, QObject::tr("Can't place object"), errors.join('\n')); + if(!errors.isEmpty()) + QMessageBox::warning(main, QObject::tr("Can't place object"), errors.join('\n')); _scenes[level]->objectsView.draw(); _scenes[level]->passabilityView.update(); @@ -449,10 +429,10 @@ void MapController::commitObstacleFill(int level) for(auto & t : selection) { auto tl = _map->getTile(t); - if(tl.blocked || tl.visitable) + if(tl.blocked() || tl.visitable()) continue; - auto terrain = tl.terType->getId(); + auto terrain = tl.getTerrainID(); _obstaclePainters[terrain]->addBlockedTile(t); } @@ -572,8 +552,6 @@ bool MapController::canPlaceObject(int level, CGObjectInstance * newObj, QString if(newObj->ID == Obj::GRAIL && objCounter >= 1) //special case for grail { - auto typeName = QString::fromStdString(newObj->typeName); - auto subTypeName = QString::fromStdString(newObj->subTypeName); error = QObject::tr("There can only be one grail object on the map."); return false; //maplimit reached } diff --git a/mapeditor/mapcontroller.h b/mapeditor/mapcontroller.h index 7b8a246eb..55235f05f 100644 --- a/mapeditor/mapcontroller.h +++ b/mapeditor/mapcontroller.h @@ -13,9 +13,8 @@ #include "maphandler.h" #include "mapview.h" -#include "../lib/modding/CModInfo.h" - VCMI_LIB_NAMESPACE_BEGIN +struct ModVerificationInfo; using ModCompatibilityInfo = std::map; class EditorObstaclePlacer; VCMI_LIB_NAMESPACE_END diff --git a/mapeditor/maphandler.cpp b/mapeditor/maphandler.cpp index e42b84b78..a7f0b2023 100644 --- a/mapeditor/maphandler.cpp +++ b/mapeditor/maphandler.cpp @@ -19,7 +19,6 @@ #include "../lib/mapObjects/CGHeroInstance.h" #include "../lib/mapObjects/ObjectTemplate.h" #include "../lib/mapObjects/MiscObjects.h" -#include "../lib/CHeroHandler.h" #include "../lib/GameConstants.h" const int tileSize = 32; @@ -94,7 +93,7 @@ void MapHandler::drawTerrainTile(QPainter & painter, int x, int y, int z) auto & tinfo = map->getTile(int3(x, y, z)); ui8 rotation = tinfo.extTileFlags % 4; - auto terrainName = tinfo.terType->getJsonKey(); + auto terrainName = tinfo.getTerrain()->getJsonKey(); if(terrainImages.at(terrainName).size() <= tinfo.terView) return; @@ -111,7 +110,7 @@ void MapHandler::drawRoad(QPainter & painter, int x, int y, int z) if(tinfoUpper && tinfoUpper->roadType) { - auto roadName = tinfoUpper->roadType->getJsonKey(); + auto roadName = tinfoUpper->getRoad()->getJsonKey(); QRect source(0, tileSize / 2, tileSize, tileSize / 2); ui8 rotation = (tinfoUpper->extTileFlags >> 4) % 4; bool hflip = (rotation == 1 || rotation == 3); @@ -124,7 +123,7 @@ void MapHandler::drawRoad(QPainter & painter, int x, int y, int z) if(tinfo.roadType) //print road from this tile { - auto roadName = tinfo.roadType->getJsonKey(); + auto roadName = tinfo.getRoad()->getJsonKey(); QRect source(0, 0, tileSize, tileSize / 2); ui8 rotation = (tinfo.extTileFlags >> 4) % 4; bool hflip = (rotation == 1 || rotation == 3); @@ -140,11 +139,11 @@ void MapHandler::drawRiver(QPainter & painter, int x, int y, int z) { auto & tinfo = map->getTile(int3(x, y, z)); - if(tinfo.riverType->getId() == River::NO_RIVER) + if(!tinfo.hasRiver()) return; //TODO: use ui8 instead of string key - auto riverName = tinfo.riverType->getJsonKey(); + auto riverName = tinfo.getRiver()->getJsonKey(); if(riverImages.at(riverName).size() <= tinfo.riverDir) return; @@ -379,7 +378,7 @@ void MapHandler::drawObjects(QPainter & painter, int x, int y, int z, const std: if(objData.objBitmap) { - auto pos = obj->getPosition(); + auto pos = obj->anchorPos(); painter.drawImage(QPoint(x * tileSize, y * tileSize), *objData.objBitmap, object.rect, Qt::AutoColor | Qt::NoOpaqueDetection); @@ -442,9 +441,9 @@ QRgb MapHandler::getTileColor(int x, int y, int z) auto & tile = map->getTile(int3(x, y, z)); - auto color = tile.terType->minimapUnblocked; - if (tile.blocked && (!tile.visitable)) - color = tile.terType->minimapBlocked; + auto color = tile.getTerrain()->minimapUnblocked; + if (tile.blocked() && (!tile.visitable())) + color = tile.getTerrain()->minimapBlocked; return qRgb(color.r, color.g, color.b); } diff --git a/mapeditor/mapsettings/abstractsettings.cpp b/mapeditor/mapsettings/abstractsettings.cpp index a1f4afc43..a21d2f24b 100644 --- a/mapeditor/mapsettings/abstractsettings.cpp +++ b/mapeditor/mapsettings/abstractsettings.cpp @@ -13,7 +13,6 @@ #include "../mapcontroller.h" #include "../../lib/mapObjects/CGHeroInstance.h" #include "../../lib/mapObjects/CGCreature.h" -#include "../../lib/CHeroHandler.h" #include "../../lib/mapObjects/CGCreature.h" //parses date for lose condition (1m 1w 1d) @@ -115,7 +114,7 @@ std::string AbstractSettings::getMonsterName(const CMap & map, int objectIdx) std::string name; if(auto monster = dynamic_cast(map.objects[objectIdx].get())) { - name = boost::str(boost::format("%1% at %2%") % monster->getObjectName() % monster->getPosition().toString()); + name = boost::str(boost::format("%1% at %2%") % monster->getObjectName() % monster->anchorPos().toString()); } return name; } diff --git a/mapeditor/mapsettings/mapsettings.cpp b/mapeditor/mapsettings/mapsettings.cpp index 7e579cb81..f94783020 100644 --- a/mapeditor/mapsettings/mapsettings.cpp +++ b/mapeditor/mapsettings/mapsettings.cpp @@ -13,10 +13,10 @@ #include "ui_mapsettings.h" #include "mainwindow.h" -#include "../../lib/CSkillHandler.h" -#include "../../lib/spells/CSpellHandler.h" #include "../../lib/CArtHandler.h" -#include "../../lib/CHeroHandler.h" +#include "../../lib/CSkillHandler.h" +#include "../../lib/entities/hero/CHeroHandler.h" +#include "../../lib/spells/CSpellHandler.h" MapSettings::MapSettings(MapController & ctrl, QWidget *parent) : diff --git a/mapeditor/mapsettings/modsettings.cpp b/mapeditor/mapsettings/modsettings.cpp index 330cb9c82..931566fb7 100644 --- a/mapeditor/mapsettings/modsettings.cpp +++ b/mapeditor/mapsettings/modsettings.cpp @@ -11,9 +11,9 @@ #include "modsettings.h" #include "ui_modsettings.h" #include "../mapcontroller.h" +#include "../../lib/modding/ModDescription.h" #include "../../lib/modding/CModHandler.h" #include "../../lib/mapping/CMapService.h" -#include "../../lib/modding/CModInfo.h" void traverseNode(QTreeWidgetItem * item, std::function action) { @@ -45,12 +45,12 @@ void ModSettings::initialize(MapController & c) QSet modsToProcess; ui->treeMods->blockSignals(true); - auto createModTreeWidgetItem = [&](QTreeWidgetItem * parent, const CModInfo & modInfo) + auto createModTreeWidgetItem = [&](QTreeWidgetItem * parent, const ModDescription & modInfo) { - auto item = new QTreeWidgetItem(parent, {QString::fromStdString(modInfo.getVerificationInfo().name), QString::fromStdString(modInfo.getVerificationInfo().version.toString())}); - item->setData(0, Qt::UserRole, QVariant(QString::fromStdString(modInfo.identifier))); + auto item = new QTreeWidgetItem(parent, {QString::fromStdString(modInfo.getName()), QString::fromStdString(modInfo.getVersion().toString())}); + item->setData(0, Qt::UserRole, QVariant(QString::fromStdString(modInfo.getID()))); item->setFlags(item->flags() | Qt::ItemIsUserCheckable); - item->setCheckState(0, controller->map()->mods.count(modInfo.identifier) ? Qt::Checked : Qt::Unchecked); + item->setCheckState(0, controller->map()->mods.count(modInfo.getID()) ? Qt::Checked : Qt::Unchecked); //set parent check if(parent && item->checkState(0) == Qt::Checked) parent->setCheckState(0, Qt::Checked); diff --git a/mapeditor/mapsettings/timedevent.cpp b/mapeditor/mapsettings/timedevent.cpp index 1b8858480..39b983295 100644 --- a/mapeditor/mapsettings/timedevent.cpp +++ b/mapeditor/mapsettings/timedevent.cpp @@ -30,7 +30,7 @@ TimedEvent::TimedEvent(MapController & c, QListWidgetItem * t, QWidget *parent) ui->eventMessageText->setPlainText(params.value("message").toString()); ui->eventAffectsCpu->setChecked(params.value("computerAffected").toBool()); ui->eventAffectsHuman->setChecked(params.value("humanAffected").toBool()); - ui->eventFirstOccurrence->setValue(params.value("firstOccurrence").toInt()); + ui->eventFirstOccurrence->setValue(params.value("firstOccurrence").toInt() + 1); ui->eventRepeatAfter->setValue(params.value("nextOccurrence").toInt()); auto playerList = params.value("players").toList(); @@ -77,7 +77,7 @@ void TimedEvent::on_TimedEvent_finished(int result) descriptor["message"] = ui->eventMessageText->toPlainText(); descriptor["humanAffected"] = QVariant::fromValue(ui->eventAffectsHuman->isChecked()); descriptor["computerAffected"] = QVariant::fromValue(ui->eventAffectsCpu->isChecked()); - descriptor["firstOccurrence"] = QVariant::fromValue(ui->eventFirstOccurrence->value()); + descriptor["firstOccurrence"] = QVariant::fromValue(ui->eventFirstOccurrence->value() - 1); descriptor["nextOccurrence"] = QVariant::fromValue(ui->eventRepeatAfter->value()); QVariantList players; diff --git a/mapeditor/mapsettings/timedevent.ui b/mapeditor/mapsettings/timedevent.ui index 6f9febf08..59ae81f48 100644 --- a/mapeditor/mapsettings/timedevent.ui +++ b/mapeditor/mapsettings/timedevent.ui @@ -72,7 +72,11 @@ - + + + 1 + + diff --git a/mapeditor/mapview.cpp b/mapeditor/mapview.cpp index d8c055af4..29aeb9aa4 100644 --- a/mapeditor/mapview.cpp +++ b/mapeditor/mapview.cpp @@ -364,7 +364,7 @@ void MapView::mousePressEvent(QMouseEvent *event) else if(controller->map()->getTile(tile).riverType && controller->map()->getTile(tile).riverType != controller->map()->getTile(tilen).riverType) continue; - else if(controller->map()->getTile(tile).terType != controller->map()->getTile(tilen).terType) + else if(controller->map()->getTile(tile).terrainType != controller->map()->getTile(tilen).terrainType) continue; } if(event->button() == Qt::LeftButton && sc->selectionTerrainView.selection().count(tilen)) diff --git a/mapeditor/playerparams.cpp b/mapeditor/playerparams.cpp index dc4cfee94..985c01d47 100644 --- a/mapeditor/playerparams.cpp +++ b/mapeditor/playerparams.cpp @@ -77,12 +77,10 @@ PlayerParams::PlayerParams(MapController & ctrl, int playerId, QWidget *parent) { if(auto town = dynamic_cast(controller.map()->objects[i].get())) { - auto * ctown = town->town; + auto * ctown = town->getTown(); if(!ctown) - { ctown = VLC->townh->randomTown; - town->town = ctown; - } + if(ctown && town->getOwner().getNum() == playerColor) { if(playerInfo.hasMainTown && playerInfo.posOfMainTown == town->pos) diff --git a/mapeditor/scenelayer.cpp b/mapeditor/scenelayer.cpp index 32713ebef..c055a05df 100644 --- a/mapeditor/scenelayer.cpp +++ b/mapeditor/scenelayer.cpp @@ -101,9 +101,9 @@ void PassabilityLayer::update() for(int i = 0; i < map->width; ++i) { auto tl = map->getTile(int3(i, j, scene->level)); - if(tl.blocked || tl.visitable) + if(tl.blocked() || tl.visitable()) { - painter.fillRect(i * 32, j * 32, 31, 31, tl.visitable ? QColor(200, 200, 0, 64) : QColor(255, 0, 0, 64)); + painter.fillRect(i * 32, j * 32, 31, 31, tl.visitable() ? QColor(200, 200, 0, 64) : QColor(255, 0, 0, 64)); } } } @@ -425,7 +425,7 @@ void ObjectsLayer::setDirty(const CGObjectInstance * object) { for(int i = 0; i < object->getWidth(); ++i) { - setDirty(object->getPosition().x - i, object->getPosition().y - j); + setDirty(object->anchorPos().x - i, object->anchorPos().y - j); } } } @@ -479,7 +479,7 @@ void SelectionObjectsLayer::draw() { if(obj != newObject) { - QRect bbox(obj->getPosition().x, obj->getPosition().y, 1, 1); + QRect bbox(obj->anchorPos().x, obj->anchorPos().y, 1, 1); for(auto & t : obj->getBlockedPos()) { QPoint topLeft(std::min(t.x, bbox.topLeft().x()), std::min(t.y, bbox.topLeft().y())); @@ -496,7 +496,7 @@ void SelectionObjectsLayer::draw() if(selectionMode == SelectionMode::MOVEMENT && (shift.x() || shift.y())) { painter.setOpacity(0.7); - auto newPos = QPoint(obj->getPosition().x, obj->getPosition().y) + shift; + auto newPos = QPoint(obj->anchorPos().x, obj->anchorPos().y) + shift; handler->drawObjectAt(painter, obj, newPos.x(), newPos.y()); } } @@ -517,7 +517,7 @@ CGObjectInstance * SelectionObjectsLayer::selectObjectAt(int x, int y, const CGO if(!object.obj || object.obj == ignore || lockedObjects.count(object.obj)) continue; - if(object.obj->visitableAt(x, y)) + if(object.obj->visitableAt(int3(x, y, scene->level))) { return const_cast(object.obj); } @@ -529,7 +529,7 @@ CGObjectInstance * SelectionObjectsLayer::selectObjectAt(int x, int y, const CGO if(!object.obj || object.obj == ignore || lockedObjects.count(object.obj)) continue; - if(object.obj->blockingAt(x, y)) + if(object.obj->blockingAt(int3(x, y, scene->level))) { return const_cast(object.obj); } @@ -541,7 +541,7 @@ CGObjectInstance * SelectionObjectsLayer::selectObjectAt(int x, int y, const CGO if(!object.obj || object.obj == ignore || lockedObjects.count(object.obj)) continue; - if(object.obj->coveringAt(x, y)) + if(object.obj->coveringAt(int3(x, y, scene->level))) { return const_cast(object.obj); } diff --git a/mapeditor/translation/chinese.ts b/mapeditor/translation/chinese.ts index a55371343..1f0647cee 100644 --- a/mapeditor/translation/chinese.ts +++ b/mapeditor/translation/chinese.ts @@ -359,7 +359,7 @@ - + View underground 查看地下 @@ -441,9 +441,9 @@ - - - + + + Update appearance 更新外观 @@ -574,92 +574,92 @@ Ctrl+Shift+= - + Confirmation 确认 - + Unsaved changes will be lost, are you sure? 未保存的改动会丢失,你确定要这么做吗? - + Open map 打开地图 - + All supported maps (*.vmap *.h3m);;VCMI maps(*.vmap);;HoMM3 maps(*.h3m) 所有支持的地图类型(*.vmap *.h3m);;VCMI地图(*.vmap);;英雄无敌3地图(*.h3m) - + Save map 保存地图 - + VCMI maps (*.vmap) VCMI地图(*.vmap) - + Type 类型 - + View surface 查看地上 - + No objects selected 未选择任何物体 - + This operation is irreversible. Do you want to continue? 此操作无法被撤销,你确定要继续么? - + Errors occurred. %1 objects were not updated 发生错误!%1 物体未完成更新 - + Save to image 保存为图片 - + Select maps to convert 选择待转换的地图 - + HoMM3 maps(*.h3m) 英雄无敌3地图文件(*.h3m) - + Choose directory to save converted maps 选择保存转换地图的目录 - + Operation completed 操作完成 - + Successfully converted %1 maps 成功转换 %1 地图 - + Failed to convert the map. Abort operation 转换地图失败,操作终止 @@ -844,7 +844,7 @@ (默认) - + Player ID: %1 玩家ID: %1 @@ -909,53 +909,67 @@ 高级 - + Compliant 屈服的 - + Friendly 友善的 - + Aggressive 好斗的 - + Hostile 有敌意的 - + Savage 野蛮的 - - + + + No patrol + 无巡逻 + + + + + %n tile(s) + + %n格 + + + + + neutral 中立 - + UNFLAGGABLE 没有旗帜 - + Can't place object 无法放置物体 - + There can only be one grail object on the map. 只能放置一个神器在地图上。 - + Hero %1 cannot be created as NEUTRAL. 英雄 %1 无法在中立阵营被创建。 @@ -1083,12 +1097,12 @@ 玩家 - + None - + Day %1 第 %1 日 @@ -1356,18 +1370,18 @@ 玩家 - + None - + Day %1 %1 天 - - + + Reward %1 奖励 %1 @@ -1433,32 +1447,32 @@ 首次发生天数 - + Repeat after (0 = no repeat) 重复周期 (0 = 不重复) - + Affected players 生效玩家 - + Resources 资源 - + type 类型 - + qty 数量 - + Ok 确定 diff --git a/mapeditor/translation/czech.ts b/mapeditor/translation/czech.ts index cfdc136c7..204b983c6 100644 --- a/mapeditor/translation/czech.ts +++ b/mapeditor/translation/czech.ts @@ -67,22 +67,22 @@ Author - + Autor Author contact (e.g. email) - + Kontakt na autora (např. email) Map Creation Time - + Čas vytvoření mapy Map Version - + Verze mapy @@ -359,7 +359,7 @@ - + View underground Zobrazit podzemí @@ -441,9 +441,9 @@ - - - + + + Update appearance Aktualizovat vzhled @@ -574,92 +574,92 @@ Ctrl+Shift+= - + Confirmation Potvrzení - + Unsaved changes will be lost, are you sure? Neuložené změny budou ztraceny, jste si jisti? - + Open map Otevřít mapu - + All supported maps (*.vmap *.h3m);;VCMI maps(*.vmap);;HoMM3 maps(*.h3m) Všechny podporované mapy (*.vmap *.h3m);; Mapy VCMI(*.vmap);;Mapy HoMM3(*.h3m) - + Save map Uložit mapu - + VCMI maps (*.vmap) Mapy VCMI (*.vmap) - + Type Druh - + View surface Zobrazit povrch - + No objects selected Nejsou vybrány žádné objekty - + This operation is irreversible. Do you want to continue? Tento úkon je nezvratný. Chcete pokračovat? - + Errors occurred. %1 objects were not updated Nastaly chyby. Nebylo aktualizováno %1 objektů - + Save to image Uložit do obrázku - + Select maps to convert Vyberte mapy pro převod - + HoMM3 maps(*.h3m) Mapy HoMM3 (*.h3m) - + Choose directory to save converted maps Vyberte složku pro uložení převedených map - + Operation completed Operace dokončena - + Successfully converted %1 maps Úspěšně převedeno %1 map - + Failed to convert the map. Abort operation Převod map selhal. Úkon zrušen @@ -788,12 +788,12 @@ Set all mods having a game content as mandatory - + Nastavte všechny modifikace obsahující herní obsah jako povinné Full content mods - + Modifikace s kompletním herním obsahem @@ -836,7 +836,7 @@ Generate hero at main - + Vytvořit hrdinu v hlavním městě @@ -844,7 +844,7 @@ (výchozí) - + Player ID: %1 ID hráče: %1 @@ -877,7 +877,7 @@ Portrait - + Portrét @@ -909,55 +909,71 @@ Expert - + Compliant Ochotná - + Friendly Přátelská - + Aggressive Agresivní - + Hostile Nepřátelská - + Savage Brutální - - + + + No patrol + Bez hlídky + + + + + %n tile(s) + + %n pole + %n pole + %n polí + + + + + neutral neutrální - + UNFLAGGABLE NEOZNAČITELNÝ - + Can't place object - Nelze umístit objekt + Nelze umístit objekt - + There can only be one grail object on the map. - + Na mapě může být pouze jeden grál. - + Hero %1 cannot be created as NEUTRAL. - + Hrdina %1 nemůže být vytvořen jako NEUTRÁLNÍ. @@ -1083,12 +1099,12 @@ Hráči - + None Žádný - + Day %1 Den %1 @@ -1129,7 +1145,7 @@ On select text - + Text při výběru @@ -1210,7 +1226,7 @@ Overflow - + Přetečení @@ -1356,18 +1372,18 @@ Hráči - + None Žádný - + Day %1 Den %1 - - + + Reward %1 Odměna %1 @@ -1433,32 +1449,32 @@ Den prvního výskytu - + Repeat after (0 = no repeat) Opakovat po (0 = bez opak.) - + Affected players Ovlivnění hráči - + Resources Zdroje - + type druh - + qty množství - + Ok Dobře @@ -1473,37 +1489,37 @@ Build all - + Postavit vše Demolish all - + Zbořit vše Enable all - + Povolit vše Disable all - + Zakázat vše Type - Druh + Typ Enabled - + Povoleno Built - + Postaveno @@ -1511,77 +1527,77 @@ Town event - + Událost ve městě General - + Hlavní Event name - Název události + Název události Type event message text - Zadejte text zprávy události + Zadejte text události Day of first occurrence - Den prvního výskytu + Den prvního výskytu Repeat after (0 = no repeat) - Opakovat po (0 = bez opak.) + Opakovat po (0 = bez opakováí) Affected players - Ovlivnění hráči + Ovlivnění hráči affects human - ovlivňuje lidi + ovlivňuje lidi affects AI - ovlivňuje AI + ovlivňuje AI Resources - Zdroje + Zdroje Buildings - Budovy + Budovy Creatures - Jednotky + Jednotky OK - + OK Creature level %1 / Creature level %1 Upgrade - + Úroveň jednotky %1 / Úroveň jednotky%1 vylepšení Day %1 - %2 - + Den %1 - %2 @@ -1589,32 +1605,32 @@ Town events - + Události ve městě Timed events - Načasované události + Načasované události Add - Přidat + Přidat Remove - Odebrat + Odebrat Day %1 - %2 - + Den %1 - %2 New event - Nová událost + Nová událost @@ -1622,17 +1638,17 @@ Spells - Kouzla + Kouzla Customize spells - Přizpůsobit kouzla + Přizpůsobit kouzla Level 1 - Úroveň 1 + 1. stupeň @@ -1641,7 +1657,7 @@ Spell that may appear in mage guild - + Kouzlo, které se může objevit ve věži kouzel @@ -1650,27 +1666,27 @@ Spell that must appear in mage guild - + Kouzlo, které se musí objevit ve věži kouzel Level 2 - Úroveň 2 + 2. stupeň Level 3 - Úroveň 3 + 3. stupeň Level 4 - Úroveň 4 + 4. stupeň Level 5 - Úroveň 5 + 5. stupeň @@ -1722,7 +1738,7 @@ Map validation results - Výsledky posudku mapy + Výsledky ověření mapy @@ -1732,27 +1748,27 @@ No factions allowed for player %1 - + Pro hráče %1 nejsou povoleny žádné frakce No players allowed to play this map - Žádní hráči nejsou dovoleni hrát tuto mapu + Tato mapa neumožňuje hru žádnému hráči Map is allowed for one player and cannot be started - Mapa je pouze pro jednoho hráče na nemůže být spuštěna + Tato mapa je určena pouze pro jednoho hráče a nelze ji spustit No human players allowed to play this map - Žádní lidští hráči nejsou dovoleni hrát tuto mapu + Na této mapě není povolen žádný lidský hráč Armored instance %1 is UNFLAGGABLE but must have NEUTRAL or player owner - + Obrněná instance %1 nemůže být označena vlajkou, ale musí mít vlastníka nebo neutrálního nebo hráče @@ -1762,17 +1778,17 @@ Spell scroll %1 doesn't have instance assigned and must be removed - + Kouzelný svitek %1 nemá přiřazenou instanci a musí být odstraněn Artifact %1 is prohibited by map settings - + Artefakt %1 je zakázán nastavením mapy Player %1 has no towns and heroes assigned - + Hráč %1 nemá přiřazena žádná města ani hrdiny @@ -1797,7 +1813,7 @@ Hero %1 has an empty type and must be removed - + Hrdina %1 nemá přiřazený typ a musí být odstraněn @@ -1923,7 +1939,7 @@ Two level map - Dvě úrovně + Dvouvrstvá mapa @@ -2016,7 +2032,7 @@ Monster strength - Síla příšer + Síla jednotek @@ -2037,7 +2053,7 @@ Water content - Obsah vody + Vodní obsah @@ -2052,22 +2068,22 @@ Roads - Cesty + Cesty Dirt - + Hlína Gravel - + Štěrk Cobblestone - + Dlažba @@ -2115,7 +2131,7 @@ Filepath of the map to open. - Cesta k souboru mapy pro otevření. + Cesta k souboru mapy, kterou chcete otevřít. @@ -2135,7 +2151,7 @@ Delete original files, for the ones split / converted. - + Odstranit původní soubory pro ty, které byly rozděleny nebo převedeny. diff --git a/mapeditor/translation/english.ts b/mapeditor/translation/english.ts index 2eec9132d..69f249ac0 100644 --- a/mapeditor/translation/english.ts +++ b/mapeditor/translation/english.ts @@ -359,7 +359,7 @@ - + View underground @@ -441,9 +441,9 @@ - - - + + + Update appearance @@ -574,92 +574,92 @@ - + Confirmation - + Unsaved changes will be lost, are you sure? - + Open map - + All supported maps (*.vmap *.h3m);;VCMI maps(*.vmap);;HoMM3 maps(*.h3m) - + Save map - + VCMI maps (*.vmap) - + Type - + View surface - + No objects selected - + This operation is irreversible. Do you want to continue? - + Errors occurred. %1 objects were not updated - + Save to image - + Select maps to convert - + HoMM3 maps(*.h3m) - + Choose directory to save converted maps - + Operation completed - + Successfully converted %1 maps - + Failed to convert the map. Abort operation @@ -844,7 +844,7 @@ - + Player ID: %1 @@ -909,53 +909,68 @@ - + Compliant - + Friendly - + Aggressive - + Hostile - + Savage - - + + + No patrol + + + + + + %n tile(s) + + + + + + + + neutral - + UNFLAGGABLE - + Can't place object - + There can only be one grail object on the map. - + Hero %1 cannot be created as NEUTRAL. @@ -1083,12 +1098,12 @@ - + None - + Day %1 @@ -1356,18 +1371,18 @@ - + None - + Day %1 - - + + Reward %1 @@ -1433,32 +1448,32 @@ - + Repeat after (0 = no repeat) - + Affected players - + Resources - + type - + qty - + Ok diff --git a/mapeditor/translation/french.ts b/mapeditor/translation/french.ts index 919ed20b9..303fe6915 100644 --- a/mapeditor/translation/french.ts +++ b/mapeditor/translation/french.ts @@ -359,7 +359,7 @@ - + View underground Voir le sous-sol @@ -441,9 +441,9 @@ - - - + + + Update appearance Mettre à jour l'apparence @@ -574,92 +574,92 @@ Ctrl+Maj+= - + Confirmation Confirmation - + Unsaved changes will be lost, are you sure? Les modifications non sauvegardées seront perdues. Êtes-vous sûr ? - + Open map Ouvrir une carte - + All supported maps (*.vmap *.h3m);;VCMI maps(*.vmap);;HoMM3 maps(*.h3m) Toutes les cartes prises en charge (*.vmap *.h3m);;Cartes VCMI (*.vmap);;Cartes HoMM3 (*.h3m) - + Save map Enregistrer la carte - + VCMI maps (*.vmap) Cartes VCMI (*.vmap) - + Type Type - + View surface Afficher la surface - + No objects selected Pas d'objets sélectionnés - + This operation is irreversible. Do you want to continue? Cette opération est irreversible. Voulez-vous continuer ? - + Errors occurred. %1 objects were not updated Erreur rencontrée. %1 objets n'ont pas étés mis à jour - + Save to image Sauvegarder en tant qu'image - + Select maps to convert Sélectionner les cartes à convertir - + HoMM3 maps(*.h3m) Cartes HoMM3(*.h3m) - + Choose directory to save converted maps Sélectionner le dossier ou sauvegarder les cartes converties - + Operation completed Opération terminée - + Successfully converted %1 maps Conversion éffectuée avec succès des %1 cartes - + Failed to convert the map. Abort operation Erreur de conversion de carte. Opération annulée @@ -844,7 +844,7 @@ (par défaut) - + Player ID: %1 Identifiant du joueur : %1 @@ -909,53 +909,68 @@ Expert - + Compliant Compérhensif - + Friendly Amical - + Aggressive Aggressif - + Hostile Hostile - + Savage Sauvage - - + + + No patrol + + + + + + %n tile(s) + + + + + + + + neutral neutre - + UNFLAGGABLE INCLASSABLE - + Can't place object Impossible de placer l'objet - + There can only be one grail object on the map. Il ne peut y avoir qu'un objet Graal sur la carte. - + Hero %1 cannot be created as NEUTRAL. Le héro %1 ne peut pas être créé en tant que NEUTRE. @@ -1083,12 +1098,12 @@ Joueurs - + None Aucune - + Day %1 Jour %1 @@ -1356,18 +1371,18 @@ Joueurs - + None Aucune - + Day %1 Jour %1 - - + + Reward %1 Récompense %1 @@ -1433,32 +1448,32 @@ Jour de la première occurrence - + Repeat after (0 = no repeat) Récurrence (0 = pas de récurrence) - + Affected players Joueurs affectés - + Resources Resources - + type type - + qty quantité - + Ok OK diff --git a/mapeditor/translation/german.ts b/mapeditor/translation/german.ts index eebafd7ee..71f0c0cd9 100644 --- a/mapeditor/translation/german.ts +++ b/mapeditor/translation/german.ts @@ -67,22 +67,22 @@ Author - + Autor Author contact (e.g. email) - + Autor-Kontakt (z.B. E-Mail) Map Creation Time - + Kartenerstellungszeitpunkt Map Version - + Kartenversion @@ -359,7 +359,7 @@ - + View underground Ansicht Untergrund @@ -441,9 +441,9 @@ - - - + + + Update appearance Aussehen aktualisieren @@ -574,92 +574,92 @@ Strg+Umschalt+= - + Confirmation Bestätigung - + Unsaved changes will be lost, are you sure? Ungespeicherte Änderungen gehen verloren, sind sie sicher? - + Open map Karte öffnen - + All supported maps (*.vmap *.h3m);;VCMI maps(*.vmap);;HoMM3 maps(*.h3m) Alle unterstützten Karten (*.vmap *.h3m);;VCMI-Karten (*.vmap);;HoMM3-Karten (*.h3m) - + Save map Karte speichern - + VCMI maps (*.vmap) VCMI-Karten (*.vmap) - + Type Typ - + View surface Oberfläche anzeigen - + No objects selected Keine Objekte selektiert - + This operation is irreversible. Do you want to continue? Diese Operation ist unumkehrbar. Möchten sie fortsetzen? - + Errors occurred. %1 objects were not updated Fehler sind aufgetreten. %1 Objekte konnten nicht aktualisiert werden - + Save to image Als Bild speichern - + Select maps to convert Zu konvertierende Karten auswählen - + HoMM3 maps(*.h3m) HoMM3-Karten (*.h3m) - + Choose directory to save converted maps Verzeichnis zum Speichern der konvertierten Karten wählen - + Operation completed Vorgang abgeschlossen - + Successfully converted %1 maps Erfolgreiche Konvertierung von %1 Karten - + Failed to convert the map. Abort operation Die Karte konnte nicht konvertiert werden. Vorgang abgebrochen @@ -844,7 +844,7 @@ (Standard) - + Player ID: %1 Spieler-ID: %1 @@ -909,55 +909,70 @@ Experte - + Compliant Konform - + Friendly Freundlich - + Aggressive Aggressiv - + Hostile Feindlich - + Savage Wild - - + + + No patrol + Keine Streife + + + + + %n tile(s) + + %n Kachel + %n Kacheln + + + + + neutral neutral - + UNFLAGGABLE UNFLAGGBAR - + Can't place object - Objekt kann nicht platziert werden + Objekt kann nicht platziert werden - + There can only be one grail object on the map. - + Es kann sich nur ein Gral auf der Karte befinden. - + Hero %1 cannot be created as NEUTRAL. - + Held %1 kann nicht als NEUTRAL erstellt werden. @@ -1083,12 +1098,12 @@ Spieler - + None Keine - + Day %1 Tag %1 @@ -1356,18 +1371,18 @@ Spieler - + None Keine - + Day %1 Tag %1 - - + + Reward %1 Belohnung %1 @@ -1433,32 +1448,32 @@ Tag des ersten Auftretens - + Repeat after (0 = no repeat) Wiederholung nach (0 = keine Wiederholung) - + Affected players Betroffene Spieler - + Resources Ressourcen - + type Typ - + qty anz. - + Ok Ok @@ -1473,37 +1488,37 @@ Build all - + Alle bauen Demolish all - + Alle zerstören Enable all - + Alle aktivieren Disable all - + Alle deaktivieren Type - Typ + Typ Enabled - + Aktiviert Built - + Gebaut @@ -1511,77 +1526,77 @@ Town event - + Stadt Ereignis General - Allgemein + Allgemein Event name - Name des Ereignisses + Ereignis-Name Type event message text - Ereignistext eingeben + Ereignistext eingeben Day of first occurrence - Tag des ersten Auftretens + Tag des ersten Auftretens Repeat after (0 = no repeat) - Wiederholung nach (0 = keine Wiederholung) + Wiederholung nach (0 = keine Wiederholung) Affected players - Betroffene Spieler + Betroffene Spieler affects human - beeinflusst Menschen + beeinflusst Menschen affects AI - beeinflusst KI + beeinflusst KI Resources - Ressourcen + Ressourcen Buildings - Gebäude + Gebäude Creatures - Kreaturen + Kreaturen OK - + OK Creature level %1 / Creature level %1 Upgrade - + Kreaturlevel %1 / Kreaturlevel %1 Aufgerüstet Day %1 - %2 - + Tag %1 - %2 @@ -1589,32 +1604,32 @@ Town events - + Stadt Ereignisse Timed events - Zeitlich begrenzte Ereignisse + Zeitlich begrenzte Ereignisse Add - Hinzufügen + Hinzufügen Remove - Entfernen + Entfernen Day %1 - %2 - + Tag %1 - %2 New event - Neues Ereignis + Neues Ereignis @@ -1622,17 +1637,17 @@ Spells - Zaubersprüche + Zaubersprüche Customize spells - Zaubersprüche anpassen + Zaubersprüche anpassen Level 1 - Level 1 + Level 1 @@ -1641,7 +1656,7 @@ Spell that may appear in mage guild - + Zauberspruch, der in der Magiergilde erscheinen kann @@ -1650,27 +1665,27 @@ Spell that must appear in mage guild - + Zauberspruch, der in der Magiergilde erscheinen muss Level 2 - Level 2 + Level 2 Level 3 - Level 3 + Level 3 Level 4 - Level 4 + Level 4 Level 5 - Level 5 + Level 5 @@ -1762,17 +1777,17 @@ Spell scroll %1 doesn't have instance assigned and must be removed - + Zauberschriftrolle %1 hat keine Instanz zugewiesen und muss entfernt werden Artifact %1 is prohibited by map settings - + Artefakt %1 ist durch Karteneinstellungen verboten Player %1 has no towns and heroes assigned - + Spieler %1 hat keine Städte und Helden zugewiesen @@ -2052,22 +2067,22 @@ Roads - Straßen + Straßen Dirt - + Erde Gravel - + Kies Cobblestone - + Kopfsteinpflaster diff --git a/mapeditor/translation/polish.ts b/mapeditor/translation/polish.ts index 4d65128c2..e9d0e63ce 100644 --- a/mapeditor/translation/polish.ts +++ b/mapeditor/translation/polish.ts @@ -359,7 +359,7 @@ - + View underground Pokaż podziemia @@ -441,9 +441,9 @@ - - - + + + Update appearance Aktualizuj wygląd @@ -574,92 +574,92 @@ Ctrl+Shift+= - + Confirmation Potwierdzenie - + Unsaved changes will be lost, are you sure? Niezapisane zmiany zostaną utracone, jesteś pewny? - + Open map Otwórz mapę - + All supported maps (*.vmap *.h3m);;VCMI maps(*.vmap);;HoMM3 maps(*.h3m) Wszystkie wspierane mapy (*.vmap *.h3m);;Mapy VCMI(*.vmap);;Mapy HoMM3(*.h3m) - + Save map Zapisz mapę - + VCMI maps (*.vmap) Mapy VCMI (*.vmap) - + Type Typ - + View surface Pokaż powierzchnię - + No objects selected Brak wybranych obiektów - + This operation is irreversible. Do you want to continue? Ta operacja jest nieodwracalna. Czy chcesz kontynuować? - + Errors occurred. %1 objects were not updated Wystąpiły błędy. %1 obiektów nie zostało zaktualizowanych - + Save to image Zapisz jako obraz - + Select maps to convert Wybierz mapy do konwersji - + HoMM3 maps(*.h3m) Mapy HoMM3(*.h3m) - + Choose directory to save converted maps Wybierz folder zapisu skonwertowanych map - + Operation completed Operacja zakończona - + Successfully converted %1 maps Pomyślnie skonwertowano %1 map - + Failed to convert the map. Abort operation Nieudana konwersja mapy. Przerywanie operacji @@ -844,7 +844,7 @@ (domyślny) - + Player ID: %1 ID gracza: %1 @@ -909,53 +909,69 @@ Ekspert - + Compliant Przyjazny - + Friendly Przychylny - + Aggressive Agresywny - + Hostile Wrogi - + Savage Nienawistny - - + + + No patrol + + + + + + %n tile(s) + + + + + + + + + neutral neutralny - + UNFLAGGABLE NIEOFLAGOWYWALNY - + Can't place object Nie można umieścić obiektu - + There can only be one grail object on the map. - + Hero %1 cannot be created as NEUTRAL. @@ -1083,12 +1099,12 @@ Gracze - + None Brak - + Day %1 Dzień %1 @@ -1356,18 +1372,18 @@ Gracze - + None Brak - + Day %1 Dzień %1 - - + + Reward %1 Nagroda %1 @@ -1433,32 +1449,32 @@ Dzień pierwszego wystąpienia - + Repeat after (0 = no repeat) Powtórz po... (0 = nigdy) - + Affected players Dotyczy graczy - + Resources Zasoby - + type typ - + qty ilość - + Ok Ok diff --git a/mapeditor/translation/portuguese.ts b/mapeditor/translation/portuguese.ts index 668d74a12..488062ea3 100644 --- a/mapeditor/translation/portuguese.ts +++ b/mapeditor/translation/portuguese.ts @@ -72,7 +72,7 @@ Author contact (e.g. email) - Contato do autor (ex.: e-mail) + Contato do autor (ex: e-mail) @@ -87,7 +87,7 @@ Limit maximum heroes level - Limite máximo do nível dos heróis + Limitar nível máximo dos heróis @@ -207,7 +207,7 @@ No special loss - Sem perda especial + Sem derrota especial @@ -359,7 +359,7 @@ - + View underground Visualizar subterrâneo @@ -371,7 +371,7 @@ Cut - Cortar + Recortar @@ -441,9 +441,9 @@ - - - + + + Update appearance Atualizar aparência @@ -516,12 +516,12 @@ Lock - Travar + Bloquear Lock objects on map to avoid unnecessary changes - Travar objetos no mapa para evitar alterações desnecessárias + Bloquear objetos no mapa para evitar alterações desnecessárias @@ -566,7 +566,7 @@ Zoom reset - Redefinir do zoom + Redefinir zoom @@ -574,92 +574,92 @@ Ctrl+Shift+= - + Confirmation Confirmação - + Unsaved changes will be lost, are you sure? As alterações não salvas serão perdidas. Tem certeza? - + Open map Abrir mapa - + All supported maps (*.vmap *.h3m);;VCMI maps(*.vmap);;HoMM3 maps(*.h3m) Todos os mapas suportados (*.vmap *.h3m);;Mapas do VCMI (*.vmap);;Mapas do HoMM3 (*.h3m) - + Save map Salvar mapa - + VCMI maps (*.vmap) Mapas do VCMI (*.vmap) - + Type Tipo - + View surface Visualizar superfície - + No objects selected Nenhum objeto selecionado - + This operation is irreversible. Do you want to continue? Esta operação é irreversível. Deseja continuar? - + Errors occurred. %1 objects were not updated Ocorreram erros. %1 objetos não foram atualizados - + Save to image Salvar como imagem - + Select maps to convert Selecionar mapas para converter - + HoMM3 maps(*.h3m) Mapas do HoMM3 (*.h3m) - + Choose directory to save converted maps Escolher diretório para salvar mapas convertidos - + Operation completed Operação concluída - + Successfully converted %1 maps %1 mapas foram convertidos com sucesso - + Failed to convert the map. Abort operation Falha ao converter o mapa. Abortar operação @@ -831,7 +831,7 @@ Random faction - Fação aleatória + Facção aleatória @@ -844,7 +844,7 @@ (padrão) - + Player ID: %1 ID do Jogador: %1 @@ -909,53 +909,68 @@ Experiente - + Compliant - Conformista + Complacente - + Friendly Amigável - + Aggressive Agressivo - + Hostile Hostil - + Savage Selvagem - - + + + No patrol + Sem patrulha + + + + + %n tile(s) + + %n bloco + %n blocos + + + + + neutral neutro - + UNFLAGGABLE NÃO TEM BANDEIRA - + Can't place object Não é possível colocar objeto - + There can only be one grail object on the map. Pode haver apenas um objeto graal no mapa. - + Hero %1 cannot be created as NEUTRAL. O herói %1 não pode ser criado como NEUTRO. @@ -1010,7 +1025,7 @@ Primary skills - Habilidades principais + Habilidades primárias @@ -1083,12 +1098,12 @@ Jogadores - + None Nenhum - + Day %1 Dia %1 @@ -1226,7 +1241,7 @@ Primary skills - Habilidades principais + Habilidades primárias @@ -1356,18 +1371,18 @@ Jogadores - + None Nenhum - + Day %1 Dia %1 - - + + Reward %1 Recompensa %1 @@ -1382,7 +1397,7 @@ Tavern rumors - Boatos da taverna + Rumores da taverna @@ -1433,32 +1448,32 @@ Dia da primeira ocorrência - + Repeat after (0 = no repeat) Repetir após (0 = não repetir) - + Affected players Jogadores afetados - + Resources Recursos - + type tipo - + qty - quantidade + qtd - + Ok Ok @@ -2052,22 +2067,22 @@ Roads - Estradas + Estradas Dirt - + Terra Gravel - + Cascalho Cobblestone - + Paralelepípedo diff --git a/mapeditor/translation/russian.ts b/mapeditor/translation/russian.ts index c0f54b7a7..38f261dcd 100644 --- a/mapeditor/translation/russian.ts +++ b/mapeditor/translation/russian.ts @@ -359,7 +359,7 @@ - + View underground Вид на подземелье @@ -441,9 +441,9 @@ - - - + + + Update appearance Обновить вид @@ -574,92 +574,92 @@ - + Confirmation - + Unsaved changes will be lost, are you sure? - + Open map Открыть карту - + All supported maps (*.vmap *.h3m);;VCMI maps(*.vmap);;HoMM3 maps(*.h3m) Все поддерживаемые карты (*.vmap *.h3m);;Карты VCMI (*.vmap);;Карты Героев III (*.h3m) - + Save map Сохранить карту - + VCMI maps (*.vmap) Карты VCMI (*.vmap) - + Type Тип - + View surface Вид на поверхность - + No objects selected - + This operation is irreversible. Do you want to continue? - + Errors occurred. %1 objects were not updated - + Save to image - + Select maps to convert - + HoMM3 maps(*.h3m) - + Choose directory to save converted maps - + Operation completed - + Successfully converted %1 maps - + Failed to convert the map. Abort operation @@ -844,7 +844,7 @@ (по умолчанию) - + Player ID: %1 Игрок: %1 @@ -909,53 +909,69 @@ - + Compliant - + Friendly - + Aggressive - + Hostile - + Savage - - + + + No patrol + + + + + + %n tile(s) + + + + + + + + + neutral - + UNFLAGGABLE - + Can't place object - + There can only be one grail object on the map. - + Hero %1 cannot be created as NEUTRAL. @@ -1083,12 +1099,12 @@ - + None Нет - + Day %1 @@ -1356,18 +1372,18 @@ - + None Нет - + Day %1 - - + + Reward %1 @@ -1433,32 +1449,32 @@ - + Repeat after (0 = no repeat) - + Affected players - + Resources - + type - + qty - + Ok ОК diff --git a/mapeditor/translation/spanish.ts b/mapeditor/translation/spanish.ts index bd6340e4f..9107e3904 100644 --- a/mapeditor/translation/spanish.ts +++ b/mapeditor/translation/spanish.ts @@ -359,7 +359,7 @@ - + View underground Ver subterráneo @@ -441,9 +441,9 @@ - - - + + + Update appearance Actualizar apariencia @@ -574,92 +574,92 @@ - + Confirmation Confirmación - + Unsaved changes will be lost, are you sure? Los cambios no guardados se perderán. Está usted seguro ? - + Open map Abrir mapa - + All supported maps (*.vmap *.h3m);;VCMI maps(*.vmap);;HoMM3 maps(*.h3m) Todos los mapas soportados (*.vmap *.h3m);;Mapas VCMI (*.vmap);;Mapas HoMM3 (*.h3m) - + Save map Guardar mapa - + VCMI maps (*.vmap) Mapas VCMI (*.vmap) - + Type Tipo - + View surface Ver superficie - + No objects selected - + This operation is irreversible. Do you want to continue? - + Errors occurred. %1 objects were not updated - + Save to image - + Select maps to convert - + HoMM3 maps(*.h3m) - + Choose directory to save converted maps - + Operation completed - + Successfully converted %1 maps - + Failed to convert the map. Abort operation @@ -844,7 +844,7 @@ (predeterminado) - + Player ID: %1 ID de jugador: %1 @@ -909,53 +909,68 @@ - + Compliant - + Friendly - + Aggressive - + Hostile - + Savage - - + + + No patrol + + + + + + %n tile(s) + + + + + + + + neutral - + UNFLAGGABLE - + Can't place object - + There can only be one grail object on the map. - + Hero %1 cannot be created as NEUTRAL. @@ -1083,12 +1098,12 @@ Jugadores - + None Ninguno - + Day %1 @@ -1356,18 +1371,18 @@ Jugadores - + None Ninguno - + Day %1 - - + + Reward %1 @@ -1433,32 +1448,32 @@ - + Repeat after (0 = no repeat) - + Affected players - + Resources - + type - + qty - + Ok Aceptar diff --git a/mapeditor/translation/ukrainian.ts b/mapeditor/translation/ukrainian.ts index bb7d4e795..ab0c88093 100644 --- a/mapeditor/translation/ukrainian.ts +++ b/mapeditor/translation/ukrainian.ts @@ -359,7 +359,7 @@ - + View underground Дивитись підземелля @@ -441,9 +441,9 @@ - - - + + + Update appearance Оновити вигляд @@ -574,92 +574,92 @@ - + Confirmation - + Unsaved changes will be lost, are you sure? - + Open map Відкрити мапу - + All supported maps (*.vmap *.h3m);;VCMI maps(*.vmap);;HoMM3 maps(*.h3m) Всі підтримувані мапи (*.vmap *.h3m);;Мапи VCMI (*.vmap);;Мапи HoMM3 (*.h3m) - + Save map Зберегти мапу - + VCMI maps (*.vmap) Мапи VCMI - + Type Тип - + View surface Дивитись поверхню - + No objects selected - + This operation is irreversible. Do you want to continue? - + Errors occurred. %1 objects were not updated - + Save to image - + Select maps to convert - + HoMM3 maps(*.h3m) - + Choose directory to save converted maps - + Operation completed - + Successfully converted %1 maps - + Failed to convert the map. Abort operation @@ -844,7 +844,7 @@ (за замовчуванням) - + Player ID: %1 Гравець %1 @@ -909,53 +909,69 @@ - + Compliant - + Friendly - + Aggressive - + Hostile - + Savage - - + + + No patrol + + + + + + %n tile(s) + + + + + + + + + neutral - + UNFLAGGABLE - + Can't place object - + There can only be one grail object on the map. - + Hero %1 cannot be created as NEUTRAL. @@ -1083,12 +1099,12 @@ - + None Відсутня - + Day %1 @@ -1356,18 +1372,18 @@ - + None Відсутня - + Day %1 - - + + Reward %1 @@ -1433,32 +1449,32 @@ - + Repeat after (0 = no repeat) - + Affected players - + Resources - + type - + qty - + Ok Підтвердити diff --git a/mapeditor/translation/vietnamese.ts b/mapeditor/translation/vietnamese.ts index 825f16c59..bf1d31fde 100644 --- a/mapeditor/translation/vietnamese.ts +++ b/mapeditor/translation/vietnamese.ts @@ -359,7 +359,7 @@ - + View underground Xem hang ngầm @@ -441,9 +441,9 @@ - - - + + + Update appearance Cập nhật hiện thị @@ -574,92 +574,92 @@ - + Confirmation Xác nhận - + 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? - + Open map Mở bản đồ - + All supported maps (*.vmap *.h3m);;VCMI maps(*.vmap);;HoMM3 maps(*.h3m) Tất cả bản đồ hỗ trợ (*.vmap *.h3m);;Bản đồ VCMI (*.vmap);;Bản đồ HoMM3 (*.h3m) - + Save map Lưu bản đồ - + VCMI maps (*.vmap) Bản đồ VCMI (*.vmap) - + Type Loại - + View surface Xem bề mặt - + No objects selected Không mục tiêu được chọn - + This operation is irreversible. Do you want to continue? Thao tác này không thể đảo ngược. Bạn muốn tiếp tục? - + Errors occurred. %1 objects were not updated Xảy ra lỗi. %1 mục tiêu không được cập nhật - + Save to image Lưu thành ảnh - + Select maps to convert - + HoMM3 maps(*.h3m) - + Choose directory to save converted maps - + Operation completed - + Successfully converted %1 maps - + Failed to convert the map. Abort operation @@ -844,7 +844,7 @@ (mặc định) - + Player ID: %1 ID người chơi: %1 @@ -909,53 +909,67 @@ - + Compliant - + Friendly - + Aggressive - + Hostile - + Savage - - + + + No patrol + + + + + + %n tile(s) + + + + + + + neutral - + UNFLAGGABLE - + Can't place object Không thể đặt vật thể - + There can only be one grail object on the map. - + Hero %1 cannot be created as NEUTRAL. @@ -1083,12 +1097,12 @@ Người chơi - + None Không - + Day %1 @@ -1356,18 +1370,18 @@ Người chơi - + None Không - + Day %1 - - + + Reward %1 @@ -1433,32 +1447,32 @@ - + Repeat after (0 = no repeat) - + Affected players - + Resources - + type - + qty - + Ok Đồng ý diff --git a/mapeditor/validator.cpp b/mapeditor/validator.cpp index fdab680b9..a2d965e49 100644 --- a/mapeditor/validator.cpp +++ b/mapeditor/validator.cpp @@ -12,12 +12,12 @@ #include "validator.h" #include "mapcontroller.h" #include "ui_validator.h" +#include "../lib/entities/hero/CHero.h" #include "../lib/mapping/CMap.h" #include "../lib/mapObjects/MapObjects.h" #include "../lib/modding/CModHandler.h" -#include "../lib/modding/CModInfo.h" +#include "../lib/modding/ModDescription.h" #include "../lib/spells/CSpellHandler.h" -#include "../lib/CHeroHandler.h" Validator::Validator(const CMap * map, QWidget *parent) : QDialog(parent), @@ -122,13 +122,13 @@ std::set Validator::validate(const CMap * map) ++amountOfHeroes[ins->getOwner()]; } - if(ins->type) + if(ins->getHeroTypeID().hasValue()) { - if(map->allowedHeroes.count(ins->getHeroType()) == 0) - issues.insert({ tr("Hero %1 is prohibited by map settings").arg(ins->type->getNameTranslated().c_str()), false }); + if(map->allowedHeroes.count(ins->getHeroTypeID()) == 0) + issues.insert({ tr("Hero %1 is prohibited by map settings").arg(ins->getHeroType()->getNameTranslated().c_str()), false }); - if(!allHeroesOnMap.insert(ins->type).second) - issues.insert({ tr("Hero %1 has duplicate on map").arg(ins->type->getNameTranslated().c_str()), false }); + if(!allHeroesOnMap.insert(ins->getHeroType()).second) + issues.insert({ tr("Hero %1 has duplicate on map").arg(ins->getHeroType()->getNameTranslated().c_str()), false }); } else if(ins->ID != Obj::RANDOM_HERO) issues.insert({ tr("Hero %1 has an empty type and must be removed").arg(ins->instanceName.c_str()), true }); diff --git a/mapeditor/vcmieditor.desktop b/mapeditor/vcmieditor.desktop index 115cf3145..468dd1c33 100644 --- a/mapeditor/vcmieditor.desktop +++ b/mapeditor/vcmieditor.desktop @@ -7,7 +7,7 @@ GenericName=Strategy Game Map Editor GenericName[cs]=Editor map strategické hry GenericName[de]=Karteneditor für Strategiespiel Comment=Map editor for the open-source recreation of Heroes of Might & Magic III -Comment[cs]=Editor map enginu s otevřeným kódem pro Heroes of Might and Magic III +Comment[cs]=Editor map pro open-source engine Heroes of Might and Magic III Comment[de]=Karteneditor für den Open-Source-Nachbau von Heroes of Might and Magic III Icon=vcmieditor Exec=vcmieditor diff --git a/mapeditor/windownewmap.cpp b/mapeditor/windownewmap.cpp index f77b6e2f7..cf67dc8f4 100644 --- a/mapeditor/windownewmap.cpp +++ b/mapeditor/windownewmap.cpp @@ -21,7 +21,7 @@ #include "../lib/serializer/JsonSerializer.h" #include "../lib/serializer/JsonDeserializer.h" -#include "jsonutils.h" +#include "../vcmiqt/jsonutils.h" #include "windownewmap.h" #include "ui_windownewmap.h" #include "mainwindow.h" diff --git a/scripting/lua/api/Creature.cpp b/scripting/lua/api/Creature.cpp index 64c13c38b..5fca4601b 100644 --- a/scripting/lua/api/Creature.cpp +++ b/scripting/lua/api/Creature.cpp @@ -46,7 +46,7 @@ const std::vector CreatureProxy::REGISTER_CUSTOM = {"getLevel", LuaMethodWrapper::invoke, false}, {"getGrowth", LuaMethodWrapper::invoke, false}, {"getHorde", LuaMethodWrapper::invoke, false}, - {"getFaction", LuaMethodWrapper::invoke, false}, + {"getFactionID", LuaMethodWrapper::invoke, false}, {"getBaseAttack", LuaMethodWrapper::invoke, false}, {"getBaseDefense", LuaMethodWrapper::invoke, false}, diff --git a/scripting/lua/api/ServerCb.cpp b/scripting/lua/api/ServerCb.cpp index aa257c6e9..445f19a0b 100644 --- a/scripting/lua/api/ServerCb.cpp +++ b/scripting/lua/api/ServerCb.cpp @@ -74,7 +74,7 @@ int ServerCbProxy::commitPackage(lua_State * L) auto * pack = static_cast(lua_touserdata(L, 1)); - object->apply(pack); + object->apply(*pack); return S.retVoid(); } @@ -96,7 +96,7 @@ int ServerCbProxy::apply(lua_State * L) if(!S.tryGet(1, pack)) return S.retVoid(); - object->apply(pack.get()); + object->apply(*pack); return S.retVoid(); } diff --git a/server/CGameHandler.cpp b/server/CGameHandler.cpp index 62d735fa7..411865ce5 100644 --- a/server/CGameHandler.cpp +++ b/server/CGameHandler.cpp @@ -29,7 +29,6 @@ #include "../lib/CCreatureHandler.h" #include "../lib/CCreatureSet.h" #include "../lib/texts/CGeneralTextHandler.h" -#include "../lib/CHeroHandler.h" #include "../lib/CPlayerState.h" #include "../lib/CRandomGenerator.h" #include "../lib/CSoundBase.h" @@ -48,6 +47,7 @@ #include "../lib/entities/building/CBuilding.h" #include "../lib/entities/faction/CTownHandler.h" +#include "../lib/entities/hero/CHeroHandler.h" #include "../lib/filesystem/FileInfo.h" #include "../lib/filesystem/Filesystem.h" @@ -156,22 +156,22 @@ void CGameHandler::levelUpHero(const CGHeroInstance * hero) sps.which = primarySkill; sps.abs = false; sps.val = 1; - sendAndApply(&sps); + sendAndApply(sps); HeroLevelUp hlu; hlu.player = hero->tempOwner; hlu.heroId = hero->id; hlu.primskill = primarySkill; - hlu.skills = hero->getLevelUpProposedSecondarySkills(heroPool->getHeroSkillsRandomGenerator(hero->getHeroType())); + hlu.skills = hero->getLevelUpProposedSecondarySkills(heroPool->getHeroSkillsRandomGenerator(hero->getHeroTypeID())); if (hlu.skills.size() == 0) { - sendAndApply(&hlu); + sendAndApply(hlu); levelUpHero(hero); } else if (hlu.skills.size() == 1 || !hero->getOwner().isValidPlayer()) { - sendAndApply(&hlu); + sendAndApply(hlu); levelUpHero(hero, hlu.skills.front()); } else if (hlu.skills.size() > 1) @@ -179,7 +179,7 @@ void CGameHandler::levelUpHero(const CGHeroInstance * hero) auto levelUpQuery = std::make_shared(this, hlu, hero); hlu.queryID = levelUpQuery->queryID; queries->addQuery(levelUpQuery); - sendAndApply(&hlu); + sendAndApply(hlu); //level up will be called on query reply } } @@ -235,33 +235,34 @@ void CGameHandler::levelUpCommander (const CCommanderInstance * c, int skill) scp.accumulatedBonus.type = BonusType::STACKS_SPEED; break; case ECommander::SPELL_POWER: - scp.accumulatedBonus.type = BonusType::MAGIC_RESISTANCE; + scp.accumulatedBonus.type = BonusType::SPELL_DAMAGE_REDUCTION; + scp.accumulatedBonus.subtype = BonusSubtypeID(SpellSchool::ANY); scp.accumulatedBonus.val = difference (VLC->creh->skillLevels, c->secondarySkills, ECommander::RESISTANCE); - sendAndApply (&scp); //additional pack + sendAndApply(scp); //additional pack scp.accumulatedBonus.type = BonusType::CREATURE_SPELL_POWER; scp.accumulatedBonus.val = difference (VLC->creh->skillLevels, c->secondarySkills, ECommander::SPELL_POWER) * 100; //like hero with spellpower = ability level - sendAndApply (&scp); //additional pack + sendAndApply(scp); //additional pack scp.accumulatedBonus.type = BonusType::CASTS; scp.accumulatedBonus.val = difference (VLC->creh->skillLevels, c->secondarySkills, ECommander::CASTS); - sendAndApply (&scp); //additional pack + sendAndApply(scp); //additional pack scp.accumulatedBonus.type = BonusType::CREATURE_ENCHANT_POWER; //send normally break; } scp.accumulatedBonus.val = difference (VLC->creh->skillLevels, c->secondarySkills, skill); - sendAndApply (&scp); + sendAndApply(scp); scp.which = SetCommanderProperty::SECONDARY_SKILL; scp.additionalInfo = skill; scp.amount = c->secondarySkills.at(skill) + 1; - sendAndApply (&scp); + sendAndApply(scp); } else if (skill >= 100) { scp.which = SetCommanderProperty::SPECIAL_SKILL; scp.accumulatedBonus = *VLC->creh->skillRequirements.at(skill-100).first; scp.additionalInfo = skill; //unnormalized - sendAndApply (&scp); + sendAndApply(scp); } expGiven(hero); } @@ -306,12 +307,12 @@ void CGameHandler::levelUpCommander(const CCommanderInstance * c) if (!skillAmount) { - sendAndApply(&clu); + sendAndApply(clu); levelUpCommander(c); } else if (skillAmount == 1 || hero->tempOwner == PlayerColor::NEUTRAL) //choose skill automatically { - sendAndApply(&clu); + sendAndApply(clu); levelUpCommander(c, *RandomGeneratorUtil::nextItem(clu.skills, getRandomGenerator())); } else if (skillAmount > 1) //apply and ask for secondary skill @@ -319,7 +320,7 @@ void CGameHandler::levelUpCommander(const CCommanderInstance * c) auto commanderLevelUp = std::make_shared(this, clu, hero); clu.queryID = commanderLevelUp->queryID; queries->addQuery(commanderLevelUp); - sendAndApply(&clu); + sendAndApply(clu); } } @@ -357,7 +358,7 @@ void CGameHandler::giveExperience(const CGHeroInstance * hero, TExpType amountTo iw.player = hero->tempOwner; iw.text.appendLocalString(EMetaText::GENERAL_TXT, 1); //can gain no more XP iw.text.replaceTextID(hero->getNameTextID()); - sendAndApply(&iw); + sendAndApply(iw); } SetPrimSkill sps; @@ -365,7 +366,7 @@ void CGameHandler::giveExperience(const CGHeroInstance * hero, TExpType amountTo sps.which = PrimarySkill::EXPERIENCE; sps.abs = false; sps.val = amountToGain; - sendAndApply(&sps); + sendAndApply(sps); //hero may level up if (hero->commander && hero->commander->alive) @@ -375,7 +376,7 @@ void CGameHandler::giveExperience(const CGHeroInstance * hero, TExpType amountTo scp.heroid = hero->id; scp.which = SetCommanderProperty::EXPERIENCE; scp.amount = amountToGain; - sendAndApply (&scp); + sendAndApply(scp); CBonusSystemNode::treeHasChanged(); } @@ -389,7 +390,7 @@ void CGameHandler::changePrimSkill(const CGHeroInstance * hero, PrimarySkill whi sps.which = which; sps.abs = abs; sps.val = val; - sendAndApply(&sps); + sendAndApply(sps); } void CGameHandler::changeSecSkill(const CGHeroInstance * hero, SecondarySkill which, int val, bool abs) @@ -404,7 +405,7 @@ void CGameHandler::changeSecSkill(const CGHeroInstance * hero, SecondarySkill wh sss.which = which; sss.val = val; sss.abs = abs; - sendAndApply(&sss); + sendAndApply(sss); if (hero->visitedTown) giveSpells(hero->visitedTown, hero); @@ -431,28 +432,46 @@ void CGameHandler::handleClientDisconnection(std::shared_ptr c) continue; auto playerConnection = vstd::find(playerConnections.second, c); - if(playerConnection != playerConnections.second.end()) + if(playerConnection == playerConnections.second.end()) + continue; + + logGlobal->trace("Player %s disconnected. Notifying remaining players", playerId.toString()); + + // this player have left the game - broadcast infowindow to all in-game players + for (auto i = gs->players.cbegin(); i!=gs->players.cend(); i++) { - std::string messageText = boost::str(boost::format("%s (cid %d) was disconnected") % playerSettings->name % c->connectionID); - playerMessages->broadcastMessage(playerId, messageText); + if (i->first == playerId) + continue; + + if (getPlayerState(i->first)->status != EPlayerStatus::INGAME) + continue; + + logGlobal->trace("Notifying player %s", i->first); + + InfoWindow out; + out.player = i->first; + out.text.appendTextID("vcmi.server.errors.playerLeft"); + out.text.replaceName(playerId); + out.components.emplace_back(ComponentType::FLAG, playerId); + sendAndApply(out); } } } -void CGameHandler::handleReceivedPack(CPackForServer * pack) +void CGameHandler::handleReceivedPack(CPackForServer & pack) { //prepare struct informing that action was applied auto sendPackageResponse = [&](bool successfullyApplied) { PackageApplied applied; - applied.player = pack->player; + applied.player = pack.player; applied.result = successfullyApplied; - applied.packType = CTypeList::getInstance().getTypeID(pack); - applied.requestID = pack->requestID; - pack->c->sendPack(&applied); + applied.packType = CTypeList::getInstance().getTypeID(&pack); + applied.requestID = pack.requestID; + pack.c->sendPack(applied); }; - if(isBlockedByQueries(pack, pack->player)) + if(isBlockedByQueries(&pack, pack.player)) { sendPackageResponse(false); } @@ -462,7 +481,7 @@ void CGameHandler::handleReceivedPack(CPackForServer * pack) try { ApplyGhNetPackVisitor applier(*this); - pack->visit(applier); + pack.visit(applier); result = applier.getResult(); } catch(ExceptionNotAllowedAction &) @@ -471,14 +490,13 @@ void CGameHandler::handleReceivedPack(CPackForServer * pack) } if(result) - logGlobal->trace("Message %s successfully applied!", typeid(*pack).name()); + logGlobal->trace("Message %s successfully applied!", typeid(pack).name()); else complain((boost::format("Got false in applying %s... that request must have been fishy!") - % typeid(*pack).name()).str()); + % typeid(pack).name()).str()); sendPackageResponse(true); } - vstd::clear_pointer(pack); } CGameHandler::CGameHandler(CVCMIServer * lobby) @@ -535,7 +553,7 @@ void CGameHandler::init(StartInfo *si, Load::ProgressAccumulator & progressTrack for (auto & elem : gs->map->allHeroes) { if(elem) - heroPool->getHeroSkillsRandomGenerator(elem->getHeroType()); // init RMG seed + heroPool->getHeroSkillsRandomGenerator(elem->getHeroTypeID()); // init RMG seed } reinitScripting(); @@ -551,12 +569,12 @@ void CGameHandler::setPortalDwelling(const CGTownInstance * town, bool forced=fa return; } - if (forced || town->creatures.at(town->town->creatures.size()).second.empty())//we need to change creature + if (forced || town->creatures.at(town->getTown()->creatures.size()).second.empty())//we need to change creature { SetAvailableCreatures ssi; ssi.tid = town->id; ssi.creatures = town->creatures; - ssi.creatures[town->town->creatures.size()].second.clear();//remove old one + ssi.creatures[town->getTown()->creatures.size()].second.clear();//remove old one std::set availableCreatures; for (const auto & dwelling : p->getOwnedObjects()) @@ -572,14 +590,14 @@ void CGameHandler::setPortalDwelling(const CGTownInstance * town, bool forced=fa if (clear) { - ssi.creatures[town->town->creatures.size()].first = std::max(1, (creatureId.toEntity(VLC)->getGrowth())/2); + ssi.creatures[town->getTown()->creatures.size()].first = std::max(1, (creatureId.toEntity(VLC)->getGrowth())/2); } else { - ssi.creatures[town->town->creatures.size()].first = creatureId.toEntity(VLC)->getGrowth(); + ssi.creatures[town->getTown()->creatures.size()].first = creatureId.toEntity(VLC)->getGrowth(); } - ssi.creatures[town->town->creatures.size()].second.push_back(creatureId); - sendAndApply(&ssi); + ssi.creatures[town->getTown()->creatures.size()].second.push_back(creatureId); + sendAndApply(ssi); } } @@ -639,7 +657,7 @@ void CGameHandler::onNewTurn() PlayerColor player = t->tempOwner; if(t->hasBuilt(BuildingID::GRAIL) - && t->town->buildings.at(BuildingID::GRAIL)->height == CBuilding::HEIGHT_SKYSHIP) + && t->getTown()->buildings.at(BuildingID::GRAIL)->height == CBuilding::HEIGHT_SKYSHIP) { // Skyship, probably easier to handle same as Veil of darkness // do it every new day before veils @@ -666,7 +684,7 @@ void CGameHandler::onNewTurn() SetAvailableArtifacts saa; saa.id = ObjectInstanceID::NONE; pickAllowedArtsSet(saa.arts, getRandomGenerator()); - sendAndApply(&saa); + sendAndApply(saa); } newTurnProcessor->onNewTurn(); @@ -753,7 +771,7 @@ void CGameHandler::giveSpells(const CGTownInstance *t, const CGHeroInstance *h) } } if (!cs.spells.empty()) - sendAndApply(&cs); + sendAndApply(cs); } bool CGameHandler::removeObject(const CGObjectInstance * obj, const PlayerColor & initiator) @@ -767,7 +785,7 @@ bool CGameHandler::removeObject(const CGObjectInstance * obj, const PlayerColor RemoveObject ro; ro.objectID = obj->id; ro.initiator = initiator; - sendAndApply(&ro); + sendAndApply(ro); checkVictoryLossConditionsForAll(); //e.g. if monster escaped (removing objs after battle is done directly by endBattle, not this function) return true; @@ -786,7 +804,7 @@ bool CGameHandler::moveHero(ObjectInstanceID hid, int3 dst, EMovementMode moveme return false; } - logGlobal->trace("Player %d (%s) wants to move hero %d from %s to %s", asker, asker.toString(), hid.getNum(), h->pos.toString(), dst.toString()); + logGlobal->trace("Player %d (%s) wants to move hero %d from %s to %s", asker, asker.toString(), hid.getNum(), h->anchorPos().toString(), dst.toString()); const int3 hmpos = h->convertToVisitablePos(dst); if (!gs->map->isInTheMap(hmpos)) @@ -812,9 +830,8 @@ bool CGameHandler::moveHero(ObjectInstanceID hid, int3 dst, EMovementMode moveme const bool embarking = !h->boat && objectToVisit && objectToVisit->ID == Obj::BOAT; const bool disembarking = h->boat - && t.terType->isLand() - && (dst == h->pos - || (h->boat->layer == EPathfindingLayer::SAIL && !t.blocked)); + && t.isLand() + && (dst == h->pos || (h->boat->layer == EPathfindingLayer::SAIL && !t.blocked())); //result structure for start - movement failed, no move points used TryMoveHero tmh; @@ -832,15 +849,15 @@ bool CGameHandler::moveHero(ObjectInstanceID hid, int3 dst, EMovementMode moveme const bool canWalkOnSea = pathfinderHelper->hasBonusOfType(BonusType::WATER_WALKING) || (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; + const bool movingOntoObstacle = t.blocked() && !t.visitable(); const bool objectCoastVisitable = objectToVisit && objectToVisit->isCoastVisitable(); - const bool movingOntoWater = !h->boat && t.terType->isWater() && !objectCoastVisitable; + const bool movingOntoWater = !h->boat && t.isWater() && !objectCoastVisitable; const auto complainRet = [&](const std::string & message) { //send info about movement failure complain(message); - sendAndApply(&tmh); + sendAndApply(tmh); return false; }; @@ -858,18 +875,18 @@ bool CGameHandler::moveHero(ObjectInstanceID hid, int3 dst, EMovementMode moveme //it's a rock or blocked and not visitable tile //OR hero is on land and dest is water and (there is not present only one object - boat) - if (!t.terType->isPassable() || (movingOntoObstacle && !canFly)) + if (!t.getTerrain()->isPassable() || (movingOntoObstacle && !canFly)) return complainRet("Cannot move hero, destination tile is blocked!"); //hero is not on boat/water walking and dst water tile doesn't contain boat/hero (objs visitable from land) -> we test back cause boat may be on top of another object (#276) if(movingOntoWater && !canFly && !canWalkOnSea) return complainRet("Cannot move hero, destination tile is on water!"); - if(h->boat && h->boat->layer == EPathfindingLayer::SAIL && t.terType->isLand() && t.blocked) + if(h->boat && h->boat->layer == EPathfindingLayer::SAIL && t.isLand() && t.blocked()) return complainRet("Cannot disembark hero, tile is blocked!"); if(distance(h->pos, dst) >= 1.5 && movementMode == EMovementMode::STANDARD) - return complainRet("Tiles are not neighboring!"); + return complainRet("Tiles " + h->pos.toString()+ " and "+ dst.toString() +" are not neighboring!"); if(h->inTownGarrison) return complainRet("Can not move garrisoned hero!"); @@ -877,7 +894,7 @@ bool CGameHandler::moveHero(ObjectInstanceID hid, int3 dst, EMovementMode moveme if(h->movementPointsRemaining() < cost && dst != h->pos && movementMode == EMovementMode::STANDARD) return complainRet("Hero doesn't have any movement points left!"); - if (transit && !canFly && !(canWalkOnSea && t.terType->isWater()) && !CGTeleport::isTeleport(objectToVisit)) + if (transit && !canFly && !(canWalkOnSea && t.isWater()) && !CGTeleport::isTeleport(objectToVisit)) return complainRet("Hero cannot transit over this tile!"); //several generic blocks of code @@ -885,7 +902,7 @@ bool CGameHandler::moveHero(ObjectInstanceID hid, int3 dst, EMovementMode moveme // should be called if hero changes tile but before applying TryMoveHero package auto leaveTile = [&]() { - for (CGObjectInstance *obj : gs->map->getTile(int3(h->pos.x-1, h->pos.y, h->pos.z)).visitableObjects) + for (CGObjectInstance *obj : gs->map->getTile(h->visitablePos()).visitableObjects) { obj->onHeroLeave(h); } @@ -907,7 +924,7 @@ bool CGameHandler::moveHero(ObjectInstanceID hid, int3 dst, EMovementMode moveme tmh.attackedFrom = std::make_optional(guardPos); tmh.result = result; - sendAndApply(&tmh); + sendAndApply(tmh); if (visitDest == VISIT_DEST && objectToVisit && objectToVisit->id == h->id) { // Hero should be always able to visit any object he is staying on even if there are guards around @@ -999,7 +1016,7 @@ bool CGameHandler::moveHero(ObjectInstanceID hid, int3 dst, EMovementMode moveme if (CGTeleport::isTeleport(objectToVisit)) visitDest = DONT_VISIT_DEST; - if (canFly || (canWalkOnSea && t.terType->isWater())) + if (canFly || (canWalkOnSea && t.isWater())) { lookForGuards = IGNORE_GUARDS; visitDest = DONT_VISIT_DEST; @@ -1030,7 +1047,7 @@ bool CGameHandler::teleportHero(ObjectInstanceID hid, ObjectInstanceID dstid, ui if (((h->getOwner() != t->getOwner()) && complain("Cannot teleport hero to another player")) - || (from->town->faction->getId() != t->town->faction->getId() + || (from->getFactionID() != t->getFactionID() && complain("Source town and destination town should belong to the same faction")) || ((!from || !from->hasBuilt(BuildingSubID::CASTLE_GATE)) @@ -1084,7 +1101,7 @@ void CGameHandler::showBlockingDialog(const IObjectInterface * caller, BlockingD auto dialogQuery = std::make_shared(this, caller, *iw); queries->addQuery(dialogQuery); iw->queryID = dialogQuery->queryID; - sendToAllClients(iw); + sendToAllClients(*iw); } void CGameHandler::showTeleportDialog(TeleportDialog *iw) @@ -1092,7 +1109,7 @@ void CGameHandler::showTeleportDialog(TeleportDialog *iw) auto dialogQuery = std::make_shared(this, *iw); queries->addQuery(dialogQuery); iw->queryID = dialogQuery->queryID; - sendToAllClients(iw); + sendToAllClients(*iw); } void CGameHandler::giveResource(PlayerColor player, GameResID which, int val) //TODO: cap according to Bersy's suggestion @@ -1110,7 +1127,7 @@ void CGameHandler::giveResources(PlayerColor player, TResources resources) sr.abs = false; sr.player = player; sr.res = resources; - sendAndApply(&sr); + sendAndApply(sr); } void CGameHandler::giveCreatures(const CArmedInstance *obj, const CGHeroInstance * h, const CCreatureSet &creatures, bool remove) @@ -1122,7 +1139,7 @@ void CGameHandler::giveCreatures(const CArmedInstance *obj, const CGHeroInstance //first we move creatures to give to make them army of object-source for (auto & elem : creatures.Slots()) { - addToSlot(StackLocation(obj, obj->getSlotFor(elem.second->type)), elem.second->type, elem.second->count); + addToSlot(StackLocation(obj, obj->getSlotFor(elem.second->getCreature())), elem.second->getCreature(), elem.second->count); } tryJoiningArmy(obj, h, remove, true); @@ -1143,7 +1160,7 @@ void CGameHandler::takeCreatures(ObjectInstanceID objid, const std::vectorSlots().begin(); i != obj->Slots().end(); i++) { - if (i->second->type == sbd.type) + if (i->second->getType() == sbd.getType()) { TQuantity take = std::min(sbd.count - collected, i->second->count); //collect as much cres as we can changeStackCount(StackLocation(obj, i->first), -take, false); @@ -1170,7 +1187,7 @@ void CGameHandler::heroVisitCastle(const CGTownInstance * obj, const CGHeroInsta vc.hid = hero->id; vc.tid = obj->id; vc.flags |= 1; - sendAndApply(&vc); + sendAndApply(vc); } visitCastleObjects(obj, hero); @@ -1194,7 +1211,7 @@ void CGameHandler::visitCastleObjects(const CGTownInstance * t, std::vectorrewardableBuildings) { - if (!t->town->buildings.at(building.first)->manualHeroVisit) + if (!t->getTown()->buildings.at(building.first)->manualHeroVisit && t->hasBuilt(building.first)) buildingsToVisit.push_back(building.first); } @@ -1210,7 +1227,7 @@ void CGameHandler::stopHeroVisitCastle(const CGTownInstance * obj, const CGHeroI HeroVisitCastle vc; vc.hid = hero->id; vc.tid = obj->id; - sendAndApply(&vc); + sendAndApply(vc); } void CGameHandler::removeArtifact(const ArtifactLocation & al) @@ -1223,7 +1240,7 @@ void CGameHandler::removeArtifact(const ObjectInstanceID & srcId, const std::vec BulkEraseArtifacts ea; ea.artHolder = srcId; ea.posPack.insert(ea.posPack.end(), slotsPack.begin(), slotsPack.end()); - sendAndApply(&ea); + sendAndApply(ea); } void CGameHandler::changeSpells(const CGHeroInstance * hero, bool give, const std::set &spells) @@ -1232,17 +1249,27 @@ void CGameHandler::changeSpells(const CGHeroInstance * hero, bool give, const st cs.hid = hero->id; cs.spells = spells; cs.learn = give; - sendAndApply(&cs); + sendAndApply(cs); +} + +void CGameHandler::setResearchedSpells(const CGTownInstance * town, int level, const std::vector & spells, bool accepted) +{ + SetResearchedSpells cs; + cs.tid = town->id; + cs.spells = spells; + cs.level = level; + cs.accepted = accepted; + sendAndApply(cs); } void CGameHandler::giveHeroBonus(GiveBonus * bonus) { - sendAndApply(bonus); + sendAndApply(*bonus); } void CGameHandler::setMovePoints(SetMovePoints * smp) { - sendAndApply(smp); + sendAndApply(*smp); } void CGameHandler::setMovePoints(ObjectInstanceID hid, int val, bool absolute) @@ -1251,7 +1278,7 @@ void CGameHandler::setMovePoints(ObjectInstanceID hid, int val, bool absolute) smp.hid = hid; smp.val = val; smp.absolute = absolute; - sendAndApply(&smp); + sendAndApply(smp); } void CGameHandler::setManaPoints(ObjectInstanceID hid, int val) @@ -1260,7 +1287,7 @@ void CGameHandler::setManaPoints(ObjectInstanceID hid, int val) sm.hid = hid; sm.val = val; sm.absolute = true; - sendAndApply(&sm); + sendAndApply(sm); } void CGameHandler::giveHero(ObjectInstanceID id, PlayerColor player, ObjectInstanceID boatId) @@ -1269,7 +1296,7 @@ void CGameHandler::giveHero(ObjectInstanceID id, PlayerColor player, ObjectInsta gh.id = id; gh.player = player; gh.boatId = boatId; - sendAndApply(&gh); + sendAndApply(gh); //Reveal fow around new hero, especially released from Prison auto h = getHero(id); @@ -1282,7 +1309,7 @@ void CGameHandler::changeObjPos(ObjectInstanceID objid, int3 newPos, const Playe cop.objid = objid; cop.nPos = newPos; cop.initiator = initiator; - sendAndApply(&cop); + sendAndApply(cop); } void CGameHandler::useScholarSkill(ObjectInstanceID fromHero, ObjectInstanceID toHero) @@ -1352,7 +1379,7 @@ void CGameHandler::useScholarSkill(ObjectInstanceID fromHero, ObjectInstanceID t } iw.text.appendLocalString(EMetaText::GENERAL_TXT, 142);//from %s iw.text.replaceTextID(h2->getNameTextID()); - sendAndApply(&cs2); + sendAndApply(cs2); } if (!cs1.spells.empty() && !cs2.spells.empty()) @@ -1380,9 +1407,9 @@ void CGameHandler::useScholarSkill(ObjectInstanceID fromHero, ObjectInstanceID t } iw.text.appendLocalString(EMetaText::GENERAL_TXT, 148);//from %s iw.text.replaceTextID(h2->getNameTextID()); - sendAndApply(&cs1); + sendAndApply(cs1); } - sendAndApply(&iw); + sendAndApply(iw); } } @@ -1399,43 +1426,43 @@ void CGameHandler::heroExchange(ObjectInstanceID hero1, ObjectInstanceID hero2) hex.player = h1->getOwner(); hex.hero1 = hero1; hex.hero2 = hero2; - sendAndApply(&hex); + sendAndApply(hex); useScholarSkill(hero1,hero2); queries->addQuery(exchange); } } -void CGameHandler::sendToAllClients(CPackForClient * pack) +void CGameHandler::sendToAllClients(CPackForClient & pack) { - logNetwork->trace("\tSending to all clients: %s", typeid(*pack).name()); + logNetwork->trace("\tSending to all clients: %s", typeid(pack).name()); for (auto c : lobby->activeConnections) c->sendPack(pack); } -void CGameHandler::sendAndApply(CPackForClient * pack) +void CGameHandler::sendAndApply(CPackForClient & pack) { sendToAllClients(pack); gs->apply(pack); - logNetwork->trace("\tApplied on gs: %s", typeid(*pack).name()); + logNetwork->trace("\tApplied on gs: %s", typeid(pack).name()); } -void CGameHandler::sendAndApply(CGarrisonOperationPack * pack) +void CGameHandler::sendAndApply(CGarrisonOperationPack & pack) { - sendAndApply(static_cast(pack)); + sendAndApply(static_cast(pack)); checkVictoryLossConditionsForAll(); } -void CGameHandler::sendAndApply(SetResources * pack) +void CGameHandler::sendAndApply(SetResources & pack) { - sendAndApply(static_cast(pack)); - checkVictoryLossConditionsForPlayer(pack->player); + sendAndApply(static_cast(pack)); + checkVictoryLossConditionsForPlayer(pack.player); } -void CGameHandler::sendAndApply(NewStructures * pack) +void CGameHandler::sendAndApply(NewStructures & pack) { - sendAndApply(static_cast(pack)); - checkVictoryLossConditionsForPlayer(getTown(pack->tid)->tempOwner); + sendAndApply(static_cast(pack)); + checkVictoryLossConditionsForPlayer(getTown(pack.tid)->tempOwner); } bool CGameHandler::isPlayerOwns(CPackForServer * pack, ObjectInstanceID id) @@ -1617,7 +1644,7 @@ bool CGameHandler::bulkSplitStack(SlotID slotSrc, ObjectInstanceID srcOwner, si3 if(actualAmount <= howMany) break; } - sendAndApply(&bulkRS); + sendAndApply(bulkRS); return true; } @@ -1659,7 +1686,7 @@ bool CGameHandler::bulkMergeStacks(SlotID slotSrc, ObjectInstanceID srcOwner) rs.count = creatureSet.getStackCount(slot); bulkRS.moves.push_back(rs); } - sendAndApply(&bulkRS); + sendAndApply(bulkRS); return true; } @@ -1746,7 +1773,7 @@ bool CGameHandler::bulkMoveArmy(ObjectInstanceID srcArmy, ObjectInstanceID destA rs.count = move.second.second; bulkRS.moves.push_back(rs); } - sendAndApply(&bulkRS); + sendAndApply(bulkRS); return true; } @@ -1828,18 +1855,14 @@ bool CGameHandler::bulkSmartSplitStack(SlotID slotSrc, ObjectInstanceID srcOwner complain((boost::format("Failure: totalCreatures=%d but check=%d") % totalCreatures % check).str()); return false; } - sendAndApply(&bulkSRS); + sendAndApply(bulkSRS); return true; } bool CGameHandler::arrangeStacks(ObjectInstanceID id1, ObjectInstanceID id2, ui8 what, SlotID p1, SlotID p2, si32 val, PlayerColor player) { - const CArmedInstance * s1 = static_cast(getObjInstance(id1)); - const CArmedInstance * s2 = static_cast(getObjInstance(id2)); - const CCreatureSet & S1 = *s1; - const CCreatureSet & S2 = *s2; - StackLocation sl1(s1, p1); - StackLocation sl2(s2, p2); + const CArmedInstance * s1 = static_cast(getObj(id1)); + const CArmedInstance * s2 = static_cast(getObj(id2)); if (s1 == nullptr || s2 == nullptr) { @@ -1847,6 +1870,11 @@ bool CGameHandler::arrangeStacks(ObjectInstanceID id1, ObjectInstanceID id2, ui8 return false; } + const CCreatureSet & S1 = *s1; + const CCreatureSet & S2 = *s2; + StackLocation sl1(s1, p1); + StackLocation sl2(s2, p2); + if (!sl1.slot.validSlot() || !sl2.slot.validSlot()) { complain(complainInvalidSlot); @@ -2008,12 +2036,12 @@ bool CGameHandler::buildStructure(ObjectInstanceID tid, BuildingID requestedID, const CGTownInstance * t = getTown(tid); if(!t) COMPLAIN_RETF("No such town (ID=%s)!", tid); - if(!t->town->buildings.count(requestedID)) - COMPLAIN_RETF("Town of faction %s does not have info about building ID=%s!", t->town->faction->getNameTranslated() % requestedID); + if(!t->getTown()->buildings.count(requestedID)) + COMPLAIN_RETF("Town of faction %s does not have info about building ID=%s!", t->getFaction()->getNameTranslated() % requestedID); if(t->hasBuilt(requestedID)) - COMPLAIN_RETF("Building %s is already built in %s", t->town->buildings.at(requestedID)->getNameTranslated() % t->getNameTranslated()); + COMPLAIN_RETF("Building %s is already built in %s", t->getTown()->buildings.at(requestedID)->getNameTranslated() % t->getNameTranslated()); - const CBuilding * requestedBuilding = t->town->buildings.at(requestedID); + const CBuilding * requestedBuilding = t->getTown()->buildings.at(requestedID); //Vector with future list of built building and buildings in auto-mode that are not yet built. std::vector remainingAutoBuildings; @@ -2053,7 +2081,7 @@ bool CGameHandler::buildStructure(ObjectInstanceID tid, BuildingID requestedID, int level = BuildingID::getLevelFromDwelling(buildingID); int upgradeNumber = BuildingID::getUpgradedFromDwelling(buildingID); - if(upgradeNumber >= t->town->creatures.at(level).size()) + if(upgradeNumber >= t->getTown()->creatures.at(level).size()) { complain(boost::str(boost::format("Error encountered when building dwelling (bid=%s):" "no creature found (upgrade number %d, level %d!") @@ -2061,7 +2089,7 @@ bool CGameHandler::buildStructure(ObjectInstanceID tid, BuildingID requestedID, return; } - const CCreature * crea = t->town->creatures.at(level).at(upgradeNumber).toCreature(); + const CCreature * crea = t->getTown()->creatures.at(level).at(upgradeNumber).toCreature(); SetAvailableCreatures ssi; ssi.tid = t->id; @@ -2069,9 +2097,9 @@ bool CGameHandler::buildStructure(ObjectInstanceID tid, BuildingID requestedID, if (ssi.creatures[level].second.empty()) // first creature in a dwelling ssi.creatures[level].first = crea->getGrowth(); ssi.creatures[level].second.push_back(crea->getId()); - sendAndApply(&ssi); + sendAndApply(ssi); } - if(t->town->buildings.at(buildingID)->subId == BuildingSubID::PORTAL_OF_SUMMONING) + if(t->getTown()->buildings.at(buildingID)->subId == BuildingSubID::PORTAL_OF_SUMMONING) { setPortalDwelling(t); } @@ -2082,9 +2110,9 @@ bool CGameHandler::buildStructure(ObjectInstanceID tid, BuildingID requestedID, { auto isMageGuild = (buildingID <= BuildingID::MAGES_GUILD_5 && buildingID >= BuildingID::MAGES_GUILD_1); auto isLibrary = isMageGuild ? false - : t->town->buildings.at(buildingID)->subId == BuildingSubID::EBuildingSubID::LIBRARY; + : t->getTown()->buildings.at(buildingID)->subId == BuildingSubID::EBuildingSubID::LIBRARY; - if(isMageGuild || isLibrary || (t->getFaction() == ETownType::CONFLUX && buildingID == BuildingID::GRAIL)) + if(isMageGuild || isLibrary || (t->getFactionID() == ETownType::CONFLUX && buildingID == BuildingID::GRAIL)) { if(t->visitingHero) giveSpells(t,t->visitingHero); @@ -2100,7 +2128,7 @@ bool CGameHandler::buildStructure(ObjectInstanceID tid, BuildingID requestedID, }; //Init the vectors - for(auto & build : t->town->buildings) + for(auto & build : t->getTown()->buildings) { if(t->hasBuilt(build.first)) { @@ -2151,7 +2179,7 @@ bool CGameHandler::buildStructure(ObjectInstanceID tid, BuildingID requestedID, } //We know what has been built, apply changes. Do this as final step to properly update town window - sendAndApply(&ns); + sendAndApply(ns); //Other post-built events. To some logic like giving spells to work gamestate changes for new building must be already in place! for(auto builtID : ns.bid) @@ -2184,7 +2212,7 @@ bool CGameHandler::visitTownBuilding(ObjectInstanceID tid, BuildingID bid) if(!t->hasBuilt(bid)) return false; - auto subID = t->town->buildings.at(bid)->subId; + auto subID = t->getTown()->buildings.at(bid)->subId; if(subID == BuildingSubID::EBuildingSubID::BANK) { @@ -2196,7 +2224,7 @@ bool CGameHandler::visitTownBuilding(ObjectInstanceID tid, BuildingID bid) return true; } - if (t->rewardableBuildings.count(bid) && t->visitingHero && t->town->buildings.at(bid)->manualHeroVisit) + if (t->rewardableBuildings.count(bid) && t->visitingHero && t->getTown()->buildings.at(bid)->manualHeroVisit) { std::vector buildingsToVisit; std::vector visitors; @@ -2220,7 +2248,7 @@ bool CGameHandler::razeStructure (ObjectInstanceID tid, BuildingID bid) rs.tid = tid; rs.bid.insert(bid); rs.destroyed = t->destroyed + 1; - sendAndApply(&rs); + sendAndApply(rs); //TODO: Remove dwellers // if (t->subID == 4 && bid == 17) //Veil of Darkness // { @@ -2228,11 +2256,65 @@ bool CGameHandler::razeStructure (ObjectInstanceID tid, BuildingID bid) // rb.whoID = t->id; // rb.source = BonusSource::TOWN_STRUCTURE; // rb.id = 17; -// sendAndApply(&rb); +// sendAndApply(rb); // } return true; } +bool CGameHandler::spellResearch(ObjectInstanceID tid, SpellID spellAtSlot, bool accepted) +{ + CGTownInstance *t = gs->getTown(tid); + + if(!getSettings().getBoolean(EGameSettings::TOWNS_SPELL_RESEARCH) && complain("Spell research not allowed!")) + return false; + if (!t->spellResearchAllowed && complain("Spell research not allowed in this town!")) + return false; + + int level = -1; + for(int i = 0; i < t->spells.size(); i++) + if(vstd::find_pos(t->spells[i], spellAtSlot) != -1) + level = i; + + if(level == -1 && complain("Spell for replacement not found!")) + return false; + + auto spells = t->spells.at(level); + + bool researchLimitExceeded = t->spellResearchCounterDay >= getSettings().getValue(EGameSettings::TOWNS_SPELL_RESEARCH_PER_DAY).Vector()[level].Float(); + if(researchLimitExceeded && complain("Already researched today!")) + return false; + + if(!accepted) + { + auto it = spells.begin() + t->spellsAtLevel(level, false); + std::rotate(it, it + 1, spells.end()); // move to end + setResearchedSpells(t, level, spells, accepted); + return true; + } + + auto costBase = TResources(getSettings().getValue(EGameSettings::TOWNS_SPELL_RESEARCH_COST).Vector()[level]); + auto costExponent = getSettings().getValue(EGameSettings::TOWNS_SPELL_RESEARCH_COST_EXPONENT_PER_RESEARCH).Vector()[level].Float(); + auto cost = costBase * std::pow(t->spellResearchAcceptedCounter + 1, costExponent); + + if(!getPlayerState(t->getOwner())->resources.canAfford(cost) && complain("Spell replacement cannot be afforded!")) + return false; + + giveResources(t->getOwner(), -cost); + + std::swap(spells.at(t->spellsAtLevel(level, false)), spells.at(vstd::find_pos(spells, spellAtSlot))); + auto it = spells.begin() + t->spellsAtLevel(level, false); + std::rotate(it, it + 1, spells.end()); // move to end + + setResearchedSpells(t, level, spells, accepted); + + if(t->visitingHero) + giveSpells(t, t->visitingHero); + if(t->garrisonHero) + giveSpells(t, t->garrisonHero); + + return true; +} + bool CGameHandler::recruitCreatures(ObjectInstanceID objid, ObjectInstanceID dstid, CreatureID crid, ui32 cram, si32 fromLvl, PlayerColor player) { const CGDwelling * dwelling = dynamic_cast(getObj(objid)); @@ -2299,7 +2381,7 @@ bool CGameHandler::recruitCreatures(ObjectInstanceID objid, ObjectInstanceID dst sac.tid = objid; sac.creatures = dwelling->creatures; sac.creatures[level].first -= cram; - sendAndApply(&sac); + sendAndApply(sac); if (warMachine) { @@ -2362,7 +2444,7 @@ bool CGameHandler::changeStackType(const StackLocation &sl, const CCreature *c) sst.army = sl.army->id; sst.slot = sl.slot; sst.type = c->getId(); - sendAndApply(&sst); + sendAndApply(sst); return true; } @@ -2374,7 +2456,7 @@ void CGameHandler::moveArmy(const CArmedInstance *src, const CArmedInstance *dst auto i = src->Slots().begin(); //iterator to stack to move StackLocation sl(src, i->first); //location of stack to move - SlotID pos = dst->getSlotFor(i->second->type); + SlotID pos = dst->getSlotFor(i->second->getCreature()); if (!pos.validSlot()) { //try to merge two other stacks to make place @@ -2421,7 +2503,7 @@ bool CGameHandler::swapGarrisonOnSiege(ObjectInstanceID tid) intown.visiting = ObjectInstanceID(); intown.garrison = town->visitingHero->id; } - sendAndApply(&intown); + sendAndApply(intown); return true; } @@ -2443,7 +2525,7 @@ bool CGameHandler::garrisonSwap(ObjectInstanceID tid) intown.tid = tid; intown.visiting = ObjectInstanceID(); intown.garrison = town->visitingHero->id; - sendAndApply(&intown); + sendAndApply(intown); return true; } else if (town->garrisonHero && !town->visitingHero) //move hero out of the garrison @@ -2460,7 +2542,7 @@ bool CGameHandler::garrisonSwap(ObjectInstanceID tid) intown.tid = tid; intown.garrison = ObjectInstanceID(); intown.visiting = town->garrisonHero->id; - sendAndApply(&intown); + sendAndApply(intown); return true; } else if (!!town->garrisonHero && town->visitingHero) //swap visiting and garrison hero @@ -2469,7 +2551,7 @@ bool CGameHandler::garrisonSwap(ObjectInstanceID tid) intown.tid = tid; intown.garrison = town->visitingHero->id; intown.visiting = town->garrisonHero->id; - sendAndApply(&intown); + sendAndApply(intown); return true; } else @@ -2519,7 +2601,7 @@ bool CGameHandler::moveArtifact(const PlayerColor & player, const ArtifactLocati if((srcSlotInfo && srcSlotInfo->locked) || (dstSlotInfo && dstSlotInfo->locked)) COMPLAIN_RET("Cannot move artifact locks."); - if(isDstSlotBackpack && srcArtifact->artType->isBig()) + if(isDstSlotBackpack && srcArtifact->getType()->isBig()) COMPLAIN_RET("Cannot put big artifacts in backpack!"); if(src.slot == ArtifactPosition::MACH4 || dstSlot == ArtifactPosition::MACH4) COMPLAIN_RET("Cannot move catapult!"); @@ -2544,13 +2626,13 @@ bool CGameHandler::moveArtifact(const PlayerColor & player, const ArtifactLocati } auto hero = getHero(dst.artHolder); - if(ArtifactUtils::checkSpellbookIsNeeded(hero, srcArtifact->artType->getId(), dstSlot)) + if(ArtifactUtils::checkSpellbookIsNeeded(hero, srcArtifact->getTypeId(), dstSlot)) giveHeroNewArtifact(hero, ArtifactID::SPELLBOOK, ArtifactPosition::SPELLBOOK); ma.artsPack0.push_back(BulkMoveArtifacts::LinkedSlots(src.slot, dstSlot)); - if(src.artHolder != dst.artHolder) + if(src.artHolder != dst.artHolder && !isDstSlotBackpack) ma.artsPack0.back().askAssemble = true; - sendAndApply(&ma); + sendAndApply(ma); return true; } @@ -2651,26 +2733,91 @@ bool CGameHandler::bulkMoveArtifacts(const PlayerColor & player, ObjectInstanceI } } } - sendAndApply(&ma); + sendAndApply(ma); return true; } -bool CGameHandler::scrollBackpackArtifacts(const PlayerColor & player, const ObjectInstanceID heroID, bool left) +bool CGameHandler::manageBackpackArtifacts(const PlayerColor & player, const ObjectInstanceID heroID, const ManageBackpackArtifacts::ManageCmd & sortType) { const auto artSet = getArtSet(heroID); - COMPLAIN_RET_FALSE_IF(artSet == nullptr, "scrollBackpackArtifacts: wrong hero's ID"); + COMPLAIN_RET_FALSE_IF(artSet == nullptr, "manageBackpackArtifacts: wrong hero's ID"); BulkMoveArtifacts bma(player, heroID, heroID, false); - - const auto backpackEnd = ArtifactPosition(ArtifactPosition::BACKPACK_START + artSet->artifactsInBackpack.size() - 1); - if(backpackEnd > ArtifactPosition::BACKPACK_START) + const auto makeSortBackpackRequest = [artSet, &bma](const std::function & getSortId) { - if(left) - bma.artsPack0.push_back(BulkMoveArtifacts::LinkedSlots(backpackEnd, ArtifactPosition::BACKPACK_START)); - else - bma.artsPack0.push_back(BulkMoveArtifacts::LinkedSlots(ArtifactPosition::BACKPACK_START, backpackEnd)); - sendAndApply(&bma); + std::map> packsSorted; + ArtifactPosition backpackSlot = ArtifactPosition::BACKPACK_START; + for(const auto & backpackSlotInfo : artSet->artifactsInBackpack) + packsSorted.try_emplace(getSortId(backpackSlotInfo)).first->second.emplace_back(backpackSlot++, ArtifactPosition::PRE_FIRST); + + for(auto & [sortId, pack] : packsSorted) + { + // Each pack of artifacts is also sorted by ArtifactID. Scrolls by SpellID + std::sort(pack.begin(), pack.end(), [artSet](const auto & slots0, const auto & slots1) -> bool + { + const auto art0 = artSet->getArt(slots0.srcPos); + const auto art1 = artSet->getArt(slots1.srcPos); + if(art0->isScroll() && art1->isScroll()) + return art0->getScrollSpellID() > art1->getScrollSpellID(); + return art0->getTypeId().num > art1->getTypeId().num; + }); + bma.artsPack0.insert(bma.artsPack0.end(), pack.begin(), pack.end()); + } + backpackSlot = ArtifactPosition::BACKPACK_START; + for(auto & slots : bma.artsPack0) + slots.dstPos = backpackSlot++; + }; + + if(sortType == ManageBackpackArtifacts::ManageCmd::SORT_BY_SLOT) + { + makeSortBackpackRequest([](const ArtSlotInfo & inf) -> int32_t + { + auto possibleSlots = inf.getArt()->getType()->getPossibleSlots(); + if (possibleSlots.find(ArtBearer::CREATURE) != possibleSlots.end() && !possibleSlots.at(ArtBearer::CREATURE).empty()) + { + return -2; + } + else if (possibleSlots.find(ArtBearer::COMMANDER) != possibleSlots.end() && !possibleSlots.at(ArtBearer::COMMANDER).empty()) + { + return -1; + } + else if (possibleSlots.find(ArtBearer::HERO) != possibleSlots.end() && !possibleSlots.at(ArtBearer::HERO).empty()) + { + return inf.getArt()->getType()->getPossibleSlots().at(ArtBearer::HERO).front().num; + } + else + { + // for grail + return -3; + } + }); } + else if(sortType == ManageBackpackArtifacts::ManageCmd::SORT_BY_COST) + { + makeSortBackpackRequest([](const ArtSlotInfo & inf) -> int32_t + { + return inf.getArt()->getType()->getPrice(); + }); + } + else if(sortType == ManageBackpackArtifacts::ManageCmd::SORT_BY_CLASS) + { + makeSortBackpackRequest([](const ArtSlotInfo & inf) -> int32_t + { + return inf.getArt()->getType()->aClass; + }); + } + else + { + const auto backpackEnd = ArtifactPosition(ArtifactPosition::BACKPACK_START + artSet->artifactsInBackpack.size() - 1); + if(backpackEnd > ArtifactPosition::BACKPACK_START) + { + if(sortType == ManageBackpackArtifacts::ManageCmd::SCROLL_LEFT) + bma.artsPack0.emplace_back(backpackEnd, ArtifactPosition::BACKPACK_START); + else + bma.artsPack0.emplace_back(ArtifactPosition::BACKPACK_START, backpackEnd); + } + } + sendAndApply(bma); return true; } @@ -2686,7 +2833,7 @@ bool CGameHandler::saveArtifactsCostume(const PlayerColor & player, const Object costume.costumeSet.emplace(slot, slotInfo->getArt()->getTypeId()); } - sendAndApply(&costume); + sendAndApply(costume); return true; } @@ -2718,15 +2865,15 @@ bool CGameHandler::switchArtifactsCostume(const PlayerColor & player, const Obje // Second, find the necessary artifacts for the costume for(const auto & artPos : costumeArtMap) { - if(const auto availableArts = artFittingSet.getAllArtPositions(artPos.second, false, false, false); !availableArts.empty()) + if(const auto slot = artFittingSet.getArtPos(artPos.second, false, false); slot != ArtifactPosition::PRE_FIRST) { bma.artsPack0.emplace_back(BulkMoveArtifacts::LinkedSlots { - artSet->getArtPos(artFittingSet.getArt(availableArts.front())), + artSet->getArtPos(artFittingSet.getArt(slot)), artPos.first }); - artFittingSet.removeArtifact(availableArts.front()); - if(ArtifactUtils::isSlotBackpack(availableArts.front())) + artFittingSet.removeArtifact(slot); + if(ArtifactUtils::isSlotBackpack(slot)) estimateBackpackSize--; } } @@ -2741,7 +2888,7 @@ bool CGameHandler::switchArtifactsCostume(const PlayerColor & player, const Obje const auto backpackCap = getSettings().getInteger(EGameSettings::HEROES_BACKPACK_CAP); if((backpackCap < 0 || estimateBackpackSize <= backpackCap) && !bma.artsPack0.empty()) - sendAndApply(&bma); + sendAndApply(bma); } return true; } @@ -2783,21 +2930,24 @@ bool CGameHandler::assembleArtifacts(ObjectInstanceID heroID, ArtifactPosition a AssembledArtifact aa; aa.al = dstLoc; - aa.builtArt = combinedArt; - sendAndApply(&aa); + aa.artId = assembleTo; + sendAndApply(aa); } else { if(!destArtifact->isCombined()) COMPLAIN_RET("assembleArtifacts: Artifact being attempted to disassemble is not a combined artifact!"); + if(!destArtifact->hasParts()) + COMPLAIN_RET("assembleArtifacts: Artifact being attempted to disassemble is fused combined artifact!"); + if(ArtifactUtils::isSlotBackpack(artifactSlot) - && !ArtifactUtils::isBackpackFreeSlots(hero, destArtifact->artType->getConstituents().size() - 1)) + && !ArtifactUtils::isBackpackFreeSlots(hero, destArtifact->getType()->getConstituents().size() - 1)) COMPLAIN_RET("assembleArtifacts: Artifact being attempted to disassemble but backpack is full!"); DisassembledArtifact da; da.al = dstLoc; - sendAndApply(&da); + sendAndApply(da); } return true; @@ -2903,11 +3053,11 @@ bool CGameHandler::buyArtifact(const IMarket *m, const CGHeroInstance *h, GameRe COMPLAIN_RET("Wrong marktet..."); bool found = false; - for (const CArtifact *&art : saa.arts) + for (ArtifactID & art : saa.arts) { - if (art && art->getId() == aid) + if (art == aid) { - art = nullptr; + art = ArtifactID(); found = true; break; } @@ -2916,7 +3066,7 @@ bool CGameHandler::buyArtifact(const IMarket *m, const CGHeroInstance *h, GameRe if (!found) COMPLAIN_RET("Cannot find selected artifact on the list"); - sendAndApply(&saa); + sendAndApply(saa); giveHeroNewArtifact(h, aid, ArtifactPosition::FIRST_AVAILABLE); return true; } @@ -2926,11 +3076,11 @@ bool CGameHandler::sellArtifact(const IMarket *m, const CGHeroInstance *h, Artif COMPLAIN_RET_FALSE_IF((!h), "Only hero can sell artifacts!"); const CArtifactInstance *art = h->getArtByInstanceId(aid); COMPLAIN_RET_FALSE_IF((!art), "There is no artifact to sell!"); - COMPLAIN_RET_FALSE_IF((!art->artType->isTradable()), "Cannot sell a war machine or spellbook!"); + COMPLAIN_RET_FALSE_IF((!art->getType()->isTradable()), "Cannot sell a war machine or spellbook!"); int resVal = 0; int dump = 1; - m->getOffer(art->artType->getId(), rid, dump, resVal, EMarketMode::ARTIFACT_RESOURCE); + m->getOffer(art->getType()->getId(), rid, dump, resVal, EMarketMode::ARTIFACT_RESOURCE); removeArtifact(ArtifactLocation(h->id, h->getArtPos(art))); giveResource(h->tempOwner, rid, resVal); @@ -3005,7 +3155,7 @@ bool CGameHandler::sellCreatures(ui32 count, const IMarket *market, const CGHero int b1; //base quantities for trade int b2; - market->getOffer(s.type->getId(), resourceID, b1, b2, EMarketMode::CREATURE_RESOURCE); + market->getOffer(s.getId(), resourceID, b1, b2, EMarketMode::CREATURE_RESOURCE); int units = count / b1; //how many base quantities we trade if (count%b1) //all offered units of resource should be used, if not -> somewhere in calculations must be an error @@ -3080,7 +3230,7 @@ bool CGameHandler::setFormation(ObjectInstanceID hid, EArmyFormation formation) ChangeFormation cf; cf.hid = hid; cf.formation = formation; - sendAndApply(&cf); + sendAndApply(cf); return true; } @@ -3137,7 +3287,7 @@ void CGameHandler::showGarrisonDialog(ObjectInstanceID upobj, ObjectInstanceID h gd.objid = upobj; gd.removableUnits = removableUnits; gd.queryID = garrisonQuery->queryID; - sendAndApply(&gd); + sendAndApply(gd); } void CGameHandler::showObjectWindow(const CGObjectInstance * object, EOpenWindowMode window, const CGHeroInstance * visitor, bool addQuery) @@ -3153,7 +3303,7 @@ void CGameHandler::showObjectWindow(const CGObjectInstance * object, EOpenWindow pack.queryID = windowQuery->queryID; queries->addQuery(windowQuery); } - sendAndApply(&pack); + sendAndApply(pack); } bool CGameHandler::isAllowedExchange(ObjectInstanceID id1, ObjectInstanceID id2) @@ -3255,7 +3405,7 @@ void CGameHandler::objectVisited(const CGObjectInstance * obj, const CGHeroInsta hv.heroId = h->id; hv.player = h->tempOwner; hv.starting = true; - sendAndApply(&hv); + sendAndApply(hv); obj->onHeroVisit(h); }; @@ -3278,7 +3428,7 @@ void CGameHandler::objectVisitEnded(const CGHeroInstance *h, PlayerColor player) hv.player = event.getPlayer(); hv.heroId = event.getHero(); hv.starting = false; - sendAndApply(&hv); + sendAndApply(hv); }; //TODO: ObjectVisitEnded should also have id of visited object, @@ -3349,14 +3499,14 @@ void CGameHandler::checkVictoryLossConditionsForPlayer(PlayerColor player) { InfoWindow iw; getVictoryLossMessage(player, victoryLossCheckResult, iw); - sendAndApply(&iw); + sendAndApply(iw); PlayerEndsGame peg; peg.player = player; peg.victoryLossCheckResult = victoryLossCheckResult; peg.statistic = StatisticDataSet(gameState()->statistic); addStatistics(peg.statistic); // add last turn befor win / loss - sendAndApply(&peg); + sendAndApply(peg); turnOrder->onPlayerEndsGame(player); @@ -3375,8 +3525,8 @@ void CGameHandler::checkVictoryLossConditionsForPlayer(PlayerColor player) getVictoryLossMessage(player, peg.victoryLossCheckResult, iw); iw.player = i->first; - sendAndApply(&iw); - sendAndApply(&peg); + sendAndApply(iw); + sendAndApply(peg); } } @@ -3420,7 +3570,7 @@ void CGameHandler::checkVictoryLossConditionsForPlayer(PlayerColor player) InfoWindow iw; getVictoryLossMessage(player, victoryLossCheckResult.invert(), iw); iw.player = pc; - sendAndApply(&iw); + sendAndApply(iw); } } checkVictoryLossConditions(playerColors); @@ -3447,7 +3597,7 @@ bool CGameHandler::dig(const CGHeroInstance *h) SetMovePoints smp; smp.hid = h->id; smp.val = 0; - sendAndApply(&smp); + sendAndApply(smp); InfoWindow iw; iw.type = EInfoWindowMode::AUTO; @@ -3460,19 +3610,19 @@ bool CGameHandler::dig(const CGHeroInstance *h) iw.text.appendName(grail); // ... " The Grail" iw.soundID = soundBase::ULTIMATEARTIFACT; giveHeroNewArtifact(h, grail, ArtifactPosition::FIRST_AVAILABLE); //give grail - sendAndApply(&iw); + sendAndApply(iw); iw.soundID = soundBase::invalid; iw.components.emplace_back(ComponentType::ARTIFACT, grail); iw.text.clear(); iw.text.appendTextID(grail.toArtifact()->getDescriptionTextID()); - sendAndApply(&iw); + sendAndApply(iw); } else { iw.text.appendLocalString(EMetaText::GENERAL_TXT, 59); //"Nothing here. \n Where could it be?" iw.soundID = soundBase::Dig; - sendAndApply(&iw); + sendAndApply(iw); } return true; @@ -3516,7 +3666,7 @@ bool CGameHandler::sacrificeCreatures(const IMarket * market, const CGHeroInstan COMPLAIN_RET("Cannot sacrifice last creature!"); } - int crid = hero->getStack(slot[i]).type->getId(); + int crid = hero->getStack(slot[i]).getId(); changeStackCount(StackLocation(hero, slot[i]), -(TQuantity)count[i]); @@ -3554,7 +3704,7 @@ bool CGameHandler::sacrificeArtifact(const IMarket * market, const CGHeroInstanc { if(auto art = artSet->getArtByInstanceId(artInstId)) { - if(art->artType->isTradable()) + if(art->getType()->isTradable()) { int dmp; int expToGive; @@ -3592,7 +3742,7 @@ bool CGameHandler::insertNewStack(const StackLocation &sl, const CCreature *c, T ins.slot = sl.slot; ins.type = c->getId(); ins.count = count; - sendAndApply(&ins); + sendAndApply(ins); return true; } @@ -3611,7 +3761,7 @@ bool CGameHandler::eraseStack(const StackLocation &sl, bool forceRemoval) EraseStack es; es.army = sl.army->id; es.slot = sl.slot; - sendAndApply(&es); + sendAndApply(es); return true; } @@ -3636,7 +3786,7 @@ bool CGameHandler::changeStackCount(const StackLocation &sl, TQuantity count, bo csc.slot = sl.slot; csc.count = count; csc.absoluteValue = absoluteValue; - sendAndApply(&csc); + sendAndApply(csc); } return true; } @@ -3669,7 +3819,7 @@ void CGameHandler::tryJoiningArmy(const CArmedInstance *src, const CArmedInstanc { for (auto i = src->stacks.begin(); i != src->stacks.end(); i++)//while there are unmoved creatures { - SlotID pos = dst->getSlotFor(i->second->type); + SlotID pos = dst->getSlotFor(i->second->getCreature()); if (pos.validSlot()) { moveStack(StackLocation(src, i->first), StackLocation(dst, pos)); @@ -3718,7 +3868,7 @@ bool CGameHandler::moveStack(const StackLocation &src, const StackLocation &dst, rs.srcSlot = src.slot; rs.dstSlot = dst.slot; rs.count = count; - sendAndApply(&rs); + sendAndApply(rs); return true; } @@ -3752,14 +3902,15 @@ bool CGameHandler::swapStacks(const StackLocation & sl1, const StackLocation & s ss.dstArmy = sl2.army->id; ss.srcSlot = sl1.slot; ss.dstSlot = sl2.slot; - sendAndApply(&ss); + sendAndApply(ss); return true; } } -bool CGameHandler::putArtifact(const ArtifactLocation & al, const CArtifactInstance * art, std::optional askAssemble) +bool CGameHandler::putArtifact(const ArtifactLocation & al, const ArtifactInstanceID & id, std::optional askAssemble) { - assert(art && art->artType); + const auto artInst = getArtInstance(id); + assert(artInst && artInst->getType()); ArtifactLocation dst(al.artHolder, ArtifactPosition::PRE_FIRST); dst.creature = al.creature; auto putTo = getArtSet(al); @@ -3767,11 +3918,11 @@ bool CGameHandler::putArtifact(const ArtifactLocation & al, const CArtifactInsta if(al.slot == ArtifactPosition::FIRST_AVAILABLE) { - dst.slot = ArtifactUtils::getArtAnyPosition(putTo, art->getTypeId()); + dst.slot = ArtifactUtils::getArtAnyPosition(putTo, artInst->getTypeId()); } else if(ArtifactUtils::isSlotBackpack(al.slot) && !al.creature.has_value()) { - dst.slot = ArtifactUtils::getArtBackpackPosition(putTo, art->getTypeId()); + dst.slot = ArtifactUtils::getArtBackpackPosition(putTo, artInst->getTypeId()); } else { @@ -3786,11 +3937,10 @@ bool CGameHandler::putArtifact(const ArtifactLocation & al, const CArtifactInsta askAssemble = false; } - if(art->canBePutAt(putTo, dst.slot)) + if(artInst->canBePutAt(putTo, dst.slot)) { - PutArtifact pa(dst, askAssemble.value()); - pa.art = art; - sendAndApply(&pa); + PutArtifact pa(id, dst, askAssemble.value()); + sendAndApply(pa); return true; } else @@ -3825,7 +3975,7 @@ bool CGameHandler::giveHeroNewArtifact( { COMPLAIN_RET_FALSE_IF(!artType->canBePutAt(h, pos, false), "Cannot put artifact in that slot!"); } - sendAndApply(&na); + sendAndApply(na); return true; } @@ -3870,7 +4020,7 @@ void CGameHandler::synchronizeArtifactHandlerLists() { UpdateArtHandlerLists uahl; uahl.allocatedArtifacts = gs->allocatedArtifacts; - sendAndApply(&uahl); + sendAndApply(uahl); } bool CGameHandler::isValidObject(const CGObjectInstance *obj) const @@ -3878,11 +4028,14 @@ bool CGameHandler::isValidObject(const CGObjectInstance *obj) const return vstd::contains(gs->map->objects, obj); } -bool CGameHandler::isBlockedByQueries(const CPack *pack, PlayerColor player) +bool CGameHandler::isBlockedByQueries(const CPackForServer *pack, PlayerColor player) { if (dynamic_cast(pack) != nullptr) return false; + if (dynamic_cast(pack) != nullptr) + return false; + auto query = queries->topQuery(player); if (query && query->blocksPack(pack)) { @@ -3957,7 +4110,7 @@ void CGameHandler::changeFogOfWar(const std::unordered_set &tiles, PlayerC return; } - sendAndApply(&fow); + sendAndApply(fow); } const CGHeroInstance * CGameHandler::getVisitingHero(const CGObjectInstance *obj) @@ -4008,7 +4161,7 @@ void CGameHandler::setObjPropertyValue(ObjectInstanceID objid, ObjProperty prop, sob.id = objid; sob.what = prop; sob.identifier = NumericID(value); - sendAndApply(&sob); + sendAndApply(sob); } void CGameHandler::setObjPropertyID(ObjectInstanceID objid, ObjProperty prop, ObjPropertyID identifier) @@ -4017,7 +4170,7 @@ void CGameHandler::setObjPropertyID(ObjectInstanceID objid, ObjProperty prop, Ob sob.id = objid; sob.what = prop; sob.identifier = identifier; - sendAndApply(&sob); + sendAndApply(sob); } void CGameHandler::setBankObjectConfiguration(ObjectInstanceID objid, const BankConfig & configuration) @@ -4025,7 +4178,7 @@ void CGameHandler::setBankObjectConfiguration(ObjectInstanceID objid, const Bank SetBankConfiguration srb; srb.objectID = objid; srb.configuration = configuration; - sendAndApply(&srb); + sendAndApply(srb); } void CGameHandler::setRewardableObjectConfiguration(ObjectInstanceID objid, const Rewardable::Configuration & configuration) @@ -4033,7 +4186,7 @@ void CGameHandler::setRewardableObjectConfiguration(ObjectInstanceID objid, cons SetRewardableConfiguration srb; srb.objectID = objid; srb.configuration = configuration; - sendAndApply(&srb); + sendAndApply(srb); } void CGameHandler::setRewardableObjectConfiguration(ObjectInstanceID townInstanceID, BuildingID buildingID, const Rewardable::Configuration & configuration) @@ -4042,12 +4195,12 @@ void CGameHandler::setRewardableObjectConfiguration(ObjectInstanceID townInstanc srb.objectID = townInstanceID; srb.buildingID = buildingID; srb.configuration = configuration; - sendAndApply(&srb); + sendAndApply(srb); } void CGameHandler::showInfoDialog(InfoWindow * iw) { - sendAndApply(iw); + sendAndApply(*iw); } vstd::RNG & CGameHandler::getRandomGenerator() @@ -4076,7 +4229,7 @@ CGObjectInstance * CGameHandler::createNewObject(const int3 & visitablePosition, throw std::runtime_error("Attempt to create object outside map at " + visitablePosition.toString()); const TerrainTile & t = gs->map->getTile(visitablePosition); - terrainType = t.terType->getId(); + terrainType = t.getTerrainID(); auto handler = VLC->objtypeh->getHandlerFor(objectID, subID); @@ -4093,8 +4246,11 @@ CGObjectInstance * CGameHandler::createNewObject(const int3 & visitablePosition, else o->appearance = handler->getTemplates().front(); + if (o->isVisitable()) + o->setAnchorPos(visitablePosition + o->getVisitableOffset()); + else + o->setAnchorPos(visitablePosition); - o->pos = visitablePosition + o->getVisitableOffset(); return o; } @@ -4132,7 +4288,7 @@ void CGameHandler::newObject(CGObjectInstance * object, PlayerColor initiator) NewObject no; no.newObject = object; no.initiator = initiator; - sendAndApply(&no); + sendAndApply(no); } void CGameHandler::startBattle(const CArmedInstance *army1, const CArmedInstance *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, const BattleLayout & layout, const CGTownInstance *town) diff --git a/server/CGameHandler.h b/server/CGameHandler.h index cdb194377..9d896f758 100644 --- a/server/CGameHandler.h +++ b/server/CGameHandler.h @@ -15,6 +15,7 @@ #include "../lib/LoadProgress.h" #include "../lib/ScriptHandler.h" #include "../lib/gameState/GameStatistics.h" +#include "../lib/networkPacks/PacksForServer.h" VCMI_LIB_NAMESPACE_BEGIN @@ -26,7 +27,6 @@ class CCommanderInstance; class EVictoryLossCheckResult; class CRandomGenerator; -struct CPack; struct CPackForServer; struct NewTurn; struct CGarrisonOperationPack; @@ -88,7 +88,7 @@ public: CVCMIServer * gameLobby() const; bool isValidObject(const CGObjectInstance *obj) const; - bool isBlockedByQueries(const CPack *pack, PlayerColor player); + bool isBlockedByQueries(const CPackForServer *pack, PlayerColor player); bool isAllowedExchange(ObjectInstanceID id1, ObjectInstanceID id2); void giveSpells(const CGTownInstance *t, const CGHeroInstance *h); @@ -107,6 +107,7 @@ public: //from IGameCallback //do sth void changeSpells(const CGHeroInstance * hero, bool give, const std::set &spells) override; + void setResearchedSpells(const CGTownInstance * town, int level, const std::vector & spells, bool accepted) override; bool removeObject(const CGObjectInstance * obj, const PlayerColor & initiator) override; void setOwner(const CGObjectInstance * obj, PlayerColor owner) override; void giveExperience(const CGHeroInstance * hero, TExpType val) override; @@ -136,12 +137,12 @@ public: bool giveHeroNewArtifact(const CGHeroInstance * h, const CArtifact * artType, const SpellID & spellId, const ArtifactPosition & pos); bool giveHeroNewArtifact(const CGHeroInstance * h, const ArtifactID & artId, const ArtifactPosition & pos) override; bool giveHeroNewScroll(const CGHeroInstance * h, const SpellID & spellId, const ArtifactPosition & pos) override; - bool putArtifact(const ArtifactLocation & al, const CArtifactInstance * art, std::optional askAssemble) override; + bool putArtifact(const ArtifactLocation & al, const ArtifactInstanceID & id, std::optional askAssemble) override; void removeArtifact(const ArtifactLocation &al) override; void removeArtifact(const ObjectInstanceID & srcId, const std::vector & slotsPack); bool moveArtifact(const PlayerColor & player, const ArtifactLocation & src, const ArtifactLocation & dst) override; bool bulkMoveArtifacts(const PlayerColor & player, ObjectInstanceID srcId, ObjectInstanceID dstId, bool swap, bool equipped, bool backpack); - bool scrollBackpackArtifacts(const PlayerColor & player, const ObjectInstanceID heroID, bool left); + bool manageBackpackArtifacts(const PlayerColor & player, const ObjectInstanceID heroID, const ManageBackpackArtifacts::ManageCmd & sortType); bool saveArtifactsCostume(const PlayerColor & player, const ObjectInstanceID heroID, uint32_t costumeIdx); bool switchArtifactsCostume(const PlayerColor & player, const ObjectInstanceID heroID, uint32_t costumeIdx); bool eraseArtifactByClient(const ArtifactLocation & al); @@ -193,7 +194,7 @@ public: void init(StartInfo *si, Load::ProgressAccumulator & progressTracking); void handleClientDisconnection(std::shared_ptr c); - void handleReceivedPack(CPackForServer * pack); + void handleReceivedPack(CPackForServer & pack); bool hasPlayerAt(PlayerColor player, std::shared_ptr c) const; bool hasBothPlayersAtSameConnection(PlayerColor left, PlayerColor right) const; @@ -218,6 +219,7 @@ public: bool buildStructure(ObjectInstanceID tid, BuildingID bid, bool force=false);//force - for events: no cost, no checkings bool visitTownBuilding(ObjectInstanceID tid, BuildingID bid); bool razeStructure(ObjectInstanceID tid, BuildingID bid); + bool spellResearch(ObjectInstanceID tid, SpellID spellAtSlot, bool accepted); bool disbandCreature( ObjectInstanceID id, SlotID pos ); bool arrangeStacks( ObjectInstanceID id1, ObjectInstanceID id2, ui8 what, SlotID p1, SlotID p2, si32 val, PlayerColor player); bool bulkMoveArmy(ObjectInstanceID srcArmy, ObjectInstanceID destArmy, SlotID srcSlot); @@ -258,11 +260,11 @@ public: #endif } - void sendToAllClients(CPackForClient * pack); - void sendAndApply(CPackForClient * pack) override; - void sendAndApply(CGarrisonOperationPack * pack); - void sendAndApply(SetResources * pack); - void sendAndApply(NewStructures * pack); + void sendToAllClients(CPackForClient & pack); + void sendAndApply(CPackForClient & pack) override; + void sendAndApply(CGarrisonOperationPack & pack); + void sendAndApply(SetResources & pack); + void sendAndApply(NewStructures & pack); void wrongPlayerMessage(CPackForServer * pack, PlayerColor expectedplayer); /// Unconditionally throws with "Action not allowed" message diff --git a/server/CVCMIServer.cpp b/server/CVCMIServer.cpp index 0d8cd9280..53b6879d8 100644 --- a/server/CVCMIServer.cpp +++ b/server/CVCMIServer.cpp @@ -15,9 +15,10 @@ #include "LobbyNetPackVisitors.h" #include "processors/PlayerMessageProcessor.h" -#include "../lib/CHeroHandler.h" #include "../lib/CPlayerState.h" #include "../lib/campaign/CampaignState.h" +#include "../lib/entities/hero/CHeroHandler.h" +#include "../lib/entities/hero/CHeroClass.h" #include "../lib/gameState/CGameState.h" #include "../lib/mapping/CMapDefines.h" #include "../lib/mapping/CMapInfo.h" @@ -49,13 +50,13 @@ public: void visitForLobby(CPackForLobby & packForLobby) override { - handler.handleReceivedPack(std::unique_ptr(&packForLobby)); + handler.handleReceivedPack(packForLobby); } void visitForServer(CPackForServer & serverPack) override { if (gh) - gh->handleReceivedPack(&serverPack); + gh->handleReceivedPack(serverPack); else logNetwork->error("Received pack for game server while in lobby!"); } @@ -231,9 +232,9 @@ bool CVCMIServer::prepareToStartGame() { //FIXME: UNGUARDED MULTITHREADED ACCESS!!! currentProgress = progressTracking.get(); - std::unique_ptr loadProgress(new LobbyLoadProgress); - loadProgress->progress = currentProgress; - announcePack(std::move(loadProgress)); + LobbyLoadProgress loadProgress; + loadProgress.progress = currentProgress; + announcePack(loadProgress); } boost::this_thread::sleep(boost::posix_time::milliseconds(50)); } @@ -244,7 +245,7 @@ bool CVCMIServer::prepareToStartGame() { case EStartMode::CAMPAIGN: logNetwork->info("Preparing to start new campaign"); - si->startTimeIso8601 = vstd::getDateTimeISO8601Basic(std::time(nullptr)); + si->startTime = std::time(nullptr); si->fileURI = mi->fileURI; si->campState->setCurrentMap(campaignMap); si->campState->setCurrentMapBonus(campaignBonus); @@ -253,7 +254,7 @@ bool CVCMIServer::prepareToStartGame() case EStartMode::NEW_GAME: logNetwork->info("Preparing to start new game"); - si->startTimeIso8601 = vstd::getDateTimeISO8601Basic(std::time(nullptr)); + si->startTime = std::time(nullptr); si->fileURI = mi->fileURI; gh->init(si.get(), progressTracking); break; @@ -297,43 +298,32 @@ void CVCMIServer::onDisconnected(const std::shared_ptr & con logNetwork->error("Network error receiving a pack. Connection has been closed"); std::shared_ptr c = findConnection(connection); - if (!c) - return; // player have already disconnected via clientDisconnected call - vstd::erase(activeConnections, c); - - if(activeConnections.empty() || hostClientId == c->connectionID) + // player may have already disconnected via clientDisconnected call + if (c) { - setState(EServerState::SHUTDOWN); - return; - } - - if(gh && getState() == EServerState::GAMEPLAY) - { - gh->handleClientDisconnection(c); - - auto lcd = std::make_unique(); - lcd->c = c; - lcd->clientId = c->connectionID; - handleReceivedPack(std::move(lcd)); + LobbyClientDisconnected lcd; + lcd.c = c; + lcd.clientId = c->connectionID; + handleReceivedPack(lcd); } } -void CVCMIServer::handleReceivedPack(std::unique_ptr pack) +void CVCMIServer::handleReceivedPack(CPackForLobby & pack) { ClientPermissionsCheckerNetPackVisitor checker(*this); - pack->visit(checker); + pack.visit(checker); if(checker.getResult()) { ApplyOnServerNetPackVisitor applier(*this); - pack->visit(applier); + pack.visit(applier); if (applier.getResult()) - announcePack(std::move(pack)); + announcePack(pack); } } -void CVCMIServer::announcePack(std::unique_ptr pack) +void CVCMIServer::announcePack(CPackForLobby & pack) { for(auto activeConnection : activeConnections) { @@ -341,19 +331,19 @@ void CVCMIServer::announcePack(std::unique_ptr pack) // Until UUID set we only pass LobbyClientConnected to this client //if(c->uuid == uuid && !dynamic_cast(pack.get())) // continue; - activeConnection->sendPack(pack.get()); + activeConnection->sendPack(pack); } ApplyOnServerAfterAnnounceNetPackVisitor applier(*this); - pack->visit(applier); + pack.visit(applier); } void CVCMIServer::announceMessage(const MetaString & txt) { logNetwork->info("Show message: %s", txt.toString()); - auto cm = std::make_unique(); - cm->message = txt; - announcePack(std::move(cm)); + LobbyShowMessage cm; + cm.message = txt; + announcePack(cm); } void CVCMIServer::announceMessage(const std::string & txt) @@ -366,10 +356,10 @@ void CVCMIServer::announceMessage(const std::string & txt) void CVCMIServer::announceTxt(const MetaString & txt, const std::string & playerName) { logNetwork->info("%s says: %s", playerName, txt.toString()); - auto cm = std::make_unique(); - cm->playerName = playerName; - cm->message = txt; - announcePack(std::move(cm)); + LobbyChatMessage cm; + cm.playerName = playerName; + cm.message = txt; + announcePack(cm); } void CVCMIServer::announceTxt(const std::string & txt, const std::string & playerName) @@ -434,9 +424,21 @@ void CVCMIServer::clientConnected(std::shared_ptr c, std::vector connection) { - connection->getConnection()->close(); + assert(vstd::contains(activeConnections, connection)); + logGlobal->trace("Received disconnection request"); vstd::erase(activeConnections, connection); + if(activeConnections.empty() || hostClientId == connection->connectionID) + { + setState(EServerState::SHUTDOWN); + return; + } + + if(gh && getState() == EServerState::GAMEPLAY) + { + gh->handleClientDisconnection(connection); + } + // PlayerReinitInterface startAiPack; // startAiPack.playerConnectionId = PlayerSettings::PLAYER_AI; // @@ -470,7 +472,7 @@ void CVCMIServer::clientDisconnected(std::shared_ptr connection) // } // // if(!startAiPack.players.empty()) -// gh->sendAndApply(&startAiPack); +// gh->sendAndApply(startAiPack); } void CVCMIServer::reconnectPlayer(int connId) @@ -497,7 +499,7 @@ void CVCMIServer::reconnectPlayer(int connId) } if(!startAiPack.players.empty()) - gh->sendAndApply(&startAiPack); + gh->sendAndApply(startAiPack); } } @@ -627,9 +629,9 @@ void CVCMIServer::updateAndPropagateLobbyState() } } - auto lus = std::make_unique(); - lus->state = *this; - announcePack(std::move(lus)); + LobbyUpdateState lus; + lus.state = *this; + announcePack(lus); } void CVCMIServer::setPlayer(PlayerColor clickedColor) diff --git a/server/CVCMIServer.h b/server/CVCMIServer.h index 46bddeb01..813352ae3 100644 --- a/server/CVCMIServer.h +++ b/server/CVCMIServer.h @@ -86,7 +86,7 @@ public: void threadHandleClient(std::shared_ptr c); - void announcePack(std::unique_ptr pack); + void announcePack(CPackForLobby & pack); bool passHost(int toConnectionId); void announceTxt(const MetaString & txt, const std::string & playerName = "system"); @@ -102,7 +102,7 @@ public: void announceMessage(const MetaString & txt); void announceMessage(const std::string & txt); - void handleReceivedPack(std::unique_ptr pack); + void handleReceivedPack(CPackForLobby & pack); void updateAndPropagateLobbyState(); diff --git a/server/GlobalLobbyProcessor.cpp b/server/GlobalLobbyProcessor.cpp index 15fd376f7..418fad4da 100644 --- a/server/GlobalLobbyProcessor.cpp +++ b/server/GlobalLobbyProcessor.cpp @@ -15,7 +15,8 @@ #include "../lib/json/JsonUtils.h" #include "../lib/VCMI_Lib.h" #include "../lib/modding/CModHandler.h" -#include "../lib/modding/CModInfo.h" +#include "../lib/modding/ModDescription.h" +#include "../lib/modding/ModVerificationInfo.h" GlobalLobbyProcessor::GlobalLobbyProcessor(CVCMIServer & owner) : owner(owner) @@ -161,7 +162,7 @@ JsonNode GlobalLobbyProcessor::getHostModList() const for (auto const & modName : VLC->modh->getActiveMods()) { - if(VLC->modh->getModInfo(modName).checkModGameplayAffecting()) + if(VLC->modh->getModInfo(modName).affectsGameplay()) info[modName] = VLC->modh->getModInfo(modName).getVerificationInfo(); } diff --git a/server/LobbyNetPackVisitors.h b/server/LobbyNetPackVisitors.h index 0905aed48..1e103890b 100644 --- a/server/LobbyNetPackVisitors.h +++ b/server/LobbyNetPackVisitors.h @@ -39,6 +39,7 @@ public: void visitLobbyChatMessage(LobbyChatMessage & pack) override; void visitLobbyGuiAction(LobbyGuiAction & pack) override; void visitLobbyPvPAction(LobbyPvPAction & pack) override; + void visitLobbyDelete(LobbyDelete & pack) override; }; class ApplyOnServerAfterAnnounceNetPackVisitor : public VCMI_LIB_WRAP_NAMESPACE(ICPackVisitor) @@ -96,4 +97,5 @@ public: void visitLobbySetDifficulty(LobbySetDifficulty & pack) override; void visitLobbyForceSetPlayer(LobbyForceSetPlayer & pack) override; void visitLobbyPvPAction(LobbyPvPAction & pack) override; + void visitLobbyDelete(LobbyDelete & pack) override; }; diff --git a/server/NetPacksLobbyServer.cpp b/server/NetPacksLobbyServer.cpp index 4cf091ac0..1b45736fb 100644 --- a/server/NetPacksLobbyServer.cpp +++ b/server/NetPacksLobbyServer.cpp @@ -22,6 +22,7 @@ #include "../lib/serializer/Connection.h" #include "../lib/mapping/CMapInfo.h" #include "../lib/mapping/CMapHeader.h" +#include "../lib/filesystem/Filesystem.h" void ClientPermissionsCheckerNetPackVisitor::visitForLobby(CPackForLobby & pack) { @@ -108,6 +109,7 @@ void ClientPermissionsCheckerNetPackVisitor::visitLobbyClientDisconnected(LobbyC void ApplyOnServerNetPackVisitor::visitLobbyClientDisconnected(LobbyClientDisconnected & pack) { + pack.c->getConnection()->close(); srv.clientDisconnected(pack.c); result = true; } @@ -127,10 +129,10 @@ void ApplyOnServerAfterAnnounceNetPackVisitor::visitLobbyClientDisconnected(Lobb } else if(pack.c->connectionID == srv.hostClientId) { - auto ph = std::make_unique(); + LobbyChangeHost ph; auto newHost = srv.activeConnections.front(); - ph->newHostConnectionId = newHost->connectionID; - srv.announcePack(std::move(ph)); + ph.newHostConnectionId = newHost->connectionID; + srv.announcePack(ph); } srv.updateAndPropagateLobbyState(); @@ -382,7 +384,6 @@ void ApplyOnServerNetPackVisitor::visitLobbyForceSetPlayer(LobbyForceSetPlayer & result = true; } - void ClientPermissionsCheckerNetPackVisitor::visitLobbyPvPAction(LobbyPvPAction & pack) { result = true; @@ -432,3 +433,32 @@ void ApplyOnServerNetPackVisitor::visitLobbyPvPAction(LobbyPvPAction & pack) } result = true; } + + +void ClientPermissionsCheckerNetPackVisitor::visitLobbyDelete(LobbyDelete & pack) +{ + result = srv.isClientHost(pack.c->connectionID); +} + +void ApplyOnServerNetPackVisitor::visitLobbyDelete(LobbyDelete & pack) +{ + if(pack.type == LobbyDelete::EType::SAVEGAME || pack.type == LobbyDelete::EType::RANDOMMAP) + { + auto res = ResourcePath(pack.name, pack.type == LobbyDelete::EType::SAVEGAME ? EResType::SAVEGAME : EResType::MAP); + auto file = boost::filesystem::canonical(*CResourceHandler::get()->getResourceName(res)); + boost::filesystem::remove(file); + if(boost::filesystem::is_empty(file.parent_path())) + boost::filesystem::remove(file.parent_path()); + } + else if(pack.type == LobbyDelete::EType::SAVEGAME_FOLDER) + { + auto res = ResourcePath("Saves/" + pack.name, EResType::DIRECTORY); + auto folder = boost::filesystem::canonical(*CResourceHandler::get()->getResourceName(res)); + boost::filesystem::remove_all(folder); + } + + LobbyUpdateState lus; + lus.state = srv; + lus.refreshList = true; + srv.announcePack(lus); +} diff --git a/server/NetPacksServer.cpp b/server/NetPacksServer.cpp index 2a7b493d4..ee23bcc9e 100644 --- a/server/NetPacksServer.cpp +++ b/server/NetPacksServer.cpp @@ -19,6 +19,7 @@ #include "queries/MapQueries.h" #include "../lib/IGameCallback.h" +#include "../lib/CPlayerState.h" #include "../lib/mapObjects/CGTownInstance.h" #include "../lib/mapObjects/CGHeroInstance.h" #include "../lib/gameState/CGameState.h" @@ -138,6 +139,14 @@ void ApplyGhNetPackVisitor::visitBuildStructure(BuildStructure & pack) result = gh.buildStructure(pack.tid, pack.bid); } +void ApplyGhNetPackVisitor::visitSpellResearch(SpellResearch & pack) +{ + gh.throwIfWrongOwner(&pack, pack.tid); + gh.throwIfPlayerNotActive(&pack); + + result = gh.spellResearch(pack.tid, pack.spellAtSlot, pack.accepted); +} + void ApplyGhNetPackVisitor::visitVisitTownBuilding(VisitTownBuilding & pack) { gh.throwIfWrongOwner(&pack, pack.tid); @@ -197,22 +206,7 @@ void ApplyGhNetPackVisitor::visitManageBackpackArtifacts(ManageBackpackArtifacts gh.throwIfPlayerNotActive(&pack); if(gh.getPlayerRelations(pack.player, gh.getOwner(pack.artHolder)) != PlayerRelations::ENEMIES) - { - if(pack.cmd == ManageBackpackArtifacts::ManageCmd::SCROLL_LEFT) - result = gh.scrollBackpackArtifacts(pack.player, pack.artHolder, true); - else if(pack.cmd == ManageBackpackArtifacts::ManageCmd::SCROLL_RIGHT) - result = gh.scrollBackpackArtifacts(pack.player, pack.artHolder, false); - else - { - gh.throwIfWrongOwner(&pack, pack.artHolder); - if(pack.cmd == ManageBackpackArtifacts::ManageCmd::SORT_BY_CLASS) - result = true; - else if(pack.cmd == ManageBackpackArtifacts::ManageCmd::SORT_BY_COST) - result = true; - else if(pack.cmd == ManageBackpackArtifacts::ManageCmd::SORT_BY_SLOT) - result = true; - } - } + result = gh.manageBackpackArtifacts(pack.player, pack.artHolder, pack.cmd); } void ApplyGhNetPackVisitor::visitManageEquippedArtifacts(ManageEquippedArtifacts & pack) @@ -290,7 +284,7 @@ void ApplyGhNetPackVisitor::visitTradeOnMarketplace(TradeOnMarketplace & pack) gh.throwAndComplain(&pack, "Can not trade - no hero!"); // TODO: check that object is actually being visited (e.g. Query exists) - if (!object->visitableAt(hero->visitablePos().x, hero->visitablePos().y)) + if (!object->visitableAt(hero->visitablePos())) gh.throwAndComplain(&pack, "Can not trade - object not visited!"); if (object->getOwner().isValidPlayer() && gh.getPlayerRelations(object->getOwner(), hero->getOwner()) == PlayerRelations::ENEMIES) @@ -396,6 +390,13 @@ void ApplyGhNetPackVisitor::visitQueryReply(QueryReply & pack) result = gh.queryReply(pack.qid, pack.reply, pack.player); } +void ApplyGhNetPackVisitor::visitSaveLocalState(SaveLocalState & pack) +{ + gh.throwIfWrongPlayer(&pack); + *gh.gameState()->getPlayerState(pack.player)->playerLocalSettings = pack.data; + result = true; +} + void ApplyGhNetPackVisitor::visitMakeAction(MakeAction & pack) { gh.throwIfWrongPlayer(&pack); diff --git a/server/ServerNetPackVisitors.h b/server/ServerNetPackVisitors.h index 34593c1da..73feeec8f 100644 --- a/server/ServerNetPackVisitors.h +++ b/server/ServerNetPackVisitors.h @@ -41,6 +41,7 @@ public: void visitBulkSmartSplitStack(BulkSmartSplitStack & pack) override; void visitDisbandCreature(DisbandCreature & pack) override; void visitBuildStructure(BuildStructure & pack) override; + void visitSpellResearch(SpellResearch & pack) override; void visitVisitTownBuilding(VisitTownBuilding & pack) override; void visitRecruitCreatures(RecruitCreatures & pack) override; void visitUpgradeCreature(UpgradeCreature & pack) override; @@ -61,4 +62,5 @@ public: void visitDigWithHero(DigWithHero & pack) override; void visitCastAdvSpell(CastAdvSpell & pack) override; void visitPlayerMessage(PlayerMessage & pack) override; + void visitSaveLocalState(SaveLocalState & pack) override; }; diff --git a/server/ServerSpellCastEnvironment.cpp b/server/ServerSpellCastEnvironment.cpp index 9213aef7b..81a9b114e 100644 --- a/server/ServerSpellCastEnvironment.cpp +++ b/server/ServerSpellCastEnvironment.cpp @@ -39,42 +39,42 @@ vstd::RNG * ServerSpellCastEnvironment::getRNG() return &gh->getRandomGenerator(); } -void ServerSpellCastEnvironment::apply(CPackForClient * pack) +void ServerSpellCastEnvironment::apply(CPackForClient & pack) { gh->sendAndApply(pack); } -void ServerSpellCastEnvironment::apply(BattleLogMessage * pack) +void ServerSpellCastEnvironment::apply(BattleLogMessage & pack) { gh->sendAndApply(pack); } -void ServerSpellCastEnvironment::apply(BattleStackMoved * pack) +void ServerSpellCastEnvironment::apply(BattleStackMoved & pack) { gh->sendAndApply(pack); } -void ServerSpellCastEnvironment::apply(BattleUnitsChanged * pack) +void ServerSpellCastEnvironment::apply(BattleUnitsChanged & pack) { gh->sendAndApply(pack); } -void ServerSpellCastEnvironment::apply(SetStackEffect * pack) +void ServerSpellCastEnvironment::apply(SetStackEffect & pack) { gh->sendAndApply(pack); } -void ServerSpellCastEnvironment::apply(StacksInjured * pack) +void ServerSpellCastEnvironment::apply(StacksInjured & pack) { gh->sendAndApply(pack); } -void ServerSpellCastEnvironment::apply(BattleObstaclesChanged * pack) +void ServerSpellCastEnvironment::apply(BattleObstaclesChanged & pack) { gh->sendAndApply(pack); } -void ServerSpellCastEnvironment::apply(CatapultAttack * pack) +void ServerSpellCastEnvironment::apply(CatapultAttack & pack) { gh->sendAndApply(pack); } @@ -104,5 +104,5 @@ void ServerSpellCastEnvironment::genericQuery(Query * request, PlayerColor color auto query = std::make_shared(gh, color, callback); request->queryID = query->queryID; gh->queries->addQuery(query); - gh->sendAndApply(request); + gh->sendAndApply(*request); } diff --git a/server/ServerSpellCastEnvironment.h b/server/ServerSpellCastEnvironment.h index e2aa9b796..7af8a0a67 100644 --- a/server/ServerSpellCastEnvironment.h +++ b/server/ServerSpellCastEnvironment.h @@ -24,15 +24,15 @@ public: vstd::RNG * getRNG() override; - void apply(CPackForClient * pack) override; + void apply(CPackForClient & pack) override; - void apply(BattleLogMessage * pack) override; - void apply(BattleStackMoved * pack) override; - void apply(BattleUnitsChanged * pack) override; - void apply(SetStackEffect * pack) override; - void apply(StacksInjured * pack) override; - void apply(BattleObstaclesChanged * pack) override; - void apply(CatapultAttack * pack) override; + void apply(BattleLogMessage & pack) override; + void apply(BattleStackMoved & pack) override; + void apply(BattleUnitsChanged & pack) override; + void apply(SetStackEffect & pack) override; + void apply(StacksInjured & pack) override; + void apply(BattleObstaclesChanged & pack) override; + void apply(CatapultAttack & pack) override; const CMap * getMap() const override; const CGameInfoCallback * getCb() const override; diff --git a/server/TurnTimerHandler.cpp b/server/TurnTimerHandler.cpp index a694e1df6..b5e6cdb75 100644 --- a/server/TurnTimerHandler.cpp +++ b/server/TurnTimerHandler.cpp @@ -60,7 +60,7 @@ void TurnTimerHandler::sendTimerUpdate(PlayerColor player) TurnTimeUpdate ttu; ttu.player = player; ttu.turnTimer = timers[player]; - gameHandler.sendAndApply(&ttu); + gameHandler.sendAndApply(ttu); lastUpdate[player] = 0; } diff --git a/server/battles/BattleActionProcessor.cpp b/server/battles/BattleActionProcessor.cpp index 2ab7665e1..767cfc065 100644 --- a/server/battles/BattleActionProcessor.cpp +++ b/server/battles/BattleActionProcessor.cpp @@ -187,7 +187,7 @@ bool BattleActionProcessor::doDefendAction(const CBattleInfoCallback & battle, c buffer.push_back(bonus2); sse.toUpdate.push_back(std::make_pair(ba.stackNumber, buffer)); - gameHandler->sendAndApply(&sse); + gameHandler->sendAndApply(sse); BattleLogMessage message; message.battleID = battle.getBattle()->getBattleID(); @@ -199,7 +199,7 @@ bool BattleActionProcessor::doDefendAction(const CBattleInfoCallback & battle, c message.lines.push_back(text); - gameHandler->sendAndApply(&message); + gameHandler->sendAndApply(message); return true; } @@ -596,7 +596,7 @@ bool BattleActionProcessor::makeBattleActionImpl(const CBattleInfoCallback & bat { StartAction startAction(ba); startAction.battleID = battle.getBattle()->getBattleID(); - gameHandler->sendAndApply(&startAction); + gameHandler->sendAndApply(startAction); } bool result = dispatchBattleAction(battle, ba); @@ -605,7 +605,7 @@ bool BattleActionProcessor::makeBattleActionImpl(const CBattleInfoCallback & bat { EndAction endAction; endAction.battleID = battle.getBattle()->getBattleID(); - gameHandler->sendAndApply(&endAction); + gameHandler->sendAndApply(endAction); } if(ba.actionType == EActionType::WAIT || ba.actionType == EActionType::DEFEND || ba.actionType == EActionType::SHOOT || ba.actionType == EActionType::MONSTER_SPELL) @@ -716,7 +716,7 @@ int BattleActionProcessor::moveStack(const CBattleInfoCallback & battle, int sta BattleUpdateGateState db; db.battleID = battle.getBattle()->getBattleID(); db.state = EGateState::OPENED; - gameHandler->sendAndApply(&db); + gameHandler->sendAndApply(db); } //inform clients about move @@ -728,7 +728,7 @@ int BattleActionProcessor::moveStack(const CBattleInfoCallback & battle, int sta sm.tilesToMove = tiles; sm.distance = path.second; sm.teleporting = false; - gameHandler->sendAndApply(&sm); + gameHandler->sendAndApply(sm); } } else //for non-flying creatures @@ -856,7 +856,7 @@ int BattleActionProcessor::moveStack(const CBattleInfoCallback & battle, int sta sm.distance = path.second; sm.teleporting = false; sm.tilesToMove = tiles; - gameHandler->sendAndApply(&sm); + gameHandler->sendAndApply(sm); tiles.clear(); } @@ -881,7 +881,7 @@ int BattleActionProcessor::moveStack(const CBattleInfoCallback & battle, int sta BattleUpdateGateState db; db.battleID = battle.getBattle()->getBattleID(); db.state = EGateState::OPENED; - gameHandler->sendAndApply(&db); + gameHandler->sendAndApply(db); } } else if (curStack->getPosition() == gateMayCloseAtHex) @@ -916,6 +916,10 @@ void BattleActionProcessor::makeAttack(const CBattleInfoCallback & battle, const if(defender && first && !counter) handleAttackBeforeCasting(battle, ranged, attacker, defender); + // If the attacker or defender is not alive before the attack action, the action should be skipped. + if((!attacker->alive()) || (defender && !defender->alive())) + return; + FireShieldInfo fireShield; BattleAttack bat; BattleLogMessage blm; @@ -1034,7 +1038,7 @@ void BattleActionProcessor::makeAttack(const CBattleInfoCallback & battle, const for (BattleStackAttacked & bsa : bat.bsa) bsa.battleID = battle.getBattle()->getBattleID(); - gameHandler->sendAndApply(&bat); + gameHandler->sendAndApply(bat); { const bool multipleTargets = bat.bsa.size() > 1; @@ -1101,7 +1105,7 @@ void BattleActionProcessor::makeAttack(const CBattleInfoCallback & battle, const StacksInjured pack; pack.battleID = battle.getBattle()->getBattleID(); pack.stacks.push_back(bsa); - gameHandler->sendAndApply(&pack); + gameHandler->sendAndApply(pack); // TODO: this is already implemented in Damage::describeEffect() { @@ -1115,7 +1119,7 @@ void BattleActionProcessor::makeAttack(const CBattleInfoCallback & battle, const } } - gameHandler->sendAndApply(&blm); + gameHandler->sendAndApply(blm); if(defender) handleAfterAttackCasting(battle, ranged, attacker, defender); @@ -1386,14 +1390,14 @@ void BattleActionProcessor::handleAfterAttackCasting(const CBattleInfoCallback & BattleUnitsChanged removeUnits; removeUnits.battleID = battle.getBattle()->getBattleID(); removeUnits.changedStacks.emplace_back(defender->unitId(), UnitChanges::EOperation::REMOVE); - gameHandler->sendAndApply(&removeUnits); - gameHandler->sendAndApply(&addUnits); + gameHandler->sendAndApply(removeUnits); + gameHandler->sendAndApply(addUnits); // send empty event to client // temporary(?) workaround to force animations to trigger StacksInjured fakeEvent; fakeEvent.battleID = battle.getBattle()->getBattleID(); - gameHandler->sendAndApply(&fakeEvent); + gameHandler->sendAndApply(fakeEvent); } if(attacker->hasBonusOfType(BonusType::DESTRUCTION, BonusCustomSubtype::destructionKillPercentage) || attacker->hasBonusOfType(BonusType::DESTRUCTION, BonusCustomSubtype::destructionKillAmount)) @@ -1430,7 +1434,7 @@ void BattleActionProcessor::handleAfterAttackCasting(const CBattleInfoCallback & si.battleID = battle.getBattle()->getBattleID(); si.stacks.push_back(bsa); - gameHandler->sendAndApply(&si); + gameHandler->sendAndApply(si); sendGenericKilledLog(battle, defender, bsa.killedAmount, false); } } @@ -1504,7 +1508,7 @@ void BattleActionProcessor::sendGenericKilledLog(const CBattleInfoCallback & bat BattleLogMessage blm; blm.battleID = battle.getBattle()->getBattleID(); addGenericKilledLog(blm, defender, killed, multiple); - gameHandler->sendAndApply(&blm); + gameHandler->sendAndApply(blm); } } diff --git a/server/battles/BattleFlowProcessor.cpp b/server/battles/BattleFlowProcessor.cpp index ce70d21fd..e98053e40 100644 --- a/server/battles/BattleFlowProcessor.cpp +++ b/server/battles/BattleFlowProcessor.cpp @@ -179,7 +179,7 @@ void BattleFlowProcessor::trySummonGuardians(const CBattleInfoCallback & battle, pack.battleID = battle.getBattle()->getBattleID(); pack.changedStacks.emplace_back(info.id, UnitChanges::EOperation::ADD); info.save(pack.changedStacks.back().data); - gameHandler->sendAndApply(&pack); + gameHandler->sendAndApply(pack); } } @@ -187,7 +187,7 @@ void BattleFlowProcessor::trySummonGuardians(const CBattleInfoCallback & battle, // temporary(?) workaround to force animations to trigger StacksInjured fakeEvent; fakeEvent.battleID = battle.getBattle()->getBattleID(); - gameHandler->sendAndApply(&fakeEvent); + gameHandler->sendAndApply(fakeEvent); } void BattleFlowProcessor::castOpeningSpells(const CBattleInfoCallback & battle) @@ -241,7 +241,7 @@ void BattleFlowProcessor::startNextRound(const CBattleInfoCallback & battle, boo BattleNextRound bnr; bnr.battleID = battle.getBattle()->getBattleID(); logGlobal->debug("Next round starts"); - gameHandler->sendAndApply(&bnr); + gameHandler->sendAndApply(bnr); // operate on copy - removing obstacles will invalidate iterator on 'battle' container auto obstacles = battle.battleGetAllObstacles(); @@ -287,7 +287,7 @@ const CStack * BattleFlowProcessor::getNextStack(const CBattleInfoCallback & bat bte.val = std::min(lostHealth, stack->valOfBonuses(BonusType::HP_REGENERATION)); if(bte.val) // anything to heal - gameHandler->sendAndApply(&bte); + gameHandler->sendAndApply(bte); } if(!next || !next->willMove()) @@ -327,7 +327,7 @@ void BattleFlowProcessor::activateNextStack(const CBattleInfoCallback & battle) removeGhosts.changedStacks.emplace_back(stack->unitId(), UnitChanges::EOperation::REMOVE); if(!removeGhosts.changedStacks.empty()) - gameHandler->sendAndApply(&removeGhosts); + gameHandler->sendAndApply(removeGhosts); gameHandler->turnTimerHandler->onBattleNextStack(battle.getBattle()->getBattleID(), *next); @@ -537,7 +537,7 @@ bool BattleFlowProcessor::rollGoodMorale(const CBattleInfoCallback & battle, con bte.effect = vstd::to_underlying(BonusType::MORALE); bte.val = 1; bte.additionalInfo = 0; - gameHandler->sendAndApply(&bte); //play animation + gameHandler->sendAndApply(bte); //play animation return true; } } @@ -571,7 +571,8 @@ void BattleFlowProcessor::onActionMade(const CBattleInfoCallback & battle, const assert(activeStack != nullptr); assert(actedStack != nullptr); - if(actedStack->castSpellThisTurn && SpellID(ba.spell).toSpell()->canCastWithoutSkip()) + // NOTE: in case of random spellcaster, (e.g. Master Genie) spell has been selected by server and was not present in action received from player + if(actedStack->castSpellThisTurn && ba.spell.hasValue() && ba.spell.toSpell()->canCastWithoutSkip()) { setActiveStack(battle, actedStack); return; @@ -620,7 +621,7 @@ bool BattleFlowProcessor::makeAutomaticAction(const CBattleInfoCallback & battle bsa.battleID = battle.getBattle()->getBattleID(); bsa.stack = stack->unitId(); bsa.askPlayerInterface = false; - gameHandler->sendAndApply(&bsa); + gameHandler->sendAndApply(bsa); bool ret = owner->makeAutomaticBattleAction(battle, ba); return ret; @@ -663,7 +664,7 @@ void BattleFlowProcessor::removeObstacle(const CBattleInfoCallback & battle, con BattleObstaclesChanged obsRem; obsRem.battleID = battle.getBattle()->getBattleID(); obsRem.changes.emplace_back(obstacle.uniqueID, ObstacleChanges::EOperation::REMOVE); - gameHandler->sendAndApply(&obsRem); + gameHandler->sendAndApply(obsRem); } void BattleFlowProcessor::stackTurnTrigger(const CBattleInfoCallback & battle, const CStack *st) @@ -705,7 +706,7 @@ void BattleFlowProcessor::stackTurnTrigger(const CBattleInfoCallback & battle, c ssp.battleID = battle.getBattle()->getBattleID(); ssp.which = BattleSetStackProperty::UNBIND; ssp.stackID = st->unitId(); - gameHandler->sendAndApply(&ssp); + gameHandler->sendAndApply(ssp); } } @@ -718,7 +719,7 @@ void BattleFlowProcessor::stackTurnTrigger(const CBattleInfoCallback & battle, c if (bte.val < b->val) //(negative) poison effect increases - update it { bte.effect = vstd::to_underlying(BonusType::POISON); - gameHandler->sendAndApply(&bte); + gameHandler->sendAndApply(bte); } } } @@ -734,7 +735,7 @@ void BattleFlowProcessor::stackTurnTrigger(const CBattleInfoCallback & battle, c bte.effect = vstd::to_underlying(BonusType::MANA_DRAIN); bte.val = manaDrained; bte.additionalInfo = opponentHero->id.getNum(); //for sanity - gameHandler->sendAndApply(&bte); + gameHandler->sendAndApply(bte); } } } @@ -754,7 +755,7 @@ void BattleFlowProcessor::stackTurnTrigger(const CBattleInfoCallback & battle, c if (gameHandler->getRandomGenerator().nextInt(99) < 10) //fixed 10% { bte.effect = vstd::to_underlying(BonusType::FEAR); - gameHandler->sendAndApply(&bte); + gameHandler->sendAndApply(bte); } } } @@ -799,7 +800,7 @@ void BattleFlowProcessor::stackTurnTrigger(const CBattleInfoCallback & battle, c ssp.absolute = false; ssp.val = cooldown; ssp.stackID = st->unitId(); - gameHandler->sendAndApply(&ssp); + gameHandler->sendAndApply(ssp); } } } @@ -813,5 +814,5 @@ void BattleFlowProcessor::setActiveStack(const CBattleInfoCallback & battle, con BattleSetActiveStack sas; sas.battleID = battle.getBattle()->getBattleID(); sas.stack = stack->unitId(); - gameHandler->sendAndApply(&sas); + gameHandler->sendAndApply(sas); } diff --git a/server/battles/BattleProcessor.cpp b/server/battles/BattleProcessor.cpp index 57d0e32d1..7a799d5db 100644 --- a/server/battles/BattleProcessor.cpp +++ b/server/battles/BattleProcessor.cpp @@ -28,10 +28,12 @@ #include "../../lib/gameState/CGameState.h" #include "../../lib/mapping/CMap.h" #include "../../lib/mapObjects/CGHeroInstance.h" +#include "../../lib/mapObjects/CGTownInstance.h" #include "../../lib/modding/IdentifierStorage.h" #include "../../lib/networkPacks/PacksForClient.h" #include "../../lib/networkPacks/PacksForClientBattle.h" #include "../../lib/CPlayerState.h" +#include BattleProcessor::BattleProcessor(CGameHandler * gameHandler) : gameHandler(gameHandler) @@ -50,7 +52,7 @@ void BattleProcessor::engageIntoBattle(PlayerColor player) pb.player = player; pb.reason = PlayerBlocked::UPCOMING_BATTLE; pb.startOrEnd = PlayerBlocked::BLOCKADE_STARTED; - gameHandler->sendAndApply(&pb); + gameHandler->sendAndApply(pb); } void BattleProcessor::restartBattle(const BattleID & battleID, const CArmedInstance *army1, const CArmedInstance *army2, int3 tile, @@ -76,7 +78,7 @@ void BattleProcessor::restartBattle(const BattleID & battleID, const CArmedInsta SetMana restoreInitialMana; restoreInitialMana.val = lastBattleQuery->initialHeroMana[i]; restoreInitialMana.hid = heroes[i]->id; - gameHandler->sendAndApply(&restoreInitialMana); + gameHandler->sendAndApply(restoreInitialMana); } } @@ -88,7 +90,7 @@ void BattleProcessor::restartBattle(const BattleID & battleID, const CArmedInsta BattleCancelled bc; bc.battleID = battleID; - gameHandler->sendAndApply(&bc); + gameHandler->sendAndApply(bc); startBattle(army1, army2, tile, hero1, hero2, layout, town); } @@ -116,7 +118,7 @@ void BattleProcessor::startBattle(const CArmedInstance *army1, const CArmedInsta GiveBonus giveBonus(GiveBonus::ETarget::OBJECT); giveBonus.id = hero1->id; giveBonus.bonus = bonus; - gameHandler->sendAndApply(&giveBonus); + gameHandler->sendAndApply(giveBonus); } } @@ -155,17 +157,25 @@ void BattleProcessor::startBattle(const CArmedInstance *army1, const CArmedInsta BattleID BattleProcessor::setupBattle(int3 tile, BattleSideArray armies, BattleSideArray heroes, const BattleLayout & layout, const CGTownInstance *town) { const auto & t = *gameHandler->getTile(tile); - TerrainId terrain = t.terType->getId(); - if (gameHandler->gameState()->map->isCoastalTile(tile)) //coastal tile is always ground + TerrainId terrain = t.getTerrainID(); + if (town) + terrain = town->getNativeTerrain(); + else if (gameHandler->gameState()->map->isCoastalTile(tile)) //coastal tile is always ground terrain = ETerrainId::SAND; - BattleField terType = gameHandler->gameState()->battleGetBattlefieldType(tile, gameHandler->getRandomGenerator()); - if (heroes[BattleSide::ATTACKER] && heroes[BattleSide::ATTACKER]->boat && heroes[BattleSide::DEFENDER] && heroes[BattleSide::DEFENDER]->boat) - terType = BattleField(*VLC->identifiers()->getIdentifier("core", "battlefield.ship_to_ship")); + BattleField battlefieldType = gameHandler->gameState()->battleGetBattlefieldType(tile, gameHandler->getRandomGenerator()); + + if (town) + { + const TerrainType* terrainData = VLC->terrainTypeHandler->getById(terrain); + battlefieldType = BattleField(*RandomGeneratorUtil::nextItem(terrainData->battleFields, gameHandler->getRandomGenerator())); + } + else if (heroes[BattleSide::ATTACKER] && heroes[BattleSide::ATTACKER]->boat && heroes[BattleSide::DEFENDER] && heroes[BattleSide::DEFENDER]->boat) + battlefieldType = BattleField(*VLC->identifiers()->getIdentifier("core", "battlefield.ship_to_ship")); //send info about battles BattleStart bs; - bs.info = BattleInfo::setupBattle(tile, terrain, terType, armies, heroes, layout, town); + bs.info = BattleInfo::setupBattle(tile, terrain, battlefieldType, armies, heroes, layout, town); bs.battleID = gameHandler->gameState()->nextBattleID; engageIntoBattle(bs.info->getSide(BattleSide::ATTACKER).color); @@ -180,7 +190,7 @@ BattleID BattleProcessor::setupBattle(int3 tile, BattleSideArrayreplayAllowed = lastBattleQuery == nullptr && onlyOnePlayerHuman; - gameHandler->sendAndApply(&bs); + gameHandler->sendAndApply(bs); return bs.battleID; } @@ -258,7 +268,7 @@ void BattleProcessor::updateGateState(const CBattleInfoCallback & battle) } if (db.state != battle.battleGetGateState()) - gameHandler->sendAndApply(&db); + gameHandler->sendAndApply(db); } bool BattleProcessor::makePlayerBattleAction(const BattleID & battleID, PlayerColor player, const BattleAction &ba) diff --git a/server/battles/BattleResultProcessor.cpp b/server/battles/BattleResultProcessor.cpp index a0a7a0e8f..411343e16 100644 --- a/server/battles/BattleResultProcessor.cpp +++ b/server/battles/BattleResultProcessor.cpp @@ -178,7 +178,7 @@ void CasualtiesAfterBattle::updateArmy(CGameHandler *gh) scp.heroid = heroWithDeadCommander; scp.which = SetCommanderProperty::ALIVE; scp.amount = 0; - gh->sendAndApply(&scp); + gh->sendAndApply(scp); } } @@ -291,7 +291,7 @@ void BattleResultProcessor::endBattle(const CBattleInfoCallback & battle) } gameHandler->turnTimerHandler->onBattleEnd(battle.getBattle()->getBattleID()); - gameHandler->sendAndApply(battleResult); + gameHandler->sendAndApply(*battleResult); if (battleResult->queryID == QueryID::NONE) endBattleConfirm(battle); @@ -384,8 +384,8 @@ void BattleResultProcessor::endBattleConfirm(const CBattleInfoCallback & battle) iw.text.replaceLocalString(EMetaText::GENERAL_TXT, 141); // " and " iw.components.emplace_back(ComponentType::SPELL, *it); } - gameHandler->sendAndApply(&iw); - gameHandler->sendAndApply(&spells); + gameHandler->sendAndApply(iw); + gameHandler->sendAndApply(spells); } } // Artifacts handling @@ -410,7 +410,7 @@ void BattleResultProcessor::endBattleConfirm(const CBattleInfoCallback & battle) const auto sendArtifacts = [this](BulkMoveArtifacts & bma) { if(!bma.artsPack0.empty()) - gameHandler->sendAndApply(&bma); + gameHandler->sendAndApply(bma); }; BulkMoveArtifacts packHero(finishingBattle->winnerHero->getOwner(), ObjectInstanceID::NONE, finishingBattle->winnerHero->id, false); @@ -466,11 +466,11 @@ void BattleResultProcessor::endBattleConfirm(const CBattleInfoCallback & battle) if(iw.components.size() >= GameConstants::INFO_WINDOW_ARTIFACTS_MAX_ITEMS) { - gameHandler->sendAndApply(&iw); + gameHandler->sendAndApply(iw); iw.components.clear(); } } - gameHandler->sendAndApply(&iw); + gameHandler->sendAndApply(iw); } if(!packHero.artsPack0.empty()) sendArtifacts(packHero); @@ -491,13 +491,13 @@ void BattleResultProcessor::endBattleConfirm(const CBattleInfoCallback & battle) } RemoveObject ro(finishingBattle->loserHero->id, finishingBattle->victor); - gameHandler->sendAndApply(&ro); + gameHandler->sendAndApply(ro); } // For draw case both heroes should be removed if(finishingBattle->isDraw() && finishingBattle->winnerHero) { RemoveObject ro(finishingBattle->winnerHero->id, finishingBattle->loser); - gameHandler->sendAndApply(&ro); + gameHandler->sendAndApply(ro); } // add statistic @@ -525,7 +525,7 @@ void BattleResultProcessor::endBattleConfirm(const CBattleInfoCallback & battle) raccepted.heroResult[BattleSide::ATTACKER].exp = battleResult->exp[BattleSide::ATTACKER]; raccepted.heroResult[BattleSide::DEFENDER].exp = battleResult->exp[BattleSide::DEFENDER]; raccepted.winnerSide = finishingBattle->winnerSide; - gameHandler->sendAndApply(&raccepted); + gameHandler->sendAndApply(raccepted); gameHandler->queries->popIfTop(battleQuery); //--> continuation (battleAfterLevelUp) occurs after level-up gameHandler->queries are handled or on removing query @@ -556,19 +556,19 @@ void BattleResultProcessor::battleAfterLevelUp(const BattleID & battleID, const const CStackBasicDescriptor raisedStack = finishingBattle->winnerHero ? finishingBattle->winnerHero->calculateNecromancy(result) : CStackBasicDescriptor(); // Give raised units to winner and show dialog, if any were raised, // units will be given after casualties are taken - const SlotID necroSlot = raisedStack.type ? finishingBattle->winnerHero->getSlotFor(raisedStack.type) : SlotID(); + const SlotID necroSlot = raisedStack.getCreature() ? finishingBattle->winnerHero->getSlotFor(raisedStack.getCreature()) : SlotID(); if (necroSlot != SlotID() && !finishingBattle->isDraw()) { finishingBattle->winnerHero->showNecromancyDialog(raisedStack, gameHandler->getRandomGenerator()); - gameHandler->addToSlot(StackLocation(finishingBattle->winnerHero, necroSlot), raisedStack.type, raisedStack.count); + gameHandler->addToSlot(StackLocation(finishingBattle->winnerHero, necroSlot), raisedStack.getCreature(), raisedStack.count); } BattleResultsApplied resultsApplied; resultsApplied.battleID = battleID; resultsApplied.player1 = finishingBattle->victor; resultsApplied.player2 = finishingBattle->loser; - gameHandler->sendAndApply(&resultsApplied); + gameHandler->sendAndApply(resultsApplied); //handle victory/loss of engaged players std::set playerColors = {finishingBattle->loser, finishingBattle->victor}; @@ -590,7 +590,7 @@ void BattleResultProcessor::battleAfterLevelUp(const BattleID & battleID, const && (!finishingBattle->winnerHero->commander || !finishingBattle->winnerHero->commander->alive)) { RemoveObject ro(finishingBattle->winnerHero->id, finishingBattle->winnerHero->getOwner()); - gameHandler->sendAndApply(&ro); + gameHandler->sendAndApply(ro); if (gameHandler->getSettings().getBoolean(EGameSettings::HEROES_RETREAT_ON_WIN_WITHOUT_TROOPS)) gameHandler->heroPool->onHeroEscaped(finishingBattle->victor, finishingBattle->winnerHero); diff --git a/server/processors/HeroPoolProcessor.cpp b/server/processors/HeroPoolProcessor.cpp index 9b436ceae..976c74b6f 100644 --- a/server/processors/HeroPoolProcessor.cpp +++ b/server/processors/HeroPoolProcessor.cpp @@ -14,10 +14,11 @@ #include "../CGameHandler.h" #include "../../lib/CRandomGenerator.h" -#include "../../lib/CHeroHandler.h" #include "../../lib/CPlayerState.h" #include "../../lib/IGameSettings.h" #include "../../lib/StartInfo.h" +#include "../../lib/entities/hero/CHeroClass.h" +#include "../../lib/entities/hero/CHero.h" #include "../../lib/mapObjects/CGTownInstance.h" #include "../../lib/mapObjects/CGHeroInstance.h" #include "../../lib/networkPacks/PacksForClient.h" @@ -47,8 +48,8 @@ TavernHeroSlot HeroPoolProcessor::selectSlotForRole(const PlayerColor & player, // try to find "better" slot to overwrite // we want to avoid overwriting retreated heroes when tavern still has slot with random hero // as well as avoid overwriting surrendered heroes if we can overwrite retreated hero - auto roleLeft = heroesPool->getSlotRole(heroes[0]->getHeroType()); - auto roleRight = heroesPool->getSlotRole(heroes[1]->getHeroType()); + auto roleLeft = heroesPool->getSlotRole(heroes[0]->getHeroTypeID()); + auto roleRight = heroesPool->getSlotRole(heroes[1]->getHeroTypeID()); if (roleLeft > roleRight) return TavernHeroSlot::RANDOM; @@ -70,9 +71,9 @@ void HeroPoolProcessor::onHeroSurrendered(const PlayerColor & color, const CGHer sah.slotID = selectSlotForRole(color, sah.roleID); sah.player = color; - sah.hid = hero->getHeroType(); + sah.hid = hero->getHeroTypeID(); sah.replenishPoints = false; - gameHandler->sendAndApply(&sah); + gameHandler->sendAndApply(sah); } void HeroPoolProcessor::onHeroEscaped(const PlayerColor & color, const CGHeroInstance * hero) @@ -82,12 +83,12 @@ void HeroPoolProcessor::onHeroEscaped(const PlayerColor & color, const CGHeroIns sah.slotID = selectSlotForRole(color, sah.roleID); sah.player = color; - sah.hid = hero->getHeroType(); + sah.hid = hero->getHeroTypeID(); sah.army.clearSlots(); - sah.army.setCreature(SlotID(0), hero->type->initialArmy.at(0).creature, 1); + sah.army.setCreature(SlotID(0), hero->getHeroType()->initialArmy.at(0).creature, 1); sah.replenishPoints = false; - gameHandler->sendAndApply(&sah); + gameHandler->sendAndApply(sah); } void HeroPoolProcessor::clearHeroFromSlot(const PlayerColor & color, TavernHeroSlot slot) @@ -98,7 +99,7 @@ void HeroPoolProcessor::clearHeroFromSlot(const PlayerColor & color, TavernHeroS sah.slotID = slot; sah.hid = HeroTypeID::NONE; sah.replenishPoints = false; - gameHandler->sendAndApply(&sah); + gameHandler->sendAndApply(sah); } void HeroPoolProcessor::selectNewHeroForSlot(const PlayerColor & color, TavernHeroSlot slot, bool needNativeHero, bool giveArmy, const HeroTypeID & nextHero) @@ -112,7 +113,7 @@ void HeroPoolProcessor::selectNewHeroForSlot(const PlayerColor & color, TavernHe if (newHero) { - sah.hid = newHero->getHeroType(); + sah.hid = newHero->getHeroTypeID(); if (giveArmy) { @@ -123,7 +124,7 @@ void HeroPoolProcessor::selectNewHeroForSlot(const PlayerColor & color, TavernHe { sah.roleID = TavernSlotRole::SINGLE_UNIT; sah.army.clearSlots(); - sah.army.setCreature(SlotID(0), newHero->type->initialArmy[0].creature, 1); + sah.army.setCreature(SlotID(0), newHero->getHeroType()->initialArmy[0].creature, 1); } } else @@ -131,7 +132,7 @@ void HeroPoolProcessor::selectNewHeroForSlot(const PlayerColor & color, TavernHe sah.hid = HeroTypeID::NONE; } - gameHandler->sendAndApply(&sah); + gameHandler->sendAndApply(sah); } void HeroPoolProcessor::onNewWeek(const PlayerColor & color) @@ -208,7 +209,7 @@ bool HeroPoolProcessor::hireHero(const ObjectInstanceID & objectID, const HeroTy for(const auto & hero : recruitableHeroes) { - if(hero->getHeroType() == heroToRecruit) + if(hero->getHeroTypeID() == heroToRecruit) recruitedHero = hero; } @@ -221,7 +222,7 @@ bool HeroPoolProcessor::hireHero(const ObjectInstanceID & objectID, const HeroTy HeroRecruited hr; hr.tid = mapObject->id; - hr.hid = recruitedHero->getHeroType(); + hr.hid = recruitedHero->getHeroTypeID(); hr.player = player; hr.tile = recruitedHero->convertFromVisitablePos(targetPos ); if(gameHandler->getTile(targetPos)->isWater() && !recruitedHero->boat) @@ -232,7 +233,7 @@ bool HeroPoolProcessor::hireHero(const ObjectInstanceID & objectID, const HeroTy } // apply netpack -> this will remove hired hero from pool - gameHandler->sendAndApply(&hr); + gameHandler->sendAndApply(hr); if(recruitableHeroes[0] == recruitedHero) selectNewHeroForSlot(player, TavernHeroSlot::NATIVE, false, false, nextHero); @@ -258,14 +259,14 @@ std::vector HeroPoolProcessor::findAvailableClassesFor(const for(const auto & elem : heroesPool->unusedHeroesFromPool()) { - if (vstd::contains(result, elem.second->type->heroClass)) + if (vstd::contains(result, elem.second->getHeroClass())) continue; bool heroAvailable = heroesPool->isHeroAvailableFor(elem.first, player); - bool heroClassBanned = elem.second->type->heroClass->tavernProbability(factionID) == 0; + bool heroClassBanned = elem.second->getHeroClass()->tavernProbability(factionID) == 0; if(heroAvailable && !heroClassBanned) - result.push_back(elem.second->type->heroClass); + result.push_back(elem.second->getHeroClass()); } return result; @@ -282,7 +283,7 @@ std::vector HeroPoolProcessor::findAvailableHeroesFor(const Pl assert(!vstd::contains(result, elem.second)); bool heroAvailable = heroesPool->isHeroAvailableFor(elem.first, player); - bool heroClassMatches = elem.second->type->heroClass == heroClass; + bool heroClassMatches = elem.second->getHeroClass() == heroClass; if(heroAvailable && heroClassMatches) result.push_back(elem.second); @@ -318,7 +319,7 @@ const CHeroClass * HeroPoolProcessor::pickClassFor(bool isNative, const PlayerCo continue; bool hasSameClass = vstd::contains_if(currentTavern, [&](const CGHeroInstance * hero){ - return hero->type->heroClass == heroClass; + return hero->getHeroClass() == heroClass; }); if (hasSameClass) diff --git a/server/processors/NewTurnProcessor.cpp b/server/processors/NewTurnProcessor.cpp index af4f807f5..501616217 100644 --- a/server/processors/NewTurnProcessor.cpp +++ b/server/processors/NewTurnProcessor.cpp @@ -18,6 +18,7 @@ #include "../../lib/IGameSettings.h" #include "../../lib/StartInfo.h" #include "../../lib/TerrainHandler.h" +#include "../../lib/constants/StringConstants.h" #include "../../lib/entities/building/CBuilding.h" #include "../../lib/entities/faction/CTownHandler.h" #include "../../lib/gameState/CGameState.h" @@ -70,7 +71,7 @@ void NewTurnProcessor::handleTimeEvents(PlayerColor color) if(objectInstance != nullptr) gameHandler->removeObject(objectInstance, PlayerColor::NEUTRAL); } - gameHandler->sendAndApply(&iw); //show dialog + gameHandler->sendAndApply(iw); //show dialog } } @@ -105,10 +106,10 @@ void NewTurnProcessor::handleTownEvents(const CGTownInstance * town) // 1. Building exists in town (don't attempt to build Lvl 5 guild in Fortress // 2. Building was not built yet // othervice, silently ignore / skip it - if (town->town->buildings.count(i) && !town->hasBuilt(i)) + if (town->getTown()->buildings.count(i) && !town->hasBuilt(i)) { gameHandler->buildStructure(town->id, i, true); - iw.components.emplace_back(ComponentType::BUILDING, BuildingTypeUniqueID(town->getFaction(), i)); + iw.components.emplace_back(ComponentType::BUILDING, BuildingTypeUniqueID(town->getFactionID(), i)); } } @@ -126,8 +127,10 @@ void NewTurnProcessor::handleTownEvents(const CGTownInstance * town) iw.components.emplace_back(ComponentType::CREATURE, town->creatures.at(i).second.back(), event.creatures.at(i)); } } + + gameHandler->sendAndApply(sac); //show dialog } - gameHandler->sendAndApply(&iw); //show dialog + gameHandler->sendAndApply(iw); //show dialog } } @@ -160,7 +163,7 @@ void NewTurnProcessor::onPlayerTurnEnded(PlayerColor which) DaysWithoutTown pack; pack.player = which; pack.daysWithoutCastle = playerState->daysWithoutCastle.value_or(0) + 1; - gameHandler->sendAndApply(&pack); + gameHandler->sendAndApply(pack); } else { @@ -169,7 +172,7 @@ void NewTurnProcessor::onPlayerTurnEnded(PlayerColor which) DaysWithoutTown pack; pack.player = which; pack.daysWithoutCastle = std::nullopt; - gameHandler->sendAndApply(&pack); + gameHandler->sendAndApply(pack); } } @@ -245,6 +248,28 @@ ResourceSet NewTurnProcessor::generatePlayerIncome(PlayerColor playerID, bool ne for (auto obj : state.getOwnedObjects()) incomeHandicapped += obj->asOwnable()->dailyIncome(); + if (!state.isHuman()) + { + // Initialize bonuses for different resources + int difficultyIndex = gameHandler->gameState()->getStartInfo()->difficulty; + const std::string & difficultyName = GameConstants::DIFFICULTY_NAMES[difficultyIndex]; + const JsonNode & weeklyBonusesConfig = gameHandler->gameState()->getSettings().getValue(EGameSettings::RESOURCES_WEEKLY_BONUSES_AI); + const JsonNode & difficultyConfig = weeklyBonusesConfig[difficultyName]; + + // Distribute weekly bonuses over 7 days, depending on the current day of the week + for (GameResID i : GameResID::ALL_RESOURCES()) + { + const std::string & name = GameConstants::RESOURCE_NAMES[i]; + int weeklyBonus = difficultyConfig[name].Integer(); + int dayOfWeek = gameHandler->gameState()->getDate(Date::DAY_OF_WEEK); + int dailyIncome = incomeHandicapped[i]; + int amountTillToday = dailyIncome * weeklyBonus * (dayOfWeek-1) / 7 / 100; + int amountAfterToday = dailyIncome * weeklyBonus * dayOfWeek / 7 / 100; + int dailyBonusToday = amountAfterToday - amountTillToday; + incomeHandicapped[static_cast(i)] += dailyBonusToday; + } + } + return incomeHandicapped; } @@ -256,7 +281,7 @@ SetAvailableCreatures NewTurnProcessor::generateTownGrowth(const CGTownInstance sac.tid = t->id; sac.creatures = t->creatures; - for (int k=0; k < t->town->creatures.size(); k++) + for (int k=0; k < t->getTown()->creatures.size(); k++) { if (t->creatures.at(k).second.empty()) continue; @@ -331,7 +356,7 @@ void NewTurnProcessor::updateNeutralTownGarrison(const CGTownInstance * t, int c sac.tid = t->id; sac.creatures = t->creatures; sac.creatures[tierToSubstract].first = creaturesLeft; - gameHandler->sendAndApply(&sac); + gameHandler->sendAndApply(sac); } }; @@ -353,9 +378,9 @@ void NewTurnProcessor::updateNeutralTownGarrison(const CGTownInstance * t, int c // Check if town garrison already has unit of specified tier for(const auto & slot : t->Slots()) { - const auto * creature = slot.second->type; + const auto * creature = slot.second->getCreature(); - if (creature->getFaction() != t->getFaction()) + if (creature->getFactionID() != t->getFactionID()) continue; if (creature->getLevel() != tierToGrow) @@ -431,7 +456,7 @@ RumorState NewTurnProcessor::pickNewRumor() rumorId = *RandomGeneratorUtil::nextItem(sRumorTypes, rand); if(rumorId == RumorState::RUMOR_GRAIL) { - rumorExtra = gameHandler->gameState()->getTile(gameHandler->gameState()->map->grailPos)->terType->getIndex(); + rumorExtra = gameHandler->gameState()->getTile(gameHandler->gameState()->map->grailPos)->getTerrainID().getNum(); break; } @@ -528,7 +553,7 @@ std::tuple NewTurnProcessor::pickWeekType(bool newMonth) { newMonster.second = VLC->creh->pickRandomMonster(gameHandler->getRandomGenerator()); } while (VLC->creh->objects[newMonster.second] && - (*VLC->townh)[VLC->creatures()->getById(newMonster.second)->getFaction()]->town == nullptr); // find first non neutral creature + (*VLC->townh)[VLC->creatures()->getById(newMonster.second)->getFactionID()]->town == nullptr); // find first non neutral creature return { EWeekType::BONUS_GROWTH, newMonster.second}; } @@ -563,7 +588,7 @@ std::vector NewTurnProcessor::updateHeroesMovementPoints() { auto ti = std::make_unique(h, 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()).terType->isLand(), ti.get()); + int32_t newMovementPoints = h->movementPointsLimitCached(gameHandler->gameState()->map->getTile(h->visitablePos()).isLand(), ti.get()); if (newMovementPoints != h->movementPointsRemaining()) result.emplace_back(h->id, newMovementPoints, true); @@ -667,7 +692,7 @@ void NewTurnProcessor::onNewTurn() bool newWeek = gameHandler->getDate(Date::DAY_OF_WEEK) == 7; //day numbers are confusing, as day was not yet switched bool newMonth = gameHandler->getDate(Date::DAY_OF_MONTH) == 28; - gameHandler->sendAndApply(&n); + gameHandler->sendAndApply(n); if (newWeek) { diff --git a/server/processors/PlayerMessageProcessor.cpp b/server/processors/PlayerMessageProcessor.cpp index 5929bbd87..38b4409d0 100644 --- a/server/processors/PlayerMessageProcessor.cpp +++ b/server/processors/PlayerMessageProcessor.cpp @@ -16,10 +16,10 @@ #include "../CVCMIServer.h" #include "../TurnTimerHandler.h" -#include "../../lib/CHeroHandler.h" #include "../../lib/CPlayerState.h" #include "../../lib/StartInfo.h" #include "../../lib/entities/building/CBuilding.h" +#include "../../lib/entities/hero/CHeroHandler.h" #include "../../lib/gameState/CGameState.h" #include "../../lib/mapObjects/CGTownInstance.h" #include "../../lib/mapObjects/CGHeroInstance.h" @@ -100,7 +100,7 @@ void PlayerMessageProcessor::commandKick(PlayerColor player, const std::vectorsendAndApply(&pc); + gameHandler->sendAndApply(pc); gameHandler->checkVictoryLossConditionsForPlayer(playerToKick); } } @@ -354,7 +354,7 @@ void PlayerMessageProcessor::cheatGiveSpells(PlayerColor player, const CGHeroIns for (int level = 1; level <= GameConstants::SPELL_LEVELS; level++) { giveBonus.bonus.subtype = BonusCustomSubtype::spellLevel(level); - gameHandler->sendAndApply(&giveBonus); + gameHandler->sendAndApply(giveBonus); } ///Give mana @@ -362,7 +362,7 @@ void PlayerMessageProcessor::cheatGiveSpells(PlayerColor player, const CGHeroIns sm.hid = hero->id; sm.val = 999; sm.absolute = true; - gameHandler->sendAndApply(&sm); + gameHandler->sendAndApply(sm); } void PlayerMessageProcessor::cheatBuildTown(PlayerColor player, const CGTownInstance * town) @@ -370,7 +370,7 @@ void PlayerMessageProcessor::cheatBuildTown(PlayerColor player, const CGTownInst if (!town) return; - for (auto & build : town->town->buildings) + for (auto & build : town->getTown()->buildings) { if (!town->hasBuilt(build.first) && !build.second->getNameTranslated().empty() @@ -520,7 +520,7 @@ void PlayerMessageProcessor::cheatMovement(PlayerColor player, const CGHeroInsta unlimited = true; } - gameHandler->sendAndApply(&smp); + gameHandler->sendAndApply(smp); GiveBonus gb(GiveBonus::ETarget::OBJECT); gb.bonus.type = BonusType::FREE_SHIP_BOARDING; @@ -565,7 +565,7 @@ void PlayerMessageProcessor::cheatVictory(PlayerColor player) PlayerCheated pc; pc.player = player; pc.winningCheatCode = true; - gameHandler->sendAndApply(&pc); + gameHandler->sendAndApply(pc); } void PlayerMessageProcessor::cheatDefeat(PlayerColor player) @@ -573,7 +573,7 @@ void PlayerMessageProcessor::cheatDefeat(PlayerColor player) PlayerCheated pc; pc.player = player; pc.losingCheatCode = true; - gameHandler->sendAndApply(&pc); + gameHandler->sendAndApply(pc); } void PlayerMessageProcessor::cheatMapReveal(PlayerColor player, bool reveal) @@ -594,7 +594,7 @@ void PlayerMessageProcessor::cheatMapReveal(PlayerColor player, bool reveal) fc.tiles.insert(hlp_tab, hlp_tab + lastUnc); delete [] hlp_tab; - gameHandler->sendAndApply(&fc); + gameHandler->sendAndApply(fc); } void PlayerMessageProcessor::cheatPuzzleReveal(PlayerColor player) @@ -612,7 +612,7 @@ void PlayerMessageProcessor::cheatPuzzleReveal(PlayerColor player) PlayerCheated pc; pc.player = color; - gameHandler->sendAndApply(&pc); + gameHandler->sendAndApply(pc); } } } @@ -715,7 +715,7 @@ bool PlayerMessageProcessor::handleCheatCode(const std::string & cheat, PlayerCo PlayerCheated pc; pc.player = i.first; - gameHandler->sendAndApply(&pc); + gameHandler->sendAndApply(pc); playerTargetedCheat = true; parameters.erase(parameters.begin()); @@ -734,7 +734,7 @@ bool PlayerMessageProcessor::handleCheatCode(const std::string & cheat, PlayerCo PlayerCheated pc; pc.player = player; - gameHandler->sendAndApply(&pc); + gameHandler->sendAndApply(pc); if (!playerTargetedCheat) executeCheatCode(cheatName, player, currObj, words); @@ -847,7 +847,7 @@ void PlayerMessageProcessor::sendSystemMessage(std::shared_ptr conn { SystemMessage sm; sm.text = message; - connection->sendPack(&sm); + connection->sendPack(sm); } void PlayerMessageProcessor::sendSystemMessage(std::shared_ptr connection, const std::string & message) @@ -861,7 +861,7 @@ void PlayerMessageProcessor::broadcastSystemMessage(MetaString message) { SystemMessage sm; sm.text = message; - gameHandler->sendToAllClients(&sm); + gameHandler->sendToAllClients(sm); } void PlayerMessageProcessor::broadcastSystemMessage(const std::string & message) @@ -874,5 +874,5 @@ void PlayerMessageProcessor::broadcastSystemMessage(const std::string & message) void PlayerMessageProcessor::broadcastMessage(PlayerColor playerSender, const std::string & message) { PlayerMessageClient temp_message(playerSender, message); - gameHandler->sendAndApply(&temp_message); + gameHandler->sendAndApply(temp_message); } diff --git a/server/processors/TurnOrderProcessor.cpp b/server/processors/TurnOrderProcessor.cpp index b50f56324..0f02ccebf 100644 --- a/server/processors/TurnOrderProcessor.cpp +++ b/server/processors/TurnOrderProcessor.cpp @@ -287,7 +287,7 @@ void TurnOrderProcessor::doStartPlayerTurn(PlayerColor which) PlayerStartsTurn pst; pst.player = which; pst.queryID = turnQuery->queryID; - gameHandler->sendAndApply(&pst); + gameHandler->sendAndApply(pst); assert(!actingPlayers.empty()); } @@ -302,7 +302,7 @@ void TurnOrderProcessor::doEndPlayerTurn(PlayerColor which) PlayerEndsTurn pet; pet.player = which; - gameHandler->sendAndApply(&pet); + gameHandler->sendAndApply(pet); if (!awaitingPlayers.empty()) tryStartTurnsForPlayers(); diff --git a/server/queries/BattleQueries.cpp b/server/queries/BattleQueries.cpp index 30b06db91..52fcbd63b 100644 --- a/server/queries/BattleQueries.cpp +++ b/server/queries/BattleQueries.cpp @@ -49,7 +49,7 @@ CBattleQuery::CBattleQuery(CGameHandler * owner): belligerents[BattleSide::DEFENDER] = nullptr; } -bool CBattleQuery::blocksPack(const CPack * pack) const +bool CBattleQuery::blocksPack(const CPackForServer * pack) const { if(dynamic_cast(pack) != nullptr) return false; diff --git a/server/queries/BattleQueries.h b/server/queries/BattleQueries.h index ae32b08b3..3cb525a28 100644 --- a/server/queries/BattleQueries.h +++ b/server/queries/BattleQueries.h @@ -30,7 +30,7 @@ public: CBattleQuery(CGameHandler * owner); CBattleQuery(CGameHandler * owner, const IBattleInfo * Bi); //TODO void notifyObjectAboutRemoval(const CGObjectInstance * visitedObject, const CGHeroInstance * visitingHero) const override; - bool blocksPack(const CPack *pack) const override; + bool blocksPack(const CPackForServer *pack) const override; void onRemoval(PlayerColor color) override; void onExposure(QueryPtr topQuery) override; }; diff --git a/server/queries/CQuery.cpp b/server/queries/CQuery.cpp index 3b9d8dd84..26a3b571c 100644 --- a/server/queries/CQuery.cpp +++ b/server/queries/CQuery.cpp @@ -81,7 +81,7 @@ void CQuery::onRemoval(PlayerColor color) } -bool CQuery::blocksPack(const CPack * pack) const +bool CQuery::blocksPack(const CPackForServer * pack) const { return false; } @@ -112,7 +112,7 @@ void CQuery::setReply(std::optional reply) } -bool CQuery::blockAllButReply(const CPack * pack) const +bool CQuery::blockAllButReply(const CPackForServer * pack) const { //We accept only query replies from correct player if(auto reply = dynamic_cast(pack)) @@ -132,7 +132,7 @@ bool CDialogQuery::endsByPlayerAnswer() const return true; } -bool CDialogQuery::blocksPack(const CPack * pack) const +bool CDialogQuery::blocksPack(const CPackForServer * pack) const { return blockAllButReply(pack); } @@ -149,7 +149,7 @@ CGenericQuery::CGenericQuery(CGameHandler * gh, PlayerColor color, std::function addPlayer(color); } -bool CGenericQuery::blocksPack(const CPack * pack) const +bool CGenericQuery::blocksPack(const CPackForServer * pack) const { return blockAllButReply(pack); } diff --git a/server/queries/CQuery.h b/server/queries/CQuery.h index 2fde16295..5cdae43d4 100644 --- a/server/queries/CQuery.h +++ b/server/queries/CQuery.h @@ -13,7 +13,7 @@ VCMI_LIB_NAMESPACE_BEGIN -struct CPack; +struct CPackForServer; class CGObjectInstance; class CGHeroInstance; @@ -43,7 +43,7 @@ public: CQuery(CGameHandler * gh); /// query can block attempting actions by player. Eg. he can't move hero during the battle. - virtual bool blocksPack(const CPack *pack) const; + virtual bool blocksPack(const CPackForServer *pack) const; /// query is removed after player gives answer (like dialogs) virtual bool endsByPlayerAnswer() const; @@ -71,7 +71,7 @@ protected: QueriesProcessor * owner; CGameHandler * gh; void addPlayer(PlayerColor color); - bool blockAllButReply(const CPack * pack) const; + bool blockAllButReply(const CPackForServer * pack) const; }; std::ostream &operator<<(std::ostream &out, const CQuery &query); @@ -82,7 +82,7 @@ class CDialogQuery : public CQuery public: CDialogQuery(CGameHandler * owner); bool endsByPlayerAnswer() const override; - bool blocksPack(const CPack *pack) const override; + bool blocksPack(const CPackForServer *pack) const override; void setReply(std::optional reply) override; protected: std::optional answer; @@ -93,7 +93,7 @@ class CGenericQuery : public CQuery public: CGenericQuery(CGameHandler * gh, PlayerColor color, std::function)> Callback); - bool blocksPack(const CPack * pack) const override; + bool blocksPack(const CPackForServer * pack) const override; bool endsByPlayerAnswer() const override; void onExposure(QueryPtr topQuery) override; void setReply(std::optional reply) override; diff --git a/server/queries/MapQueries.cpp b/server/queries/MapQueries.cpp index 2881368d4..a2ae04911 100644 --- a/server/queries/MapQueries.cpp +++ b/server/queries/MapQueries.cpp @@ -23,7 +23,7 @@ TimerPauseQuery::TimerPauseQuery(CGameHandler * owner, PlayerColor player): addPlayer(player); } -bool TimerPauseQuery::blocksPack(const CPack *pack) const +bool TimerPauseQuery::blocksPack(const CPackForServer *pack) const { return blockAllButReply(pack); } @@ -58,7 +58,7 @@ CGarrisonDialogQuery::CGarrisonDialogQuery(CGameHandler * owner, const CArmedIns addPlayer(down->tempOwner); } -bool CGarrisonDialogQuery::blocksPack(const CPack * pack) const +bool CGarrisonDialogQuery::blocksPack(const CPackForServer * pack) const { std::set ourIds; ourIds.insert(this->exchangingArmies[0]->id); @@ -143,7 +143,7 @@ void OpenWindowQuery::onExposure(QueryPtr topQuery) //do nothing - wait for reply } -bool OpenWindowQuery::blocksPack(const CPack *pack) const +bool OpenWindowQuery::blocksPack(const CPackForServer *pack) const { if (mode == EOpenWindowMode::RECRUITMENT_FIRST || mode == EOpenWindowMode::RECRUITMENT_ALL) { @@ -273,7 +273,7 @@ void CHeroMovementQuery::onRemoval(PlayerColor color) pb.player = color; pb.reason = PlayerBlocked::ONGOING_MOVEMENT; pb.startOrEnd = PlayerBlocked::BLOCKADE_ENDED; - gh->sendAndApply(&pb); + gh->sendAndApply(pb); } void CHeroMovementQuery::onAdding(PlayerColor color) @@ -282,5 +282,5 @@ void CHeroMovementQuery::onAdding(PlayerColor color) pb.player = color; pb.reason = PlayerBlocked::ONGOING_MOVEMENT; pb.startOrEnd = PlayerBlocked::BLOCKADE_STARTED; - gh->sendAndApply(&pb); + gh->sendAndApply(pb); } diff --git a/server/queries/MapQueries.h b/server/queries/MapQueries.h index 41b06d3b7..f202e11b7 100644 --- a/server/queries/MapQueries.h +++ b/server/queries/MapQueries.h @@ -25,7 +25,7 @@ class TimerPauseQuery : public CQuery public: TimerPauseQuery(CGameHandler * owner, PlayerColor player); - bool blocksPack(const CPack *pack) const override; + bool blocksPack(const CPackForServer *pack) const override; void onAdding(PlayerColor color) override; void onRemoval(PlayerColor color) override; bool endsByPlayerAnswer() const override; @@ -54,7 +54,7 @@ public: CGarrisonDialogQuery(CGameHandler * owner, const CArmedInstance *up, const CArmedInstance *down); void notifyObjectAboutRemoval(const CGObjectInstance * visitedObject, const CGHeroInstance * visitingHero) const override; - bool blocksPack(const CPack *pack) const override; + bool blocksPack(const CPackForServer *pack) const override; }; //yes/no and component selection dialogs @@ -75,7 +75,7 @@ class OpenWindowQuery : public CDialogQuery public: OpenWindowQuery(CGameHandler * owner, const CGHeroInstance *hero, EOpenWindowMode mode); - bool blocksPack(const CPack *pack) const override; + bool blocksPack(const CPackForServer *pack) const override; void onExposure(QueryPtr topQuery) override; }; diff --git a/server/queries/VisitQueries.cpp b/server/queries/VisitQueries.cpp index ea53b3648..f5f34fbb3 100644 --- a/server/queries/VisitQueries.cpp +++ b/server/queries/VisitQueries.cpp @@ -24,7 +24,7 @@ VisitQuery::VisitQuery(CGameHandler * owner, const CGObjectInstance * Obj, const addPlayer(Hero->tempOwner); } -bool VisitQuery::blocksPack(const CPack * pack) const +bool VisitQuery::blocksPack(const CPackForServer * pack) const { //During the visit itself ALL actions are blocked. //(However, the visit may trigger a query above that'll pass some.) @@ -50,7 +50,6 @@ void MapObjectVisitQuery::onRemoval(PlayerColor color) { gh->objectVisitEnded(visitingHero, players.front()); - //TODO or should it be destructor? //Can object visit affect 2 players and what would be desired behavior? if(removeObjectAfterVisit) gh->removeObject(visitedObject, color); @@ -78,7 +77,7 @@ void TownBuildingVisitQuery::onAdded(PlayerColor color) while (!visitedBuilding.empty() && owner->topQuery(color).get() == this) { visitingHero = visitedBuilding.back().hero; - auto * building = visitedTown->rewardableBuildings.at(visitedBuilding.back().building); + const auto * building = visitedTown->rewardableBuildings.at(visitedBuilding.back().building); building->onHeroVisit(visitingHero); visitedBuilding.pop_back(); } diff --git a/server/queries/VisitQueries.h b/server/queries/VisitQueries.h index d2f554a49..fe41df45f 100644 --- a/server/queries/VisitQueries.h +++ b/server/queries/VisitQueries.h @@ -26,7 +26,7 @@ public: const CGObjectInstance * visitedObject; const CGHeroInstance * visitingHero; - bool blocksPack(const CPack * pack) const final; + bool blocksPack(const CPackForServer * pack) const final; }; class MapObjectVisitQuery final : public VisitQuery diff --git a/test/entity/CCreatureTest.cpp b/test/entity/CCreatureTest.cpp index 590754e73..3d963df09 100644 --- a/test/entity/CCreatureTest.cpp +++ b/test/entity/CCreatureTest.cpp @@ -100,7 +100,7 @@ TEST_F(CCreatureTest, DISABLED_JsonUpdate) EXPECT_EQ(subject->getFightValue(), 2420); EXPECT_EQ(subject->getLevel(), 6); - EXPECT_EQ(subject->getFaction().getNum(), 55); + EXPECT_EQ(subject->getFactionID().getNum(), 55); EXPECT_TRUE(subject->isDoubleWide()); } diff --git a/test/entity/CHeroClassTest.cpp b/test/entity/CHeroClassTest.cpp index db109ae94..23eb7266b 100644 --- a/test/entity/CHeroClassTest.cpp +++ b/test/entity/CHeroClassTest.cpp @@ -9,7 +9,7 @@ */ #include "StdInc.h" -#include "../../lib/CHeroHandler.h" +#include "../../lib/entities/hero/CHeroClass.h" namespace test { diff --git a/test/entity/CHeroTest.cpp b/test/entity/CHeroTest.cpp index edea854a5..92f0e190d 100644 --- a/test/entity/CHeroTest.cpp +++ b/test/entity/CHeroTest.cpp @@ -9,7 +9,7 @@ */ #include "StdInc.h" -#include "../../lib/CHeroHandler.h" +#include "../../lib/entities/hero/CHero.h" namespace test { diff --git a/test/game/CGameStateTest.cpp b/test/game/CGameStateTest.cpp index be71314fc..9313a0f8b 100644 --- a/test/game/CGameStateTest.cpp +++ b/test/game/CGameStateTest.cpp @@ -63,42 +63,42 @@ public: return true; } - void apply(CPackForClient * pack) override + void apply(CPackForClient & pack) override { gameState->apply(pack); } - void apply(BattleLogMessage * pack) override + void apply(BattleLogMessage & pack) override { gameState->apply(pack); } - void apply(BattleStackMoved * pack) override + void apply(BattleStackMoved & pack) override { gameState->apply(pack); } - void apply(BattleUnitsChanged * pack) override + void apply(BattleUnitsChanged & pack) override { gameState->apply(pack); } - void apply(SetStackEffect * pack) override + void apply(SetStackEffect & pack) override { gameState->apply(pack); } - void apply(StacksInjured * pack) override + void apply(StacksInjured & pack) override { gameState->apply(pack); } - void apply(BattleObstaclesChanged * pack) override + void apply(BattleObstaclesChanged & pack) override { gameState->apply(pack); } - void apply(CatapultAttack * pack) override + void apply(CatapultAttack & pack) override { gameState->apply(pack); } @@ -196,7 +196,7 @@ public: const auto & t = *gameCallback->getTile(tile); - auto terrain = t.terType->getId(); + auto terrain = t.getTerrainID(); BattleField terType(0); BattleLayout layout = BattleLayout::createDefaultLayout(gameState->callback, attacker, defender); @@ -207,7 +207,7 @@ public: BattleStart bs; bs.info = battle; ASSERT_EQ(gameState->currentBattles.size(), 0); - gameCallback->sendAndApply(&bs); + gameCallback->sendAndApply(bs); ASSERT_EQ(gameState->currentBattles.size(), 1); } @@ -236,7 +236,7 @@ TEST_F(CGameStateTest, DISABLED_issue2765) na.artHolder = defender->id; na.artId = ArtifactID::BALLISTA; na.pos = ArtifactPosition::MACH1; - gameCallback->sendAndApply(&na); + gameCallback->sendAndApply(na); } startTestBattle(attacker, defender); @@ -253,7 +253,7 @@ TEST_F(CGameStateTest, DISABLED_issue2765) BattleUnitsChanged pack; pack.changedStacks.emplace_back(info.id, UnitChanges::EOperation::ADD); info.save(pack.changedStacks.back().data); - gameCallback->sendAndApply(&pack); + gameCallback->sendAndApply(pack); } const CStack * att = nullptr; @@ -324,7 +324,7 @@ TEST_F(CGameStateTest, DISABLED_battleResurrection) na.artHolder = attacker->id; na.artId = ArtifactID::SPELLBOOK; na.pos = ArtifactPosition::SPELLBOOK; - gameCallback->sendAndApply(&na); + gameCallback->sendAndApply(na); } startTestBattle(attacker, defender); @@ -343,7 +343,7 @@ TEST_F(CGameStateTest, DISABLED_battleResurrection) BattleUnitsChanged pack; pack.changedStacks.emplace_back(info.id, UnitChanges::EOperation::ADD); info.save(pack.changedStacks.back().data); - gameCallback->sendAndApply(&pack); + gameCallback->sendAndApply(pack); } { @@ -358,7 +358,7 @@ TEST_F(CGameStateTest, DISABLED_battleResurrection) BattleUnitsChanged pack; pack.changedStacks.emplace_back(info.id, UnitChanges::EOperation::ADD); info.save(pack.changedStacks.back().data); - gameCallback->sendAndApply(&pack); + gameCallback->sendAndApply(pack); } CStack * unit = gameState->currentBattles.front()->getStack(unitId); diff --git a/test/map/CMapEditManagerTest.cpp b/test/map/CMapEditManagerTest.cpp index b1e7f56e6..542167082 100644 --- a/test/map/CMapEditManagerTest.cpp +++ b/test/map/CMapEditManagerTest.cpp @@ -38,17 +38,17 @@ TEST(MapManager, DrawTerrain_Type) static const int3 squareCheck[] = { int3(5,5,0), int3(5,4,0), int3(4,4,0), int3(4,5,0) }; for(const auto & tile : squareCheck) { - EXPECT_EQ(map->getTile(tile).terType->getId(), ETerrainId::GRASS); + EXPECT_EQ(map->getTile(tile).getTerrainID(), ETerrainId::GRASS); } // Concat to square editManager->getTerrainSelection().select(int3(6, 5, 0)); editManager->drawTerrain(ETerrainId::GRASS, 10, &rand); - EXPECT_EQ(map->getTile(int3(6, 4, 0)).terType->getId(), ETerrainId::GRASS); + EXPECT_EQ(map->getTile(int3(6, 4, 0)).getTerrainID(), ETerrainId::GRASS); editManager->getTerrainSelection().select(int3(6, 5, 0)); editManager->drawTerrain(ETerrainId::LAVA, 10, &rand); - EXPECT_EQ(map->getTile(int3(4, 4, 0)).terType->getId(), ETerrainId::GRASS); - EXPECT_EQ(map->getTile(int3(7, 4, 0)).terType->getId(), ETerrainId::LAVA); + EXPECT_EQ(map->getTile(int3(4, 4, 0)).getTerrainID(), ETerrainId::GRASS); + EXPECT_EQ(map->getTile(int3(7, 4, 0)).getTerrainID(), ETerrainId::LAVA); // Special case water,rock editManager->getTerrainSelection().selectRange(MapRect(int3(10, 10, 0), 10, 5)); @@ -57,7 +57,7 @@ TEST(MapManager, DrawTerrain_Type) editManager->drawTerrain(ETerrainId::GRASS, 10, &rand); editManager->getTerrainSelection().select(int3(21, 16, 0)); editManager->drawTerrain(ETerrainId::GRASS, 10, &rand); - EXPECT_EQ(map->getTile(int3(20, 15, 0)).terType->getId(), ETerrainId::GRASS); + EXPECT_EQ(map->getTile(int3(20, 15, 0)).getTerrainID(), ETerrainId::GRASS); // Special case non water,rock static const int3 diagonalCheck[] = { int3(31,42,0), int3(32,42,0), int3(32,43,0), int3(33,43,0), int3(33,44,0), @@ -68,7 +68,7 @@ TEST(MapManager, DrawTerrain_Type) editManager->getTerrainSelection().select(tile); } editManager->drawTerrain(ETerrainId::GRASS, 10, &rand); - EXPECT_EQ(map->getTile(int3(35, 44, 0)).terType->getId(), ETerrainId::WATER); + EXPECT_EQ(map->getTile(int3(35, 44, 0)).getTerrainID(), ETerrainId::WATER); // Rock case editManager->getTerrainSelection().selectRange(MapRect(int3(1, 1, 1), 15, 15)); @@ -77,7 +77,7 @@ TEST(MapManager, DrawTerrain_Type) int3(8, 7, 1), int3(4, 8, 1), int3(5, 8, 1), int3(6, 8, 1)}); editManager->getTerrainSelection().setSelection(vec); editManager->drawTerrain(ETerrainId::ROCK, 10, &rand); - EXPECT_TRUE(!map->getTile(int3(5, 6, 1)).terType->isPassable() || !map->getTile(int3(7, 8, 1)).terType->isPassable()); + EXPECT_TRUE(!map->getTile(int3(5, 6, 1)).getTerrain()->isPassable() || !map->getTile(int3(7, 8, 1)).getTerrain()->isPassable()); //todo: add checks here and enable, also use smaller size #if 0 @@ -144,7 +144,7 @@ TEST(MapManager, DrawTerrain_View) int3 pos((si32)posVector[0].Float(), (si32)posVector[1].Float(), (si32)posVector[2].Float()); const auto & originalTile = originalMap->getTile(pos); editManager->getTerrainSelection().selectRange(MapRect(pos, 1, 1)); - editManager->drawTerrain(originalTile.terType->getId(), 10, &gen); + editManager->drawTerrain(originalTile.getTerrainID(), 10, &gen); const auto & tile = map->getTile(pos); bool isInRange = false; for(const auto & range : mapping) diff --git a/test/map/MapComparer.cpp b/test/map/MapComparer.cpp index 28ace85c2..61e1f018e 100644 --- a/test/map/MapComparer.cpp +++ b/test/map/MapComparer.cpp @@ -133,7 +133,7 @@ void checkEqual(const ObjectTemplate & actual, const ObjectTemplate & expected) void checkEqual(const TerrainTile & actual, const TerrainTile & expected) { //fatal fail here on any error - VCMI_REQUIRE_FIELD_EQUAL(terType); + VCMI_REQUIRE_FIELD_EQUAL(terrainType); VCMI_REQUIRE_FIELD_EQUAL(terView); VCMI_REQUIRE_FIELD_EQUAL(riverType); VCMI_REQUIRE_FIELD_EQUAL(riverDir); @@ -143,9 +143,6 @@ void checkEqual(const TerrainTile & actual, const TerrainTile & expected) ASSERT_EQ(actual.blockingObjects.size(), expected.blockingObjects.size()); ASSERT_EQ(actual.visitableObjects.size(), expected.visitableObjects.size()); - - VCMI_REQUIRE_FIELD_EQUAL(visitable); - VCMI_REQUIRE_FIELD_EQUAL(blocked); } //MapComparer @@ -203,8 +200,8 @@ void MapComparer::compareObject(const CGObjectInstance * actual, const CGObjectI EXPECT_EQ(actual->instanceName, expected->instanceName); EXPECT_EQ(typeid(actual).name(), typeid(expected).name());//todo: remove and use just comparison - std::string actualFullID = boost::str(boost::format("%s(%d)|%s(%d) %d") % actual->typeName % actual->ID % actual->subTypeName % actual->subID % actual->tempOwner); - std::string expectedFullID = boost::str(boost::format("%s(%d)|%s(%d) %d") % expected->typeName % expected->ID % expected->subTypeName % expected->subID % expected->tempOwner); + std::string actualFullID = boost::str(boost::format("(%d)|(%d) %d") % actual->ID % actual->subID % actual->tempOwner); + std::string expectedFullID = boost::str(boost::format("(%d)|(%d) %d") % expected->ID % expected->subID % expected->tempOwner); EXPECT_EQ(actualFullID, expectedFullID); diff --git a/test/mock/BattleFake.h b/test/mock/BattleFake.h index 42ff10e07..59f49a80d 100644 --- a/test/mock/BattleFake.h +++ b/test/mock/BattleFake.h @@ -81,9 +81,9 @@ public: #endif template - void accept(T * pack) + void accept(T & pack) { - pack->applyBattle(this); + pack.applyBattle(this); } const IBattleInfo * getBattle() const override diff --git a/test/mock/mock_Creature.h b/test/mock/mock_Creature.h index 6dacd4815..cb51a595a 100644 --- a/test/mock/mock_Creature.h +++ b/test/mock/mock_Creature.h @@ -44,7 +44,7 @@ public: MOCK_CONST_METHOD0(getLevel, int32_t()); MOCK_CONST_METHOD0(getGrowth, int32_t()); MOCK_CONST_METHOD0(getHorde, int32_t()); - MOCK_CONST_METHOD0(getFaction, FactionID()); + MOCK_CONST_METHOD0(getFactionID, FactionID()); MOCK_CONST_METHOD0(getBaseAttack, int32_t()); MOCK_CONST_METHOD0(getBaseDefense, int32_t()); diff --git a/test/mock/mock_IGameCallback.cpp b/test/mock/mock_IGameCallback.cpp index 43428b698..378d43b95 100644 --- a/test/mock/mock_IGameCallback.cpp +++ b/test/mock/mock_IGameCallback.cpp @@ -27,7 +27,7 @@ void GameCallbackMock::setGameState(CGameState * gameState) gs = gameState; } -void GameCallbackMock::sendAndApply(CPackForClient * pack) +void GameCallbackMock::sendAndApply(CPackForClient & pack) { upperCallback->apply(pack); } diff --git a/test/mock/mock_IGameCallback.h b/test/mock/mock_IGameCallback.h index 1f2456a2f..3ae574345 100644 --- a/test/mock/mock_IGameCallback.h +++ b/test/mock/mock_IGameCallback.h @@ -44,6 +44,7 @@ public: void showInfoDialog(InfoWindow * iw) override {} void changeSpells(const CGHeroInstance * hero, bool give, const std::set &spells) override {} + void setResearchedSpells(const CGTownInstance * town, int level, const std::vector & spells, bool accepted) override {} bool removeObject(const CGObjectInstance * obj, const PlayerColor & initiator) override {return false;} void createBoat(const int3 & visitablePosition, BoatId type, PlayerColor initiator) override {} void setOwner(const CGObjectInstance * objid, PlayerColor owner) override {} @@ -72,7 +73,7 @@ public: bool giveHeroNewArtifact(const CGHeroInstance * h, const ArtifactID & artId, const ArtifactPosition & pos) override {return false;} bool giveHeroNewScroll(const CGHeroInstance * h, const SpellID & spellId, const ArtifactPosition & pos) override {return false;} - bool putArtifact(const ArtifactLocation & al, const CArtifactInstance * art, std::optional askAssemble) override {return false;} + bool putArtifact(const ArtifactLocation & al, const ArtifactInstanceID & id, std::optional askAssemble) override {return false;} void removeArtifact(const ArtifactLocation &al) override {} bool moveArtifact(const PlayerColor & player, const ArtifactLocation & al1, const ArtifactLocation & al2) override {return false;} @@ -95,7 +96,7 @@ public: void castSpell(const spells::Caster * caster, SpellID spellID, const int3 &pos) override {} ///useful callback methods - void sendAndApply(CPackForClient * pack) override; + void sendAndApply(CPackForClient & pack) override; vstd::RNG & getRandomGenerator() override; diff --git a/test/mock/mock_ServerCallback.h b/test/mock/mock_ServerCallback.h index 967f55e74..faefcfa04 100644 --- a/test/mock/mock_ServerCallback.h +++ b/test/mock/mock_ServerCallback.h @@ -20,13 +20,13 @@ public: MOCK_METHOD1(complain, void(const std::string &)); MOCK_METHOD0(getRNG, vstd::RNG *()); - MOCK_METHOD1(apply, void(CPackForClient *)); + MOCK_METHOD1(apply, void(CPackForClient &)); - MOCK_METHOD1(apply, void(BattleLogMessage *)); - MOCK_METHOD1(apply, void(BattleStackMoved *)); - MOCK_METHOD1(apply, void(BattleUnitsChanged *)); - MOCK_METHOD1(apply, void(SetStackEffect *)); - MOCK_METHOD1(apply, void(StacksInjured *)); - MOCK_METHOD1(apply, void(BattleObstaclesChanged *)); - MOCK_METHOD1(apply, void(CatapultAttack *)); + MOCK_METHOD1(apply, void(BattleLogMessage &)); + MOCK_METHOD1(apply, void(BattleStackMoved &)); + MOCK_METHOD1(apply, void(BattleUnitsChanged &)); + MOCK_METHOD1(apply, void(SetStackEffect &)); + MOCK_METHOD1(apply, void(StacksInjured &)); + MOCK_METHOD1(apply, void(BattleObstaclesChanged &)); + MOCK_METHOD1(apply, void(CatapultAttack &)); }; diff --git a/test/mock/mock_battle_Unit.h b/test/mock/mock_battle_Unit.h index 5f2b03bf0..7560bfdc8 100644 --- a/test/mock/mock_battle_Unit.h +++ b/test/mock/mock_battle_Unit.h @@ -82,7 +82,7 @@ public: MOCK_CONST_METHOD1(willMove, bool(int)); MOCK_CONST_METHOD1(waited, bool(int)); - MOCK_CONST_METHOD0(getFaction, FactionID()); + MOCK_CONST_METHOD0(getFactionID, FactionID()); MOCK_CONST_METHOD1(battleQueuePhase, battle::BattlePhases::Type(int)); diff --git a/test/mock/mock_spells_Spell.h b/test/mock/mock_spells_Spell.h index 2d0ae31d1..512649dae 100644 --- a/test/mock/mock_spells_Spell.h +++ b/test/mock/mock_spells_Spell.h @@ -47,6 +47,7 @@ public: MOCK_CONST_METHOD0(isSpecial, bool()); MOCK_CONST_METHOD0(isMagical, bool()); MOCK_CONST_METHOD0(canCastOnSelf, bool()); + MOCK_CONST_METHOD0(canCastOnlyOnSelf, bool()); MOCK_CONST_METHOD0(canCastWithoutSkip, bool()); MOCK_CONST_METHOD1(hasSchool, bool(SpellSchool)); MOCK_CONST_METHOD1(forEachSchool, void(const SchoolCallback &)); diff --git a/test/scripting/LuaSpellEffectAPITest.cpp b/test/scripting/LuaSpellEffectAPITest.cpp index a144485a3..f9c4a1b7d 100644 --- a/test/scripting/LuaSpellEffectAPITest.cpp +++ b/test/scripting/LuaSpellEffectAPITest.cpp @@ -157,18 +157,18 @@ TEST_F(LuaSpellEffectAPITest, DISABLED_ApplyMoveUnit) BattleStackMoved expected; BattleStackMoved actual; - auto checkMove = [&](BattleStackMoved * pack) + auto checkMove = [&](BattleStackMoved & pack) { - EXPECT_EQ(pack->stack, 42); - EXPECT_EQ(pack->teleporting, true); - EXPECT_EQ(pack->distance, 0); + EXPECT_EQ(pack.stack, 42); + EXPECT_EQ(pack.teleporting, true); + EXPECT_EQ(pack.distance, 0); std::vector toMove(1, hex2); - EXPECT_EQ(pack->tilesToMove, toMove); + EXPECT_EQ(pack.tilesToMove, toMove); }; - EXPECT_CALL(serverMock, apply(Matcher(_))).WillOnce(Invoke(checkMove)); + EXPECT_CALL(serverMock, apply(Matcher(_))).WillOnce(Invoke(checkMove)); context->callGlobal(&serverMock, "apply", params); } diff --git a/test/spells/effects/CatapultTest.cpp b/test/spells/effects/CatapultTest.cpp index 9743f9378..e6e2d2f18 100644 --- a/test/spells/effects/CatapultTest.cpp +++ b/test/spells/effects/CatapultTest.cpp @@ -134,7 +134,7 @@ TEST_F(CatapultApplyTest, DISABLED_DamageToIntactPart) EXPECT_CALL(*battleFake, getWallState(_)).WillRepeatedly(Return(EWallState::DESTROYED)); EXPECT_CALL(*battleFake, getWallState(Eq(targetPart))).WillRepeatedly(Return(EWallState::INTACT)); EXPECT_CALL(*battleFake, setWallState(Eq(targetPart), Eq(EWallState::DAMAGED))).Times(1); - EXPECT_CALL(serverMock, apply(Matcher(_))).Times(1); + EXPECT_CALL(serverMock, apply(Matcher(_))).Times(1); EffectTarget target; target.emplace_back(); diff --git a/test/spells/effects/CloneTest.cpp b/test/spells/effects/CloneTest.cpp index b68124242..d64896c5c 100644 --- a/test/spells/effects/CloneTest.cpp +++ b/test/spells/effects/CloneTest.cpp @@ -148,8 +148,8 @@ public: battleFake->setupEmptyBattlefield(); - EXPECT_CALL(serverMock, apply(Matcher(_))).Times(2); - EXPECT_CALL(serverMock, apply(Matcher(_))).Times(1); + EXPECT_CALL(serverMock, apply(Matcher(_))).Times(2); + EXPECT_CALL(serverMock, apply(Matcher(_))).Times(1); EXPECT_CALL(mechanicsMock, getEffectDuration()).WillOnce(Return(effectDuration)); EXPECT_CALL(*battleFake, getUnitsIf(_)).Times(AtLeast(1)); diff --git a/test/spells/effects/DamageTest.cpp b/test/spells/effects/DamageTest.cpp index 227764405..f4a3583a7 100644 --- a/test/spells/effects/DamageTest.cpp +++ b/test/spells/effects/DamageTest.cpp @@ -109,7 +109,7 @@ TEST_F(DamageApplyTest, DISABLED_DoesDamageToAliveUnit) targetUnitState->localInit(&unitEnvironmentMock); EXPECT_CALL(targetUnit, acquireState()).WillOnce(Return(targetUnitState)); EXPECT_CALL(*battleFake, setUnitState(Eq(unitId),_, Lt(0))).Times(1); - EXPECT_CALL(serverMock, apply(Matcher(_))).Times(1); + EXPECT_CALL(serverMock, apply(Matcher(_))).Times(1); EXPECT_CALL(serverMock, describeChanges()).WillRepeatedly(Return(false)); setupDefaultRNG(); @@ -174,7 +174,7 @@ TEST_F(DamageApplyTest, DISABLED_DoesDamageByPercent) EXPECT_CALL(targetUnit, acquireState()).WillOnce(Return(targetUnitState)); EXPECT_CALL(*battleFake, setUnitState(Eq(unitId),_, Lt(0))).Times(1); - EXPECT_CALL(serverMock, apply(Matcher(_))).Times(1); + EXPECT_CALL(serverMock, apply(Matcher(_))).Times(1); EXPECT_CALL(serverMock, describeChanges()).WillRepeatedly(Return(false)); setupDefaultRNG(); @@ -218,7 +218,7 @@ TEST_F(DamageApplyTest, DISABLED_DoesDamageByCount) EXPECT_CALL(targetUnit, acquireState()).WillOnce(Return(targetUnitState)); EXPECT_CALL(*battleFake, setUnitState(Eq(unitId), _, Lt(0))).Times(1); - EXPECT_CALL(serverMock, apply(Matcher(_))).Times(1); + EXPECT_CALL(serverMock, apply(Matcher(_))).Times(1); EXPECT_CALL(serverMock, describeChanges()).WillRepeatedly(Return(false)); setupDefaultRNG(); diff --git a/test/spells/effects/DispelTest.cpp b/test/spells/effects/DispelTest.cpp index 5817293d1..4087498cc 100644 --- a/test/spells/effects/DispelTest.cpp +++ b/test/spells/effects/DispelTest.cpp @@ -209,7 +209,7 @@ TEST_F(DispelApplyTest, DISABLED_RemovesEffects) EXPECT_CALL(mechanicsMock, getSpellIndex()).Times(AtLeast(1)).WillRepeatedly(Return(neutralID.toEnum())); - EXPECT_CALL(serverMock, apply(Matcher(_))).Times(1); + EXPECT_CALL(serverMock, apply(Matcher(_))).Times(1); EXPECT_CALL(serverMock, describeChanges()).WillRepeatedly(Return(false)); setDefaultExpectations(); diff --git a/test/spells/effects/EffectFixture.cpp b/test/spells/effects/EffectFixture.cpp index cf4478786..0f31118c7 100644 --- a/test/spells/effects/EffectFixture.cpp +++ b/test/spells/effects/EffectFixture.cpp @@ -92,13 +92,13 @@ void EffectFixture::setUp() ON_CALL(serverMock, getRNG()).WillByDefault(Return(&rngMock)); - ON_CALL(serverMock, apply(Matcher(_))).WillByDefault(Invoke(battleFake.get(), &battle::BattleFake::accept)); - ON_CALL(serverMock, apply(Matcher(_))).WillByDefault(Invoke(battleFake.get(), &battle::BattleFake::accept)); - ON_CALL(serverMock, apply(Matcher(_))).WillByDefault(Invoke(battleFake.get(), &battle::BattleFake::accept)); - ON_CALL(serverMock, apply(Matcher(_))).WillByDefault(Invoke(battleFake.get(), &battle::BattleFake::accept)); - ON_CALL(serverMock, apply(Matcher(_))).WillByDefault(Invoke(battleFake.get(), &battle::BattleFake::accept)); - ON_CALL(serverMock, apply(Matcher(_))).WillByDefault(Invoke(battleFake.get(), &battle::BattleFake::accept)); - ON_CALL(serverMock, apply(Matcher(_))).WillByDefault(Invoke(battleFake.get(), &battle::BattleFake::accept)); + ON_CALL(serverMock, apply(Matcher(_))).WillByDefault(Invoke(battleFake.get(), &battle::BattleFake::accept)); + ON_CALL(serverMock, apply(Matcher(_))).WillByDefault(Invoke(battleFake.get(), &battle::BattleFake::accept)); + ON_CALL(serverMock, apply(Matcher(_))).WillByDefault(Invoke(battleFake.get(), &battle::BattleFake::accept)); + ON_CALL(serverMock, apply(Matcher(_))).WillByDefault(Invoke(battleFake.get(), &battle::BattleFake::accept)); + ON_CALL(serverMock, apply(Matcher(_))).WillByDefault(Invoke(battleFake.get(), &battle::BattleFake::accept)); + ON_CALL(serverMock, apply(Matcher(_))).WillByDefault(Invoke(battleFake.get(), &battle::BattleFake::accept)); + ON_CALL(serverMock, apply(Matcher(_))).WillByDefault(Invoke(battleFake.get(), &battle::BattleFake::accept)); } static int64_t getInt64Range(int64_t lower, int64_t upper) diff --git a/test/spells/effects/HealTest.cpp b/test/spells/effects/HealTest.cpp index 8cfdb396b..a3b5e13b0 100644 --- a/test/spells/effects/HealTest.cpp +++ b/test/spells/effects/HealTest.cpp @@ -375,8 +375,8 @@ TEST_P(HealApplyTest, DISABLED_Heals) EXPECT_CALL(actualCaster, getCasterUnitId()).WillRepeatedly(Return(-1)); - EXPECT_CALL(serverMock, apply(Matcher(_))).Times(1); - EXPECT_CALL(serverMock, apply(Matcher(_))).Times(AtLeast(1)); + EXPECT_CALL(serverMock, apply(Matcher(_))).Times(1); + EXPECT_CALL(serverMock, apply(Matcher(_))).Times(AtLeast(1)); setupDefaultRNG(); diff --git a/test/spells/effects/SacrificeTest.cpp b/test/spells/effects/SacrificeTest.cpp index 40c04bc4e..6b6e07740 100644 --- a/test/spells/effects/SacrificeTest.cpp +++ b/test/spells/effects/SacrificeTest.cpp @@ -203,8 +203,8 @@ TEST_F(SacrificeApplyTest, DISABLED_ResurrectsTarget) EXPECT_CALL(targetUnit, acquire()).WillOnce(Return(targetUnitState)); - EXPECT_CALL(serverMock, apply(Matcher(_))).Times(AtLeast(1)); - EXPECT_CALL(serverMock, apply(Matcher(_))).Times(AtLeast(1)); + EXPECT_CALL(serverMock, apply(Matcher(_))).Times(AtLeast(1)); + EXPECT_CALL(serverMock, apply(Matcher(_))).Times(AtLeast(1)); setupDefaultRNG(); diff --git a/test/spells/effects/SummonTest.cpp b/test/spells/effects/SummonTest.cpp index ebbd5563c..00815b3cd 100644 --- a/test/spells/effects/SummonTest.cpp +++ b/test/spells/effects/SummonTest.cpp @@ -225,7 +225,7 @@ TEST_P(SummonApplyTest, DISABLED_SpawnsNewUnit) EXPECT_CALL(*battleFake, nextUnitId()).WillOnce(Return(unitId)); EXPECT_CALL(*battleFake, addUnit(Eq(unitId), _)).WillOnce(Invoke(this, &SummonApplyTest::onUnitAdded)); - EXPECT_CALL(serverMock, apply(Matcher(_))).Times(1); + EXPECT_CALL(serverMock, apply(Matcher(_))).Times(1); EffectTarget target; target.emplace_back(unitPosition); @@ -261,7 +261,7 @@ TEST_P(SummonApplyTest, DISABLED_UpdatesOldUnit) EXPECT_CALL(unit, unitId()).WillOnce(Return(unitId)); - EXPECT_CALL(serverMock, apply(Matcher(_))).Times(1); + EXPECT_CALL(serverMock, apply(Matcher(_))).Times(1); unitsFake.setDefaultBonusExpectations(); diff --git a/test/spells/effects/TeleportTest.cpp b/test/spells/effects/TeleportTest.cpp index 00f46bfd1..327dc6ab1 100644 --- a/test/spells/effects/TeleportTest.cpp +++ b/test/spells/effects/TeleportTest.cpp @@ -71,7 +71,7 @@ TEST_F(TeleportApplyTest, DISABLED_MovesUnit) EXPECT_CALL(*battleFake, moveUnit(Eq(unitId), Eq(destination))); EXPECT_CALL(mechanicsMock, getEffectLevel()).WillRepeatedly(Return(0)); - EXPECT_CALL(serverMock, apply(Matcher(_))).Times(1); + EXPECT_CALL(serverMock, apply(Matcher(_))).Times(1); Target target; target.emplace_back(&unit, BattleHex()); diff --git a/test/spells/effects/TimedTest.cpp b/test/spells/effects/TimedTest.cpp index bfd4a61e9..3f3f56ff1 100644 --- a/test/spells/effects/TimedTest.cpp +++ b/test/spells/effects/TimedTest.cpp @@ -118,7 +118,7 @@ TEST_P(TimedApplyTest, DISABLED_ChangesBonuses) setDefaultExpectations(); - EXPECT_CALL(serverMock, apply(Matcher(_))).Times(1); + EXPECT_CALL(serverMock, apply(Matcher(_))).Times(1); subject->apply(&serverMock, &mechanicsMock, target); diff --git a/vcmiqt/CMakeLists.txt b/vcmiqt/CMakeLists.txt new file mode 100644 index 000000000..e33f6ee48 --- /dev/null +++ b/vcmiqt/CMakeLists.txt @@ -0,0 +1,46 @@ +set(vcmiqt_SRCS + StdInc.cpp + + jsonutils.cpp + launcherdirs.cpp +) + +set(vcmiqt_HEADERS + StdInc.h + + jsonutils.h + launcherdirs.h + convpathqstring.h + vcmiqt.h +) + +assign_source_group(${vcmiqt_SRCS} ${vcmiqt_HEADERS}) + +if(ENABLE_STATIC_LIBS OR NOT (ENABLE_EDITOR AND ENABLE_LAUNCHER)) + add_library(vcmiqt STATIC ${vcmiqt_SRCS} ${vcmiqt_HEADERS}) + target_compile_definitions(vcmiqt PRIVATE VCMIQT_STATIC) +else() + add_library(vcmiqt SHARED ${vcmiqt_SRCS} ${vcmiqt_HEADERS}) + target_compile_definitions(vcmiqt PRIVATE VCMIQT_SHARED) +endif() + +if(WIN32) + set_target_properties(vcmiqt + PROPERTIES + OUTPUT_NAME "VCMI_vcmiqt" + PROJECT_LABEL "VCMI_vcmiqt" + ) +endif() + +target_link_libraries(vcmiqt vcmi Qt${QT_VERSION_MAJOR}::Core) + +target_include_directories(vcmiqt PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR} +) + +if(NOT ENABLE_STATIC_LIBS OR (ENABLE_EDITOR AND ENABLE_LAUNCHER)) + install(TARGETS vcmiqt RUNTIME DESTINATION ${LIB_DIR} LIBRARY DESTINATION ${LIB_DIR}) +endif() + +vcmi_set_output_dir(vcmiqt "") +enable_pch(vcmiqt) diff --git a/vcmiqt/StdInc.cpp b/vcmiqt/StdInc.cpp new file mode 100644 index 000000000..02b98775f --- /dev/null +++ b/vcmiqt/StdInc.cpp @@ -0,0 +1,12 @@ +/* + * StdInc.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 + * + */ +// Creates the precompiled header +#include "StdInc.h" + diff --git a/vcmiqt/StdInc.h b/vcmiqt/StdInc.h new file mode 100644 index 000000000..402615a9e --- /dev/null +++ b/vcmiqt/StdInc.h @@ -0,0 +1,20 @@ +/* + * StdInc.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 "../Global.h" + +#include +#include +#include + +#include "convpathqstring.h" + +VCMI_LIB_USING_NAMESPACE diff --git a/vcmiqt/convpathqstring.h b/vcmiqt/convpathqstring.h new file mode 100644 index 000000000..a71469edd --- /dev/null +++ b/vcmiqt/convpathqstring.h @@ -0,0 +1,29 @@ +/* + * convpathqstring.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 + +inline QString pathToQString(const boost::filesystem::path & path) +{ +#ifdef VCMI_WINDOWS + return QString::fromStdWString(path.wstring()); +#else + return QString::fromStdString(path.string()); +#endif +} + +inline boost::filesystem::path qstringToPath(const QString & path) +{ +#ifdef VCMI_WINDOWS + return boost::filesystem::path(path.toStdWString()); +#else + return boost::filesystem::path(path.toUtf8().data()); +#endif +} diff --git a/launcher/jsonutils.cpp b/vcmiqt/jsonutils.cpp similarity index 94% rename from launcher/jsonutils.cpp rename to vcmiqt/jsonutils.cpp index e4eb884d6..5815a46be 100644 --- a/launcher/jsonutils.cpp +++ b/vcmiqt/jsonutils.cpp @@ -79,7 +79,7 @@ QVariant toVariant(const JsonNode & node) return QVariant(); } -QVariant JsonFromFile(QString filename) +JsonNode jsonFromFile(QString filename) { QFile file(filename); if(!file.open(QFile::ReadOnly)) @@ -90,7 +90,7 @@ QVariant JsonFromFile(QString filename) const auto data = file.readAll(); JsonNode node(reinterpret_cast(data.data()), data.size(), filename.toStdString()); - return toVariant(node); + return node; } JsonNode toJson(QVariant object) @@ -113,10 +113,10 @@ JsonNode toJson(QVariant object) return ret; } -void JsonToFile(QString filename, QVariant object) +void jsonToFile(QString filename, const JsonNode & object) { std::fstream file(qstringToPath(filename).c_str(), std::ios::out | std::ios_base::binary); - file << toJson(object).toString(); + file << object.toCompactString(); } } diff --git a/launcher/jsonutils.h b/vcmiqt/jsonutils.h similarity index 57% rename from launcher/jsonutils.h rename to vcmiqt/jsonutils.h index 791711eb0..f65d666b4 100644 --- a/launcher/jsonutils.h +++ b/vcmiqt/jsonutils.h @@ -9,6 +9,8 @@ */ #pragma once +#include "vcmiqt.h" + #include VCMI_LIB_NAMESPACE_BEGIN @@ -17,11 +19,11 @@ class JsonNode; namespace JsonUtils { -QVariant toVariant(const JsonNode & node); -QVariant JsonFromFile(QString filename); +VCMIQT_LINKAGE QVariant toVariant(const JsonNode & node); +VCMIQT_LINKAGE JsonNode jsonFromFile(QString filename); -JsonNode toJson(QVariant object); -void JsonToFile(QString filename, QVariant object); +VCMIQT_LINKAGE JsonNode toJson(QVariant object); +VCMIQT_LINKAGE void jsonToFile(QString filename, const JsonNode & object); } VCMI_LIB_NAMESPACE_END diff --git a/launcher/launcherdirs.cpp b/vcmiqt/launcherdirs.cpp similarity index 100% rename from launcher/launcherdirs.cpp rename to vcmiqt/launcherdirs.cpp diff --git a/launcher/launcherdirs.h b/vcmiqt/launcherdirs.h similarity index 68% rename from launcher/launcherdirs.h rename to vcmiqt/launcherdirs.h index a8b5ea1c4..91c7c0221 100644 --- a/launcher/launcherdirs.h +++ b/vcmiqt/launcherdirs.h @@ -9,12 +9,14 @@ */ #pragma once +#include "vcmiqt.h" + /// similar to lib/VCMIDirs, controls where all launcher-related data will be stored namespace CLauncherDirs { - void prepare(); + VCMIQT_LINKAGE void prepare(); - QString downloadsPath(); - QString modsPath(); - QString mapsPath(); + VCMIQT_LINKAGE QString downloadsPath(); + VCMIQT_LINKAGE QString modsPath(); + VCMIQT_LINKAGE QString mapsPath(); } diff --git a/vcmiqt/vcmiqt.h b/vcmiqt/vcmiqt.h new file mode 100644 index 000000000..4783eaaae --- /dev/null +++ b/vcmiqt/vcmiqt.h @@ -0,0 +1,19 @@ +/* + * vcmiqt.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 + * + */ + +#include + +#ifdef VCMIQT_STATIC +# define VCMIQT_LINKAGE +#elif defined(VCMIQT_SHARED) +# define VCMIQT_LINKAGE Q_DECL_EXPORT +#else +# define VCMIQT_LINKAGE Q_DECL_IMPORT +#endif