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 937882025..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 @@ -146,20 +183,24 @@ jobs: HEROES_3_DATA_PASSWORD: ${{ secrets.HEROES_3_DATA_PASSWORD }} if: ${{ env.HEROES_3_DATA_PASSWORD != '' && matrix.test == 1 }} run: | - wget --progress=dot:giga https://github.com/vcmi-mods/vcmi-test-data/releases/download/v1.0/h3_assets.zip + if [[ ${{github.repository_owner}} == vcmi ]] + then + data_url="https://github.com/vcmi-mods/vcmi-test-data/releases/download/v1.0/h3_assets.zip" + else + data_url="https://github.com/${{github.repository_owner}}/vcmi-test-data/releases/download/v1.0/h3_assets.zip" + fi + wget --progress=dot:giga "$data_url" -O h3_assets.zip 7za x h3_assets.zip -p$HEROES_3_DATA_PASSWORD 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 \ @@ -171,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' @@ -202,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 @@ -236,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 @@ -247,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 }} @@ -262,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 @@ -337,19 +391,20 @@ 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 \ -o -path ./osx -prune -o -type f \ - -not -name '*.png' -and -not -name '*.vcxproj*' -and -not -name '*.props' -and -not -name '*.wav' -and -not -name '*.webm' -and -not -name '*.ico' -and -not -name '*.bat' -print0 | \ + -not -name '*.png' -and -not -name '*.ttf' -and -not -name '*.wav' -and -not -name '*.webm' -and -not -name '*.ico' -and -not -name '*.bat' -print0 | \ { ! xargs -0 grep -l -z -P '\r\n'; } - 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/.gitignore b/.gitignore index 28ba6ab2e..334daa1db 100644 --- a/.gitignore +++ b/.gitignore @@ -43,6 +43,7 @@ VCMI_VS11.sdf *.ipch VCMI_VS11.opensdf .DS_Store +.directory CMakeUserPresets.json compile_commands.json fuzzylite.pc diff --git a/.gitmodules b/.gitmodules index b484aea8e..d6b206fe1 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,7 +1,7 @@ [submodule "test/googletest"] path = test/googletest url = https://github.com/google/googletest - branch = v1.13.x + branch = v1.15.x [submodule "AI/FuzzyLite"] path = AI/FuzzyLite url = https://github.com/fuzzylite/fuzzylite.git diff --git a/AI/BattleAI/AttackPossibility.cpp b/AI/BattleAI/AttackPossibility.cpp index b2d4c769f..c55de09b2 100644 --- a/AI/BattleAI/AttackPossibility.cpp +++ b/AI/BattleAI/AttackPossibility.cpp @@ -12,6 +12,10 @@ #include "../../lib/CStack.h" // TODO: remove // Eventually only IBattleInfoCallback and battle::Unit should be used, // CUnitState should be private and CStack should be removed completely +#include "../../lib/spells/CSpellHandler.h" +#include "../../lib/spells/ISpellMechanics.h" +#include "../../lib/spells/ObstacleCasterProxy.h" +#include "../../lib/battle/CObstacleInstance.h" uint64_t averageDmg(const DamageRange & range) { @@ -25,9 +29,64 @@ void DamageCache::cacheDamage(const battle::Unit * attacker, const battle::Unit damageCache[attacker->unitId()][defender->unitId()] = static_cast(damage) / attacker->getCount(); } - -void DamageCache::buildDamageCache(std::shared_ptr hb, int side) +void DamageCache::buildObstacleDamageCache(std::shared_ptr hb, BattleSide side) { + for(const auto & obst : hb->battleGetAllObstacles(side)) + { + auto spellObstacle = dynamic_cast(obst.get()); + + if(!spellObstacle || !obst->triggersEffects()) + continue; + + auto triggerAbility = VLC->spells()->getById(obst->getTrigger()); + auto triggerIsNegative = triggerAbility->isNegative() || triggerAbility->isDamage(); + + if(!triggerIsNegative) + continue; + + std::unique_ptr cast = nullptr; + std::unique_ptr caster = nullptr; + if(spellObstacle->obstacleType == SpellCreatedObstacle::EObstacleType::SPELL_CREATED) + { + const auto * hero = hb->battleGetFightingHero(spellObstacle->casterSide); + caster = std::make_unique(hb->getSidePlayer(spellObstacle->casterSide), hero, *spellObstacle); + cast = std::make_unique(spells::BattleCast(hb.get(), caster.get(), spells::Mode::PASSIVE, obst->getTrigger().toSpell())); + } + + auto affectedHexes = obst->getAffectedTiles(); + auto stacks = hb->battleGetUnitsIf([](const battle::Unit * u) -> bool { + return u->alive() && !u->isTurret() && u->getPosition().isValid(); + }); + + auto inner = std::make_shared(hb->env, hb); + + for(auto stack : stacks) + { + auto updated = inner->getForUpdate(stack->unitId()); + + spells::Target target; + target.push_back(spells::Destination(updated.get())); + + if(cast) + cast->castEval(inner->getServerCallback(), target); + + auto damageDealt = stack->getAvailableHealth() - updated->getAvailableHealth(); + + for(auto hex : affectedHexes) + { + obstacleDamage[hex][stack->unitId()] = damageDealt; + } + } + } +} + +void DamageCache::buildDamageCache(std::shared_ptr hb, BattleSide side) +{ + if(parent == nullptr) + { + buildObstacleDamageCache(hb, side); + } + auto stacks = hb->battleGetUnitsIf([=](const battle::Unit * u) -> bool { return u->isValidTarget(); @@ -70,6 +129,23 @@ int64_t DamageCache::getDamage(const battle::Unit * attacker, const battle::Unit return damageCache[attacker->unitId()][defender->unitId()] * attacker->getCount(); } +int64_t DamageCache::getObstacleDamage(BattleHex hex, const battle::Unit * defender) +{ + if(parent) + return parent->getObstacleDamage(hex, defender); + + auto damages = obstacleDamage.find(hex); + + if(damages == obstacleDamage.end()) + return 0; + + auto damage = damages->second.find(defender->unitId()); + + return damage == damages->second.end() + ? 0 + : damage->second; +} + int64_t DamageCache::getOriginalDamage(const battle::Unit * attacker, const battle::Unit * defender, std::shared_ptr hb) { if(parent) @@ -93,6 +169,8 @@ int64_t DamageCache::getOriginalDamage(const battle::Unit * attacker, const batt AttackPossibility::AttackPossibility(BattleHex from, BattleHex dest, const BattleAttackInfo & attack) : from(from), dest(dest), attack(attack) { + this->attack.attackerPos = from; + this->attack.defenderPos = dest; } float AttackPossibility::damageDiff() const @@ -199,6 +277,8 @@ int64_t AttackPossibility::evaluateBlockedShootersDmg( if(attackInfo.shooting) return 0; + std::set checkedUnits; + auto attacker = attackInfo.attacker; auto hexes = attacker->getSurroundingHexes(hex); for(BattleHex tile : hexes) @@ -206,9 +286,13 @@ int64_t AttackPossibility::evaluateBlockedShootersDmg( auto st = state->battleGetUnitByPos(tile, true); if(!st || !state->battleMatchOwner(st, attacker)) continue; + if(vstd::contains(checkedUnits, st->unitId())) + continue; if(!state->battleCanShoot(st)) continue; + checkedUnits.insert(st->unitId()); + // FIXME: provide distance info for Jousting bonus BattleAttackInfo rangeAttackInfo(st, attacker, 0, true); rangeAttackInfo.defenderPos = hex; @@ -218,9 +302,10 @@ int64_t AttackPossibility::evaluateBlockedShootersDmg( auto rangeDmg = state->battleEstimateDamage(rangeAttackInfo); auto meleeDmg = state->battleEstimateDamage(meleeAttackInfo); + auto cachedDmg = damageCache.getOriginalDamage(st, attacker, state); int64_t gain = averageDmg(rangeDmg.damage) - averageDmg(meleeDmg.damage) + 1; - res += gain; + res += gain * cachedDmg / std::max(1, averageDmg(rangeDmg.damage)); } return res; @@ -243,7 +328,7 @@ AttackPossibility AttackPossibility::evaluate( std::vector defenderHex; if(attackInfo.shooting) - defenderHex = defender->getHexes(); + defenderHex.push_back(defender->getPosition()); else defenderHex = CStack::meleeAttackHexes(attacker, defender, hex); @@ -261,63 +346,114 @@ AttackPossibility AttackPossibility::evaluate( if (!attackInfo.shooting) ap.attackerState->setPosition(hex); - std::vector units; + std::vector defenderUnits; + std::vector retaliatedUnits = {attacker}; + std::vector affectedUnits; if (attackInfo.shooting) - units = state->getAttackedBattleUnits(attacker, defHex, true, BattleHex::INVALID); + defenderUnits = state->getAttackedBattleUnits(attacker, defender, defHex, true, hex, defender->getPosition()); else - units = state->getAttackedBattleUnits(attacker, defHex, false, hex); - - // ensure the defender is also affected - bool addDefender = true; - for(auto unit : units) { - if (unit->unitId() == defender->unitId()) + defenderUnits = state->getAttackedBattleUnits(attacker, defender, defHex, false, hex, defender->getPosition()); + retaliatedUnits = state->getAttackedBattleUnits(defender, attacker, hex, false, defender->getPosition(), hex); + + // attacker can not melle-attack itself but still can hit that place where it was before moving + vstd::erase_if(defenderUnits, [attacker](const battle::Unit * u) -> bool { return u->unitId() == attacker->unitId(); }); + + if(!vstd::contains_if(retaliatedUnits, [attacker](const battle::Unit * u) -> bool { return u->unitId() == attacker->unitId(); })) { - addDefender = false; - break; + retaliatedUnits.push_back(attacker); + } + + auto obstacleDamage = damageCache.getObstacleDamage(hex, attacker); + + if(obstacleDamage > 0) + { + ap.attackerDamageReduce += calculateDamageReduce(nullptr, attacker, obstacleDamage, damageCache, state); + + ap.attackerState->damage(obstacleDamage); } } - if(addDefender) - units.push_back(defender); - - for(auto u : units) + // ensure the defender is also affected + if(!vstd::contains_if(defenderUnits, [defender](const battle::Unit * u) -> bool { return u->unitId() == defender->unitId(); })) { - if(!ap.attackerState->alive()) - break; + defenderUnits.push_back(defender); + } + + affectedUnits = defenderUnits; + vstd::concatenate(affectedUnits, retaliatedUnits); + + logAi->trace("Attacked battle units count %d, %d->%d", affectedUnits.size(), hex.hex, defHex.hex); + + std::map> defenderStates; + + for(auto u : affectedUnits) + { + if(u->unitId() == attacker->unitId()) + continue; auto defenderState = u->acquireState(); - ap.affectedUnits.push_back(defenderState); - for(int i = 0; i < totalAttacks; i++) + ap.affectedUnits.push_back(defenderState); + defenderStates[u->unitId()] = defenderState; + } + + for(int i = 0; i < totalAttacks; i++) + { + if(!ap.attackerState->alive() || !defenderStates[defender->unitId()]->alive()) + break; + + for(auto u : defenderUnits) { + auto defenderState = defenderStates.at(u->unitId()); + int64_t damageDealt; - int64_t damageReceived; float defenderDamageReduce; float attackerDamageReduce; DamageEstimation retaliation; auto attackDmg = state->battleEstimateDamage(ap.attack, &retaliation); - vstd::amin(attackDmg.damage.min, defenderState->getAvailableHealth()); - vstd::amin(attackDmg.damage.max, defenderState->getAvailableHealth()); - - vstd::amin(retaliation.damage.min, ap.attackerState->getAvailableHealth()); - vstd::amin(retaliation.damage.max, ap.attackerState->getAvailableHealth()); - damageDealt = averageDmg(attackDmg.damage); - defenderDamageReduce = calculateDamageReduce(attacker, defender, damageDealt, damageCache, state); + vstd::amin(damageDealt, defenderState->getAvailableHealth()); + + defenderDamageReduce = calculateDamageReduce(attacker, u, damageDealt, damageCache, state); ap.attackerState->afterAttack(attackInfo.shooting, false); //FIXME: use ranged retaliation - damageReceived = 0; attackerDamageReduce = 0; - if (!attackInfo.shooting && defenderState->ableToRetaliate() && !counterAttacksBlocked) + if (!attackInfo.shooting && u->unitId() == defender->unitId() && defenderState->ableToRetaliate() && !counterAttacksBlocked) { - damageReceived = averageDmg(retaliation.damage); - attackerDamageReduce = calculateDamageReduce(defender, attacker, damageReceived, damageCache, state); + for(auto retaliated : retaliatedUnits) + { + if(retaliated->unitId() == attacker->unitId()) + { + int64_t damageReceived = averageDmg(retaliation.damage); + + vstd::amin(damageReceived, ap.attackerState->getAvailableHealth()); + + attackerDamageReduce = calculateDamageReduce(defender, retaliated, damageReceived, damageCache, state); + ap.attackerState->damage(damageReceived); + } + else + { + auto retaliationCollateral = state->battleEstimateDamage(defender, retaliated, 0); + int64_t damageReceived = averageDmg(retaliationCollateral.damage); + + vstd::amin(damageReceived, retaliated->getAvailableHealth()); + + if(defender->unitSide() == retaliated->unitSide()) + defenderDamageReduce += calculateDamageReduce(defender, retaliated, damageReceived, damageCache, state); + else + ap.collateralDamageReduce += calculateDamageReduce(defender, retaliated, damageReceived, damageCache, state); + + defenderStates.at(retaliated->unitId())->damage(damageReceived); + } + + } + defenderState->afterAttack(attackInfo.shooting, true); } @@ -331,21 +467,30 @@ AttackPossibility AttackPossibility::evaluate( if(attackerSide == u->unitSide()) ap.collateralDamageReduce += defenderDamageReduce; - if(u->unitId() == defender->unitId() || - (!attackInfo.shooting && CStack::isMeleeAttackPossible(u, attacker, hex))) + if(u->unitId() == defender->unitId() + || (!attackInfo.shooting && CStack::isMeleeAttackPossible(u, attacker, hex))) { //FIXME: handle RANGED_RETALIATION ? ap.attackerDamageReduce += attackerDamageReduce; } - ap.attackerState->damage(damageReceived); defenderState->damage(damageDealt); - if (!ap.attackerState->alive() || !defenderState->alive()) - break; + if(u->unitId() == defender->unitId()) + { + ap.defenderDead = !defenderState->alive(); + } } } +#if BATTLE_TRACE_LEVEL>=2 + logAi->trace("BattleAI AP: %s -> %s at %d from %d, affects %d units: d:%lld a:%lld c:%lld s:%lld", + attackInfo.attacker->unitType()->getJsonKey(), + attackInfo.defender->unitType()->getJsonKey(), + (int)ap.dest, (int)ap.from, (int)ap.affectedUnits.size(), + ap.defenderDamageReduce, ap.attackerDamageReduce, ap.collateralDamageReduce, ap.shootersBlockedDmg); +#endif + if(!bestAp.dest.isValid() || ap.attackValue() > bestAp.attackValue()) bestAp = ap; } diff --git a/AI/BattleAI/AttackPossibility.h b/AI/BattleAI/AttackPossibility.h index b8ff77218..3ef8e1523 100644 --- a/AI/BattleAI/AttackPossibility.h +++ b/AI/BattleAI/AttackPossibility.h @@ -18,16 +18,20 @@ class DamageCache { private: std::unordered_map> damageCache; + std::map> obstacleDamage; DamageCache * parent; + void buildObstacleDamageCache(std::shared_ptr hb, BattleSide side); + public: DamageCache() : parent(nullptr) {} DamageCache(DamageCache * parent) : parent(parent) {} void cacheDamage(const battle::Unit * attacker, const battle::Unit * defender, std::shared_ptr hb); int64_t getDamage(const battle::Unit * attacker, const battle::Unit * defender, std::shared_ptr hb); + int64_t getObstacleDamage(BattleHex hex, const battle::Unit * defender); int64_t getOriginalDamage(const battle::Unit * attacker, const battle::Unit * defender, std::shared_ptr hb); - void buildDamageCache(std::shared_ptr hb, int side); + void buildDamageCache(std::shared_ptr hb, BattleSide side); }; /// @@ -49,6 +53,7 @@ public: float attackerDamageReduce = 0; //usually by counter-attack float collateralDamageReduce = 0; // friendly fire (usually by two-hex attacks) int64_t shootersBlockedDmg = 0; + bool defenderDead = false; AttackPossibility(BattleHex from, BattleHex dest, const BattleAttackInfo & attack_); diff --git a/AI/BattleAI/BattleAI.cbp b/AI/BattleAI/BattleAI.cbp deleted file mode 100644 index b2c8bcfc0..000000000 --- a/AI/BattleAI/BattleAI.cbp +++ /dev/null @@ -1,106 +0,0 @@ - - - - - - diff --git a/AI/BattleAI/BattleAI.cpp b/AI/BattleAI/BattleAI.cpp index 41a885b89..9b15eafb6 100644 --- a/AI/BattleAI/BattleAI.cpp +++ b/AI/BattleAI/BattleAI.cpp @@ -23,15 +23,17 @@ #include "../../lib/battle/BattleAction.h" #include "../../lib/battle/BattleStateInfoForRetreat.h" #include "../../lib/battle/CObstacleInstance.h" +#include "../../lib/StartInfo.h" #include "../../lib/CStack.h" // TODO: remove // Eventually only IBattleInfoCallback and battle::Unit should be used, // CUnitState should be private and CStack should be removed completely +#include "../../lib/logging/VisualLogger.h" #define LOGL(text) print(text) #define LOGFL(text, formattingEl) print(boost::str(boost::format(text) % formattingEl)) CBattleAI::CBattleAI() - : side(-1), + : side(BattleSide::NONE), wasWaitingForRealize(false), wasUnlockingGs(false) { @@ -47,6 +49,17 @@ CBattleAI::~CBattleAI() } } +void logHexNumbers() +{ +#if BATTLE_TRACE_LEVEL >= 1 + logVisual->updateWithLock("hexes", [](IVisualLogBuilder & b) + { + for(BattleHex hex = BattleHex(0); hex < GameConstants::BFIELD_SIZE; hex = BattleHex(hex + 1)) + b.addText(hex, std::to_string(hex.hex)); + }); +#endif +} + void CBattleAI::initBattleInterface(std::shared_ptr ENV, std::shared_ptr CB) { env = ENV; @@ -57,6 +70,8 @@ void CBattleAI::initBattleInterface(std::shared_ptr ENV, std::share CB->waitTillRealize = false; CB->unlockGsWhenWaiting = false; movesSkippedByDefense = 0; + + logHexNumbers(); } void CBattleAI::initBattleInterface(std::shared_ptr ENV, std::shared_ptr CB, AutocombatPreferences autocombatPreferences) @@ -86,7 +101,7 @@ void CBattleAI::yourTacticPhase(const BattleID & battleID, int distance) cb->battleMakeTacticAction(battleID, BattleAction::makeEndOFTacticPhase(cb->getBattle(battleID)->battleGetTacticsSide())); } -static float getStrengthRatio(std::shared_ptr cb, int side) +static float getStrengthRatio(std::shared_ptr cb, BattleSide side) { auto stacks = cb->battleGetAllStacks(); auto our = 0; @@ -108,6 +123,11 @@ static float getStrengthRatio(std::shared_ptr cb, int side) return enemy == 0 ? 1.0f : static_cast(our) / enemy; } +int getSimulationTurnsCount(const StartInfo * startInfo) +{ + return startInfo->difficulty < 4 ? 2 : 10; +} + void CBattleAI::activeStack(const BattleID & battleID, const CStack * stack ) { LOG_TRACE_PARAMS(logAi, "stack: %s", stack->nodeName()); @@ -140,18 +160,19 @@ void CBattleAI::activeStack(const BattleID & battleID, const CStack * stack ) logAi->trace("Build evaluator and targets"); #endif - BattleEvaluator evaluator(env, cb, stack, playerID, battleID, side, getStrengthRatio(cb->getBattle(battleID), side)); + BattleEvaluator evaluator( + env, cb, stack, playerID, battleID, side, + getStrengthRatio(cb->getBattle(battleID), side), + getSimulationTurnsCount(env->game()->getStartInfo())); result = evaluator.selectStackAction(stack); - if(autobattlePreferences.enableSpellsUsage && !skipCastUntilNextBattle && evaluator.canCastSpell()) + if(autobattlePreferences.enableSpellsUsage && evaluator.canCastSpell()) { auto spelCasted = evaluator.attemptCastingSpell(stack); if(spelCasted) return; - - skipCastUntilNextBattle = true; } logAi->trace("Spellcast attempt completed in %lld", timeElapsed(start)); @@ -176,7 +197,7 @@ void CBattleAI::activeStack(const BattleID & battleID, const CStack * stack ) movesSkippedByDefense = 0; } - logAi->trace("BattleAI decission made in %lld", timeElapsed(start)); + logAi->trace("BattleAI decision made in %lld", timeElapsed(start)); cb->battleMakeUnitAction(battleID, result); } @@ -206,7 +227,7 @@ BattleAction CBattleAI::useCatapult(const BattleID & battleID, const CStack * st { auto wallState = cb->getBattle(battleID)->battleGetWallState(wallPart); - if(wallState == EWallState::REINFORCED || wallState == EWallState::INTACT || wallState == EWallState::DAMAGED) + if(wallState != EWallState::NONE && wallState != EWallState::DESTROYED) { targetHex = cb->getBattle(battleID)->wallPartToBattleHex(wallPart); break; @@ -229,12 +250,10 @@ BattleAction CBattleAI::useCatapult(const BattleID & battleID, const CStack * st return attack; } -void CBattleAI::battleStart(const BattleID & battleID, const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool Side, bool replayAllowed) +void CBattleAI::battleStart(const BattleID & battleID, const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, BattleSide Side, bool replayAllowed) { LOG_TRACE(logAi); side = Side; - - skipCastUntilNextBattle = false; } void CBattleAI::print(const std::string &text) const diff --git a/AI/BattleAI/BattleAI.h b/AI/BattleAI/BattleAI.h index 497a77f3f..18df749f0 100644 --- a/AI/BattleAI/BattleAI.h +++ b/AI/BattleAI/BattleAI.h @@ -27,7 +27,7 @@ struct CurrentOffensivePotential std::map ourAttacks; std::map enemyAttacks; - CurrentOffensivePotential(ui8 side) + CurrentOffensivePotential(BattleSide side) { for(auto stack : cbc->battleGetStacks()) { @@ -50,11 +50,11 @@ struct CurrentOffensivePotential return ourPotential - enemyPotential; } }; -*/ // These lines may be usefull but they are't used in the code. +*/ // These lines may be useful but they are't used in the code. class CBattleAI : public CBattleGameInterface { - int side; + BattleSide side; std::shared_ptr cb; std::shared_ptr env; @@ -62,7 +62,6 @@ class CBattleAI : public CBattleGameInterface bool wasWaitingForRealize; bool wasUnlockingGs; int movesSkippedByDefense; - bool skipCastUntilNextBattle; public: CBattleAI(); @@ -80,7 +79,7 @@ public: BattleAction useCatapult(const BattleID & battleID, const CStack *stack); BattleAction useHealingTent(const BattleID & battleID, const CStack *stack); - void battleStart(const BattleID & battleID, const CCreatureSet * army1, const CCreatureSet * army2, int3 tile, const CGHeroInstance * hero1, const CGHeroInstance * hero2, bool Side, bool replayAllowed) override; + void battleStart(const BattleID & battleID, const CCreatureSet * army1, const CCreatureSet * army2, int3 tile, const CGHeroInstance * hero1, const CGHeroInstance * hero2, BattleSide side, bool replayAllowed) override; //void actionFinished(const BattleAction &action) override;//occurs AFTER every action taken by any stack or by the hero //void actionStarted(const BattleAction &action) override;//occurs BEFORE every action taken by any stack or by the hero //void battleAttack(const BattleAttack *ba) override; //called when stack is performing attack @@ -93,7 +92,7 @@ public: //void battleSpellCast(const BattleSpellCast *sc) override; //void battleStacksEffectsSet(const SetStackEffect & sse) override;//called when a specific effect is set to stacks //void battleTriggerEffect(const BattleTriggerEffect & bte) override; - //void battleStart(const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool side) override; //called by engine when battle starts; side=0 - left, side=1 - right + //void battleStart(const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, BattleSide side) override; //called by engine when battle starts; side=0 - left, side=1 - right //void battleCatapultAttacked(const CatapultAttack & ca) override; //called when catapult makes an attack AutocombatPreferences autobattlePreferences = AutocombatPreferences(); }; diff --git a/AI/BattleAI/BattleAI.vcxproj b/AI/BattleAI/BattleAI.vcxproj deleted file mode 100644 index 9509985e6..000000000 --- a/AI/BattleAI/BattleAI.vcxproj +++ /dev/null @@ -1,168 +0,0 @@ - - - - - Debug - Win32 - - - Debug - x64 - - - RD - Win32 - - - RD - x64 - - - - {C0300513-E845-43B4-9A4F-E8817EAEF57C} - BattleAI - 10.0 - - - - DynamicLibrary - true - MultiByte - v140_xp - - - DynamicLibrary - true - MultiByte - v140_xp - - - DynamicLibrary - false - true - MultiByte - v142 - - - DynamicLibrary - false - true - MultiByte - v140_xp - - - - - - - - - - - - - - - - - - - - - - - - - - - $(VCMI_Out)\AI\ - - - $(VCMI_Out)\AI\ - - - $(VCMI_Out)/AI - - - $(VCMI_Out)\AI\ - - - - Use - StdInc.h - /Zm159 %(AdditionalOptions) - - - VCMI_lib.lib;%(AdditionalDependencies) - ..\..\..\libs;..\..;.. - - - - - Level3 - Disabled - Use - StdInc.h - /Zm159 %(AdditionalOptions) - - - VCMI_lib.lib;%(AdditionalDependencies) - - - - - Use - StdInc.h - true - MaxSpeed - - - VCMI_lib.lib;%(AdditionalDependencies) - $(VCMI_Out) - - - - - Use - StdInc.h - /Zm159 %(AdditionalOptions) - - - VCMI_lib.lib;%(AdditionalDependencies) - - - - - - - - - - - - %(PreprocessorDefinitions) - Create - StdInc.h - Create - Create - Create - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/AI/BattleAI/BattleEvaluator.cpp b/AI/BattleAI/BattleEvaluator.cpp index ba3df7786..af2220b7d 100644 --- a/AI/BattleAI/BattleEvaluator.cpp +++ b/AI/BattleAI/BattleEvaluator.cpp @@ -17,6 +17,7 @@ #include "../../lib/CStopWatch.h" #include "../../lib/CThreadHelper.h" #include "../../lib/mapObjects/CGTownInstance.h" +#include "../../lib/entities/building/TownFortifications.h" #include "../../lib/spells/CSpellHandler.h" #include "../../lib/spells/ISpellMechanics.h" #include "../../lib/battle/BattleStateInfoForRetreat.h" @@ -49,6 +50,43 @@ SpellTypes spellType(const CSpell * spell) return SpellTypes::OTHER; } +BattleEvaluator::BattleEvaluator( + std::shared_ptr env, + std::shared_ptr cb, + const battle::Unit * activeStack, + PlayerColor playerID, + BattleID battleID, + BattleSide side, + float strengthRatio, + int simulationTurnsCount) + :scoreEvaluator(cb->getBattle(battleID), env, strengthRatio, simulationTurnsCount), + cachedAttack(), playerID(playerID), side(side), env(env), + cb(cb), strengthRatio(strengthRatio), battleID(battleID), simulationTurnsCount(simulationTurnsCount) +{ + hb = std::make_shared(env.get(), cb->getBattle(battleID)); + damageCache.buildDamageCache(hb, side); + + targets = std::make_unique(activeStack, damageCache, hb); +} + +BattleEvaluator::BattleEvaluator( + std::shared_ptr env, + std::shared_ptr cb, + std::shared_ptr hb, + DamageCache & damageCache, + const battle::Unit * activeStack, + PlayerColor playerID, + BattleID battleID, + BattleSide side, + float strengthRatio, + int simulationTurnsCount) + :scoreEvaluator(cb->getBattle(battleID), env, strengthRatio, simulationTurnsCount), + cachedAttack(), playerID(playerID), side(side), env(env), cb(cb), hb(hb), + damageCache(damageCache), strengthRatio(strengthRatio), battleID(battleID), simulationTurnsCount(simulationTurnsCount) +{ + targets = std::make_unique(activeStack, damageCache, hb); +} + std::vector BattleEvaluator::getBrokenWallMoatHexes() const { std::vector result; @@ -81,6 +119,14 @@ std::vector BattleEvaluator::getBrokenWallMoatHexes() const return result; } +bool BattleEvaluator::hasWorkingTowers() const +{ + bool keepIntact = cb->getBattle(battleID)->battleGetWallState(EWallPart::KEEP) != EWallState::NONE && cb->getBattle(battleID)->battleGetWallState(EWallPart::KEEP) != EWallState::DESTROYED; + bool upperIntact = cb->getBattle(battleID)->battleGetWallState(EWallPart::UPPER_TOWER) != EWallState::NONE && cb->getBattle(battleID)->battleGetWallState(EWallPart::UPPER_TOWER) != EWallState::DESTROYED; + bool bottomIntact = cb->getBattle(battleID)->battleGetWallState(EWallPart::BOTTOM_TOWER) != EWallState::NONE && cb->getBattle(battleID)->battleGetWallState(EWallPart::BOTTOM_TOWER) != EWallState::DESTROYED; + return keepIntact || upperIntact || bottomIntact; +} + std::optional BattleEvaluator::findBestCreatureSpell(const CStack *stack) { //TODO: faerie dragon type spell should be selected by server @@ -123,6 +169,14 @@ BattleAction BattleEvaluator::selectStackAction(const CStack * stack) auto moveTarget = scoreEvaluator.findMoveTowardsUnreachable(stack, *targets, damageCache, hb); float score = EvaluationResult::INEFFECTIVE_SCORE; + auto enemyMellee = hb->getUnitsIf([this](const battle::Unit* u) -> bool + { + return u->unitSide() == BattleSide::ATTACKER && !hb->battleCanShoot(u); + }); + bool siegeDefense = stack->unitSide() == BattleSide::DEFENDER + && !stack->canShoot() + && hasWorkingTowers() + && !enemyMellee.empty(); if(targets->possibleAttacks.empty() && bestSpellcast.has_value()) { @@ -136,11 +190,13 @@ BattleAction BattleEvaluator::selectStackAction(const CStack * stack) logAi->trace("Evaluating attack for %s", stack->getDescription()); #endif - auto evaluationResult = scoreEvaluator.findBestTarget(stack, *targets, damageCache, hb); + auto evaluationResult = scoreEvaluator.findBestTarget(stack, *targets, damageCache, hb, siegeDefense); auto & bestAttack = evaluationResult.bestAttack; - cachedAttack = bestAttack; - cachedScore = evaluationResult.score; + cachedAttack.ap = bestAttack; + cachedAttack.score = evaluationResult.score; + cachedAttack.turn = 0; + cachedAttack.waited = evaluationResult.wait; //TODO: consider more complex spellcast evaluation, f.e. because "re-retaliation" during enemy move in same turn for melee attack etc. if(bestSpellcast.has_value() && bestSpellcast->value > bestAttack.damageDiff()) @@ -167,7 +223,7 @@ BattleAction BattleEvaluator::selectStackAction(const CStack * stack) score ); - if (moveTarget.scorePerTurn <= score) + if (moveTarget.score <= score) { if(evaluationResult.wait) { @@ -186,37 +242,59 @@ BattleAction BattleEvaluator::selectStackAction(const CStack * stack) { return BattleAction::makeDefend(stack); } - else + + bool isTargetOutsideFort = !hb->battleIsInsideWalls(bestAttack.from); + bool siegeDefense = stack->unitSide() == BattleSide::DEFENDER + && !bestAttack.attack.shooting + && hasWorkingTowers() + && !enemyMellee.empty() + && isTargetOutsideFort; + + if(siegeDefense) { - activeActionMade = true; - return BattleAction::makeMeleeAttack(stack, bestAttack.attack.defender->getPosition(), bestAttack.from); + logAi->trace("Evaluating exchange at %d self-defense", stack->getPosition().hex); + + BattleAttackInfo bai(stack, stack, 0, false); + AttackPossibility apDefend(stack->getPosition(), stack->getPosition(), bai); + + float defenseValue = scoreEvaluator.evaluateExchange(apDefend, 0, *targets, damageCache, hb); + + if((defenseValue > score && score <= 0) || (defenseValue > 2 * score && score > 0)) + { + return BattleAction::makeDefend(stack); + } } + + activeActionMade = true; + return BattleAction::makeMeleeAttack(stack, bestAttack.attack.defenderPos, bestAttack.from); } } } } - //ThreatMap threatsToUs(stack); // These lines may be usefull but they are't used in the code. - if(moveTarget.scorePerTurn > score) + //ThreatMap threatsToUs(stack); // These lines may be useful but they are't used in the code. + if(moveTarget.score > score) { score = moveTarget.score; - cachedAttack = moveTarget.cachedAttack; - cachedScore = score; + cachedAttack.ap = moveTarget.cachedAttack; + cachedAttack.score = score; + cachedAttack.turn = moveTarget.turnsToRich; if(stack->waited()) { logAi->debug( - "Moving %s towards hex %s[%d], score: %2f/%2f", + "Moving %s towards hex %s[%d], score: %2f", stack->getDescription(), moveTarget.cachedAttack->attack.defender->getDescription(), moveTarget.cachedAttack->attack.defender->getPosition().hex, - moveTarget.score, - moveTarget.scorePerTurn); + moveTarget.score); - return goTowardsNearest(stack, moveTarget.positions); + return goTowardsNearest(stack, moveTarget.positions, *targets); } else { + cachedAttack.waited = true; + return BattleAction::makeWait(stack); } } @@ -224,7 +302,7 @@ BattleAction BattleEvaluator::selectStackAction(const CStack * stack) if(score <= EvaluationResult::INEFFECTIVE_SCORE && !stack->hasBonusOfType(BonusType::FLYING) && stack->unitSide() == BattleSide::ATTACKER - && cb->getBattle(battleID)->battleGetSiegeLevel() >= CGTownInstance::CITADEL) + && cb->getBattle(battleID)->battleGetFortifications().hasMoat) { auto brokenWallMoat = getBrokenWallMoatHexes(); @@ -235,7 +313,7 @@ BattleAction BattleEvaluator::selectStackAction(const CStack * stack) if(stack->doubleWide() && vstd::contains(brokenWallMoat, stack->getPosition())) return BattleAction::makeMove(stack, stack->getPosition().cloneInDirection(BattleHex::RIGHT)); else - return goTowardsNearest(stack, brokenWallMoat); + return goTowardsNearest(stack, brokenWallMoat, *targets); } } @@ -249,11 +327,55 @@ uint64_t timeElapsed(std::chrono::time_point return std::chrono::duration_cast(end - start).count(); } -BattleAction BattleEvaluator::goTowardsNearest(const CStack * stack, std::vector hexes) +BattleAction BattleEvaluator::moveOrAttack(const CStack * stack, BattleHex hex, const PotentialTargets & targets) +{ + auto additionalScore = 0; + std::optional attackOnTheWay; + + for(auto & target : targets.possibleAttacks) + { + if(!target.attack.shooting && target.from == hex && target.attackValue() > additionalScore) + { + additionalScore = target.attackValue(); + attackOnTheWay = target; + } + } + + if(attackOnTheWay) + { + activeActionMade = true; + return BattleAction::makeMeleeAttack(stack, attackOnTheWay->attack.defender->getPosition(), attackOnTheWay->from); + } + else + { + if(stack->position == hex) + return BattleAction::makeDefend(stack); + else + return BattleAction::makeMove(stack, hex); + } +} + +BattleAction BattleEvaluator::goTowardsNearest(const CStack * stack, std::vector hexes, const PotentialTargets & targets) { auto reachability = cb->getBattle(battleID)->getReachability(stack); auto avHexes = cb->getBattle(battleID)->battleGetAvailableHexes(reachability, stack, false); + auto enemyMellee = hb->getUnitsIf([this](const battle::Unit* u) -> bool + { + return u->unitSide() == BattleSide::ATTACKER && !hb->battleCanShoot(u); + }); + + bool siegeDefense = stack->unitSide() == BattleSide::DEFENDER + && hasWorkingTowers() + && !enemyMellee.empty(); + + if (siegeDefense) + { + vstd::erase_if(avHexes, [&](const BattleHex& hex) { + return !cb->getBattle(battleID)->battleIsInsideWalls(hex); + }); + } + if(!avHexes.size() || !hexes.size()) //we are blocked or dest is blocked { return BattleAction::makeDefend(stack); @@ -261,56 +383,45 @@ BattleAction BattleEvaluator::goTowardsNearest(const CStack * stack, std::vector std::vector targetHexes = hexes; - for(int i = 0; i < 5; i++) - { - std::sort(targetHexes.begin(), targetHexes.end(), [&](BattleHex h1, BattleHex h2) -> bool - { - return reachability.distances.at(h1) < reachability.distances.at(h2); - }); + vstd::erase_if(targetHexes, [](const BattleHex & hex) { return !hex.isValid(); }); - for(auto hex : targetHexes) + std::sort(targetHexes.begin(), targetHexes.end(), [&](BattleHex h1, BattleHex h2) -> bool { - if(vstd::contains(avHexes, hex)) - { - return BattleAction::makeMove(stack, hex); - } - - if(stack->coversPos(hex)) - { - logAi->warn("Warning: already standing on neighbouring tile!"); - //We shouldn't even be here... - return BattleAction::makeDefend(stack); - } - } - - if(reachability.distances.at(targetHexes.front()) <= GameConstants::BFIELD_SIZE) - { - break; - } - - std::vector copy = targetHexes; - - for(auto hex : copy) - vstd::concatenate(targetHexes, hex.allNeighbouringTiles()); - - vstd::erase_if(targetHexes, [](const BattleHex & hex) {return !hex.isValid();}); - vstd::removeDuplicates(targetHexes); - } + return reachability.distances[h1] < reachability.distances[h2]; + }); BattleHex bestNeighbor = targetHexes.front(); - if(reachability.distances.at(bestNeighbor) > GameConstants::BFIELD_SIZE) + if(reachability.distances[bestNeighbor] > GameConstants::BFIELD_SIZE) { + logAi->trace("No richable hexes."); return BattleAction::makeDefend(stack); } + // this turn + for(auto hex : targetHexes) + { + if(vstd::contains(avHexes, hex)) + { + return moveOrAttack(stack, hex, targets); + } + + if(stack->coversPos(hex)) + { + logAi->warn("Warning: already standing on neighbouring hex!"); + //We shouldn't even be here... + return BattleAction::makeDefend(stack); + } + } + + // not this turn scoreEvaluator.updateReachabilityMap(hb); if(stack->hasBonusOfType(BonusType::FLYING)) { 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()); }; @@ -343,7 +454,7 @@ BattleAction BattleEvaluator::goTowardsNearest(const CStack * stack, std::vector return scoreEvaluator.checkPositionBlocksOurStacks(*hb, stack, hex) ? BLOCKED_STACK_PENALTY + distance : distance; }); - return BattleAction::makeMove(stack, *nearestAvailableHex); + return moveOrAttack(stack, *nearestAvailableHex, targets); } else { @@ -357,11 +468,16 @@ BattleAction BattleEvaluator::goTowardsNearest(const CStack * stack, std::vector if(vstd::contains(avHexes, currentDest) && !scoreEvaluator.checkPositionBlocksOurStacks(*hb, stack, currentDest)) - return BattleAction::makeMove(stack, currentDest); + { + return moveOrAttack(stack, currentDest, targets); + } currentDest = reachability.predecessors[currentDest]; } } + + logAi->error("We should either detect that hexes are unreachable or make a move!"); + return BattleAction::makeDefend(stack); } bool BattleEvaluator::canCastSpell() @@ -391,7 +507,7 @@ bool BattleEvaluator::attemptCastingSpell(const CStack * activeStack) vstd::erase_if(possibleSpells, [](const CSpell *s) { - return spellType(s) != SpellTypes::BATTLE || s->getTargetType() == spells::AimType::LOCATION; + return spellType(s) != SpellTypes::BATTLE; }); LOGFL("I know how %d of them works.", possibleSpells.size()); @@ -402,9 +518,6 @@ bool BattleEvaluator::attemptCastingSpell(const CStack * activeStack) { spells::BattleCast temp(cb->getBattle(battleID).get(), hero, spells::Mode::HERO, spell); - if(spell->getTargetType() == spells::AimType::LOCATION) - continue; - const bool FAST = true; for(auto & target : temp.findPotentialTargets(FAST)) @@ -573,7 +686,15 @@ bool BattleEvaluator::attemptCastingSpell(const CStack * activeStack) auto & ps = possibleCasts[i]; #if BATTLE_TRACE_LEVEL >= 1 - logAi->trace("Evaluating %s", ps.spell->getNameTranslated()); + if(ps.dest.empty()) + logAi->trace("Evaluating %s", ps.spell->getNameTranslated()); + else + { + auto psFirst = ps.dest.front(); + auto strWhere = psFirst.unitValue ? psFirst.unitValue->getDescription() : std::to_string(psFirst.hexValue.hex); + + logAi->trace("Evaluating %s at %s", ps.spell->getNameTranslated(), strWhere); + } #endif auto state = std::make_shared(env.get(), cb->getBattle(battleID)); @@ -581,7 +702,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 { @@ -591,40 +712,57 @@ bool BattleEvaluator::attemptCastingSpell(const CStack * activeStack) DamageCache safeCopy = damageCache; DamageCache innerCache(&safeCopy); + innerCache.buildDamageCache(state, side); - if(needFullEval || !cachedAttack) + if(cachedAttack.ap && cachedAttack.waited) + { + state->makeWait(activeStack); + } + + if(needFullEval || !cachedAttack.ap) { #if BATTLE_TRACE_LEVEL >= 1 logAi->trace("Full evaluation is started due to stack speed affected."); #endif PotentialTargets innerTargets(activeStack, innerCache, state); - BattleExchangeEvaluator innerEvaluator(state, env, strengthRatio); + BattleExchangeEvaluator innerEvaluator(state, env, strengthRatio, simulationTurnsCount); + + innerEvaluator.updateReachabilityMap(state); + + auto moveTarget = innerEvaluator.findMoveTowardsUnreachable(activeStack, innerTargets, innerCache, state); if(!innerTargets.possibleAttacks.empty()) { - innerEvaluator.updateReachabilityMap(state); - auto newStackAction = innerEvaluator.findBestTarget(activeStack, innerTargets, innerCache, state); - ps.value = newStackAction.score; + ps.value = std::max(moveTarget.score, newStackAction.score); } else { - ps.value = 0; + ps.value = moveTarget.score; } } else { - ps.value = scoreEvaluator.evaluateExchange(*cachedAttack, 0, *targets, innerCache, state); - } + auto updatedAttacker = state->getForUpdate(cachedAttack.ap->attack.attacker->unitId()); + auto updatedDefender = state->getForUpdate(cachedAttack.ap->attack.defender->unitId()); + auto updatedBai = BattleAttackInfo( + updatedAttacker.get(), + updatedDefender.get(), + cachedAttack.ap->attack.chargeDistance, + cachedAttack.ap->attack.shooting); + auto updatedAttack = AttackPossibility::evaluate(updatedBai, cachedAttack.ap->from, innerCache, state); + + ps.value = scoreEvaluator.evaluateExchange(updatedAttack, cachedAttack.turn, *targets, innerCache, state); + } for(const auto & unit : allUnits) { - if (!unit->isValidTarget()) + if(!unit->isValidTarget(true)) continue; - + auto newHealth = unit->getAvailableHealth(); auto oldHealth = vstd::find_or(healthOfStack, unit->unitId(), 0); // old health value may not exist for newly summoned units @@ -635,7 +773,7 @@ bool BattleEvaluator::attemptCastingSpell(const CStack * activeStack) auto dpsReduce = AttackPossibility::calculateDamageReduce( nullptr, - originalDefender && originalDefender->alive() ? originalDefender : unit, + originalDefender && originalDefender->alive() ? originalDefender : unit, damage, innerCache, state); @@ -645,23 +783,49 @@ bool BattleEvaluator::attemptCastingSpell(const CStack * activeStack) if(ourUnit * goodEffect == 1) { - if(ourUnit && goodEffect && (unit->isClone() || unit->isGhost())) + auto isMagical = state->getForUpdate(unit->unitId())->summoned + || unit->isClone() + || unit->isGhost(); + + if(ourUnit && goodEffect && isMagical) continue; ps.value += dpsReduce * scoreEvaluator.getPositiveEffectMultiplier(); } else - ps.value -= dpsReduce * scoreEvaluator.getNegativeEffectMultiplier(); + // discourage AI making collateral damage with spells + 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 } } + #if BATTLE_TRACE_LEVEL >= 1 logAi->trace("Total score: %2f", ps.value); #endif @@ -672,13 +836,12 @@ bool BattleEvaluator::attemptCastingSpell(const CStack * activeStack) LOGFL("Evaluation took %d ms", timer.getDiff()); - auto pscValue = [](const PossibleSpellcast &ps) -> float - { - return ps.value; - }; - auto castToPerform = *vstd::maxElementByFun(possibleCasts, pscValue); + auto castToPerform = *vstd::maxElementByFun(possibleCasts, [](const PossibleSpellcast & ps) -> float + { + return ps.value; + }); - if(castToPerform.value > cachedScore) + if(castToPerform.value > cachedAttack.score && !vstd::isAlmostEqual(castToPerform.value, cachedAttack.score)) { LOGFL("Best spell is %s (value %d). Will cast.", castToPerform.spell->getNameTranslated() % castToPerform.value); BattleAction spellcast; @@ -686,7 +849,7 @@ bool BattleEvaluator::attemptCastingSpell(const CStack * activeStack) spellcast.spell = castToPerform.spell->id; spellcast.setTarget(castToPerform.dest); spellcast.side = side; - spellcast.stackNumber = (!side) ? -1 : -2; + spellcast.stackNumber = -1; cb->battleMakeSpellAction(battleID, spellcast); activeActionMade = true; diff --git a/AI/BattleAI/BattleEvaluator.h b/AI/BattleAI/BattleEvaluator.h index 6198d56a4..4c4bb1e5b 100644 --- a/AI/BattleAI/BattleEvaluator.h +++ b/AI/BattleAI/BattleEvaluator.h @@ -22,6 +22,14 @@ VCMI_LIB_NAMESPACE_END class EnemyInfo; +struct CachedAttack +{ + std::optional ap; + float score = EvaluationResult::INEFFECTIVE_SCORE; + uint8_t turn = 255; + bool waited = false; +}; + class BattleEvaluator { std::unique_ptr targets; @@ -30,23 +38,25 @@ class BattleEvaluator std::shared_ptr cb; std::shared_ptr env; bool activeActionMade = false; - std::optional cachedAttack; + CachedAttack cachedAttack; PlayerColor playerID; BattleID battleID; - int side; - float cachedScore; + BattleSide side; DamageCache damageCache; float strengthRatio; + int simulationTurnsCount; public: BattleAction selectStackAction(const CStack * stack); bool attemptCastingSpell(const CStack * stack); bool canCastSpell(); std::optional findBestCreatureSpell(const CStack * stack); - BattleAction goTowardsNearest(const CStack * stack, std::vector hexes); + BattleAction goTowardsNearest(const CStack * stack, std::vector hexes, const PotentialTargets & targets); std::vector getBrokenWallMoatHexes() const; + bool hasWorkingTowers() const; void evaluateCreatureSpellcast(const CStack * stack, PossibleSpellcast & ps); //for offensive damaging spells only void print(const std::string & text) const; + BattleAction moveOrAttack(const CStack * stack, BattleHex hex, const PotentialTargets & targets); BattleEvaluator( std::shared_ptr env, @@ -54,16 +64,9 @@ public: const battle::Unit * activeStack, PlayerColor playerID, BattleID battleID, - int side, - float strengthRatio) - :scoreEvaluator(cb->getBattle(battleID), env, strengthRatio), cachedAttack(), playerID(playerID), side(side), env(env), cb(cb), strengthRatio(strengthRatio), battleID(battleID) - { - hb = std::make_shared(env.get(), cb->getBattle(battleID)); - damageCache.buildDamageCache(hb, side); - - targets = std::make_unique(activeStack, damageCache, hb); - cachedScore = EvaluationResult::INEFFECTIVE_SCORE; - } + BattleSide side, + float strengthRatio, + int simulationTurnsCount); BattleEvaluator( std::shared_ptr env, @@ -73,11 +76,7 @@ public: const battle::Unit * activeStack, PlayerColor playerID, BattleID battleID, - int side, - float strengthRatio) - :scoreEvaluator(cb->getBattle(battleID), env, strengthRatio), cachedAttack(), playerID(playerID), side(side), env(env), cb(cb), hb(hb), damageCache(damageCache), strengthRatio(strengthRatio), battleID(battleID) - { - targets = std::make_unique(activeStack, damageCache, hb); - cachedScore = EvaluationResult::INEFFECTIVE_SCORE; - } + BattleSide side, + float strengthRatio, + int simulationTurnsCount); }; diff --git a/AI/BattleAI/BattleExchangeVariant.cpp b/AI/BattleAI/BattleExchangeVariant.cpp index 3bbd25302..c4675afc0 100644 --- a/AI/BattleAI/BattleExchangeVariant.cpp +++ b/AI/BattleAI/BattleExchangeVariant.cpp @@ -9,16 +9,17 @@ */ #include "StdInc.h" #include "BattleExchangeVariant.h" +#include "BattleEvaluator.h" #include "../../lib/CStack.h" AttackerValue::AttackerValue() : value(0), - isRetalitated(false) + isRetaliated(false) { } MoveTarget::MoveTarget() - : positions(), cachedAttack(), score(EvaluationResult::INEFFECTIVE_SCORE), scorePerTurn(EvaluationResult::INEFFECTIVE_SCORE) + : positions(), cachedAttack(), score(EvaluationResult::INEFFECTIVE_SCORE) { turnsToRich = 1; } @@ -28,102 +29,97 @@ float BattleExchangeVariant::trackAttack( std::shared_ptr hb, DamageCache & damageCache) { + if(!ap.attackerState) + { + logAi->trace("Skipping fake ap attack"); + return 0; + } + auto attacker = hb->getForUpdate(ap.attack.attacker->unitId()); - const std::string cachingStringBlocksRetaliation = "type_BLOCKS_RETALIATION"; - static const auto selectorBlocksRetaliation = Selector::type()(BonusType::BLOCKS_RETALIATION); - const bool counterAttacksBlocked = attacker->hasBonus(selectorBlocksRetaliation, cachingStringBlocksRetaliation); - - float attackValue = 0; + float attackValue = ap.attackValue(); auto affectedUnits = ap.affectedUnits; + dpsScore.ourDamageReduce += ap.attackerDamageReduce + ap.collateralDamageReduce; + dpsScore.enemyDamageReduce += ap.defenderDamageReduce + ap.shootersBlockedDmg; + attackerValue[attacker->unitId()].value = attackValue; + affectedUnits.push_back(ap.attackerState); for(auto affectedUnit : affectedUnits) { auto unitToUpdate = hb->getForUpdate(affectedUnit->unitId()); + auto damageDealt = unitToUpdate->getAvailableHealth() - affectedUnit->getAvailableHealth(); + + if(damageDealt > 0) + { + unitToUpdate->damage(damageDealt); + } if(unitToUpdate->unitSide() == attacker->unitSide()) { if(unitToUpdate->unitId() == attacker->unitId()) { - auto defender = hb->getForUpdate(ap.attack.defender->unitId()); - - if(!defender->alive() || counterAttacksBlocked || ap.attack.shooting || !defender->ableToRetaliate()) - continue; - - auto retaliationDamage = damageCache.getDamage(defender.get(), unitToUpdate.get(), hb); - auto attackerDamageReduce = AttackPossibility::calculateDamageReduce(defender.get(), unitToUpdate.get(), retaliationDamage, damageCache, hb); - - attackValue -= attackerDamageReduce; - dpsScore.ourDamageReduce += attackerDamageReduce; - attackerValue[unitToUpdate->unitId()].isRetalitated = true; - - unitToUpdate->damage(retaliationDamage); - defender->afterAttack(false, true); + unitToUpdate->afterAttack(ap.attack.shooting, false); #if BATTLE_TRACE_LEVEL>=1 logAi->trace( - "%s -> %s, ap retalitation, %s, dps: %2f, score: %2f", - defender->getDescription(), - unitToUpdate->getDescription(), + "%s -> %s, ap retaliation, %s, dps: %lld", + hb->getForUpdate(ap.attack.defender->unitId())->getDescription(), + ap.attack.attacker->getDescription(), ap.attack.shooting ? "shot" : "mellee", - retaliationDamage, - attackerDamageReduce); + damageDealt); #endif } else { - auto collateralDamage = damageCache.getDamage(attacker.get(), unitToUpdate.get(), hb); - auto collateralDamageReduce = AttackPossibility::calculateDamageReduce(attacker.get(), unitToUpdate.get(), collateralDamage, damageCache, hb); - - attackValue -= collateralDamageReduce; - dpsScore.ourDamageReduce += collateralDamageReduce; - - unitToUpdate->damage(collateralDamage); - #if BATTLE_TRACE_LEVEL>=1 logAi->trace( - "%s -> %s, ap collateral, %s, dps: %2f, score: %2f", - attacker->getDescription(), + "%s, ap collateral, dps: %lld", unitToUpdate->getDescription(), - ap.attack.shooting ? "shot" : "mellee", - collateralDamage, - collateralDamageReduce); + damageDealt); #endif } } else { - int64_t attackDamage = damageCache.getDamage(attacker.get(), unitToUpdate.get(), hb); - float defenderDamageReduce = AttackPossibility::calculateDamageReduce(attacker.get(), unitToUpdate.get(), attackDamage, damageCache, hb); - - attackValue += defenderDamageReduce; - dpsScore.enemyDamageReduce += defenderDamageReduce; - attackerValue[attacker->unitId()].value += defenderDamageReduce; - - unitToUpdate->damage(attackDamage); + if(unitToUpdate->unitId() == ap.attack.defender->unitId()) + { + if(unitToUpdate->ableToRetaliate() && !affectedUnit->ableToRetaliate()) + { + unitToUpdate->afterAttack(ap.attack.shooting, true); + } #if BATTLE_TRACE_LEVEL>=1 - logAi->trace( - "%s -> %s, ap attack, %s, dps: %2f, score: %2f", - attacker->getDescription(), - unitToUpdate->getDescription(), - ap.attack.shooting ? "shot" : "mellee", - attackDamage, - defenderDamageReduce); + logAi->trace( + "%s -> %s, ap attack, %s, dps: %lld", + attacker->getDescription(), + ap.attack.defender->getDescription(), + ap.attack.shooting ? "shot" : "mellee", + damageDealt); #endif + } + else + { +#if BATTLE_TRACE_LEVEL>=1 + logAi->trace( + "%s, ap enemy collateral, dps: %lld", + unitToUpdate->getDescription(), + damageDealt); +#endif + } } } #if BATTLE_TRACE_LEVEL >= 1 - logAi->trace("ap shooters blocking: %lld", ap.shootersBlockedDmg); + logAi->trace( + "ap score: our: %2f, enemy: %2f, collateral: %2f, blocked: %2f", + ap.attackerDamageReduce, + ap.defenderDamageReduce, + ap.collateralDamageReduce, + ap.shootersBlockedDmg); #endif - attackValue += ap.shootersBlockedDmg; - dpsScore.enemyDamageReduce += ap.shootersBlockedDmg; - attacker->afterAttack(ap.attack.shooting, false); - return attackValue; } @@ -185,7 +181,7 @@ float BattleExchangeVariant::trackAttack( if(isOurAttack) { dpsScore.ourDamageReduce += attackerDamageReduce; - attackerValue[attacker->unitId()].isRetalitated = true; + attackerValue[attacker->unitId()].isRetaliated = true; } else { @@ -218,7 +214,8 @@ EvaluationResult BattleExchangeEvaluator::findBestTarget( const battle::Unit * activeStack, PotentialTargets & targets, DamageCache & damageCache, - std::shared_ptr hb) + std::shared_ptr hb, + bool siegeDefense) { EvaluationResult result(targets.bestAction()); @@ -230,13 +227,15 @@ EvaluationResult BattleExchangeEvaluator::findBestTarget( auto hbWaited = std::make_shared(env.get(), hb); - hbWaited->getForUpdate(activeStack->unitId())->waiting = true; - hbWaited->getForUpdate(activeStack->unitId())->waitedThisTurn = true; + hbWaited->makeWait(activeStack); updateReachabilityMap(hbWaited); for(auto & ap : targets.possibleAttacks) { + if (siegeDefense && !hb->battleIsInsideWalls(ap.from)) + continue; + float score = evaluateExchange(ap, 0, targets, damageCache, hbWaited); if(score > result.score) @@ -259,6 +258,7 @@ EvaluationResult BattleExchangeEvaluator::findBestTarget( updateReachabilityMap(hb); if(result.bestAttack.attack.shooting + && !result.bestAttack.defenderDead && !activeStack->waited() && hb->battleHasShootingPenalty(activeStack, result.bestAttack.dest)) { @@ -268,9 +268,13 @@ EvaluationResult BattleExchangeEvaluator::findBestTarget( for(auto & ap : targets.possibleAttacks) { - float score = evaluateExchange(ap, 0, targets, damageCache, hb); + if (siegeDefense && !hb->battleIsInsideWalls(ap.from)) + continue; - if(score > result.score || (vstd::isAlmostEqual(score, result.score) && result.wait)) + float score = evaluateExchange(ap, 0, targets, damageCache, hb); + bool sameScoreButWaited = vstd::isAlmostEqual(score, result.score) && result.wait; + + if(score > result.score || sameScoreButWaited) { result.score = score; result.bestAttack = ap; @@ -285,6 +289,36 @@ EvaluationResult BattleExchangeEvaluator::findBestTarget( return result; } +ReachabilityInfo getReachabilityWithEnemyBypass( + const battle::Unit * activeStack, + DamageCache & damageCache, + std::shared_ptr state) +{ + ReachabilityInfo::Parameters params(activeStack, activeStack->getPosition()); + + if(!params.flying) + { + for(const auto * unit : state->battleAliveUnits()) + { + if(unit->unitSide() == activeStack->unitSide()) + continue; + + auto dmg = damageCache.getOriginalDamage(activeStack, unit, state); + auto turnsToKill = unit->getAvailableHealth() / std::max(dmg, (int64_t)1); + + vstd::amin(turnsToKill, 100); + + for(auto & hex : unit->getHexes()) + if(hex.isAvailable()) //towers can have <0 pos; we don't also want to overwrite side columns + params.destructibleEnemyTurns[hex] = turnsToKill * unit->getMovementRange(); + } + + params.bypassEnemyStacks = true; + } + + return state->getReachability(params); +} + MoveTarget BattleExchangeEvaluator::findMoveTowardsUnreachable( const battle::Unit * activeStack, PotentialTargets & targets, @@ -294,6 +328,8 @@ MoveTarget BattleExchangeEvaluator::findMoveTowardsUnreachable( MoveTarget result; BattleExchangeVariant ev; + logAi->trace("Find move towards unreachable. Enemies count %d", targets.unreachableEnemies.size()); + if(targets.unreachableEnemies.empty()) return result; @@ -304,17 +340,17 @@ MoveTarget BattleExchangeEvaluator::findMoveTowardsUnreachable( updateReachabilityMap(hb); - auto dists = cb->getReachability(activeStack); + auto dists = getReachabilityWithEnemyBypass(activeStack, damageCache, hb); + auto flying = activeStack->hasBonusOfType(BonusType::FLYING); for(const battle::Unit * enemy : targets.unreachableEnemies) { - std::vector adjacentStacks = getAdjacentUnits(enemy); - auto closestStack = *vstd::minElementByFun(adjacentStacks, [&](const battle::Unit * u) -> int64_t - { - return dists.distToNearestNeighbour(activeStack, u) * 100000 - activeStack->getTotalHealth(); - }); + logAi->trace( + "Checking movement towards %d of %s", + enemy->getCount(), + enemy->creatureId().toCreature()->getNameSingularTranslated()); - auto distance = dists.distToNearestNeighbour(activeStack, closestStack); + auto distance = dists.distToNearestNeighbour(activeStack, enemy); if(distance >= GameConstants::BFIELD_SIZE) continue; @@ -322,31 +358,109 @@ MoveTarget BattleExchangeEvaluator::findMoveTowardsUnreachable( if(distance <= speed) continue; + float penaltyMultiplier = 1.0f; // Default multiplier, no penalty + float closestAllyDistance = std::numeric_limits::max(); + + for (const battle::Unit* ally : hb->battleAliveUnits()) { + if (ally == activeStack) + continue; + if (ally->unitSide() != activeStack->unitSide()) + continue; + + float allyDistance = dists.distToNearestNeighbour(ally, enemy); + if (allyDistance < closestAllyDistance) + { + closestAllyDistance = allyDistance; + } + } + + // If an ally is closer to the enemy, compute the penaltyMultiplier + if (closestAllyDistance < distance) { + penaltyMultiplier = closestAllyDistance / distance; // Ratio of distances + } + auto turnsToRich = (distance - 1) / speed + 1; - auto hexes = closestStack->getSurroundingHexes(); - auto enemySpeed = closestStack->getMovementRange(); + auto hexes = enemy->getSurroundingHexes(); + auto enemySpeed = enemy->getMovementRange(); auto speedRatio = speed / static_cast(enemySpeed); - auto multiplier = speedRatio > 1 ? 1 : speedRatio; + auto multiplier = (speedRatio > 1 ? 1 : speedRatio) * penaltyMultiplier; - if(enemy->canShoot()) - multiplier *= 1.5f; - - for(auto hex : hexes) + for(auto & hex : hexes) { // FIXME: provide distance info for Jousting bonus - auto bai = BattleAttackInfo(activeStack, closestStack, 0, cb->battleCanShoot(activeStack)); + auto bai = BattleAttackInfo(activeStack, enemy, 0, cb->battleCanShoot(activeStack)); auto attack = AttackPossibility::evaluate(bai, hex, damageCache, hb); attack.shootersBlockedDmg = 0; // we do not want to count on it, it is not for sure auto score = calculateExchange(attack, turnsToRich, targets, damageCache, hb); - auto scorePerTurn = BattleScore(score.enemyDamageReduce * std::sqrt(multiplier / turnsToRich), score.ourDamageReduce); - if(result.scorePerTurn < scoreValue(scorePerTurn)) + score.enemyDamageReduce *= multiplier; + +#if BATTLE_TRACE_LEVEL >= 1 + logAi->trace("Multiplier: %f, turns: %d, current score %f, new score %f", multiplier, turnsToRich, result.score, scoreValue(score)); +#endif + + if(result.score < scoreValue(score) + || (result.turnsToRich > turnsToRich && vstd::isAlmostEqual(result.score, scoreValue(score)))) { - result.scorePerTurn = scoreValue(scorePerTurn); result.score = scoreValue(score); - result.positions = closestStack->getAttackableHexes(activeStack); + result.positions.clear(); + +#if BATTLE_TRACE_LEVEL >= 1 + logAi->trace("New high score"); +#endif + + for(const BattleHex & initialEnemyHex : enemy->getAttackableHexes(activeStack)) + { + BattleHex enemyHex = initialEnemyHex; + + while(!flying && dists.distances[enemyHex] > speed && dists.predecessors.at(enemyHex).isValid()) + { + enemyHex = dists.predecessors.at(enemyHex); + + if(dists.accessibility[enemyHex] == EAccessibility::ALIVE_STACK) + { + auto defenderToBypass = hb->battleGetUnitByPos(enemyHex); + + if(defenderToBypass) + { +#if BATTLE_TRACE_LEVEL >= 1 + logAi->trace("Found target to bypass at %d", enemyHex.hex); +#endif + + auto attackHex = dists.predecessors[enemyHex]; + auto baiBypass = BattleAttackInfo(activeStack, defenderToBypass, 0, cb->battleCanShoot(activeStack)); + auto attackBypass = AttackPossibility::evaluate(baiBypass, attackHex, damageCache, hb); + + auto adjacentStacks = getAdjacentUnits(enemy); + + adjacentStacks.push_back(defenderToBypass); + vstd::removeDuplicates(adjacentStacks); + + auto bypassScore = calculateExchange( + attackBypass, + dists.distances[attackHex], + targets, + damageCache, + hb, + adjacentStacks); + + if(scoreValue(bypassScore) > result.score) + { + result.score = scoreValue(bypassScore); + +#if BATTLE_TRACE_LEVEL >= 1 + logAi->trace("New high score after bypass %f", scoreValue(bypassScore)); +#endif + } + } + } + } + + result.positions.push_back(enemyHex); + } + result.cachedAttack = attack; result.turnsToRich = turnsToRich; } @@ -390,7 +504,8 @@ ReachabilityData BattleExchangeEvaluator::getExchangeUnits( const AttackPossibility & ap, uint8_t turn, PotentialTargets & targets, - std::shared_ptr hb) const + std::shared_ptr hb, + std::vector additionalUnits) const { ReachabilityData result; @@ -398,13 +513,29 @@ ReachabilityData BattleExchangeEvaluator::getExchangeUnits( if(!ap.attack.shooting) hexes.push_back(ap.from); - std::vector allReachableUnits; - + std::vector allReachableUnits = additionalUnits; + for(auto hex : hexes) { vstd::concatenate(allReachableUnits, turn == 0 ? reachabilityMap.at(hex) : getOneTurnReachableUnits(turn, hex)); } + if(!ap.attack.attacker->isTurret()) + { + for(auto hex : ap.attack.attacker->getHexes()) + { + auto unitsReachingAttacker = turn == 0 ? reachabilityMap.at(hex) : getOneTurnReachableUnits(turn, hex); + for(auto unit : unitsReachingAttacker) + { + if(unit->unitSide() != ap.attack.attacker->unitSide()) + { + allReachableUnits.push_back(unit); + result.enemyUnitsReachingAttacker.insert(unit->unitId()); + } + } + } + } + vstd::removeDuplicates(allReachableUnits); auto copy = allReachableUnits; @@ -440,7 +571,7 @@ ReachabilityData BattleExchangeEvaluator::getExchangeUnits( for(auto unit : allReachableUnits) { - auto accessible = !unit->canShoot(); + auto accessible = !unit->canShoot() || vstd::contains(additionalUnits, unit); if(!accessible) { @@ -464,14 +595,14 @@ ReachabilityData BattleExchangeEvaluator::getExchangeUnits( for(auto unit : turnOrder[turn]) { if(vstd::contains(allReachableUnits, unit)) - result.units.push_back(unit); + result.units[turn].push_back(unit); } - } - vstd::erase_if(result.units, [&](const battle::Unit * u) -> bool - { - return !hb->battleGetUnitByID(u->unitId())->alive(); - }); + vstd::erase_if(result.units[turn], [&](const battle::Unit * u) -> bool + { + return !hb->battleGetUnitByID(u->unitId())->alive(); + }); + } return result; } @@ -502,13 +633,14 @@ BattleScore BattleExchangeEvaluator::calculateExchange( uint8_t turn, PotentialTargets & targets, DamageCache & damageCache, - std::shared_ptr hb) const + std::shared_ptr hb, + std::vector additionalUnits) const { #if BATTLE_TRACE_LEVEL>=1 logAi->trace("Battle exchange at %d", ap.attack.shooting ? ap.dest.hex : ap.from.hex); #endif - if(cb->battleGetMySide() == BattlePerspective::LEFT_SIDE + if(cb->battleGetMySide() == BattleSide::LEFT_SIDE && cb->battleGetGateState() == EGateState::BLOCKED && ap.attack.defender->coversPos(BattleHex::GATE_BRIDGE)) { @@ -521,7 +653,7 @@ BattleScore BattleExchangeEvaluator::calculateExchange( if(hb->battleGetUnitByID(ap.attack.defender->unitId())->alive()) enemyStacks.push_back(ap.attack.defender); - ReachabilityData exchangeUnits = getExchangeUnits(ap, turn, targets, hb); + ReachabilityData exchangeUnits = getExchangeUnits(ap, turn, targets, hb, additionalUnits); if(exchangeUnits.units.empty()) { @@ -531,22 +663,25 @@ BattleScore BattleExchangeEvaluator::calculateExchange( auto exchangeBattle = std::make_shared(env.get(), hb); BattleExchangeVariant v; - for(auto unit : exchangeUnits.units) + for(int exchangeTurn = 0; exchangeTurn < exchangeUnits.units.size(); exchangeTurn++) { - if(unit->isTurret()) - continue; - - bool isOur = exchangeBattle->battleMatchOwner(ap.attack.attacker, unit, true); - auto & attackerQueue = isOur ? ourStacks : enemyStacks; - auto u = exchangeBattle->getForUpdate(unit->unitId()); - - if(u->alive() && !vstd::contains(attackerQueue, unit)) + for(auto unit : exchangeUnits.units.at(exchangeTurn)) { - attackerQueue.push_back(unit); + if(unit->isTurret()) + continue; + + bool isOur = exchangeBattle->battleMatchOwner(ap.attack.attacker, unit, true); + auto & attackerQueue = isOur ? ourStacks : enemyStacks; + auto u = exchangeBattle->getForUpdate(unit->unitId()); + + if(u->alive() && !vstd::contains(attackerQueue, unit)) + { + attackerQueue.push_back(unit); #if BATTLE_TRACE_LEVEL - logAi->trace("Exchanging: %s", u->getDescription()); + logAi->trace("Exchanging: %s", u->getDescription()); #endif + } } } @@ -560,122 +695,166 @@ BattleScore BattleExchangeEvaluator::calculateExchange( bool canUseAp = true; - for(auto activeUnit : exchangeUnits.units) + std::set blockedShooters; + + int totalTurnsCount = simulationTurnsCount >= turn + turnOrder.size() + ? simulationTurnsCount + : turn + turnOrder.size(); + + for(int exchangeTurn = 0; exchangeTurn < simulationTurnsCount; exchangeTurn++) { - bool isOur = exchangeBattle->battleMatchOwner(ap.attack.attacker, activeUnit, true); - battle::Units & attackerQueue = isOur ? ourStacks : enemyStacks; - battle::Units & oppositeQueue = isOur ? enemyStacks : ourStacks; + bool isMovingTurm = exchangeTurn < turn; + int queueTurn = exchangeTurn >= exchangeUnits.units.size() + ? exchangeUnits.units.size() - 1 + : exchangeTurn; - auto attacker = exchangeBattle->getForUpdate(activeUnit->unitId()); - - if(!attacker->alive()) + for(auto activeUnit : exchangeUnits.units.at(queueTurn)) { + bool isOur = exchangeBattle->battleMatchOwner(ap.attack.attacker, activeUnit, true); + battle::Units & attackerQueue = isOur ? ourStacks : enemyStacks; + battle::Units & oppositeQueue = isOur ? enemyStacks : ourStacks; + + auto attacker = exchangeBattle->getForUpdate(activeUnit->unitId()); + auto shooting = exchangeBattle->battleCanShoot(attacker.get()) + && !vstd::contains(blockedShooters, attacker->unitId()); + + if(!attacker->alive()) + { #if BATTLE_TRACE_LEVEL>=1 - logAi->trace( "Attacker is dead"); + logAi->trace("Attacker is dead"); #endif - continue; - } - - auto targetUnit = ap.attack.defender; - - if(!isOur || !exchangeBattle->battleGetUnitByID(targetUnit->unitId())->alive()) - { - auto estimateAttack = [&](const battle::Unit * u) -> float - { - auto stackWithBonuses = exchangeBattle->getForUpdate(u->unitId()); - auto score = v.trackAttack( - attacker, - stackWithBonuses, - exchangeBattle->battleCanShoot(stackWithBonuses.get()), - isOur, - damageCache, - hb, - true); - -#if BATTLE_TRACE_LEVEL>=1 - logAi->trace("Best target selector %s->%s score = %2f", attacker->getDescription(), stackWithBonuses->getDescription(), score); -#endif - - return score; - }; - - auto unitsInOppositeQueueExceptInaccessible = oppositeQueue; - - vstd::erase_if(unitsInOppositeQueueExceptInaccessible, [&](const battle::Unit * u)->bool - { - return vstd::contains(exchangeUnits.shooters, u); - }); - - if(!unitsInOppositeQueueExceptInaccessible.empty()) - { - targetUnit = *vstd::maxElementByFun(unitsInOppositeQueueExceptInaccessible, estimateAttack); + continue; } - else + + if(isMovingTurm && !shooting + && !vstd::contains(exchangeUnits.enemyUnitsReachingAttacker, attacker->unitId())) { - auto reachable = exchangeBattle->battleGetUnitsIf([this, &exchangeBattle, &attacker](const battle::Unit * u) -> bool +#if BATTLE_TRACE_LEVEL>=1 + logAi->trace("Attacker is moving"); +#endif + + continue; + } + + auto targetUnit = ap.attack.defender; + + if(!isOur || !exchangeBattle->battleGetUnitByID(targetUnit->unitId())->alive()) + { +#if BATTLE_TRACE_LEVEL>=2 + logAi->trace("Best target selector for %s", attacker->getDescription()); +#endif + auto estimateAttack = [&](const battle::Unit * u) -> float + { + auto stackWithBonuses = exchangeBattle->getForUpdate(u->unitId()); + auto score = v.trackAttack( + attacker, + stackWithBonuses, + exchangeBattle->battleCanShoot(stackWithBonuses.get()), + isOur, + damageCache, + hb, + true); + +#if BATTLE_TRACE_LEVEL>=2 + logAi->trace("Best target selector %s->%s score = %2f", attacker->getDescription(), stackWithBonuses->getDescription(), score); +#endif + + return score; + }; + + auto unitsInOppositeQueueExceptInaccessible = oppositeQueue; + + vstd::erase_if(unitsInOppositeQueueExceptInaccessible, [&](const battle::Unit * u)->bool { - if(u->unitSide() == attacker->unitSide()) - return false; - - if(!exchangeBattle->getForUpdate(u->unitId())->alive()) - return false; - - if (!u->getPosition().isValid()) - return false; // e.g. tower shooters - - return vstd::contains_if(reachabilityMap.at(u->getPosition()), [&attacker](const battle::Unit * other) -> bool - { - return attacker->unitId() == other->unitId(); - }); + return vstd::contains(exchangeUnits.shooters, u); }); - if(!reachable.empty()) + if(!isOur + && exchangeTurn == 0 + && exchangeUnits.units.at(exchangeTurn).at(0)->unitId() != ap.attack.attacker->unitId() + && !vstd::contains(exchangeUnits.enemyUnitsReachingAttacker, attacker->unitId())) { - targetUnit = *vstd::maxElementByFun(reachable, estimateAttack); + vstd::erase_if(unitsInOppositeQueueExceptInaccessible, [&](const battle::Unit * u) -> bool + { + return u->unitId() == ap.attack.attacker->unitId(); + }); + } + + if(!unitsInOppositeQueueExceptInaccessible.empty()) + { + targetUnit = *vstd::maxElementByFun(unitsInOppositeQueueExceptInaccessible, estimateAttack); } else { + auto reachable = exchangeBattle->battleGetUnitsIf([this, &exchangeBattle, &attacker](const battle::Unit * u) -> bool + { + if(u->unitSide() == attacker->unitSide()) + return false; + + if(!exchangeBattle->getForUpdate(u->unitId())->alive()) + return false; + + if(!u->getPosition().isValid()) + return false; // e.g. tower shooters + + return vstd::contains_if(reachabilityMap.at(u->getPosition()), [&attacker](const battle::Unit * other) -> bool + { + return attacker->unitId() == other->unitId(); + }); + }); + + if(!reachable.empty()) + { + targetUnit = *vstd::maxElementByFun(reachable, estimateAttack); + } + else + { #if BATTLE_TRACE_LEVEL>=1 - logAi->trace("Battle queue is empty and no reachable enemy."); + logAi->trace("Battle queue is empty and no reachable enemy."); #endif - continue; + continue; + } } } - } - auto defender = exchangeBattle->getForUpdate(targetUnit->unitId()); - auto shooting = exchangeBattle->battleCanShoot(attacker.get()); - const int totalAttacks = attacker->getTotalAttacks(shooting); + auto defender = exchangeBattle->getForUpdate(targetUnit->unitId()); + const int totalAttacks = attacker->getTotalAttacks(shooting); - if(canUseAp && activeUnit->unitId() == ap.attack.attacker->unitId() - && targetUnit->unitId() == ap.attack.defender->unitId()) - { - v.trackAttack(ap, exchangeBattle, damageCache); - } - else - { - for(int i = 0; i < totalAttacks; i++) + if(canUseAp && activeUnit->unitId() == ap.attack.attacker->unitId() + && targetUnit->unitId() == ap.attack.defender->unitId()) { - v.trackAttack(attacker, defender, shooting, isOur, damageCache, exchangeBattle); - - if(!attacker->alive() || !defender->alive()) - break; + v.trackAttack(ap, exchangeBattle, damageCache); } + else + { + for(int i = 0; i < totalAttacks; i++) + { + v.trackAttack(attacker, defender, shooting, isOur, damageCache, exchangeBattle); + + if(!attacker->alive() || !defender->alive()) + break; + } + } + + if(!shooting) + blockedShooters.insert(defender->unitId()); + + canUseAp = false; + + vstd::erase_if(attackerQueue, [&](const battle::Unit * u) -> bool + { + return !exchangeBattle->battleGetUnitByID(u->unitId())->alive(); + }); + + vstd::erase_if(oppositeQueue, [&](const battle::Unit * u) -> bool + { + return !exchangeBattle->battleGetUnitByID(u->unitId())->alive(); + }); } - canUseAp = false; - - vstd::erase_if(attackerQueue, [&](const battle::Unit * u) -> bool - { - return !exchangeBattle->battleGetUnitByID(u->unitId())->alive(); - }); - - vstd::erase_if(oppositeQueue, [&](const battle::Unit * u) -> bool - { - return !exchangeBattle->battleGetUnitByID(u->unitId())->alive(); - }); + exchangeBattle->nextRound(); } // avoid blocking path for stronger stack by weaker stack @@ -687,11 +866,28 @@ BattleScore BattleExchangeEvaluator::calculateExchange( for(auto hex : hexes) reachabilityMap[hex] = getOneTurnReachableUnits(turn, hex); + auto score = v.getScore(); + + if(simulationTurnsCount < totalTurnsCount) + { + float scalingRatio = simulationTurnsCount / static_cast(totalTurnsCount); + + score.enemyDamageReduce *= scalingRatio; + score.ourDamageReduce *= scalingRatio; + } + + if(turn > 0) + { + auto turnMultiplier = 1 - std::min(0.2, 0.05 * turn); + + score.enemyDamageReduce *= turnMultiplier; + } + #if BATTLE_TRACE_LEVEL>=1 - logAi->trace("Exchange score: enemy: %2f, our -%2f", v.getScore().enemyDamageReduce, v.getScore().ourDamageReduce); + logAi->trace("Exchange score: enemy: %2f, our -%2f", score.enemyDamageReduce, score.ourDamageReduce); #endif - return v.getScore(); + return score; } bool BattleExchangeEvaluator::canBeHitThisTurn(const AttackPossibility & ap) diff --git a/AI/BattleAI/BattleExchangeVariant.h b/AI/BattleAI/BattleExchangeVariant.h index 610e47166..dbbbaab3d 100644 --- a/AI/BattleAI/BattleExchangeVariant.h +++ b/AI/BattleAI/BattleExchangeVariant.h @@ -45,7 +45,7 @@ struct BattleScore struct AttackerValue { float value; - bool isRetalitated; + bool isRetaliated; BattleHex position; AttackerValue(); @@ -54,7 +54,6 @@ struct AttackerValue struct MoveTarget { float score; - float scorePerTurn; std::vector positions; std::optional cachedAttack; uint8_t turnsToRich; @@ -64,7 +63,7 @@ struct MoveTarget struct EvaluationResult { - static const int64_t INEFFECTIVE_SCORE = -10000; + static const int64_t INEFFECTIVE_SCORE = -100000000; AttackPossibility bestAttack; MoveTarget bestMove; @@ -113,13 +112,15 @@ private: struct ReachabilityData { - std::vector units; + std::map> units; // shooters which are within mellee attack and mellee units std::vector melleeAccessible; // far shooters std::vector shooters; + + std::set enemyUnitsReachingAttacker; }; class BattleExchangeEvaluator @@ -131,6 +132,7 @@ private: std::map> reachabilityMap; std::vector turnOrder; float negativeEffectMultiplier; + int simulationTurnsCount; float scoreValue(const BattleScore & score) const; @@ -139,7 +141,8 @@ private: uint8_t turn, PotentialTargets & targets, DamageCache & damageCache, - std::shared_ptr hb) const; + std::shared_ptr hb, + std::vector additionalUnits = {}) const; bool canBeHitThisTurn(const AttackPossibility & ap); @@ -147,15 +150,17 @@ public: BattleExchangeEvaluator( std::shared_ptr cb, std::shared_ptr env, - float strengthRatio): cb(cb), env(env) { - negativeEffectMultiplier = strengthRatio >= 1 ? 1 : strengthRatio; + float strengthRatio, + int simulationTurnsCount): cb(cb), env(env), simulationTurnsCount(simulationTurnsCount){ + negativeEffectMultiplier = strengthRatio >= 1 ? 1 : strengthRatio * strengthRatio; } EvaluationResult findBestTarget( const battle::Unit * activeStack, PotentialTargets & targets, DamageCache & damageCache, - std::shared_ptr hb); + std::shared_ptr hb, + bool siegeDefense = false); float evaluateExchange( const AttackPossibility & ap, @@ -171,7 +176,8 @@ public: const AttackPossibility & ap, uint8_t turn, PotentialTargets & targets, - std::shared_ptr hb) const; + std::shared_ptr hb, + std::vector additionalUnits = {}) const; bool checkPositionBlocksOurStacks(HypotheticBattle & hb, const battle::Unit * unit, BattleHex position); diff --git a/AI/BattleAI/CMakeLists.txt b/AI/BattleAI/CMakeLists.txt index b30be26ce..0540be017 100644 --- a/AI/BattleAI/CMakeLists.txt +++ b/AI/BattleAI/CMakeLists.txt @@ -37,11 +37,7 @@ else() endif() target_include_directories(BattleAI PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) -target_link_libraries(BattleAI PRIVATE vcmi TBB::tbb) +target_link_libraries(BattleAI PRIVATE vcmi) vcmi_set_output_dir(BattleAI "AI") enable_pch(BattleAI) - -if(APPLE_IOS AND NOT USING_CONAN) - install(IMPORTED_RUNTIME_ARTIFACTS TBB::tbb LIBRARY DESTINATION ${LIB_DIR}) # CMake 3.21+ -endif() diff --git a/AI/BattleAI/PotentialTargets.cpp b/AI/BattleAI/PotentialTargets.cpp index a341921e6..f38415ef7 100644 --- a/AI/BattleAI/PotentialTargets.cpp +++ b/AI/BattleAI/PotentialTargets.cpp @@ -10,6 +10,7 @@ #include "StdInc.h" #include "PotentialTargets.h" #include "../../lib/CStack.h"//todo: remove +#include "../../lib/mapObjects/CGTownInstance.h" PotentialTargets::PotentialTargets( const battle::Unit * attacker, diff --git a/AI/BattleAI/StackWithBonuses.cpp b/AI/BattleAI/StackWithBonuses.cpp index 1f6711612..149eb33f5 100644 --- a/AI/BattleAI/StackWithBonuses.cpp +++ b/AI/BattleAI/StackWithBonuses.cpp @@ -12,6 +12,7 @@ #include +#include "../../lib/battle/BattleLayout.h" #include "../../lib/CStack.h" #include "../../lib/ScriptHandler.h" #include "../../lib/networkPacks/PacksForClientBattle.h" @@ -116,7 +117,7 @@ uint32_t StackWithBonuses::unitId() const return id; } -ui8 StackWithBonuses::unitSide() const +BattleSide StackWithBonuses::unitSide() const { return side; } @@ -132,10 +133,10 @@ SlotID StackWithBonuses::unitSlot() const } TConstBonusListPtr StackWithBonuses::getAllBonuses(const CSelector & selector, const CSelector & limit, - const CBonusSystemNode * root, const std::string & cachingStr) const + const std::string & cachingStr) const { auto ret = std::make_shared(); - TConstBonusListPtr originalList = origBearer->getAllBonuses(selector, limit, root, cachingStr); + TConstBonusListPtr originalList = origBearer->getAllBonuses(selector, limit, cachingStr); vstd::copy_if(*originalList, std::back_inserter(*ret), [this](const std::shared_ptr & b) { @@ -467,7 +468,7 @@ int64_t HypotheticBattle::getActualDamage(const DamageRange & damage, int32_t at return (damage.min + damage.max) / 2; } -std::vector HypotheticBattle::getUsedSpells(ui8 side) const +std::vector HypotheticBattle::getUsedSpells(BattleSide side) const { // TODO return {}; @@ -479,10 +480,9 @@ int3 HypotheticBattle::getLocation() const return int3(-1, -1, -1); } -bool HypotheticBattle::isCreatureBank() const +BattleLayout HypotheticBattle::getLayout() const { - // TODO - return false; + return subject->getBattle()->getLayout(); } int64_t HypotheticBattle::getTreeVersion() const @@ -502,10 +502,18 @@ ServerCallback * HypotheticBattle::getServerCallback() return serverCallback.get(); } +void HypotheticBattle::makeWait(const battle::Unit * activeStack) +{ + auto unit = getForUpdate(activeStack->unitId()); + + resetActiveUnit(); + unit->waiting = true; + unit->waitedThisTurn = true; +} + HypotheticBattle::HypotheticServerCallback::HypotheticServerCallback(HypotheticBattle * owner_) :owner(owner_) { - } void HypotheticBattle::HypotheticServerCallback::complain(const std::string & problem) @@ -523,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 6a50f6f84..3a34cc761 100644 --- a/AI/BattleAI/StackWithBonuses.h +++ b/AI/BattleAI/StackWithBonuses.h @@ -21,23 +21,43 @@ class HypotheticBattle; ///Fake random generator, used by AI to evaluate random server behavior -class RNGStub : public vstd::RNG +class RNGStub final : public vstd::RNG { public: - vstd::TRandI64 getInt64Range(int64_t lower, int64_t upper) override + int nextInt() override { - return [=]()->int64_t - { - return (lower + upper)/2; - }; + return 0; } - vstd::TRand getDoubleRange(double lower, double upper) override + int nextBinomialInt(int coinsCount, double coinChance) override { - return [=]()->double - { - return (lower + upper)/2; - }; + return coinsCount * coinChance; + } + + int nextInt(int lower, int upper) override + { + return (lower + upper) / 2; + } + int64_t nextInt64(int64_t lower, int64_t upper) override + { + return (lower + upper) / 2; + } + double nextDouble(double lower, double upper) override + { + return (lower + upper) / 2; + } + + int nextInt(int upper) override + { + return upper / 2; + } + int64_t nextInt64(int64_t upper) override + { + return upper / 2; + } + double nextDouble(double upper) override + { + return upper / 2; } }; @@ -65,13 +85,13 @@ public: int32_t unitBaseAmount() const override; uint32_t unitId() const override; - ui8 unitSide() const override; + BattleSide unitSide() const override; PlayerColor unitOwner() const override; SlotID unitSlot() const override; ///IBonusBearer TConstBonusListPtr getAllBonuses(const CSelector & selector, const CSelector & limit, - const CBonusSystemNode * root = nullptr, const std::string & cachingStr = "") const override; + const std::string & cachingStr = "") const override; int64_t getTreeVersion() const override; @@ -91,7 +111,7 @@ private: const CCreature * type; ui32 baseAmount; uint32_t id; - ui8 side; + BattleSide side; PlayerColor player; SlotID slot; }; @@ -138,12 +158,19 @@ public: uint32_t nextUnitId() const override; int64_t getActualDamage(const DamageRange & damage, int32_t attackerCount, vstd::RNG & rng) const override; - std::vector getUsedSpells(ui8 side) const override; + std::vector getUsedSpells(BattleSide side) const override; int3 getLocation() const override; - bool isCreatureBank() const override; + BattleLayout getLayout() const override; int64_t getTreeVersion() const; + void makeWait(const battle::Unit * activeStack); + + void resetActiveUnit() + { + activeUnitId = -1; + } + #if SCRIPTING_ENABLED scripting::Pool * getContextPool() const override; #endif @@ -162,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/BattleAI/ThreatMap.cpp b/AI/BattleAI/ThreatMap.cpp index 5b3bab584..14cce4f56 100644 --- a/AI/BattleAI/ThreatMap.cpp +++ b/AI/BattleAI/ThreatMap.cpp @@ -70,4 +70,4 @@ ThreatMap::ThreatMap(const CStack *Endangered) : endangered(Endangered) }); } } -*/ // These lines may be usefull but they are't used in the code. +*/ // These lines may be useful but they are't used in the code. diff --git a/AI/BattleAI/ThreatMap.h b/AI/BattleAI/ThreatMap.h index 2d800a72c..a480b8004 100644 --- a/AI/BattleAI/ThreatMap.h +++ b/AI/BattleAI/ThreatMap.h @@ -22,4 +22,4 @@ public: std::array sufferedDamage; ThreatMap(const CStack *Endangered); -};*/ // These lines may be usefull but they are't used in the code. +};*/ // These lines may be useful but they are't used in the code. diff --git a/AI/CMakeLists.txt b/AI/CMakeLists.txt index 8326fef64..7231c65e5 100644 --- a/AI/CMakeLists.txt +++ b/AI/CMakeLists.txt @@ -8,10 +8,6 @@ else() option(FORCE_BUNDLED_FL "Force to use FuzzyLite included into VCMI's source tree" OFF) endif() -if(TBB_FOUND AND MSVC) - install_vcpkg_imported_tgt(TBB::tbb) -endif() - #FuzzyLite uses MSVC pragmas in headers, so, we need to disable -Wunknown-pragmas if(MINGW) add_compile_options(-Wno-unknown-pragmas) diff --git a/AI/EmptyAI/CEmptyAI.cpp b/AI/EmptyAI/CEmptyAI.cpp index 4291663e7..92a98ca38 100644 --- a/AI/EmptyAI/CEmptyAI.cpp +++ b/AI/EmptyAI/CEmptyAI.cpp @@ -14,14 +14,6 @@ #include "../../lib/CStack.h" #include "../../lib/battle/BattleAction.h" -void CEmptyAI::saveGame(BinarySerializer & h) -{ -} - -void CEmptyAI::loadGame(BinaryDeserializer & h) -{ -} - void CEmptyAI::initGameInterface(std::shared_ptr ENV, std::shared_ptr CB) { cb = CB; diff --git a/AI/EmptyAI/CEmptyAI.h b/AI/EmptyAI/CEmptyAI.h index 78b0353f4..e465c2409 100644 --- a/AI/EmptyAI/CEmptyAI.h +++ b/AI/EmptyAI/CEmptyAI.h @@ -19,9 +19,6 @@ class CEmptyAI : public CGlobalAI std::shared_ptr cb; public: - void saveGame(BinarySerializer & h) override; - void loadGame(BinaryDeserializer & h) override; - void initGameInterface(std::shared_ptr ENV, std::shared_ptr CB) override; void yourTurn(QueryID queryID) override; void yourTacticPhase(const BattleID & battleID, int distance) override; diff --git a/AI/EmptyAI/EmptyAI.cbp b/AI/EmptyAI/EmptyAI.cbp deleted file mode 100644 index 304884fd5..000000000 --- a/AI/EmptyAI/EmptyAI.cbp +++ /dev/null @@ -1,86 +0,0 @@ - - - - - - diff --git a/AI/EmptyAI/EmptyAI.vcxproj b/AI/EmptyAI/EmptyAI.vcxproj deleted file mode 100644 index 42204cccd..000000000 --- a/AI/EmptyAI/EmptyAI.vcxproj +++ /dev/null @@ -1,181 +0,0 @@ - - - - - Debug - Win32 - - - Debug - x64 - - - RD - Win32 - - - RD - x64 - - - - - - - Create - Create - Create - Create - - - - - - - - {C41C4EB6-6F74-4F37-9FB0-9FA6BF377837} - EmptyAI - 10.0 - - - - DynamicLibrary - true - MultiByte - v140_xp - - - DynamicLibrary - true - MultiByte - v140_xp - - - DynamicLibrary - false - true - MultiByte - v142 - - - DynamicLibrary - false - true - MultiByte - v140_xp - - - - - - - - - - - - - - - - - - - - - - - - - - - $(VCMI_Out)\AI\ - $(IncludePath) - $(LibraryPath) - - - $(VCMI_Out)\AI\ - - - $(VCMI_Out)/AI - - - $(VCMI_Out)\AI\ - - - - Level3 - Disabled - %(AdditionalIncludeDirectories) - Use - StdInc.h - %(PreprocessorDefinitions) - /Zm130 %(AdditionalOptions) - - - true - VCMI_lib.lib;%(AdditionalDependencies) - $(OutDir)EmptyAI.dll - ..\..\..\libs;..\.. - - - - - Level3 - Disabled - %(AdditionalIncludeDirectories) - Use - StdInc.h - _WINDLL;%(PreprocessorDefinitions) - - - true - VCMI_lib.lib;%(AdditionalDependencies) - $(OutDir)EmptyAI.dll - - - - - Level3 - MaxSpeed - true - true - %(AdditionalIncludeDirectories) - Use - StdInc.h - _WINDLL;%(PreprocessorDefinitions) - true - - - true - true - true - VCMI_lib.lib;%(AdditionalDependencies) - $(OutDir)EmptyAI.dll - $(VCMI_Out) - - - - - Level3 - MaxSpeed - true - true - %(AdditionalIncludeDirectories) - Use - StdInc.h - _WINDLL;%(PreprocessorDefinitions) - /Zm130 %(AdditionalOptions) - - - true - true - true - VCMI_lib.lib;%(AdditionalDependencies) - $(OutDir)EmptyAI.dll - - - - - - \ No newline at end of file diff --git a/AI/FuzzyLite.cbp b/AI/FuzzyLite.cbp deleted file mode 100644 index 44dd5fb51..000000000 --- a/AI/FuzzyLite.cbp +++ /dev/null @@ -1,285 +0,0 @@ - - - - - - diff --git a/AI/Nullkiller/AIGateway.cpp b/AI/Nullkiller/AIGateway.cpp index 10ea97081..ac23c2149 100644 --- a/AI/Nullkiller/AIGateway.cpp +++ b/AI/Nullkiller/AIGateway.cpp @@ -12,22 +12,21 @@ #include "../../lib/ArtifactUtils.h" #include "../../lib/UnlockGuard.h" #include "../../lib/StartInfo.h" +#include "../../lib/entities/building/CBuilding.h" #include "../../lib/mapObjects/MapObjects.h" #include "../../lib/mapObjects/ObjectTemplate.h" #include "../../lib/mapObjects/CGHeroInstance.h" #include "../../lib/CConfigHandler.h" -#include "../../lib/CHeroHandler.h" -#include "../../lib/GameSettings.h" +#include "../../lib/IGameSettings.h" #include "../../lib/gameState/CGameState.h" #include "../../lib/serializer/CTypeList.h" -#include "../../lib/serializer/BinarySerializer.h" -#include "../../lib/serializer/BinaryDeserializer.h" #include "../../lib/networkPacks/PacksForClient.h" #include "../../lib/networkPacks/PacksForClientBattle.h" #include "../../lib/networkPacks/PacksForServer.h" #include "../../lib/networkPacks/StackLocation.h" #include "../../lib/battle/BattleStateInfoForRetreat.h" #include "../../lib/battle/BattleInfo.h" +#include "../../lib/CPlayerState.h" #include "AIGateway.h" #include "Goals/Goals.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; @@ -287,6 +281,9 @@ void AIGateway::tileRevealed(const std::unordered_set & pos) for(const CGObjectInstance * obj : myCb->getVisitableObjs(tile)) addVisitableObj(obj); } + + if (nullkiller->settings->isUpdateHitmapOnTileReveal()) + nullkiller->dangerHitMap->reset(); } void AIGateway::heroExchangeStarted(ObjectInstanceID hero1, ObjectInstanceID hero2, QueryID query) @@ -500,7 +497,7 @@ void AIGateway::objectPropertyChanged(const SetObjectProperty * sop) if(relations == PlayerRelations::ENEMIES) { //we want to visit objects owned by oppponents - //addVisitableObj(obj); // TODO: Remove once save compatability broken. In past owned objects were removed from this set + //addVisitableObj(obj); // TODO: Remove once save compatibility broken. In past owned objects were removed from this set nullkiller->memory->markObjectUnvisited(obj); } else if(relations == PlayerRelations::SAME_PLAYER && obj->ID == Obj::TOWN) @@ -554,7 +551,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); } @@ -568,6 +565,7 @@ void AIGateway::initGameInterface(std::shared_ptr env, std::shared_ LOG_TRACE(logAi); myCb = CB; cbc = CB; + this->env = env; NET_EVENT_HANDLER; playerID = *myCb->getPlayerID(); @@ -603,7 +601,7 @@ void AIGateway::heroGotLevel(const CGHeroInstance * hero, PrimarySkill pskill, s if(hPtr.validAndSet()) { - std::unique_lock lockGuard(nullkiller->aiStateMutex); + std::unique_lock lockGuard(nullkiller->aiStateMutex); nullkiller->heroManager->update(); @@ -648,7 +646,14 @@ void AIGateway::showBlockingDialog(const std::string & text, const std::vectordangerEvaluator->evaluateDanger(target, hero.get()); auto ratio = static_cast(danger) / hero->getTotalStrength(); - answer = topObj->id == goalObjectID; // no if we do not aim to visit this object + answer = true; + + if(topObj->id != goalObjectID && nullkiller->dangerEvaluator->evaluateDanger(topObj) > 0) + { + // no if we do not aim to visit this object + answer = false; + } + logAi->trace("Query hook: %s(%s) by %s danger ratio %f", target.toString(), topObj->getObjectName(), hero.name(), ratio); if(cb->getObj(goalObjectID, false)) @@ -663,7 +668,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; } @@ -683,7 +688,7 @@ void AIGateway::showBlockingDialog(const std::string & text, const std::vector mxLock(nullkiller->aiStateMutex); + std::unique_lock mxLock(nullkiller->aiStateMutex); // TODO: Find better way to understand it is Chest of Treasures if(hero.validAndSet() @@ -705,7 +710,7 @@ void AIGateway::showTeleportDialog(const CGHeroInstance * hero, TeleportChannelI NET_EVENT_HANDLER; status.addQuery(askID, boost::str(boost::format("Teleport dialog query with %d exits") % exits.size())); - int choosenExit = -1; + int chosenExit = -1; if(impassable) { nullkiller->memory->knownTeleportChannels[channel]->passability = TeleportChannel::IMPASSABLE; @@ -714,14 +719,14 @@ void AIGateway::showTeleportDialog(const CGHeroInstance * hero, TeleportChannelI { auto neededExit = std::make_pair(destinationTeleport, destinationTeleportPos); if(destinationTeleport != ObjectInstanceID() && vstd::contains(exits, neededExit)) - choosenExit = vstd::find_pos(exits, neededExit); + chosenExit = vstd::find_pos(exits, neededExit); } for(auto exit : exits) { if(status.channelProbing() && exit.first == destinationTeleport) { - choosenExit = vstd::find_pos(exits, exit); + chosenExit = vstd::find_pos(exits, exit); break; } else @@ -739,7 +744,7 @@ void AIGateway::showTeleportDialog(const CGHeroInstance * hero, TeleportChannelI requestActionASAP([=]() { - answerQuery(askID, choosenExit); + answerQuery(askID, chosenExit); }); } @@ -756,7 +761,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); } @@ -772,27 +777,6 @@ void AIGateway::showMapObjectSelectDialog(QueryID askID, const Component & icon, requestActionASAP([=](){ answerQuery(askID, selectedObject.getNum()); }); } -void AIGateway::saveGame(BinarySerializer & h) -{ - NET_EVENT_HANDLER; - nullkiller->memory->removeInvisibleObjects(myCb.get()); - - CAdventureAI::saveGame(h); - serializeInternal(h); -} - -void AIGateway::loadGame(BinaryDeserializer & h) -{ - //NET_EVENT_HANDLER; - - #if 0 - //disabled due to issue 2890 - registerGoals(h); - #endif // 0 - CAdventureAI::loadGame(h); - serializeInternal(h); -} - bool AIGateway::makePossibleUpgrades(const CArmedInstance * obj) { if(!obj) @@ -825,7 +809,7 @@ void AIGateway::makeTurn() auto day = cb->getDate(Date::DAY); logAi->info("Player %d (%s) starting turn, day %d", playerID, playerID.toString(), day); - boost::shared_lock gsLock(CGameState::mutex); + boost::shared_lock gsLock(CGameState::mutex); setThreadName("AIGateway::makeTurn"); if(nullkiller->isOpenMap()) @@ -877,7 +861,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: @@ -885,7 +869,7 @@ void AIGateway::performObjectInteraction(const CGObjectInstance * obj, HeroPtr h { makePossibleUpgrades(h.get()); - std::unique_lock lockGuard(nullkiller->aiStateMutex); + std::unique_lock lockGuard(nullkiller->aiStateMutex); if(!h->visitedTown->garrisonHero || !nullkiller->isHeroLocked(h->visitedTown->garrisonHero)) moveCreaturesToHero(h->visitedTown); @@ -1069,7 +1053,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 { @@ -1082,7 +1066,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 @@ -1093,8 +1077,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)) { @@ -1143,10 +1127,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) { @@ -1167,7 +1151,7 @@ void AIGateway::recruitCreatures(const CGDwelling * d, const CArmedInstance * re } } -void AIGateway::battleStart(const BattleID & battleID, const CCreatureSet * army1, const CCreatureSet * army2, int3 tile, const CGHeroInstance * hero1, const CGHeroInstance * hero2, bool side, bool replayAllowed) +void AIGateway::battleStart(const BattleID & battleID, const CCreatureSet * army1, const CCreatureSet * army2, int3 tile, const CGHeroInstance * hero1, const CGHeroInstance * hero2, BattleSide side, bool replayAllowed) { NET_EVENT_HANDLER; assert(!playerID.isValidPlayer() || status.getBattle() == UPCOMING_BATTLE); @@ -1187,6 +1171,17 @@ void AIGateway::battleEnd(const BattleID & battleID, const BattleResult * br, Qu battlename.clear(); CAdventureAI::battleEnd(battleID, br, queryID); + + // gosolo + if(queryID != QueryID::NONE && myCb->getPlayerState(playerID)->isHuman()) + { + status.addQuery(queryID, "Confirm battle query"); + + requestActionASAP([=]() + { + answerQuery(queryID, 0); + }); + } } void AIGateway::waitTillFree() @@ -1319,6 +1314,11 @@ bool AIGateway::moveHeroToTile(int3 dst, HeroPtr h) auto doTeleportMovement = [&](ObjectInstanceID exitId, int3 exitPos) { + if(cb->getObj(exitId) && cb->getObj(exitId)->ID == Obj::WHIRLPOOL) + { + nullkiller->armyFormation->rearrangeArmyForWhirlpool(*h); + } + destinationTeleport = exitId; if(exitPos.valid()) destinationTeleportPos = exitPos; @@ -1340,6 +1340,7 @@ bool AIGateway::moveHeroToTile(int3 dst, HeroPtr h) status.setChannelProbing(true); for(auto exit : teleportChannelProbingList) doTeleportMovement(exit, int3(-1)); + teleportChannelProbingList.clear(); status.setChannelProbing(false); @@ -1450,8 +1451,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; } @@ -1473,7 +1474,7 @@ void AIGateway::tryRealize(Goals::Trade & g) //trade if(cb->getResourceAmount(GameResID(g.resID)) >= g.value) //goal is already fulfilled. Why we need this check, anyway? throw goalFulfilledException(sptr(g)); - int accquiredResources = 0; + int acquiredResources = 0; if(const CGObjectInstance * obj = cb->getObj(ObjectInstanceID(g.objid), false)) { if(const auto * m = dynamic_cast(obj)) @@ -1492,9 +1493,9 @@ void AIGateway::tryRealize(Goals::Trade & g) //trade //TODO trade only as much as needed if (toGive) //don't try to sell 0 resources { - cb->trade(m, EMarketMode::RESOURCE_RESOURCE, res, GameResID(g.resID), toGive); - accquiredResources = static_cast(toGet * (it->resVal / toGive)); - logAi->debug("Traded %d of %s for %d of %s at %s", toGive, res, accquiredResources, g.resID, obj->getObjectName()); + cb->trade(m->getObjInstanceID(), EMarketMode::RESOURCE_RESOURCE, res, GameResID(g.resID), toGive); + acquiredResources = static_cast(toGet * (it->resVal / toGive)); + logAi->debug("Traded %d of %s for %d of %s at %s", toGive, res, acquiredResources, g.resID, obj->getObjectName()); } if (cb->getResourceAmount(GameResID(g.resID))) throw goalFulfilledException(sptr(g)); //we traded all we needed @@ -1565,7 +1566,7 @@ void AIGateway::requestActionASAP(std::function whatToDo) { setThreadName("AIGateway::requestActionASAP::whatToDo"); SET_GLOBAL_STATE(this); - boost::shared_lock gsLock(CGameState::mutex); + boost::shared_lock gsLock(CGameState::mutex); whatToDo(); }); diff --git a/AI/Nullkiller/AIGateway.h b/AI/Nullkiller/AIGateway.h index f9315b0ec..a4a8a845a 100644 --- a/AI/Nullkiller/AIGateway.h +++ b/AI/Nullkiller/AIGateway.h @@ -16,9 +16,7 @@ #include "../../lib/CThreadHelper.h" #include "../../lib/GameConstants.h" #include "../../lib/VCMI_Lib.h" -#include "../../lib/CBuildingHandler.h" #include "../../lib/CCreatureHandler.h" -#include "../../lib/CTownHandler.h" #include "../../lib/mapObjects/MiscObjects.h" #include "../../lib/spells/CSpellHandler.h" #include "Pathfinding/AIPathfinder.h" @@ -59,15 +57,6 @@ public: void attemptedAnsweringQuery(QueryID queryID, int answerRequestID); void receivedAnswerConfirmation(int answerRequestID, int result); void heroVisit(const CGObjectInstance * obj, bool started); - - - template void serialize(Handler & h) - { - h & battle; - h & remainingQueries; - h & requestToQueryID; - h & havingTurn; - } }; // The gateway is responsible for AI events handling. Copied from VCAI.h and refined a bit @@ -104,7 +93,7 @@ public: AIGateway(); virtual ~AIGateway(); - //TODO: extract to apropriate goals + //TODO: extract to appropriate goals void tryRealize(Goals::DigAtTile & g); void tryRealize(Goals::Trade & g); @@ -119,8 +108,6 @@ public: void showGarrisonDialog(const CArmedInstance * up, const CGHeroInstance * down, bool removableUnits, QueryID queryID) override; //all stacks operations between these objects become allowed, interface has to call onEnd when done void showTeleportDialog(const CGHeroInstance * hero, TeleportChannelID channel, TTeleportExitsList exits, bool impassable, QueryID askID) override; void showMapObjectSelectDialog(QueryID askID, const Component & icon, const MetaString & title, const MetaString & description, const std::vector & objects) override; - void saveGame(BinarySerializer & h) override; //saving - void loadGame(BinaryDeserializer & h) override; //loading void finish() override; void availableCreaturesChanged(const CGDwelling * town) override; @@ -169,7 +156,7 @@ public: void showWorldViewEx(const std::vector & objectPositions, bool showTerrain) override; std::optional makeSurrenderRetreatDecision(const BattleID & battleID, const BattleStateInfoForRetreat & battleState) override; - void battleStart(const BattleID & battleID, const CCreatureSet * army1, const CCreatureSet * army2, int3 tile, const CGHeroInstance * hero1, const CGHeroInstance * hero2, bool side, bool replayAllowed) override; + void battleStart(const BattleID & battleID, const CCreatureSet * army1, const CCreatureSet * army2, int3 tile, const CGHeroInstance * hero1, const CGHeroInstance * hero2, BattleSide side, bool replayAllowed) override; void battleEnd(const BattleID & battleID, const BattleResult * br, QueryID queryID) override; void makeTurn(); @@ -202,17 +189,6 @@ public: void answerQuery(QueryID queryID, int selection); //special function that can be called ONLY from game events handling thread and will send request ASAP void requestActionASAP(std::function whatToDo); - - template void serializeInternal(Handler & h) - { - h & nullkiller->memory->knownTeleportChannels; - h & nullkiller->memory->knownSubterraneanGates; - h & destinationTeleport; - h & nullkiller->memory->visitableObjs; - h & nullkiller->memory->alreadyVisited; - h & status; - h & battlename; - } }; } diff --git a/AI/Nullkiller/AIUtility.cpp b/AI/Nullkiller/AIUtility.cpp index ae14897ac..4ee0e960e 100644 --- a/AI/Nullkiller/AIUtility.cpp +++ b/AI/Nullkiller/AIUtility.cpp @@ -14,11 +14,10 @@ #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" -#include "../../lib/GameSettings.h" +#include "../../lib/IGameSettings.h" #include @@ -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; @@ -430,9 +429,16 @@ bool shouldVisit(const Nullkiller * ai, const CGHeroInstance * h, const CGObject return false; } - if(obj->wasVisited(h)) //it must pointer to hero instance, heroPtr calls function wasVisited(ui8 player); + if(obj->wasVisited(h)) return false; + auto rewardable = dynamic_cast(obj); + + if(rewardable && rewardable->getAvailableRewards(h, Rewardable::EEventType::EVENT_FIRST_VISIT).empty()) + { + return false; + } + return true; } @@ -441,9 +447,9 @@ bool townHasFreeTavern(const CGTownInstance * town) if(!town->hasBuilt(BuildingID::TAVERN)) return false; if(!town->visitingHero) return true; - bool canMoveVisitingHeroToGarnison = !town->getUpperArmy()->stacksCount(); + bool canMoveVisitingHeroToGarrison = !town->getUpperArmy()->stacksCount(); - return canMoveVisitingHeroToGarnison; + return canMoveVisitingHeroToGarrison; } uint64_t getHeroArmyStrengthWithCommander(const CGHeroInstance * hero, const CCreatureSet * heroArmy) diff --git a/AI/Nullkiller/AIUtility.h b/AI/Nullkiller/AIUtility.h index 98e234247..4275bea9f 100644 --- a/AI/Nullkiller/AIUtility.h +++ b/AI/Nullkiller/AIUtility.h @@ -40,9 +40,7 @@ /*********************** TBB.h ********************/ #include "../../lib/VCMI_Lib.h" -#include "../../lib/CBuildingHandler.h" #include "../../lib/CCreatureHandler.h" -#include "../../lib/CTownHandler.h" #include "../../lib/spells/CSpellHandler.h" #include "../../lib/CStopWatch.h" #include "../../lib/mapObjects/CGHeroInstance.h" @@ -63,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; @@ -110,13 +103,6 @@ public: const CGHeroInstance * get(bool doWeExpectNull = false) const; const CGHeroInstance * get(const CPlayerSpecificInfoCallback * cb, bool doWeExpectNull = false) const; bool validAndSet() const; - - - template void serialize(Handler & handler) - { - handler & h; - handler & hid; - } }; enum BattleState @@ -141,12 +127,6 @@ struct ObjectIdRef ObjectIdRef(const CGObjectInstance * obj); bool operator<(const ObjectIdRef & rhs) const; - - - template void serialize(Handler & h) - { - h & id; - } }; template @@ -228,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..a8eb72c34 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(); @@ -316,6 +309,8 @@ std::vector ArmyManager::getArmyAvailableToBuy( ? dynamic_cast(dwelling) : nullptr; + std::set alreadyDisbanded; + for(int i = dwelling->creatures.size() - 1; i >= 0; i--) { auto ci = infoFromDC(dwelling->creatures[i]); @@ -329,18 +324,71 @@ std::vector ArmyManager::getArmyAvailableToBuy( if(!ci.count) continue; + // Calculate the market value of the new stack + TResources newStackValue = ci.creID.toCreature()->getFullRecruitCost() * ci.count; + SlotID dst = hero->getSlotFor(ci.creID); + + // Keep track of the least valuable slot in the hero's army + SlotID leastValuableSlot; + TResources leastValuableStackValue; + leastValuableStackValue[6] = std::numeric_limits::max(); + bool shouldDisband = false; if(!hero->hasStackAtSlot(dst)) //need another new slot for this stack { - if(!freeHeroSlots) //no more place for stacks - continue; + if(!freeHeroSlots) // No free slots; consider replacing + { + // Check for the least valuable existing stack + for (auto& slot : hero->Slots()) + { + if (alreadyDisbanded.find(slot.first) != alreadyDisbanded.end()) + continue; + + if(slot.second->getCreatureID() != CreatureID::NONE) + { + TResources currentStackValue = slot.second->getCreatureID().toCreature()->getFullRecruitCost() * slot.second->getCount(); + + if (town && slot.second->getCreatureID().toCreature()->getFactionID() == town->getFactionID()) + continue; + + if(currentStackValue.marketValue() < leastValuableStackValue.marketValue()) + { + leastValuableStackValue = currentStackValue; + leastValuableSlot = slot.first; + } + } + } + + // Decide whether to replace the least valuable stack + if(newStackValue.marketValue() <= leastValuableStackValue.marketValue()) + { + continue; // Skip if the new stack isn't worth replacing + } + else + { + shouldDisband = true; + } + } else + { freeHeroSlots--; //new slot will be occupied + } } vstd::amin(ci.count, availableRes / ci.creID.toCreature()->getFullRecruitCost()); //max count we can afford - if(!ci.count) continue; + int disbandMalus = 0; + + if (shouldDisband) + { + disbandMalus = leastValuableStackValue / ci.creID.toCreature()->getFullRecruitCost(); + alreadyDisbanded.insert(leastValuableSlot); + } + + ci.count -= disbandMalus; + + if(ci.count <= 0) + continue; ci.level = i; //this is important for Dungeon Summoning Portal creaturesInDwellings.push_back(ci); diff --git a/AI/Nullkiller/Analyzers/ArmyManager.h b/AI/Nullkiller/Analyzers/ArmyManager.h index 96b178c6d..0fe37ee91 100644 --- a/AI/Nullkiller/Analyzers/ArmyManager.h +++ b/AI/Nullkiller/Analyzers/ArmyManager.h @@ -14,8 +14,6 @@ #include "../../../lib/GameConstants.h" #include "../../../lib/VCMI_Lib.h" -#include "../../../lib/CTownHandler.h" -#include "../../../lib/CBuildingHandler.h" namespace NKAI { diff --git a/AI/Nullkiller/Analyzers/BuildAnalyzer.cpp b/AI/Nullkiller/Analyzers/BuildAnalyzer.cpp index a50ccc255..01dfa0a82 100644 --- a/AI/Nullkiller/Analyzers/BuildAnalyzer.cpp +++ b/AI/Nullkiller/Analyzers/BuildAnalyzer.cpp @@ -10,13 +10,14 @@ #include "../StdInc.h" #include "../Engine/Nullkiller.h" #include "../Engine/Nullkiller.h" +#include "../../../lib/entities/building/CBuilding.h" 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(); @@ -30,17 +31,14 @@ void BuildAnalyzer::updateTownDwellings(TownDevelopmentInfo & developmentInfo) } } - BuildingID prefixes[] = {BuildingID::DWELL_UP_FIRST, BuildingID::DWELL_FIRST}; - - for(int level = 0; level < GameConstants::CREATURES_PER_TOWN; level++) + for(int level = 0; level < developmentInfo.town->getTown()->creatures.size(); level++) { logAi->trace("Checking dwelling level %d", level); BuildingInfo nextToBuild = BuildingInfo(); - for(BuildingID prefix : prefixes) + for(int upgradeIndex : {1, 0}) { - BuildingID building = BuildingID(prefix + level); - + BuildingID building = BuildingID(BuildingID::getDwellingFromLevel(level, upgradeIndex)); if(!vstd::contains(buildings, building)) continue; // no such building in town @@ -74,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)); @@ -100,10 +105,17 @@ int32_t convertToGold(const TResources & res) + 125 * (res[EGameResID::GEMS] + res[EGameResID::CRYSTAL] + res[EGameResID::MERCURY] + res[EGameResID::SULFUR]); } +TResources withoutGold(TResources other) +{ + other[GameResID::GOLD] = 0; + + return other; +} + TResources BuildAnalyzer::getResourcesRequiredNow() const { auto resourcesAvailable = ai->getFreeResources(); - auto result = requiredResources - resourcesAvailable; + auto result = withoutGold(armyCost) + requiredResources - resourcesAvailable; result.positive(); @@ -113,7 +125,7 @@ TResources BuildAnalyzer::getResourcesRequiredNow() const TResources BuildAnalyzer::getTotalResourcesRequired() const { auto resourcesAvailable = ai->getFreeResources(); - auto result = totalDevelopmentCost - resourcesAvailable; + auto result = totalDevelopmentCost + withoutGold(armyCost) - resourcesAvailable; result.positive(); @@ -135,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()); @@ -147,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) @@ -165,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); } @@ -192,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; @@ -203,8 +214,8 @@ BuildingInfo BuildAnalyzer::getBuildingOrPrerequisite( if(BuildingID::DWELL_FIRST <= toBuild && toBuild <= BuildingID::DWELL_UP_LAST) { - creatureLevel = (toBuild - BuildingID::DWELL_FIRST) % GameConstants::CREATURES_PER_TOWN; - creatureUpgrade = (toBuild - BuildingID::DWELL_FIRST) / GameConstants::CREATURES_PER_TOWN; + creatureLevel = BuildingID::getLevelFromDwelling(toBuild); + creatureUpgrade = BuildingID::getUpgradedFromDwelling(toBuild); } else if(toBuild == BuildingID::HORDE_1 || toBuild == BuildingID::HORDE_1_UPGR) { @@ -231,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); @@ -267,7 +284,7 @@ BuildingInfo BuildAnalyzer::getBuildingOrPrerequisite( BuildingInfo prerequisite = getBuildingOrPrerequisite(town, missingBuildings[0], excludeDwellingDependencies); - prerequisite.buildCostWithPrerequisits += info.buildCost; + prerequisite.buildCostWithPrerequisites += info.buildCost; prerequisite.creatureCost = info.creatureCost; prerequisite.creatureGrows = info.creatureGrows; prerequisite.creatureLevel = info.creatureLevel; @@ -275,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; } @@ -308,9 +333,7 @@ void BuildAnalyzer::updateDailyIncome() const CGMine* mine = dynamic_cast(obj); if(mine) - { - dailyIncome[mine->producedResource.getNum()] += mine->producedQuantity; - } + dailyIncome += mine->dailyIncome(); } for(const CGTownInstance* town : towns) @@ -323,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; } @@ -340,7 +363,8 @@ void TownDevelopmentInfo::addExistingDwelling(const BuildingInfo & existingDwell void TownDevelopmentInfo::addBuildingToBuild(const BuildingInfo & nextToBuild) { - townDevelopmentCost += nextToBuild.buildCostWithPrerequisits; + townDevelopmentCost += nextToBuild.buildCostWithPrerequisites; + townDevelopmentCost += withoutGold(nextToBuild.armyCost); if(nextToBuild.canBuild) { @@ -361,7 +385,7 @@ BuildingInfo::BuildingInfo() creatureGrows = 0; creatureID = CreatureID::NONE; buildCost = 0; - buildCostWithPrerequisits = 0; + buildCostWithPrerequisites = 0; prerequisitesCount = 0; name.clear(); armyStrength = 0; @@ -376,7 +400,7 @@ BuildingInfo::BuildingInfo( { id = building->bid; buildCost = building->resources; - buildCostWithPrerequisits = building->resources; + buildCostWithPrerequisites = building->resources; dailyIncome = building->produce; exists = town->hasBuilt(id); prerequisitesCount = 1; diff --git a/AI/Nullkiller/Analyzers/BuildAnalyzer.h b/AI/Nullkiller/Analyzers/BuildAnalyzer.h index 019d3245f..430922d94 100644 --- a/AI/Nullkiller/Analyzers/BuildAnalyzer.h +++ b/AI/Nullkiller/Analyzers/BuildAnalyzer.h @@ -22,7 +22,7 @@ class DLL_EXPORT BuildingInfo public: BuildingID id; TResources buildCost; - TResources buildCostWithPrerequisits; + TResources buildCostWithPrerequisites; int creatureGrows; uint8_t creatureLevel; TResources creatureCost; diff --git a/AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.cpp b/AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.cpp index 6d11d01ba..5a8d0a24f 100644 --- a/AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.cpp +++ b/AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.cpp @@ -13,6 +13,7 @@ #include "../Engine/Nullkiller.h" #include "../pforeach.h" #include "../../../lib/CRandomGenerator.h" +#include "../../../lib/logging/VisualLogger.h" namespace NKAI { @@ -24,6 +25,41 @@ double HitMapInfo::value() const return danger / std::sqrt(turn / 3.0f + 1); } +void logHitmap(PlayerColor playerID, DangerHitMapAnalyzer & data) +{ +#if NKAI_TRACE_LEVEL >= 1 + logVisual->updateWithLock(playerID.toString() + ".danger.max", [&data](IVisualLogBuilder & b) + { + foreach_tile_pos([&b, &data](const int3 & pos) + { + auto & treat = data.getTileThreat(pos).maximumDanger; + b.addText(pos, std::to_string(treat.danger)); + + if(treat.hero.validAndSet()) + { + b.addText(pos, std::to_string(treat.turn)); + b.addText(pos, treat.hero->getNameTranslated()); + } + }); + }); + + logVisual->updateWithLock(playerID.toString() + ".danger.fast", [&data](IVisualLogBuilder & b) + { + foreach_tile_pos([&b, &data](const int3 & pos) + { + auto & treat = data.getTileThreat(pos).fastestDanger; + b.addText(pos, std::to_string(treat.danger)); + + if(treat.hero.validAndSet()) + { + b.addText(pos, std::to_string(treat.turn)); + b.addText(pos, treat.hero->getNameTranslated()); + } + }); + }); +#endif +} + void DangerHitMapAnalyzer::updateHitMap() { if(hitMapUpToDate) @@ -53,6 +89,13 @@ void DangerHitMapAnalyzer::updateHitMap() heroes[hero->tempOwner][hero] = HeroRole::MAIN; } + if(obj->ID == Obj::TOWN) + { + auto town = dynamic_cast(obj); + + if(town->garrisonHero) + heroes[town->garrisonHero->tempOwner][town->garrisonHero] = HeroRole::MAIN; + } } auto ourTowns = cb->getTownsInfo(); @@ -96,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()) @@ -144,6 +188,8 @@ void DangerHitMapAnalyzer::updateHitMap() } logAi->trace("Danger hit map updated in %ld", timeElapsed(start)); + + logHitmap(ai->playerID, *this); } void DangerHitMapAnalyzer::calculateTileOwners() @@ -270,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 @@ -302,6 +348,7 @@ std::set DangerHitMapAnalyzer::getOneTurnAccessibleObj void DangerHitMapAnalyzer::reset() { hitMapUpToDate = false; + tileOwnersUpToDate = false; } } 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 44b66b46b..be3526fda 100644 --- a/AI/Nullkiller/Analyzers/HeroManager.cpp +++ b/AI/Nullkiller/Analyzers/HeroManager.cpp @@ -11,8 +11,7 @@ #include "../StdInc.h" #include "../Engine/Nullkiller.h" #include "../../../lib/mapObjects/MapObjects.h" -#include "../../../lib/CHeroHandler.h" -#include "../../../lib/GameSettings.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,15 +191,13 @@ 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() - || heroCount >= VLC->settings()->getInteger(EGameSettings::HEROES_PER_PLAYER_ON_MAP_CAP) - || heroCount >= VLC->settings()->getInteger(EGameSettings::HEROES_PER_PLAYER_TOTAL_CAP); + 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); } float HeroManager::getFightingStrengthCached(const CGHeroInstance * hero) const @@ -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 3c7ce1598..383f3c13a 100644 --- a/AI/Nullkiller/Analyzers/HeroManager.h +++ b/AI/Nullkiller/Analyzers/HeroManager.h @@ -14,8 +14,6 @@ #include "../../../lib/GameConstants.h" #include "../../../lib/VCMI_Lib.h" -#include "../../../lib/CTownHandler.h" -#include "../../../lib/CBuildingHandler.h" namespace NKAI { @@ -58,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 7b9607390..dd7173b74 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)); @@ -146,6 +147,13 @@ std::optional ObjectClusterizer::getBlocker(const AIPa return blocker; } + auto danger = ai->dangerEvaluator->evaluateDanger(blocker); + + if(danger > 0 && blocker->isBlockedVisitable() && isObjectRemovable(blocker)) + { + return blocker; + } + return std::optional< const CGObjectInstance *>(); } @@ -467,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; @@ -488,12 +498,14 @@ 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; + bool interestingObject = path.turn() <= 2 || priority > (ai->settings->isUseFuzzy() ? 0.5f : 0); if(interestingObject) { 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..b8bb5e470 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( @@ -63,7 +64,7 @@ Goals::TGoalVec BuyArmyBehavior::decompose(const Nullkiller * ai) const if(reinforcement) { - tasks.push_back(Goals::sptr(Goals::BuyArmy(town, reinforcement).setpriority(5))); + tasks.push_back(Goals::sptr(Goals::BuyArmy(town, reinforcement).setpriority(reinforcement))); } } } diff --git a/AI/Nullkiller/Behaviors/CaptureObjectsBehavior.cpp b/AI/Nullkiller/Behaviors/CaptureObjectsBehavior.cpp index 37480f305..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( @@ -212,7 +207,7 @@ void CaptureObjectsBehavior::decomposeObjects( vstd::concatenate(tasksLocal, getVisitGoals(paths, nullkiller, objToVisit, specificObjects)); } - std::lock_guard lock(sync); // FIXME: consider using tbb::parallel_reduce instead to avoid mutex overhead + std::lock_guard lock(sync); // FIXME: consider using tbb::parallel_reduce instead to avoid mutex overhead vstd::concatenate(result, tasksLocal); }); } diff --git a/AI/Nullkiller/Behaviors/DefenceBehavior.cpp b/AI/Nullkiller/Behaviors/DefenceBehavior.cpp index f6bf88ced..a76008523 100644 --- a/AI/Nullkiller/Behaviors/DefenceBehavior.cpp +++ b/AI/Nullkiller/Behaviors/DefenceBehavior.cpp @@ -130,7 +130,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 +141,7 @@ bool handleGarrisonHeroFromPreviousTurn(const CGTownInstance * town, Goals::TGoa { tasks.push_back(Goals::sptr(Goals::DismissHero(heroToDismiss).setpriority(5))); - return true; + return false; } } } @@ -158,11 +158,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()); @@ -240,7 +239,7 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta if(path.turn() <= threat.turn - 2) { #if NKAI_TRACE_LEVEL >= 1 - logAi->trace("Defer defence of %s by %s because he has enough time to reach the town next trun", + logAi->trace("Defer defence of %s by %s because he has enough time to reach the town next turn", town->getObjectName(), path.targetHero->getObjectName()); #endif @@ -250,6 +249,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 +270,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 +302,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 +353,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 +406,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) { @@ -415,6 +419,21 @@ void DefenceBehavior::evaluateRecruitingHero(Goals::TGoalVec & tasks, const HitM if(hero->getTotalStrength() < threat.danger) continue; + bool heroAlreadyHiredInOtherTown = false; + for (const auto& task : tasks) + { + if (auto recruitGoal = dynamic_cast(task.get())) + { + if (recruitGoal->getHero() == hero) + { + heroAlreadyHiredInOtherTown = true; + break; + } + } + } + if (heroAlreadyHiredInOtherTown) + continue; + auto myHeroes = ai->cb->getHeroesInfo(); #if NKAI_TRACE_LEVEL >= 1 @@ -451,7 +470,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 cab2707f3..b9c6e568d 100644 --- a/AI/Nullkiller/Behaviors/ExplorationBehavior.cpp +++ b/AI/Nullkiller/Behaviors/ExplorationBehavior.cpp @@ -33,46 +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: - 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::WHIRLPOOL: { - case Obj::MONOLITH_TWO_WAY: - case Obj::SUBTERRANEAN_GATE: - 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..fd87347a6 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,89 @@ 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 + || bestHeroToHire->getArmyCost() > GameConstants::HERO_GOLD_COST / 2.0 + || (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/StartupBehavior.cpp b/AI/Nullkiller/Behaviors/StartupBehavior.cpp index 859d4975f..9d0a12b18 100644 --- a/AI/Nullkiller/Behaviors/StartupBehavior.cpp +++ b/AI/Nullkiller/Behaviors/StartupBehavior.cpp @@ -79,6 +79,15 @@ bool needToRecruitHero(const Nullkiller * ai, const CGTownInstance * startupTown bool isGoldPile = dynamic_cast(obj) && dynamic_cast(obj)->resourceID() == EGameResID::GOLD; + auto rewardable = dynamic_cast(obj); + + if(rewardable) + { + for(auto & info : rewardable->configuration.info) + if(info.reward.resources[EGameResID::GOLD] > 0) + isGoldPile = true; + } + if(isGoldPile || obj->ID == Obj::TREASURE_CHEST || obj->ID == Obj::CAMPFIRE 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/CMakeLists.txt b/AI/Nullkiller/CMakeLists.txt index 8ea2634b4..83fc3bff6 100644 --- a/AI/Nullkiller/CMakeLists.txt +++ b/AI/Nullkiller/CMakeLists.txt @@ -8,6 +8,7 @@ set(Nullkiller_SRCS Pathfinding/Actions/QuestAction.cpp Pathfinding/Actions/BuyArmyAction.cpp Pathfinding/Actions/BoatActions.cpp + Pathfinding/Actions/WhirlpoolAction.cpp Pathfinding/Actions/TownPortalAction.cpp Pathfinding/Actions/AdventureSpellCastMovementActions.cpp Pathfinding/Rules/AILayerTransitionRule.cpp @@ -79,6 +80,7 @@ set(Nullkiller_HEADERS Pathfinding/Actions/QuestAction.h Pathfinding/Actions/BuyArmyAction.h Pathfinding/Actions/BoatActions.h + Pathfinding/Actions/WhirlpoolAction.h Pathfinding/Actions/TownPortalAction.h Pathfinding/Actions/AdventureSpellCastMovementActions.h Pathfinding/Rules/AILayerTransitionRule.h @@ -155,11 +157,7 @@ else() endif() target_include_directories(Nullkiller PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) -target_link_libraries(Nullkiller PUBLIC vcmi fuzzylite::fuzzylite TBB::tbb) +target_link_libraries(Nullkiller PUBLIC vcmi fuzzylite::fuzzylite) vcmi_set_output_dir(Nullkiller "AI") enable_pch(Nullkiller) - -if(APPLE_IOS AND NOT USING_CONAN) - install(IMPORTED_RUNTIME_ARTIFACTS TBB::tbb LIBRARY DESTINATION ${LIB_DIR}) # CMake 3.21+ -endif() diff --git a/AI/Nullkiller/Engine/FuzzyEngines.cpp b/AI/Nullkiller/Engine/FuzzyEngines.cpp index a7fb1b26c..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() { @@ -208,12 +207,6 @@ float TacticalAdvantageEngine::getTacticalAdvantage(const CArmedInstance * we, c enemyFlyers->setValue(enemyStructure.flyers); enemySpeed->setValue(enemyStructure.maxSpeed); - bool bank = dynamic_cast(enemy); - if(bank) - bankPresent->setValue(1); - else - bankPresent->setValue(0); - const CGTownInstance * fort = dynamic_cast(enemy); if(fort) castleWalls->setValue(fort->fortLevel()); diff --git a/AI/Nullkiller/Engine/FuzzyHelper.cpp b/AI/Nullkiller/Engine/FuzzyHelper.cpp index ed7abb7c1..a5fcd6a1a 100644 --- a/AI/Nullkiller/Engine/FuzzyHelper.cpp +++ b/AI/Nullkiller/Engine/FuzzyHelper.cpp @@ -20,25 +20,6 @@ namespace NKAI { -ui64 FuzzyHelper::estimateBankDanger(const CBank * bank) -{ - //this one is not fuzzy anymore, just calculate weighted average - - auto objectInfo = bank->getObjectHandler()->getObjectInfo(bank->appearance); - - CBankInfo * bankInfo = dynamic_cast(objectInfo.get()); - - ui64 totalStrength = 0; - ui8 totalChance = 0; - for(auto config : bankInfo->getPossibleGuards(bank->cb)) - { - totalStrength += config.second.totalStrength * config.first; - totalChance += config.first; - } - return totalStrength / std::max(totalChance, 1); //avoid division by zero - -} - ui64 FuzzyHelper::evaluateDanger(const int3 & tile, const CGHeroInstance * visitor, bool checkGuards) { auto cb = ai->cb.get(); @@ -71,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) @@ -136,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; @@ -158,30 +148,14 @@ ui64 FuzzyHelper::evaluateDanger(const CGObjectInstance * obj) return 0; [[fallthrough]]; } - case Obj::MONSTER: - case Obj::GARRISON: - case Obj::GARRISON2: - case Obj::CREATURE_GENERATOR1: - case Obj::CREATURE_GENERATOR4: - case Obj::MINE: - case Obj::ABANDONED_MINE: - case Obj::PANDORAS_BOX: - case Obj::CRYPT: //crypt - case Obj::CREATURE_BANK: //crebank - case Obj::DRAGON_UTOPIA: - case Obj::SHIPWRECK: //shipwreck - case Obj::DERELICT_SHIP: //derelict ship + default: { const CArmedInstance * a = dynamic_cast(obj); - return a->getArmyStrength(); + if (a) + return a->getArmyStrength(); + else + return 0; } - case Obj::PYRAMID: - { - return estimateBankDanger(dynamic_cast(obj)); - } - default: - return 0; } } - } diff --git a/AI/Nullkiller/Engine/FuzzyHelper.h b/AI/Nullkiller/Engine/FuzzyHelper.h index b203916ad..455da61e6 100644 --- a/AI/Nullkiller/Engine/FuzzyHelper.h +++ b/AI/Nullkiller/Engine/FuzzyHelper.h @@ -10,12 +10,6 @@ #pragma once #include "FuzzyEngines.h" -VCMI_LIB_NAMESPACE_BEGIN - -class CBank; - -VCMI_LIB_NAMESPACE_END - namespace NKAI { @@ -30,8 +24,6 @@ private: public: FuzzyHelper(const Nullkiller * ai): ai(ai) {} - ui64 estimateBankDanger(const CBank * bank); //TODO: move to another class? - ui64 evaluateDanger(const CGObjectInstance * obj); ui64 evaluateDanger(const int3 & tile, const CGHeroInstance * visitor, bool checkGuards = true); }; diff --git a/AI/Nullkiller/Engine/Nullkiller.cpp b/AI/Nullkiller/Engine/Nullkiller.cpp index fc13ba6c7..b2c3d0eb1 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); } }); @@ -216,7 +223,7 @@ void Nullkiller::decompose(Goals::TGoalVec & result, Goals::TSubgoal behavior, i void Nullkiller::resetAiState() { - std::unique_lock lockGuard(aiStateMutex); + std::unique_lock lockGuard(aiStateMutex); lockedResources = TResources(); scanDepth = ScanDepth::MAIN_FULL; @@ -236,7 +243,7 @@ void Nullkiller::updateAiState(int pass, bool fast) { boost::this_thread::interruption_point(); - std::unique_lock lockGuard(aiStateMutex); + std::unique_lock lockGuard(aiStateMutex); auto start = std::chrono::high_resolution_clock::now(); @@ -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,21 +379,30 @@ 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; - updateAiState(i, true); + bool fastUpdate = true; + + if (bestTask->getHero() != nullptr) + fastUpdate = false; + + updateAiState(i, fastUpdate); } else { @@ -382,7 +410,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,14 +419,26 @@ 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("Decission madel in %ld", timeElapsed(start)); + logAi->debug("Decision madel in %ld", timeElapsed(start)); if(selectedTasks.empty()) { @@ -438,7 +477,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 +502,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 +512,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 +609,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 9f563ee32..fe08da693 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_STRENGHT (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) { } @@ -118,35 +131,17 @@ int32_t estimateTownIncome(CCallback * cb, const CGObjectInstance * target, cons return booster * (town->hasFort() && town->tempOwner != PlayerColor::NEUTRAL ? booster * 500 : 250); } -TResources getCreatureBankResources(const CGObjectInstance * target, const CGHeroInstance * hero) +int32_t getResourcesGoldReward(const TResources & res) { - //Fixme: unused variable hero + int32_t result = 0; - auto objectInfo = target->getObjectHandler()->getObjectInfo(target->appearance); - CBankInfo * bankInfo = dynamic_cast(objectInfo.get()); - auto resources = bankInfo->getPossibleResourcesReward(); - TResources result = TResources(); - int sum = 0; - - for(auto & reward : resources) + for(auto r : GameResID::ALL_RESOURCES()) { - result += reward.data * reward.chance; - sum += reward.chance; + if(res[r] > 0) + result += r == EGameResID::GOLD ? res[r] : res[r] * 100; } - return sum > 1 ? result / sum : result; -} - -uint64_t getResourcesGoldReward(const TResources & res) -{ - int nonGoldResources = res[EGameResID::GEMS] - + res[EGameResID::SULFUR] - + res[EGameResID::WOOD] - + res[EGameResID::ORE] - + res[EGameResID::CRYSTAL] - + res[EGameResID::MERCURY]; - - return res[EGameResID::GOLD] + 100 * nonGoldResources; + return result; } uint64_t getCreatureBankArmyReward(const CGObjectInstance * target, const CGHeroInstance * hero) @@ -173,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 { @@ -243,16 +238,16 @@ 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; } } return cost; } -static uint64_t evaluateArtifactArmyValue(const CArtifactInstance * art) +static uint64_t evaluateArtifactArmyValue(const CArtifact * art) { - if(art->artType->getId() == ArtifactID::SPELL_SCROLL) + if(art->getId() == ArtifactID::SPELL_SCROLL) return 1500; auto statsValue = @@ -267,8 +262,10 @@ static uint64_t evaluateArtifactArmyValue(const CArtifactInstance * art) auto classValue = 0; - switch(art->artType->aClass) + switch(art->aClass) { + case CArtifact::EartClass::ART_TREASURE: + //FALL_THROUGH case CArtifact::EartClass::ART_MINOR: classValue = 1000; break; @@ -302,22 +299,15 @@ uint64_t RewardEvaluator::getArmyReward( { case Obj::HILL_FORT: return ai->armyManager->calculateCreaturesUpgrade(army, target, ai->cb->getResourceAmount()).upgradeValue; - case Obj::CREATURE_BANK: - return getCreatureBankArmyReward(target, hero); case Obj::CREATURE_GENERATOR1: case Obj::CREATURE_GENERATOR2: case Obj::CREATURE_GENERATOR3: case Obj::CREATURE_GENERATOR4: return getDwellingArmyValue(ai->cb.get(), target, checkGold); - case Obj::CRYPT: - case Obj::SHIPWRECK: - case Obj::SHIPWRECK_SURVIVOR: - case Obj::WARRIORS_TOMB: - return 1000; + case Obj::SPELL_SCROLL: + //FALL_THROUGH case Obj::ARTIFACT: - return evaluateArtifactArmyValue(dynamic_cast(target)->storedArtifact); - case Obj::DRAGON_UTOPIA: - return 10000; + return evaluateArtifactArmyValue(dynamic_cast(target)->storedArtifact->getType()); case Obj::HERO: return relations == PlayerRelations::ENEMIES ? enemyArmyEliminationRewardRatio * dynamic_cast(target)->getArmyStrength() @@ -328,8 +318,46 @@ uint64_t RewardEvaluator::getArmyReward( case Obj::MAGIC_SPRING: return getManaRecoveryArmyReward(hero); default: - return 0; + break; } + + auto rewardable = dynamic_cast(target); + + if(rewardable) + { + auto totalValue = 0; + + for(int index : rewardable->getAvailableRewards(hero, Rewardable::EEventType::EVENT_FIRST_VISIT)) + { + auto & info = rewardable->configuration.info[index]; + + auto rewardValue = 0; + + if(!info.reward.artifacts.empty()) + { + for(auto artID : info.reward.artifacts) + { + const auto * art = dynamic_cast(VLC->artifacts()->getById(artID)); + + rewardValue += evaluateArtifactArmyValue(art); + } + } + + if(!info.reward.creatures.empty()) + { + for(const auto & stackInfo : info.reward.creatures) + { + rewardValue += stackInfo.getType()->getAIValue() * stackInfo.getCount(); + } + } + + totalValue += rewardValue > 0 ? rewardValue / (info.reward.artifacts.size() + info.reward.creatures.size()) : 0; + } + + return totalValue; + } + + return 0; } uint64_t RewardEvaluator::getArmyGrowth( @@ -468,12 +496,29 @@ 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())); } -float RewardEvaluator::getStrategicalValue(const CGObjectInstance * target) const +float RewardEvaluator::getResourceRequirementStrength(const TResources & res) const +{ + float sum = 0.0f; + + for(TResources::nziterator it(res); it.valid(); it++) + { + //Evaluate resources used for construction. Gold is evaluated separately. + if(it->resType != EGameResID::GOLD) + { + sum += 0.1f * it->resVal * getResourceRequirementStrength(it->resType) + + 0.05f * it->resVal * getTotalResourceRequirementStrength(it->resType); + } + } + + return sum; +} + +float RewardEvaluator::getStrategicalValue(const CGObjectInstance * target, const CGHeroInstance * hero) const { if(!target) return 0; @@ -491,24 +536,10 @@ float RewardEvaluator::getStrategicalValue(const CGObjectInstance * target) cons case Obj::RESOURCE: { auto resource = dynamic_cast(target); - return resource->resourceID() == EGameResID::GOLD - ? 0 - : 0.2f * getTotalResourceRequirementStrength(resource->resourceID()) + 0.4f * getResourceRequirementStrength(resource->resourceID()); - } - - case Obj::CREATURE_BANK: - { - auto resourceReward = getCreatureBankResources(target, nullptr); - float sum = 0.0f; - for (TResources::nziterator it (resourceReward); it.valid(); it++) - { - //Evaluate resources used for construction. Gold is evaluated separately. - if (it->resType != EGameResID::GOLD) - { - sum += 0.1f * it->resVal * getResourceRequirementStrength(it->resType); - } - } - return sum; + TResources res; + res[resource->resourceID()] = resource->amount; + + return getResourceRequirementStrength(res); } case Obj::TOWN: @@ -546,6 +577,70 @@ float RewardEvaluator::getStrategicalValue(const CGObjectInstance * target) cons case Obj::KEYMASTER: return 0.6f; + default: + break; + } + + auto rewardable = dynamic_cast(target); + + if(rewardable && hero) + { + auto resourceReward = 0.0f; + + for(int index : rewardable->getAvailableRewards(hero, Rewardable::EEventType::EVENT_FIRST_VISIT)) + { + resourceReward += getResourceRequirementStrength(rewardable->configuration.info[index].reward.resources); + } + + return resourceReward; + } + + 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; } @@ -593,11 +688,11 @@ float RewardEvaluator::getSkillReward(const CGObjectInstance * target, const CGH case Obj::ARENA: return 2; case Obj::SHRINE_OF_MAGIC_INCANTATION: - return 0.2f; + return 0.25f; case Obj::SHRINE_OF_MAGIC_GESTURE: - return 0.3f; + return 1.0f; case Obj::SHRINE_OF_MAGIC_THOUGHT: - return 0.5f; + return 2.0f; case Obj::LIBRARY_OF_ENLIGHTENMENT: return 8; case Obj::WITCH_HUT: @@ -605,15 +700,55 @@ float RewardEvaluator::getSkillReward(const CGObjectInstance * target, const CGH case Obj::PANDORAS_BOX: //Can contains experience, spells, or skills (only on custom maps) return 2.5f; - case Obj::PYRAMID: - return 3.0f; case Obj::HERO: return ai->cb->getPlayerRelations(target->tempOwner, ai->playerID) == PlayerRelations::ENEMIES ? enemyHeroEliminationSkillRewardRatio * dynamic_cast(target)->level : 0; + default: - return 0; + break; } + + auto rewardable = dynamic_cast(target); + + if(rewardable) + { + auto totalValue = 0.0f; + + for(int index : rewardable->getAvailableRewards(hero, Rewardable::EEventType::EVENT_FIRST_VISIT)) + { + auto & info = rewardable->configuration.info[index]; + + auto rewardValue = 0.0f; + + if(!info.reward.spells.empty()) + { + for(auto spellID : info.reward.spells) + { + const spells::Spell * spell = VLC->spells()->getById(spellID); + + if(hero->canLearnSpell(spell) && !hero->spellbookContainsSpell(spellID)) + { + rewardValue += std::sqrt(spell->getLevel()) / 4.0f; + } + } + + totalValue += rewardValue / info.reward.spells.size(); + } + + if(!info.reward.primary.empty()) + { + for(auto value : info.reward.primary) + { + totalValue += value; + } + } + } + + return totalValue; + } + + return 0; } const HitMapInfo & RewardEvaluator::getEnemyHeroDanger(const int3 & tile, uint8_t turn) const @@ -635,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; @@ -671,22 +806,6 @@ int32_t RewardEvaluator::getGoldReward(const CGObjectInstance * target, const CG auto * mine = dynamic_cast(target); return dailyIncomeMultiplier * (mine->producedResource == GameResID::GOLD ? 1000 : 75); } - case Obj::MYSTICAL_GARDEN: - case Obj::WINDMILL: - return 100; - case Obj::CAMPFIRE: - return 800; - case Obj::WAGON: - return 100; - case Obj::CREATURE_BANK: - return getResourcesGoldReward(getCreatureBankResources(target, hero)); - case Obj::CRYPT: - case Obj::DERELICT_SHIP: - return 3000; - case Obj::DRAGON_UTOPIA: - return 10000; - case Obj::SEA_CHEST: - return 1500; case Obj::PANDORAS_BOX: return 2500; case Obj::PRISON: @@ -697,8 +816,26 @@ int32_t RewardEvaluator::getGoldReward(const CGObjectInstance * target, const CG ? heroEliminationBonus + enemyArmyEliminationGoldRewardRatio * getArmyCost(dynamic_cast(target)) : 0; default: - return 0; + break; } + + auto rewardable = dynamic_cast(target); + + if(rewardable) + { + auto goldReward = 0; + + for(int index : rewardable->getAvailableRewards(hero, Rewardable::EEventType::EVENT_FIRST_VISIT)) + { + auto & info = rewardable->configuration.info[index]; + + goldReward += getResourcesGoldReward(info.reward.resources); + } + + return goldReward; + } + + return 0; } class HeroExchangeEvaluator : public IEvaluationContextBuilder @@ -714,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; } }; @@ -732,6 +871,7 @@ public: evaluationContext.armyReward += upgradeValue; evaluationContext.addNonCriticalStrategicalValue(upgradeValue / (float)armyUpgrade.hero->getArmyStrength()); + evaluationContext.isArmyUpgrade = true; } }; @@ -746,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(); + } } }; @@ -772,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); } } @@ -824,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()); } @@ -845,6 +1006,9 @@ public: Goals::ExecuteHeroChain & chain = dynamic_cast(*task); const AIPath & path = chain.getPath(); + if (vstd::isAlmostZero(path.movementCost())) + return; + vstd::amax(evaluationContext.danger, path.getTotalDanger()); evaluationContext.movementCost += path.movementCost(); evaluationContext.closestWayRatio = chain.closestWayRatio; @@ -854,14 +1018,24 @@ public: for(auto & node : path.nodes) { vstd::amax(costsPerHero[node.targetHero], node.cost); + if (node.layer == EPathfindingLayer::SAIL) + evaluationContext.involvesSailing = true; } + float highestCostForSingleHero = 0; for(auto pair : costsPerHero) { auto role = evaluationContext.evaluator.ai->heroManager->getHeroRole(pair.first); - evaluationContext.movementCostByRole[role] += pair.second; + if (pair.second > highestCostForSingleHero) + highestCostForSingleHero = pair.second; } + if (highestCostForSingleHero > 1 && costsPerHero.size() > 1) + { + //Chains that involve more than 1 hero doing something for more than a turn are too expensive in my book. They often involved heroes doing nothing just standing there waiting to fulfill their part of the chain. + return; + } + evaluationContext.movementCost *= costsPerHero.size(); //further deincentivise chaining as it often involves bringing back the army afterwards auto hero = task->hero; bool checkGold = evaluationContext.danger == 0; @@ -880,10 +1054,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().isValidPlayer() && ai->cb->getPlayerRelations(ai->playerID, target->getOwner()) == PlayerRelations::ENEMIES) + evaluationContext.isEnemy = true; evaluationContext.goldCost += evaluationContext.evaluator.getGoldCost(target, hero, army); + if(evaluationContext.danger > 0) + evaluationContext.skillReward += (float)evaluationContext.danger / (float)hero->getArmyStrength(); } + evaluationContext.armyInvolvement += army->getArmyCost(); - 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()); } @@ -924,6 +1106,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; @@ -949,6 +1132,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); @@ -957,6 +1148,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); } } }; @@ -1000,8 +1194,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.buildCostWithPrerequisits[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) { @@ -1028,7 +1228,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) { @@ -1048,7 +1259,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); @@ -1090,6 +1301,7 @@ EvaluationContext PriorityEvaluator::buildEvaluationContext(Goals::TSubgoal goal for(auto subgoal : parts) { context.goldCost += subgoal->goldCost; + context.buildingCost += subgoal->buildingCost; for(auto builder : evaluationContextBuilders) { @@ -1100,7 +1312,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); @@ -1113,47 +1325,284 @@ 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; + const bool amIInDanger = ai->cb->getTownsInfo().empty() || (evaluationContext.isDefend && evaluationContext.threatTurns == 0); + const float maxWillingToLose = amIInDanger ? 1 : ai->settings->getMaxArmyLossTarget(); + + bool arriveNextWeek = false; + if (ai->cb->getDate(Date::DAY_OF_WEEK) + evaluationContext.turn > 7 && priorityTier < PriorityTier::FAR_KILL) + arriveNextWeek = true; + +#if NKAI_TRACE_LEVEL >= 2 + logAi->trace("BEFORE: priorityTier %d, Evaluated %s, loss: %f, turn: %d, turns main: %f, scout: %f, army-involvement: %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 isEnemy: %d arriveNextWeek: %d", + priorityTier, + task->toString(), + evaluationContext.armyLossPersentage, + (int)evaluationContext.turn, + evaluationContext.movementCostByRole[HeroRole::MAIN], + evaluationContext.movementCostByRole[HeroRole::SCOUT], + evaluationContext.armyInvolvement, + 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, + evaluationContext.isEnemy, + arriveNextWeek); +#endif + + switch (priorityTier) + { + case PriorityTier::INSTAKILL: //Take towns / kill heroes in immediate reach + { + if (evaluationContext.turn > 0) + return 0; + if (evaluationContext.movementCost >= 1) + return 0; + if(evaluationContext.conquestValue > 0) + score = evaluationContext.armyInvolvement; + 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; + 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; + break; + } + case PriorityTier::KILL: //Take towns / kill heroes that are further away + //FALL_THROUGH + case PriorityTier::FAR_KILL: + { + if (evaluationContext.turn > 0 && evaluationContext.isHero) + return 0; + if (arriveNextWeek && evaluationContext.isEnemy) + return 0; + if (evaluationContext.conquestValue > 0) + score = evaluationContext.armyInvolvement; + 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; + if (vstd::isAlmostZero(evaluationContext.armyLossPersentage) && evaluationContext.closestWayRatio < 1.0) + return 0; + score = 1000; + 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; + if (vstd::isAlmostZero(evaluationContext.armyLossPersentage) && evaluationContext.closestWayRatio < 1.0) + return 0; + score = 1000; + if (evaluationContext.movementCost > 0) + score /= evaluationContext.movementCost; + break; + } + case PriorityTier::HUNTER_GATHER: //Collect guarded stuff + //FALL_THROUGH + case PriorityTier::FAR_HUNTER_GATHER: + { + 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; + if (vstd::isAlmostZero(evaluationContext.armyLossPersentage) && evaluationContext.closestWayRatio < 1.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; + 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; + if (evaluationContext.closestWayRatio < 1.0) + return 0; + score = 1000; + 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 = evaluationContext.armyInvolvement; + 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, army-involvement: %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, evaluationContext.movementCostByRole[HeroRole::MAIN], evaluationContext.movementCostByRole[HeroRole::SCOUT], + evaluationContext.armyInvolvement, 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 02dbb2fad..a403ee6bd 100644 --- a/AI/Nullkiller/Engine/PriorityEvaluator.h +++ b/AI/Nullkiller/Engine/PriorityEvaluator.h @@ -39,7 +39,9 @@ public: int getGoldCost(const CGObjectInstance * target, const CGHeroInstance * hero, const CCreatureSet * army) const; float getEnemyHeroStrategicalValue(const CGHeroInstance * enemy) const; float getResourceRequirementStrength(int resType) const; - float getStrategicalValue(const CGObjectInstance * target) 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; @@ -47,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 @@ -64,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); @@ -90,7 +106,22 @@ 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, + FAR_KILL, + FAR_HUNTER_GATHER, + DEFEND + }; private: const Nullkiller * ai; diff --git a/AI/Nullkiller/Engine/Settings.cpp b/AI/Nullkiller/Engine/Settings.cpp index db4e3f455..6cc7a5266 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,42 @@ 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) + updateHitmapOnTileReveal(false), + 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(); + maxArmyLossTarget = node["maxArmyLossTarget"].Float(); + safeAttackRatio = node["safeAttackRatio"].Float(); + allowObjectGraph = node["allowObjectGraph"].Bool(); + updateHitmapOnTileReveal = node["updateHitmapOnTileReveal"].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..ac8b718ec 100644 --- a/AI/Nullkiller/Engine/Settings.h +++ b/AI/Nullkiller/Engine/Settings.h @@ -25,21 +25,37 @@ namespace NKAI int mainHeroTurnDistanceLimit; int scoutHeroTurnDistanceLimit; int maxpass; + int pathfinderBucketsCount; + int pathfinderBucketSize; float maxGoldPressure; + float retreatThresholdRelative; + float retreatThresholdAbsolute; + float safeAttackRatio; + float maxArmyLossTarget; bool allowObjectGraph; bool useTroopsFromGarrisons; + bool updateHitmapOnTileReveal; 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; } + float getMaxArmyLossTarget() const { return maxArmyLossTarget; } 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 isUpdateHitmapOnTileReveal() const { return updateHitmapOnTileReveal; } 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 c73cb57e3..414c8a03d 100644 --- a/AI/Nullkiller/Goals/BuildThis.cpp +++ b/AI/Nullkiller/Goals/BuildThis.cpp @@ -12,7 +12,7 @@ #include "../AIGateway.h" #include "../AIUtility.h" #include "../../../lib/constants/StringConstants.h" - +#include "../../../lib/entities/building/CBuilding.h" namespace NKAI { @@ -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/BuyArmy.cpp b/AI/Nullkiller/Goals/BuyArmy.cpp index 9e2f52bff..cdd7d9d84 100644 --- a/AI/Nullkiller/Goals/BuyArmy.cpp +++ b/AI/Nullkiller/Goals/BuyArmy.cpp @@ -34,13 +34,13 @@ void BuyArmy::accept(AIGateway * ai) ui64 valueBought = 0; //buy the stacks with largest AI value - auto upgradeSuccessfull = ai->makePossibleUpgrades(town); + auto upgradeSuccessful = ai->makePossibleUpgrades(town); auto armyToBuy = ai->nullkiller->armyManager->getArmyAvailableToBuy(town->getUpperArmy(), town); if(armyToBuy.empty()) { - if(upgradeSuccessfull) + if(upgradeSuccessful) return; throw cannotFulfillGoalException("No creatures to buy."); @@ -58,7 +58,36 @@ void BuyArmy::accept(AIGateway * ai) if(ci.count) { - cb->recruitCreatures(town, town->getUpperArmy(), ci.creID, ci.count, ci.level); + if (town->getUpperArmy()->stacksCount() == GameConstants::ARMY_SIZE) + { + SlotID lowestValueSlot; + int lowestValue = std::numeric_limits::max(); + for (auto slot : town->getUpperArmy()->Slots()) + { + if (slot.second->getCreatureID() != CreatureID::NONE) + { + int currentStackMarketValue = + slot.second->getCreatureID().toCreature()->getFullRecruitCost().marketValue() * slot.second->getCount(); + + if (slot.second->getCreatureID().toCreature()->getFactionID() == town->getFactionID()) + continue; + + if (currentStackMarketValue < lowestValue) + { + lowestValue = currentStackMarketValue; + lowestValueSlot = slot.first; + } + } + } + if (lowestValueSlot.validSlot()) + { + cb->dismissCreature(town->getUpperArmy(), lowestValueSlot); + } + } + if (town->getUpperArmy()->stacksCount() < GameConstants::ARMY_SIZE || town->getUpperArmy()->getSlotFor(ci.creID).validSlot()) //It is possible we don't scrap despite we wanted to due to not scrapping stacks that fit our faction + { + cb->recruitCreatures(town, town->getUpperArmy(), ci.creID, ci.count, ci.level); + } valueBought += ci.count * ci.creID.toCreature()->getAIValue(); } } diff --git a/AI/Nullkiller/Goals/CGoal.h b/AI/Nullkiller/Goals/CGoal.h index 21d6cc863..cffa4991f 100644 --- a/AI/Nullkiller/Goals/CGoal.h +++ b/AI/Nullkiller/Goals/CGoal.h @@ -37,12 +37,6 @@ namespace Goals { return new T(static_cast(*this)); //casting enforces template instantiation } - template void serialize(Handler & h) - { - h & static_cast(*this); - //h & goalType & isElementar & isAbstract & priority; - //h & value & resID & objid & aid & tile & hero & town & bid; - } bool operator==(const AbstractGoal & g) const override { 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/CompleteQuest.cpp b/AI/Nullkiller/Goals/CompleteQuest.cpp index f774a2295..aeceb9871 100644 --- a/AI/Nullkiller/Goals/CompleteQuest.cpp +++ b/AI/Nullkiller/Goals/CompleteQuest.cpp @@ -12,7 +12,7 @@ #include "../Behaviors/CaptureObjectsBehavior.h" #include "../AIGateway.h" #include "../../../lib/VCMI_Lib.h" -#include "../../../lib/CGeneralTextHandler.h" +#include "../../../lib/texts/CGeneralTextHandler.h" namespace NKAI { 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 ef6d30bc8..0391a4585 100644 --- a/AI/Nullkiller/Goals/ExecuteHeroChain.cpp +++ b/AI/Nullkiller/Goals/ExecuteHeroChain.cpp @@ -22,11 +22,17 @@ ExecuteHeroChain::ExecuteHeroChain(const AIPath & path, const CGObjectInstance * { hero = path.targetHero; tile = path.targetTile(); + closestWayRatio = 1; if(obj) { objid = obj->id.getNum(); - targetName = obj->typeName + tile.toString(); + +#if NKAI_TRACE_LEVEL >= 1 + targetName = obj->getObjectName() + tile.toString(); +#else + targetName = obj->getTypeName() + tile.toString(); +#endif } else { @@ -80,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); @@ -191,6 +198,26 @@ void ExecuteHeroChain::accept(AIGateway * ai) } } + auto findWhirlpool = [&ai](const int3 & pos) -> ObjectInstanceID + { + auto objs = ai->myCb->getVisitableObjs(pos); + auto whirlpool = std::find_if(objs.begin(), objs.end(), [](const CGObjectInstance * o)->bool + { + return o->ID == Obj::WHIRLPOOL; + }); + + return whirlpool != objs.end() ? dynamic_cast(*whirlpool)->id : ObjectInstanceID(-1); + }; + + auto sourceWhirlpool = findWhirlpool(hero->visitablePos()); + auto targetWhirlpool = findWhirlpool(node->coord); + + if(i != chainPath.nodes.size() - 1 && sourceWhirlpool.hasValue() && sourceWhirlpool == targetWhirlpool) + { + logAi->trace("AI exited whirlpool at %s but expected at %s", hero->visitablePos().toString(), node->coord.toString()); + continue; + } + if(hero->movementPointsRemaining()) { try @@ -234,7 +261,7 @@ void ExecuteHeroChain::accept(AIGateway * ai) if(node->turns == 0) { logAi->error( - "Unable to complete chain. Expected hero %s to arive to %s but he is at %s", + "Unable to complete chain. Expected hero %s to arrive to %s but he is at %s", hero->getNameTranslated(), node->coord.toString(), hero->visitablePos().toString()); @@ -260,7 +287,11 @@ void ExecuteHeroChain::accept(AIGateway * ai) std::string ExecuteHeroChain::toString() const { +#if NKAI_TRACE_LEVEL >= 1 + return "ExecuteHeroChain " + targetName + " by " + chainPath.toString(); +#else return "ExecuteHeroChain " + targetName + " by " + chainPath.targetHero->getNameTranslated(); +#endif } bool ExecuteHeroChain::moveHeroToTile(AIGateway * ai, const CGHeroInstance * hero, const int3 & tile) diff --git a/AI/Nullkiller/Goals/ExploreNeighbourTile.cpp b/AI/Nullkiller/Goals/ExploreNeighbourTile.cpp index 4f6a4903e..6efa0e0b4 100644 --- a/AI/Nullkiller/Goals/ExploreNeighbourTile.cpp +++ b/AI/Nullkiller/Goals/ExploreNeighbourTile.cpp @@ -59,7 +59,7 @@ void ExploreNeighbourTile::accept(AIGateway * ai) return; } - auto danger = ai->nullkiller->pathfinder->getStorage()->evaluateDanger(target, hero, true); + auto danger = ai->nullkiller->dangerEvaluator->evaluateDanger(target, hero, true); if(danger > 0 || !ai->moveHeroToTile(target, hero)) { diff --git a/AI/Nullkiller/Goals/RecruitHero.cpp b/AI/Nullkiller/Goals/RecruitHero.cpp index c6a6c4d4e..810a6162c 100644 --- a/AI/Nullkiller/Goals/RecruitHero.cpp +++ b/AI/Nullkiller/Goals/RecruitHero.cpp @@ -68,10 +68,13 @@ void RecruitHero::accept(AIGateway * ai) throw cannotFulfillGoalException("Town " + t->nodeName() + " is occupied. Cannot recruit hero!"); cb->recruitHero(t, heroToHire); - ai->nullkiller->heroManager->update(); - if(t->visitingHero) - ai->moveHeroToTile(t->visitablePos(), t->visitingHero.get()); + { + 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/ArmyFormation.cpp b/AI/Nullkiller/Helpers/ArmyFormation.cpp index 464cb10d0..26d8c7d40 100644 --- a/AI/Nullkiller/Helpers/ArmyFormation.cpp +++ b/AI/Nullkiller/Helpers/ArmyFormation.cpp @@ -14,27 +14,37 @@ namespace NKAI { -void ArmyFormation::rearrangeArmyForSiege(const CGTownInstance * town, const CGHeroInstance * attacker) +void ArmyFormation::rearrangeArmyForWhirlpool(const CGHeroInstance * hero) { - auto freeSlots = attacker->getFreeSlotsQueue(); + addSingleCreatureStacks(hero); +} + +void ArmyFormation::addSingleCreatureStacks(const CGHeroInstance * hero) +{ + auto freeSlots = hero->getFreeSlotsQueue(); while(!freeSlots.empty()) { - auto weakestCreature = vstd::minElementByFun(attacker->Slots(), [](const std::pair & slot) -> int + auto weakestCreature = vstd::minElementByFun(hero->Slots(), [](const std::pair & slot) -> int { return slot.second->getCount() == 1 ? std::numeric_limits::max() : slot.second->getCreatureID().toCreature()->getAIValue(); }); - if(weakestCreature == attacker->Slots().end() || weakestCreature->second->getCount() == 1) + if(weakestCreature == hero->Slots().end() || weakestCreature->second->getCount() == 1) { break; } - cb->splitStack(attacker, attacker, weakestCreature->first, freeSlots.front(), 1); + cb->splitStack(hero, hero, weakestCreature->first, freeSlots.front(), 1); freeSlots.pop(); } +} + +void ArmyFormation::rearrangeArmyForSiege(const CGTownInstance * town, const CGHeroInstance * attacker) +{ + addSingleCreatureStacks(attacker); if(town->fortLevel() > CGTownInstance::FORT) { diff --git a/AI/Nullkiller/Helpers/ArmyFormation.h b/AI/Nullkiller/Helpers/ArmyFormation.h index 817c6158d..37ef2df47 100644 --- a/AI/Nullkiller/Helpers/ArmyFormation.h +++ b/AI/Nullkiller/Helpers/ArmyFormation.h @@ -13,8 +13,6 @@ #include "../../../lib/GameConstants.h" #include "../../../lib/VCMI_Lib.h" -#include "../../../lib/CTownHandler.h" -#include "../../../lib/CBuildingHandler.h" namespace NKAI { @@ -33,6 +31,10 @@ public: ArmyFormation(std::shared_ptr CB, const Nullkiller * ai): cb(CB) {} void rearrangeArmyForSiege(const CGTownInstance * town, const CGHeroInstance * attacker); + + void rearrangeArmyForWhirlpool(const CGHeroInstance * hero); + + void addSingleCreatureStacks(const CGHeroInstance * hero); }; } diff --git a/AI/Nullkiller/Helpers/ExplorationHelper.cpp b/AI/Nullkiller/Helpers/ExplorationHelper.cpp index 0be612ee0..0c17e0cc2 100644 --- a/AI/Nullkiller/Helpers/ExplorationHelper.cpp +++ b/AI/Nullkiller/Helpers/ExplorationHelper.cpp @@ -49,7 +49,7 @@ bool ExplorationHelper::scanSector(int scanRadius) { int3 tile = int3(0, 0, ourPos.z); - const auto & slice = (*(ts->fogOfWarMap))[ourPos.z]; + const auto & slice = ts->fogOfWarMap[ourPos.z]; for(tile.x = ourPos.x - scanRadius; tile.x <= ourPos.x + scanRadius; tile.x++) { @@ -75,13 +75,13 @@ bool ExplorationHelper::scanMap() foreach_tile_pos([&](const int3 & pos) { - if((*(ts->fogOfWarMap))[pos.z][pos.x][pos.y]) + if(ts->fogOfWarMap[pos.z][pos.x][pos.y]) { bool hasInvisibleNeighbor = false; foreach_neighbour(cbp, pos, [&](CCallback * cbp, int3 neighbour) { - if(!(*(ts->fogOfWarMap))[neighbour.z][neighbour.x][neighbour.y]) + if(!ts->fogOfWarMap[neighbour.z][neighbour.x][neighbour.y]) { hasInvisibleNeighbor = true; } @@ -107,7 +107,7 @@ bool ExplorationHelper::scanMap() allowDeadEndCancellation = false; logAi->debug("Exploration scan all possible tiles for hero %s", hero->getNameTranslated()); - boost::multi_array potentialTiles = *ts->fogOfWarMap; + boost::multi_array potentialTiles = ts->fogOfWarMap; std::vector tilesToExploreFrom = edgeTiles; // WARNING: POTENTIAL BUG @@ -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; @@ -191,7 +191,7 @@ int ExplorationHelper::howManyTilesWillBeDiscovered(const int3 & pos) const int ret = 0; int3 npos = int3(0, 0, pos.z); - const auto & slice = (*(ts->fogOfWarMap))[pos.z]; + const auto & slice = ts->fogOfWarMap[pos.z]; for(npos.x = pos.x - sightRadius; npos.x <= pos.x + sightRadius; npos.x++) { diff --git a/AI/Nullkiller/Helpers/ExplorationHelper.h b/AI/Nullkiller/Helpers/ExplorationHelper.h index 994d2809a..cd4427de1 100644 --- a/AI/Nullkiller/Helpers/ExplorationHelper.h +++ b/AI/Nullkiller/Helpers/ExplorationHelper.h @@ -13,8 +13,6 @@ #include "../../../lib/GameConstants.h" #include "../../../lib/VCMI_Lib.h" -#include "../../../lib/CTownHandler.h" -#include "../../../lib/CBuildingHandler.h" #include "../Goals/AbstractGoal.h" namespace NKAI diff --git a/AI/Nullkiller/Pathfinding/AINodeStorage.cpp b/AI/Nullkiller/Pathfinding/AINodeStorage.cpp index 5f0fda82f..a40fbd7d2 100644 --- a/AI/Nullkiller/Pathfinding/AINodeStorage.cpp +++ b/AI/Nullkiller/Pathfinding/AINodeStorage.cpp @@ -10,6 +10,7 @@ #include "StdInc.h" #include "AINodeStorage.h" #include "Actions/TownPortalAction.h" +#include "Actions/WhirlpoolAction.h" #include "../Goals/Goals.h" #include "../AIGateway.h" #include "../Engine/Nullkiller.h" @@ -25,10 +26,10 @@ namespace NKAI { std::shared_ptr> AISharedStorage::shared; -uint64_t AISharedStorage::version = 0; +uint32_t AISharedStorage::version = 0; boost::mutex AISharedStorage::locker; -std::set commitedTiles; -std::set commitedTilesInitial; +std::set committedTiles; +std::set committedTilesInitial; const uint64_t FirstActorMask = 1; @@ -36,19 +37,19 @@ const uint64_t MIN_ARMY_STRENGTH_FOR_CHAIN = 5000; const uint64_t MIN_ARMY_STRENGTH_FOR_NEXT_ACTOR = 1000; const uint64_t CHAIN_MAX_DEPTH = 4; -const bool DO_NOT_SAVE_TO_COMMITED_TILES = false; +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]; @@ -91,13 +92,21 @@ void AIPathNode::addSpecialAction(std::shared_ptr action) } } -AINodeStorage::AINodeStorage(const Nullkiller * ai, const int3 & Sizes) - : sizes(Sizes), ai(ai), cb(ai->cb.get()), nodes(Sizes) +int AINodeStorage::getBucketCount() const { - accesibility = std::make_unique>( - boost::extents[sizes.z][sizes.x][sizes.y][EPathfindingLayer::NUM_LAYERS]); + return ai->settings->getPathfinderBucketsCount(); +} - dangerEvaluator.reset(new FuzzyHelper(ai)); +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, ai->settings->getPathfinderBucketSize() * ai->settings->getPathfinderBucketsCount()) +{ + accessibility = std::make_unique>( + boost::extents[sizes.z][sizes.x][sizes.y][EPathfindingLayer::NUM_LAYERS]); } AINodeStorage::~AINodeStorage() = default; @@ -131,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) @@ -157,7 +166,7 @@ void AINodeStorage::initialize(const PathfinderOptions & options, const CGameSta void AINodeStorage::clear() { actors.clear(); - commitedTiles.clear(); + committedTiles.clear(); heroChainPass = EHeroChainPass::INITIAL; heroChainTurn = 0; heroChainMaxTurns = 1; @@ -170,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)) @@ -179,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]; @@ -224,7 +233,6 @@ std::vector AINodeStorage::getInitialNodes() AIPathNode * initialNode = allocated.value(); - initialNode->inPQ = false; initialNode->pq = nullptr; initialNode->turns = actor->initialTurn; initialNode->moveRemains = actor->initialMovement; @@ -256,10 +264,45 @@ void AINodeStorage::commit(CDestinationNodeInfo & destination, const PathNodeInf { commit(dstNode, srcNode, destination.action, destination.turn, destination.movementLeft, destination.cost); - if(srcNode->specialAction || srcNode->chainOther) + // regular pathfinder can not go directly through whirlpool + bool isWhirlpoolTeleport = destination.nodeObject + && destination.nodeObject->ID == Obj::WHIRLPOOL; + + if(srcNode->specialAction + || srcNode->chainOther + || isWhirlpoolTeleport) { // there is some action on source tile which should be performed before we can bypass it - destination.node->theNodeBefore = source.node; + dstNode->theNodeBefore = source.node; + + if(isWhirlpoolTeleport) + { + if(dstNode->actor->creatureSet->Slots().size() == 1 + && dstNode->actor->creatureSet->Slots().begin()->second->getCount() == 1) + { + return; + } + + auto weakest = vstd::minElementByFun(dstNode->actor->creatureSet->Slots(), [](std::pair pair) -> int + { + return pair.second->getCount() * pair.second->getCreatureID().toCreature()->getAIValue(); + }); + + if(weakest == dstNode->actor->creatureSet->Slots().end()) + { + logAi->debug("Empty army entering whirlpool detected at tile %s", dstNode->coord.toString()); + destination.blocked = true; + + return; + } + + if(dstNode->actor->creatureSet->getFreeSlots().size()) + dstNode->armyLoss += weakest->second->getCreatureID().toCreature()->getAIValue(); + else + dstNode->armyLoss += (weakest->second->getCount() + 1) / 2 * weakest->second->getCreatureID().toCreature()->getAIValue(); + + dstNode->specialAction = AIPathfinding::WhirlpoolAction::instance; + } } if(dstNode->specialAction && dstNode->actor) @@ -276,7 +319,7 @@ void AINodeStorage::commit( int turn, int movementLeft, float cost, - bool saveToCommited) const + bool saveToCommitted) const { destination->action = action; destination->setCost(cost); @@ -290,7 +333,7 @@ void AINodeStorage::commit( #if NKAI_PATHFINDER_TRACE_LEVEL >= 2 logAi->trace( - "Commited %s -> %s, layer: %d, cost: %f, turn: %s, mp: %d, hero: %s, mask: %x, army: %lld", + "Committed %s -> %s, layer: %d, cost: %f, turn: %s, mp: %d, hero: %s, mask: %x, army: %lld", source->coord.toString(), destination->coord.toString(), destination->layer, @@ -302,9 +345,9 @@ void AINodeStorage::commit( destination->actor->armyValue); #endif - if(saveToCommited && destination->turns <= heroChainTurn) + if(saveToCommitted && destination->turns <= heroChainTurn) { - commitedTiles.insert(destination->coord); + committedTiles.insert(destination->coord); } if(destination->turns == source->turns) @@ -374,7 +417,7 @@ bool AINodeStorage::increaseHeroChainTurnLimit() return false; heroChainTurn++; - commitedTiles.clear(); + committedTiles.clear(); for(auto layer : phisycalLayers) { @@ -384,7 +427,7 @@ bool AINodeStorage::increaseHeroChainTurnLimit() { if(node.turns <= heroChainTurn && node.action != EPathNodeAction::UNKNOWN) { - commitedTiles.insert(pos); + committedTiles.insert(pos); return true; } @@ -453,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) @@ -545,7 +588,7 @@ bool AINodeStorage::calculateHeroChain() heroChainPass = EHeroChainPass::CHAIN; heroChain.clear(); - std::vector data(commitedTiles.begin(), commitedTiles.end()); + std::vector data(committedTiles.begin(), committedTiles.end()); if(data.size() > 100) { @@ -576,7 +619,7 @@ bool AINodeStorage::calculateHeroChain() task.flushResult(heroChain); } - commitedTiles.clear(); + committedTiles.clear(); return !heroChain.empty(); } @@ -592,7 +635,7 @@ bool AINodeStorage::selectFirstActor() }); chainMask = strongest->chainMask; - commitedTilesInitial = commitedTiles; + committedTilesInitial = committedTiles; return true; } @@ -627,7 +670,7 @@ bool AINodeStorage::selectNextActor() return false; chainMask = nextActor->get()->chainMask; - commitedTiles = commitedTilesInitial; + committedTiles = committedTilesInitial; return true; } @@ -654,7 +697,7 @@ void HeroChainCalculationTask::cleanupInefectiveChains(std::vectortrace( - "Skip exchange %s[%x] -> %s[%x] at %s is ineficient", + "Skip exchange %s[%x] -> %s[%x] at %s is inefficient", chainInfo.otherParent->actor->toString(), chainInfo.otherParent->actor->chainMask, chainInfo.carrierParent->actor->toString(), @@ -686,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; @@ -754,7 +798,7 @@ void HeroChainCalculationTask::calculateHeroChain( if(hasLessMp && hasLessExperience) { #if NKAI_PATHFINDER_TRACE_LEVEL >= 2 - logAi->trace("Exchange at %s is ineficient. Blocked.", carrier->coord.toString()); + logAi->trace("Exchange at %s is inefficient. Blocked.", carrier->coord.toString()); #endif return; } @@ -823,7 +867,7 @@ void HeroChainCalculationTask::addHeroChain(const std::vector chainInfo.turns, chainInfo.moveRemains, chainInfo.getCost(), - DO_NOT_SAVE_TO_COMMITED_TILES); + DO_NOT_SAVE_TO_COMMITTED_TILES); if(carrier->specialAction || carrier->chainOther) { @@ -928,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; } @@ -1015,8 +1059,8 @@ std::vector AINodeStorage::calculateTeleportations( for(auto & neighbour : accessibleExits) { - auto node = getOrCreateNode(neighbour, source.node->layer, srcNode->actor); - + std::optional node = getOrCreateNode(neighbour, source.node->layer, srcNode->actor); + if(!node) continue; @@ -1027,7 +1071,7 @@ std::vector AINodeStorage::calculateTeleportations( return neighbours; } -struct TowmPortalFinder +struct TownPortalFinder { const std::vector & initialNodes; MasteryLevel::Type townPortalSkillLevel; @@ -1040,7 +1084,7 @@ struct TowmPortalFinder SpellID spellID; const CSpell * townPortal; - TowmPortalFinder( + TownPortalFinder( const ChainActor * actor, const std::vector & initialNodes, std::vector targetTowns, @@ -1117,7 +1161,7 @@ struct TowmPortalFinder bestNode->turns, bestNode->moveRemains - movementNeeded, movementCost, - DO_NOT_SAVE_TO_COMMITED_TILES); + DO_NOT_SAVE_TO_COMMITTED_TILES); node->theNodeBefore = bestNode; node->addSpecialAction(std::make_shared(targetTown)); @@ -1146,7 +1190,7 @@ void AINodeStorage::calculateTownPortal( return; // no towns no need to run loop further } - TowmPortalFinder townPortalFinder(actor, initialNodes, towns, this); + TownPortalFinder townPortalFinder(actor, initialNodes, towns, this); if(townPortalFinder.actorCanCastTownPortal()) { @@ -1163,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) @@ -1279,7 +1328,7 @@ bool AINodeStorage::isOtherChainBetter( { #if NKAI_PATHFINDER_TRACE_LEVEL >= 2 logAi->trace( - "Block ineficient battle move %s->%s, hero: %s[%X], army %lld, mp diff: %i", + "Block inefficient battle move %s->%s, hero: %s[%X], army %lld, mp diff: %i", source->coord.toString(), candidateNode.coord.toString(), candidateNode.actor->hero->getNameTranslated(), @@ -1303,7 +1352,7 @@ bool AINodeStorage::isOtherChainBetter( { #if NKAI_PATHFINDER_TRACE_LEVEL >= 2 logAi->trace( - "Block ineficient move because of stronger army %s->%s, hero: %s[%X], army %lld, mp diff: %i", + "Block inefficient move because of stronger army %s->%s, hero: %s[%X], army %lld, mp diff: %i", source->coord.toString(), candidateNode.coord.toString(), candidateNode.actor->hero->getNameTranslated(), @@ -1329,7 +1378,7 @@ bool AINodeStorage::isOtherChainBetter( #if NKAI_PATHFINDER_TRACE_LEVEL >= 2 logAi->trace( - "Block ineficient move because of stronger hero %s->%s, hero: %s[%X], army %lld, mp diff: %i", + "Block inefficient move because of stronger hero %s->%s, hero: %s[%X], army %lld, mp diff: %i", source->coord.toString(), candidateNode.coord.toString(), candidateNode.actor->hero->getNameTranslated(), @@ -1384,7 +1433,33 @@ void AINodeStorage::calculateChainInfo(std::vector & paths, const int3 & path.targetHero = node.actor->hero; path.heroArmy = node.actor->creatureSet; path.armyLoss = node.armyLoss; - path.targetObjectDanger = evaluateDanger(pos, path.targetHero, !node.actor->allowBattle); + 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) + { + if(node.theNodeBefore) + { + auto prevNode = getAINode(node.theNodeBefore); + + if(node.coord == prevNode->coord && node.actor->hero == prevNode->actor->hero) + { + paths.pop_back(); + continue; + } + else + { + path.armyLoss = prevNode->armyLoss; + } + } + else + { + path.armyLoss = 0; + } + } path.targetObjectArmyLoss = evaluateArmyLoss( path.targetHero, @@ -1509,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 3ac849851..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; } @@ -44,14 +41,17 @@ enum DayFlags : ui8 struct AIPathNode : public CGPathNode { + std::shared_ptr specialAction; + + const AIPathNode * chainOther; + const ChainActor * actor; + uint64_t danger; uint64_t armyLoss; + uint32_t version; + int16_t manaCost; DayFlags dayFlags; - const AIPathNode * chainOther; - std::shared_ptr specialAction; - const ChainActor * actor; - uint64_t version; void addSpecialAction(std::shared_ptr action); @@ -152,9 +152,9 @@ class AISharedStorage std::shared_ptr> nodes; public: static boost::mutex locker; - static uint64_t version; + static uint32_t version; - AISharedStorage(int3 mapSize); + AISharedStorage(int3 sizes, int numChains); ~AISharedStorage(); STRONG_INLINE @@ -169,11 +169,10 @@ class AINodeStorage : public INodeStorage private: int3 sizes; - std::unique_ptr> accesibility; + std::unique_ptr> accessibility; const CPlayerSpecificInfoCallback * cb; const Nullkiller * ai; - std::unique_ptr dangerEvaluator; AISharedStorage nodes; std::vector> actors; std::vector heroChain; @@ -195,6 +194,9 @@ public: bool selectFirstActor(); bool selectNextActor(); + int getBucketCount() const; + int getBucketSize() const; + std::vector getInitialNodes() override; virtual void calculateNeighbours( @@ -218,7 +220,7 @@ public: int turn, int movementLeft, float cost, - bool saveToCommited = true) const; + bool saveToCommitted = true) const; inline const AIPathNode * getAINode(const CGPathNode * node) const { @@ -261,7 +263,7 @@ public: const AIPathNode & candidateNode, const AIPathNode & other) const; - bool isMovementIneficient(const PathNodeInfo & source, CDestinationNodeInfo & destination) const + bool isMovementInefficient(const PathNodeInfo & source, CDestinationNodeInfo & destination) const { return hasBetterChain(source, destination); } @@ -282,26 +284,21 @@ public: bool calculateHeroChain(); bool calculateHeroChainFinal(); - inline uint64_t evaluateDanger(const int3 & tile, const CGHeroInstance * hero, bool checkGuards) const - { - return dangerEvaluator->evaluateDanger(tile, hero, checkGuards); - } - uint64_t evaluateArmyLoss(const CGHeroInstance * hero, uint64_t armyValue, uint64_t danger) const; inline EPathAccessibility getAccessibility(const int3 & tile, EPathfindingLayer layer) const { - return (*this->accesibility)[tile.z][tile.x][tile.y][layer]; + return (*this->accessibility)[tile.z][tile.x][tile.y][layer]; } inline void resetTile(const int3 & tile, EPathfindingLayer layer, EPathAccessibility tileAccessibility) { - (*this->accesibility)[tile.z][tile.x][tile.y][layer] = tileAccessibility; + (*this->accessibility)[tile.z][tile.x][tile.y][layer] = tileAccessibility; } 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 d4a3a651a..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" @@ -44,10 +44,12 @@ namespace AIPathfinding Nullkiller * ai, std::shared_ptr nodeStorage, bool allowBypassObjects) - :PathfinderConfig(nodeStorage, makeRuleset(cb, ai, nodeStorage, allowBypassObjects)), aiNodeStorage(nodeStorage) + :PathfinderConfig(nodeStorage, cb, makeRuleset(cb, ai, nodeStorage, allowBypassObjects)), aiNodeStorage(nodeStorage) { options.canUseCast = true; options.allowLayerTransitioningAfterBattle = true; + options.useTeleportWhirlpool = true; + options.forceUseTeleportWhirlpool = true; } AIPathfinderConfig::~AIPathfinderConfig() = default; diff --git a/AI/Nullkiller/Pathfinding/Actions/WhirlpoolAction.cpp b/AI/Nullkiller/Pathfinding/Actions/WhirlpoolAction.cpp new file mode 100644 index 000000000..56625a1b3 --- /dev/null +++ b/AI/Nullkiller/Pathfinding/Actions/WhirlpoolAction.cpp @@ -0,0 +1,55 @@ +/* +* WhirlpoolAction.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 "../../Goals/AdventureSpellCast.h" +#include "../../../../lib/mapObjects/MapObjects.h" +#include "WhirlpoolAction.h" +#include "../../AIGateway.h" + +namespace NKAI +{ + +using namespace AIPathfinding; + +std::shared_ptr WhirlpoolAction::instance = std::make_shared(); + +void WhirlpoolAction::execute(AIGateway * ai, const CGHeroInstance * hero) const +{ + ai->nullkiller->armyFormation->rearrangeArmyForWhirlpool(hero); +} + +std::string WhirlpoolAction::toString() const +{ + return "Prepare for whirlpool"; +} +/* +bool TownPortalAction::canAct(const CGHeroInstance * hero, const AIPathNode * source) const +{ +#ifdef VCMI_TRACE_PATHFINDER + logAi->trace( + "Hero %s has %d mana and needed %d and already spent %d", + hero->name, + hero->mana, + getManaCost(hero), + source->manaCost); +#endif + + return hero->mana >= source->manaCost + getManaCost(hero); +} + +uint32_t TownPortalAction::getManaCost(const CGHeroInstance * hero) const +{ + SpellID summonBoat = SpellID::TOWN_PORTAL; + + return hero->getSpellCost(summonBoat.toSpell()); +}*/ + +} diff --git a/AI/Nullkiller/Pathfinding/Actions/WhirlpoolAction.h b/AI/Nullkiller/Pathfinding/Actions/WhirlpoolAction.h new file mode 100644 index 000000000..704b94b0f --- /dev/null +++ b/AI/Nullkiller/Pathfinding/Actions/WhirlpoolAction.h @@ -0,0 +1,35 @@ +/* +* WhirlpoolAction.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 "SpecialAction.h" +#include "../../../../lib/mapObjects/MapObjects.h" +#include "../../Goals/AdventureSpellCast.h" + +namespace NKAI +{ +namespace AIPathfinding +{ + class WhirlpoolAction : public SpecialAction + { + public: + WhirlpoolAction() + { + } + + static std::shared_ptr instance; + + void execute(AIGateway * ai, const CGHeroInstance * hero) const override; + + std::string toString() const override; + }; +} +} diff --git a/AI/Nullkiller/Pathfinding/Actors.cpp b/AI/Nullkiller/Pathfinding/Actors.cpp index cccd55a57..8db9230cc 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; } @@ -217,7 +217,7 @@ ExchangeResult HeroExchangeMap::tryExchangeNoLock(const ChainActor * other) ExchangeResult result; { - boost::shared_lock lock(sync, boost::try_to_lock); + boost::shared_lock lock(sync, boost::try_to_lock); if(!lock.owns_lock()) { @@ -237,7 +237,7 @@ ExchangeResult HeroExchangeMap::tryExchangeNoLock(const ChainActor * other) } { - boost::unique_lock uniqueLock(sync, boost::try_to_lock); + boost::unique_lock uniqueLock(sync, boost::try_to_lock); if(!uniqueLock.owns_lock()) { @@ -374,10 +374,12 @@ HeroExchangeArmy * HeroExchangeMap::tryUpgrade( for(auto & creatureToBuy : buyArmy) { auto targetSlot = target->getSlotFor(creatureToBuy.creID.toCreature()); - - target->addToSlot(targetSlot, creatureToBuy.creID, creatureToBuy.count); - target->armyCost += creatureToBuy.creID.toCreature()->getFullRecruitCost() * creatureToBuy.count; - target->requireBuyArmy = true; + if (targetSlot.validSlot()) + { + target->addToSlot(targetSlot, creatureToBuy.creID, creatureToBuy.count); + target->armyCost += creatureToBuy.creID.toCreature()->getFullRecruitCost() * creatureToBuy.count; + target->requireBuyArmy = true; + } } } @@ -440,7 +442,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/Nullkiller/Pathfinding/GraphPaths.cpp b/AI/Nullkiller/Pathfinding/GraphPaths.cpp index 150d37c1a..136e8491c 100644 --- a/AI/Nullkiller/Pathfinding/GraphPaths.cpp +++ b/AI/Nullkiller/Pathfinding/GraphPaths.cpp @@ -160,7 +160,7 @@ void GraphPaths::dumpToLog() const node.previous.coord.toString(), tile.first.toString(), node.cost, - node.danger); + node.linkDanger); } logBuilder.addLine(node.previous.coord, tile.first); @@ -169,14 +169,17 @@ void GraphPaths::dumpToLog() const }); } -bool GraphPathNode::tryUpdate(const GraphPathNodePointer & pos, const GraphPathNode & prev, const ObjectLink & link) +bool GraphPathNode::tryUpdate( + const GraphPathNodePointer & pos, + const GraphPathNode & prev, + const ObjectLink & link) { auto newCost = prev.cost + link.cost; if(newCost < cost) { previous = pos; - danger = prev.danger + link.danger; + linkDanger = link.danger; cost = newCost; return true; @@ -199,7 +202,7 @@ void GraphPaths::addChainInfo(std::vector & paths, int3 tile, const CGHe std::vector tilesToPass; - uint64_t danger = node.danger; + uint64_t danger = node.linkDanger; float cost = node.cost; bool allowBattle = false; @@ -212,13 +215,13 @@ void GraphPaths::addChainInfo(std::vector & paths, int3 tile, const CGHe if(currentTile == pathNodes.end()) break; - auto currentNode = currentTile->second[current.nodeType]; + auto & currentNode = currentTile->second[current.nodeType]; if(!currentNode.previous.valid()) break; allowBattle = allowBattle || currentNode.nodeType == GrapthPathNodeType::BATTLE; - vstd::amax(danger, currentNode.danger); + vstd::amax(danger, currentNode.linkDanger); vstd::amax(cost, currentNode.cost); tilesToPass.push_back(current); @@ -239,9 +242,13 @@ void GraphPaths::addChainInfo(std::vector & paths, int3 tile, const CGHe if(path.targetHero != hero) continue; - for(auto graphTile = tilesToPass.rbegin(); graphTile != tilesToPass.rend(); graphTile++) + uint64_t loss = 0; + uint64_t strength = getHeroArmyStrengthWithCommander(path.targetHero, path.heroArmy); + + for(auto graphTile = ++tilesToPass.rbegin(); graphTile != tilesToPass.rend(); graphTile++) { AIPathNodeInfo n; + auto & node = getNode(*graphTile); n.coord = graphTile->coord; n.cost = cost; @@ -249,7 +256,21 @@ void GraphPaths::addChainInfo(std::vector & paths, int3 tile, const CGHe n.danger = danger; n.targetHero = hero; n.parentIndex = -1; - n.specialAction = getNode(*graphTile).specialAction; + n.specialAction = node.specialAction; + + if(node.linkDanger > 0) + { + auto additionalLoss = ai->pathfinder->getStorage()->evaluateArmyLoss(path.targetHero, strength, node.linkDanger); + loss += additionalLoss; + + if(strength > additionalLoss) + strength -= additionalLoss; + else + { + strength = 0; + break; + } + } if(n.specialAction) { @@ -264,8 +285,13 @@ void GraphPaths::addChainInfo(std::vector & paths, int3 tile, const CGHe path.nodes.insert(path.nodes.begin(), n); } - path.armyLoss += ai->pathfinder->getStorage()->evaluateArmyLoss(path.targetHero, path.heroArmy->getArmyStrength(), danger); - path.targetObjectDanger = ai->pathfinder->getStorage()->evaluateDanger(tile, path.targetHero, !allowBattle); + if(strength == 0) + { + continue; + } + + path.armyLoss += loss; + path.targetObjectDanger = ai->dangerEvaluator->evaluateDanger(tile, path.targetHero, !allowBattle); path.targetObjectArmyLoss = ai->pathfinder->getStorage()->evaluateArmyLoss(path.targetHero, path.heroArmy->getArmyStrength(), path.targetObjectDanger); paths.push_back(path); @@ -287,7 +313,7 @@ void GraphPaths::quickAddChainInfoWithBlocker(std::vector & paths, int3 std::vector tilesToPass; - uint64_t danger = targetNode.danger; + uint64_t danger = targetNode.linkDanger; float cost = targetNode.cost; bool allowBattle = false; @@ -303,7 +329,7 @@ void GraphPaths::quickAddChainInfoWithBlocker(std::vector & paths, int3 auto currentNode = currentTile->second[current.nodeType]; allowBattle = allowBattle || currentNode.nodeType == GrapthPathNodeType::BATTLE; - vstd::amax(danger, currentNode.danger); + vstd::amax(danger, currentNode.linkDanger); vstd::amax(cost, currentNode.cost); tilesToPass.push_back(current); @@ -330,7 +356,7 @@ void GraphPaths::quickAddChainInfoWithBlocker(std::vector & paths, int3 path.heroArmy = entryPath.heroArmy; path.exchangeCount = entryPath.exchangeCount; path.armyLoss = entryPath.armyLoss + ai->pathfinder->getStorage()->evaluateArmyLoss(path.targetHero, path.heroArmy->getArmyStrength(), danger); - path.targetObjectDanger = ai->pathfinder->getStorage()->evaluateDanger(tile, path.targetHero, !allowBattle); + path.targetObjectDanger = ai->dangerEvaluator->evaluateDanger(tile, path.targetHero, !allowBattle); path.targetObjectArmyLoss = ai->pathfinder->getStorage()->evaluateArmyLoss(path.targetHero, path.heroArmy->getArmyStrength(), path.targetObjectDanger); AIPathNodeInfo n; @@ -341,7 +367,7 @@ void GraphPaths::quickAddChainInfoWithBlocker(std::vector & paths, int3 // final node n.coord = tile; n.cost = targetNode.cost; - n.danger = targetNode.danger; + n.danger = danger; n.parentIndex = path.nodes.size(); path.nodes.push_back(n); @@ -368,7 +394,7 @@ void GraphPaths::quickAddChainInfoWithBlocker(std::vector & paths, int3 n.coord = graphTile->coord; n.cost = node.cost; n.turns = static_cast(node.cost); - n.danger = node.danger; + n.danger = danger; n.specialAction = node.specialAction; n.parentIndex = path.nodes.size(); diff --git a/AI/Nullkiller/Pathfinding/GraphPaths.h b/AI/Nullkiller/Pathfinding/GraphPaths.h index 83167eabe..a41781f68 100644 --- a/AI/Nullkiller/Pathfinding/GraphPaths.h +++ b/AI/Nullkiller/Pathfinding/GraphPaths.h @@ -67,7 +67,7 @@ struct GraphPathNode GrapthPathNodeType nodeType = GrapthPathNodeType::NORMAL; GraphPathNodePointer previous; float cost = BAD_COST; - uint64_t danger = 0; + uint64_t linkDanger = 0; const CGObjectInstance * obj = nullptr; std::shared_ptr specialAction; diff --git a/AI/Nullkiller/Pathfinding/ObjectGraphCalculator.cpp b/AI/Nullkiller/Pathfinding/ObjectGraphCalculator.cpp index 539a93fda..8905572c9 100644 --- a/AI/Nullkiller/Pathfinding/ObjectGraphCalculator.cpp +++ b/AI/Nullkiller/Pathfinding/ObjectGraphCalculator.cpp @@ -164,7 +164,7 @@ void ObjectGraphCalculator::calculateConnections(const int3 & pos, std::vectorvisitablePos(); auto fromObj = actorObjectMap[path.targetHero]; - auto danger = ai->pathfinder->getStorage()->evaluateDanger(pos, path.targetHero, true); + auto danger = ai->dangerEvaluator->evaluateDanger(pos, path.targetHero, true); auto updated = target->tryAddConnection( from, pos, @@ -220,7 +220,7 @@ void ObjectGraphCalculator::calculateConnections(const int3 & pos, std::vectorpathfinder->getStorage()->evaluateDanger(pos2, path1.targetHero, true); + auto danger = ai->dangerEvaluator->evaluateDanger(pos2, path1.targetHero, true); auto updated = target->tryAddConnection( pos1, @@ -321,7 +321,7 @@ void ObjectGraphCalculator::addObjectActor(const CGObjectInstance * obj) void ObjectGraphCalculator::addJunctionActor(const int3 & visitablePos, bool isVirtualBoat) { - std::lock_guard lock(syncLock); + std::lock_guard lock(syncLock); auto internalCb = temporaryActorHeroes.front()->cb; auto objectActor = temporaryActorHeroes.emplace_back(std::make_unique(internalCb)).get(); diff --git a/AI/Nullkiller/Pathfinding/Rules/AILayerTransitionRule.cpp b/AI/Nullkiller/Pathfinding/Rules/AILayerTransitionRule.cpp index f38286ee0..8d95d6224 100644 --- a/AI/Nullkiller/Pathfinding/Rules/AILayerTransitionRule.cpp +++ b/AI/Nullkiller/Pathfinding/Rules/AILayerTransitionRule.cpp @@ -164,7 +164,7 @@ namespace AIPathfinding if(hero->canCastThisSpell(summonBoatSpell) && hero->getSpellSchoolLevel(summonBoatSpell) >= MasteryLevel::ADVANCED) { - // TODO: For lower school level we might need to check the existance of some boat + // TODO: For lower school level we might need to check the existence of some boat summonableVirtualBoats[hero] = std::make_shared(); } } diff --git a/AI/Nullkiller/Pathfinding/Rules/AIMovementAfterDestinationRule.cpp b/AI/Nullkiller/Pathfinding/Rules/AIMovementAfterDestinationRule.cpp index 2acfe0bd9..ebb482a1a 100644 --- a/AI/Nullkiller/Pathfinding/Rules/AIMovementAfterDestinationRule.cpp +++ b/AI/Nullkiller/Pathfinding/Rules/AIMovementAfterDestinationRule.cpp @@ -11,9 +11,11 @@ #include "AIMovementAfterDestinationRule.h" #include "../Actions/BattleAction.h" #include "../Actions/QuestAction.h" +#include "../Actions/WhirlpoolAction.h" #include "../../Goals/Invalid.h" #include "AIPreviousNodeRule.h" #include "../../../../lib/pathfinder/PathfinderOptions.h" +#include "../../../../lib/pathfinder/CPathfinder.h" namespace NKAI { @@ -34,7 +36,7 @@ namespace AIPathfinding const PathfinderConfig * pathfinderConfig, CPathfinderHelper * pathfinderHelper) const { - if(nodeStorage->isMovementIneficient(source, destination)) + if(nodeStorage->isMovementInefficient(source, destination)) { destination.node->locked = true; destination.blocked = true; @@ -225,7 +227,7 @@ namespace AIPathfinding return false; } - auto danger = nodeStorage->evaluateDanger(destination.coord, nodeStorage->getHero(destination.node), true); + auto danger = ai->dangerEvaluator->evaluateDanger(destination.coord, nodeStorage->getHero(destination.node), true); if(danger) { @@ -311,7 +313,7 @@ namespace AIPathfinding } auto hero = nodeStorage->getHero(source.node); - uint64_t danger = nodeStorage->evaluateDanger(destination.coord, hero, true); + uint64_t danger = ai->dangerEvaluator->evaluateDanger(destination.coord, hero, true); uint64_t actualArmyValue = srcNode->actor->armyValue - srcNode->armyLoss; uint64_t loss = nodeStorage->evaluateArmyLoss(hero, actualArmyValue, danger); diff --git a/AI/StupidAI/StupidAI.cbp b/AI/StupidAI/StupidAI.cbp deleted file mode 100644 index c11e07b2e..000000000 --- a/AI/StupidAI/StupidAI.cbp +++ /dev/null @@ -1,84 +0,0 @@ - - - - - - diff --git a/AI/StupidAI/StupidAI.cpp b/AI/StupidAI/StupidAI.cpp index 3c152de13..53919bb70 100644 --- a/AI/StupidAI/StupidAI.cpp +++ b/AI/StupidAI/StupidAI.cpp @@ -18,7 +18,7 @@ #include "../../lib/CRandomGenerator.h" CStupidAI::CStupidAI() - : side(-1) + : side(BattleSide::NONE) , wasWaitingForRealize(false) , wasUnlockingGs(false) { @@ -262,7 +262,7 @@ void CStupidAI::battleStacksEffectsSet(const BattleID & battleID, const SetStack print("battleStacksEffectsSet called"); } -void CStupidAI::battleStart(const BattleID & battleID, const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool Side, bool replayAllowed) +void CStupidAI::battleStart(const BattleID & battleID, const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, BattleSide Side, bool replayAllowed) { print("battleStart called"); side = Side; @@ -296,7 +296,11 @@ BattleAction CStupidAI::goTowards(const BattleID & battleID, const CStack * stac for(auto hex : hexes) { if(vstd::contains(avHexes, hex)) + { + if(stack->position == hex) + return BattleAction::makeDefend(stack); return BattleAction::makeMove(stack, hex); + } if(stack->coversPos(hex)) { @@ -336,7 +340,11 @@ BattleAction CStupidAI::goTowards(const BattleID & battleID, const CStack * stac } if(vstd::contains(avHexes, currentDest)) + { + if(stack->position == currentDest) + return BattleAction::makeDefend(stack); return BattleAction::makeMove(stack, currentDest); + } currentDest = reachability.predecessors[currentDest]; } diff --git a/AI/StupidAI/StupidAI.h b/AI/StupidAI/StupidAI.h index 7c81a864c..302bd5590 100644 --- a/AI/StupidAI/StupidAI.h +++ b/AI/StupidAI/StupidAI.h @@ -17,7 +17,7 @@ class EnemyInfo; class CStupidAI : public CBattleGameInterface { - int side; + BattleSide side; std::shared_ptr cb; std::shared_ptr env; @@ -47,7 +47,7 @@ public: void battleSpellCast(const BattleID & battleID, const BattleSpellCast *sc) override; void battleStacksEffectsSet(const BattleID & battleID, const SetStackEffect & sse) override;//called when a specific effect is set to stacks //void battleTriggerEffect(const BattleTriggerEffect & bte) override; - void battleStart(const BattleID & battleID, const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool side, bool replayAllowed) override; //called by engine when battle starts; side=0 - left, side=1 - right + void battleStart(const BattleID & battleID, const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, BattleSide side, bool replayAllowed) override; //called by engine when battle starts; side=0 - left, side=1 - right void battleCatapultAttacked(const BattleID & battleID, const CatapultAttack & ca) override; //called when catapult makes an attack private: diff --git a/AI/StupidAI/StupidAI.vcxproj b/AI/StupidAI/StupidAI.vcxproj deleted file mode 100644 index 3cfcafb70..000000000 --- a/AI/StupidAI/StupidAI.vcxproj +++ /dev/null @@ -1,152 +0,0 @@ - - - - - Debug - Win32 - - - Debug - x64 - - - RD - Win32 - - - RD - x64 - - - - {15DABC90-234A-4B6B-9EEB-777C4768B82B} - StupidAI - 10.0 - - - - DynamicLibrary - true - MultiByte - v140_xp - - - DynamicLibrary - true - MultiByte - v140_xp - - - DynamicLibrary - false - true - MultiByte - v142 - - - DynamicLibrary - false - true - MultiByte - v140_xp - - - - - - - - - - - - - - - - - - - - - - - - - - - $(VCMI_Out)\AI\ - - - $(VCMI_Out)\AI\ - - - $(VCMI_Out)/AI - - - $(VCMI_Out)\AI\ - - - - Use - StdInc.h - /Zm150 %(AdditionalOptions) - - - VCMI_lib.lib;%(AdditionalDependencies) - -Zm150 %(AdditionalOptions) - ..\..\..\libs;..\..;.. - - - - - Use - StdInc.h - /Zm150 %(AdditionalOptions) - - - VCMI_lib.lib;%(AdditionalDependencies) - - - - - Use - StdInc.h - true - - - VCMI_lib.lib;%(AdditionalDependencies) - $(VCMI_Out) - - - - - Use - StdInc.h - /Zm150 %(AdditionalOptions) - - - VCMI_lib.lib;%(AdditionalDependencies) - - - - - - %(PreprocessorDefinitions) - Create - StdInc.h - Create - Create - Create - - - - - - - - - - - - \ No newline at end of file diff --git a/AI/VCAI/AIUtility.cpp b/AI/VCAI/AIUtility.cpp index ac68fa71b..ff391587f 100644 --- a/AI/VCAI/AIUtility.cpp +++ b/AI/VCAI/AIUtility.cpp @@ -15,8 +15,6 @@ #include "../../lib/UnlockGuard.h" #include "../../lib/CConfigHandler.h" -#include "../../lib/CHeroHandler.h" -#include "../../lib/mapObjects/CBank.h" #include "../../lib/mapObjects/CGTownInstance.h" #include "../../lib/mapObjects/CQuest.h" #include "../../lib/mapping/CMapDefines.h" @@ -188,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; } @@ -249,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 37a007b9a..c1a19d858 100644 --- a/AI/VCAI/AIUtility.h +++ b/AI/VCAI/AIUtility.h @@ -10,9 +10,7 @@ #pragma once #include "../../lib/VCMI_Lib.h" -#include "../../lib/CBuildingHandler.h" #include "../../lib/CCreatureHandler.h" -#include "../../lib/CTownHandler.h" #include "../../lib/spells/CSpellHandler.h" #include "../../lib/CStopWatch.h" #include "../../lib/mapObjects/CGHeroInstance.h" @@ -27,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; @@ -68,14 +64,6 @@ public: const CGHeroInstance * get(bool doWeExpectNull = false) const; bool validAndSet() const; - - - template void serialize(Handler & h) - { - h & this->h; - h & hid; - h & name; - } }; enum BattleState @@ -100,12 +88,6 @@ struct ObjectIdRef ObjectIdRef(const CGObjectInstance * obj); bool operator<(const ObjectIdRef & rhs) const; - - - template void serialize(Handler & h) - { - h & id; - } }; struct TimeCheck 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/ArmyManager.h b/AI/VCAI/ArmyManager.h index 5e29b41b1..b81b7da0c 100644 --- a/AI/VCAI/ArmyManager.h +++ b/AI/VCAI/ArmyManager.h @@ -14,8 +14,6 @@ #include "../../lib/GameConstants.h" #include "../../lib/VCMI_Lib.h" -#include "../../lib/CTownHandler.h" -#include "../../lib/CBuildingHandler.h" #include "VCAI.h" struct SlotInfo diff --git a/AI/VCAI/BuildingManager.cpp b/AI/VCAI/BuildingManager.cpp index f791a594f..60d971086 100644 --- a/AI/VCAI/BuildingManager.cpp +++ b/AI/VCAI/BuildingManager.cpp @@ -13,6 +13,7 @@ #include "../../CCallback.h" #include "../../lib/mapObjects/MapObjects.h" +#include "../../lib/entities/building/CBuilding.h" bool BuildingManager::tryBuildThisStructure(const CGTownInstance * t, BuildingID building, unsigned int maxDays) { @@ -22,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) { @@ -50,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) @@ -142,9 +143,9 @@ static const std::vector basicGoldSource = { BuildingID::TOWN_HALL, static const std::vector defence = { BuildingID::FORT, BuildingID::CITADEL, BuildingID::CASTLE }; static const std::vector capitolAndRequirements = { BuildingID::FORT, BuildingID::CITADEL, BuildingID::CASTLE, BuildingID::CAPITOL }; static const std::vector unitsSource = { BuildingID::DWELL_LVL_1, 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_4, BuildingID::DWELL_LVL_5, BuildingID::DWELL_LVL_6, BuildingID::DWELL_LVL_7, BuildingID::DWELL_LVL_8 }; static const std::vector unitsUpgrade = { BuildingID::DWELL_LVL_1_UP, 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_4_UP, BuildingID::DWELL_LVL_5_UP, BuildingID::DWELL_LVL_6_UP, BuildingID::DWELL_LVL_7_UP, BuildingID::DWELL_LVL_8_UP }; static const std::vector unitGrowth = { BuildingID::HORDE_1, BuildingID::HORDE_1_UPGR, BuildingID::HORDE_2, BuildingID::HORDE_2_UPGR }; static const std::vector _spells = { BuildingID::MAGES_GUILD_1, BuildingID::MAGES_GUILD_2, BuildingID::MAGES_GUILD_3, BuildingID::MAGES_GUILD_4, BuildingID::MAGES_GUILD_5 }; @@ -195,7 +196,7 @@ bool BuildingManager::getBuildingOptions(const CGTownInstance * t) return true; //workaround for mantis #2696 - build capitol with separate algorithm if it is available - if(vstd::contains(t->builtBuildings, BuildingID::CITY_HALL) && getMaxPossibleGoldBuilding(t) == BuildingID::CAPITOL) + if(t->hasBuilt(BuildingID::CITY_HALL) && getMaxPossibleGoldBuilding(t) == BuildingID::CAPITOL) { if(tryBuildNextStructure(t, capitolAndRequirements)) return true; @@ -219,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/BuildingManager.h b/AI/VCAI/BuildingManager.h index 3eb88605e..d8199fc5c 100644 --- a/AI/VCAI/BuildingManager.h +++ b/AI/VCAI/BuildingManager.h @@ -14,8 +14,6 @@ #include "../../lib/GameConstants.h" #include "../../lib/VCMI_Lib.h" -#include "../../lib/CTownHandler.h" -#include "../../lib/CBuildingHandler.h" #include "VCAI.h" struct DLL_EXPORT PotentialBuilding diff --git a/AI/VCAI/FuzzyEngines.cpp b/AI/VCAI/FuzzyEngines.cpp index 95c3dc4a9..05db1b15a 100644 --- a/AI/VCAI/FuzzyEngines.cpp +++ b/AI/VCAI/FuzzyEngines.cpp @@ -219,12 +219,6 @@ float TacticalAdvantageEngine::getTacticalAdvantage(const CArmedInstance * we, c enemyFlyers->setValue(enemyStructure.flyers); enemySpeed->setValue(enemyStructure.maxSpeed); - bool bank = dynamic_cast(enemy); - if(bank) - bankPresent->setValue(1); - else - bankPresent->setValue(0); - const CGTownInstance * fort = dynamic_cast(enemy); if(fort) castleWalls->setValue(fort->fortLevel()); diff --git a/AI/VCAI/FuzzyHelper.cpp b/AI/VCAI/FuzzyHelper.cpp index 776c3b98f..6293851e4 100644 --- a/AI/VCAI/FuzzyHelper.cpp +++ b/AI/VCAI/FuzzyHelper.cpp @@ -16,7 +16,6 @@ #include "../../lib/mapObjectConstructors/AObjectTypeHandler.h" #include "../../lib/mapObjectConstructors/CObjectClassesHandler.h" #include "../../lib/mapObjectConstructors/CBankInstanceConstructor.h" -#include "../../lib/mapObjects/CBank.h" #include "../../lib/mapObjects/CGCreature.h" #include "../../lib/mapObjects/CGDwelling.h" #include "../../lib/gameState/InfoAboutArmy.h" @@ -62,25 +61,6 @@ Goals::TSubgoal FuzzyHelper::chooseSolution(Goals::TGoalVec vec) return result; } -ui64 FuzzyHelper::estimateBankDanger(const CBank * bank) -{ - //this one is not fuzzy anymore, just calculate weighted average - - auto objectInfo = bank->getObjectHandler()->getObjectInfo(bank->appearance); - - CBankInfo * bankInfo = dynamic_cast(objectInfo.get()); - - ui64 totalStrength = 0; - ui8 totalChance = 0; - for(auto config : bankInfo->getPossibleGuards(bank->cb)) - { - totalStrength += config.second.totalStrength * config.first; - totalChance += config.first; - } - return totalStrength / std::max(totalChance, 1); //avoid division by zero - -} - float FuzzyHelper::evaluate(Goals::VisitTile & g) { if(g.parent) @@ -301,32 +281,13 @@ ui64 FuzzyHelper::evaluateDanger(const CGObjectInstance * obj, const VCAI * ai) cb->getTownInfo(obj, iat); return iat.army.getStrength(); } - case Obj::MONSTER: - { - //TODO!!!!!!!! - const CGCreature * cre = dynamic_cast(obj); - return cre->getArmyStrength(); - } - case Obj::CREATURE_GENERATOR1: - case Obj::CREATURE_GENERATOR4: - { - const CGDwelling * d = dynamic_cast(obj); - return d->getArmyStrength(); - } - case Obj::MINE: - case Obj::ABANDONED_MINE: + default: { const CArmedInstance * a = dynamic_cast(obj); - return a->getArmyStrength(); + if (a) + return a->getArmyStrength(); + else + return 0; } - case Obj::CRYPT: //crypt - case Obj::CREATURE_BANK: //crebank - case Obj::DRAGON_UTOPIA: - case Obj::SHIPWRECK: //shipwreck - case Obj::DERELICT_SHIP: //derelict ship - case Obj::PYRAMID: - return estimateBankDanger(dynamic_cast(obj)); - default: - return 0; } } diff --git a/AI/VCAI/FuzzyHelper.h b/AI/VCAI/FuzzyHelper.h index 7542e7f70..dc0709a67 100644 --- a/AI/VCAI/FuzzyHelper.h +++ b/AI/VCAI/FuzzyHelper.h @@ -10,12 +10,6 @@ #pragma once #include "FuzzyEngines.h" -VCMI_LIB_NAMESPACE_BEGIN - -class CBank; - -VCMI_LIB_NAMESPACE_END - class DLL_EXPORT FuzzyHelper { public: @@ -42,8 +36,6 @@ public: float evaluate(Goals::AbstractGoal & g); void setPriority(Goals::TSubgoal & g); - ui64 estimateBankDanger(const CBank * bank); //TODO: move to another class? - Goals::TSubgoal chooseSolution(Goals::TGoalVec vec); //std::shared_ptr chooseSolution (std::vector> & vec); diff --git a/AI/VCAI/Goals/AbstractGoal.h b/AI/VCAI/Goals/AbstractGoal.h index 5dc130682..6aa77c3d5 100644 --- a/AI/VCAI/Goals/AbstractGoal.h +++ b/AI/VCAI/Goals/AbstractGoal.h @@ -10,9 +10,7 @@ #pragma once #include "../../../lib/VCMI_Lib.h" -#include "../../../lib/CBuildingHandler.h" #include "../../../lib/CCreatureHandler.h" -#include "../../../lib/CTownHandler.h" #include "../AIUtility.h" struct HeroPtr; @@ -73,7 +71,6 @@ namespace Goals public: bool operator==(const TSubgoal & rhs) const; bool operator<(const TSubgoal & rhs) const; - //TODO: serialize? }; using TGoalVec = std::vector; @@ -175,21 +172,5 @@ namespace Goals // { // return !(*this == g); // } - - template void serialize(Handler & h) - { - h & goalType; - h & isElementar; - h & isAbstract; - h & priority; - h & value; - h & resID; - h & objid; - h & aid; - h & tile; - h & hero; - h & town; - h & bid; - } }; } diff --git a/AI/VCAI/Goals/BuildThis.cpp b/AI/VCAI/Goals/BuildThis.cpp index 378db7283..62e3a1649 100644 --- a/AI/VCAI/Goals/BuildThis.cpp +++ b/AI/VCAI/Goals/BuildThis.cpp @@ -17,6 +17,7 @@ #include "../BuildingManager.h" #include "../../../lib/mapObjects/CGTownInstance.h" #include "../../../lib/constants/StringConstants.h" +#include "../../../lib/entities/building/CBuilding.h" using namespace Goals; @@ -41,7 +42,7 @@ TSubgoal BuildThis::whatToDoToAchieve() { case EBuildingState::ALLOWED: town = candidateTown; - break; //TODO: look for prerequisites? this is not our reponsibility + break; //TODO: look for prerequisites? this is not our responsibility default: continue; } @@ -55,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/CGoal.h b/AI/VCAI/Goals/CGoal.h index b2f20dc77..7332277b7 100644 --- a/AI/VCAI/Goals/CGoal.h +++ b/AI/VCAI/Goals/CGoal.h @@ -70,12 +70,6 @@ namespace Goals return ptr; } - template void serialize(Handler & h) - { - h & static_cast(*this); - //h & goalType & isElementar & isAbstract & priority; - //h & value & resID & objid & aid & tile & hero & town & bid; - } bool operator==(const AbstractGoal & g) const override { diff --git a/AI/VCAI/Goals/ClearWayTo.cpp b/AI/VCAI/Goals/ClearWayTo.cpp index ddea93732..328c4711d 100644 --- a/AI/VCAI/Goals/ClearWayTo.cpp +++ b/AI/VCAI/Goals/ClearWayTo.cpp @@ -73,7 +73,7 @@ TGoalVec ClearWayTo::getAllPossibleSubgoals() if(ret.empty()) { logAi->warn("There is no known way to clear the way to tile %s", tile.toString()); - throw goalFulfilledException(sptr(ClearWayTo(tile))); //make sure asigned hero gets unlocked + throw goalFulfilledException(sptr(ClearWayTo(tile))); //make sure assigned hero gets unlocked } return ret; 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/Explore.cpp b/AI/VCAI/Goals/Explore.cpp index 4c25c74c0..294103598 100644 --- a/AI/VCAI/Goals/Explore.cpp +++ b/AI/VCAI/Goals/Explore.cpp @@ -53,7 +53,7 @@ namespace Goals { int3 tile = int3(0, 0, ourPos.z); - const auto & slice = (*(ts->fogOfWarMap))[ourPos.z]; + const auto & slice = ts->fogOfWarMap[ourPos.z]; for(tile.x = ourPos.x - scanRadius; tile.x <= ourPos.x + scanRadius; tile.x++) { @@ -81,13 +81,13 @@ namespace Goals foreach_tile_pos([&](const int3 & pos) { - if((*(ts->fogOfWarMap))[pos.z][pos.x][pos.y]) + if(ts->fogOfWarMap[pos.z][pos.x][pos.y]) { bool hasInvisibleNeighbor = false; foreach_neighbour(cbp, pos, [&](CCallback * cbp, int3 neighbour) { - if(!(*(ts->fogOfWarMap))[neighbour.z][neighbour.x][neighbour.y]) + if(!ts->fogOfWarMap[neighbour.z][neighbour.x][neighbour.y]) { hasInvisibleNeighbor = true; } @@ -171,7 +171,7 @@ namespace Goals { foreach_neighbour(cbp, tile, [&](CCallback * cbp, int3 neighbour) { - if((*(ts->fogOfWarMap))[neighbour.z][neighbour.x][neighbour.y]) + if(ts->fogOfWarMap[neighbour.z][neighbour.x][neighbour.y]) { out.push_back(neighbour); } @@ -184,7 +184,7 @@ namespace Goals int ret = 0; int3 npos = int3(0, 0, pos.z); - const auto & slice = (*(ts->fogOfWarMap))[pos.z]; + const auto & slice = ts->fogOfWarMap[pos.z]; for(npos.x = pos.x - sightRadius; npos.x <= pos.x + sightRadius; npos.x++) { 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/GatherArmy.cpp b/AI/VCAI/Goals/GatherArmy.cpp index e6e0557f0..490e137df 100644 --- a/AI/VCAI/Goals/GatherArmy.cpp +++ b/AI/VCAI/Goals/GatherArmy.cpp @@ -152,8 +152,7 @@ TGoalVec GatherArmy::getAllPossibleSubgoals() { for(auto & creatureID : creLevel.second) { - auto creature = VLC->creh->objects[creatureID]; - if(ai->ah->freeResources().canAfford(creature->getFullRecruitCost())) + if(ai->ah->freeResources().canAfford(creatureID.toCreature()->getFullRecruitCost())) objs.push_back(obj); //TODO: reserve resources? } } diff --git a/AI/VCAI/Goals/GatherTroops.cpp b/AI/VCAI/Goals/GatherTroops.cpp index ed4e1a6c0..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 * GameConstants::CREATURES_PER_TOWN); + 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/Goals/VisitHero.cpp b/AI/VCAI/Goals/VisitHero.cpp index 5e57743fd..8f75ea636 100644 --- a/AI/VCAI/Goals/VisitHero.cpp +++ b/AI/VCAI/Goals/VisitHero.cpp @@ -53,7 +53,7 @@ TSubgoal VisitHero::whatToDoToAchieve() bool VisitHero::fulfillsMe(TSubgoal goal) { - //TODO: VisitObj shoudl not be used for heroes, but... + //TODO: VisitObj should not be used for heroes, but... if(goal->goalType == VISIT_TILE) { auto obj = cb->getObj(ObjectInstanceID(objid)); diff --git a/AI/VCAI/Goals/Win.cpp b/AI/VCAI/Goals/Win.cpp index d2ab613e3..01ec05c7a 100644 --- a/AI/VCAI/Goals/Win.cpp +++ b/AI/VCAI/Goals/Win.cpp @@ -74,7 +74,7 @@ TSubgoal Win::whatToDoToAchieve() case EventCondition::HAVE_BUILDING: { // TODO build other buildings apart from Grail - // goal.objectType = buidingID to build + // goal.objectType = buildingID to build // goal.object = optional, town in which building should be built // Represents "Improve town" condition from H3 (but unlike H3 it consists from 2 separate conditions) 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 d2a87874a..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) @@ -308,7 +308,7 @@ bool AINodeStorage::hasBetterChain(const PathNodeInfo & source, CDestinationNode { #ifdef VCMI_TRACE_PATHFINDER logAi->trace( - "Block ineficient move %s:->%s, mask=%i, mp diff: %i", + "Block inefficient move %s:->%s, mask=%i, mp diff: %i", source.coord.toString(), destination.coord.toString(), destinationNode->chainMask, diff --git a/AI/VCAI/Pathfinding/AIPathfinderConfig.cpp b/AI/VCAI/Pathfinding/AIPathfinderConfig.cpp index dc067db74..7ac1bff6e 100644 --- a/AI/VCAI/Pathfinding/AIPathfinderConfig.cpp +++ b/AI/VCAI/Pathfinding/AIPathfinderConfig.cpp @@ -39,7 +39,7 @@ namespace AIPathfinding CPlayerSpecificInfoCallback * cb, VCAI * ai, std::shared_ptr nodeStorage) - :PathfinderConfig(nodeStorage, makeRuleset(cb, ai, nodeStorage)), hero(nodeStorage->getHero()) + :PathfinderConfig(nodeStorage, cb, makeRuleset(cb, ai, nodeStorage)), hero(nodeStorage->getHero()) { options.ignoreGuards = false; options.useEmbarkAndDisembark = true; diff --git a/AI/VCAI/Pathfinding/Rules/AILayerTransitionRule.cpp b/AI/VCAI/Pathfinding/Rules/AILayerTransitionRule.cpp index 4e857df98..d7b044ffd 100644 --- a/AI/VCAI/Pathfinding/Rules/AILayerTransitionRule.cpp +++ b/AI/VCAI/Pathfinding/Rules/AILayerTransitionRule.cpp @@ -79,7 +79,7 @@ namespace AIPathfinding if(hero->canCastThisSpell(summonBoatSpell) && hero->getSpellSchoolLevel(summonBoatSpell) >= MasteryLevel::ADVANCED) { - // TODO: For lower school level we might need to check the existance of some boat + // TODO: For lower school level we might need to check the existence of some boat summonableVirtualBoat.reset(new SummonBoatAction()); } } diff --git a/AI/VCAI/ResourceManager.cpp b/AI/VCAI/ResourceManager.cpp index b69c4e9bc..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) { @@ -66,7 +64,7 @@ TResources ResourceManager::estimateIncome() const return ret; } -void ResourceManager::reserveResoures(const TResources & res, Goals::TSubgoal goal) +void ResourceManager::reserveResources(const TResources & res, Goals::TSubgoal goal) { if (!goal->invalid()) tryPush(ResourceObjective(res, goal)); @@ -315,7 +313,7 @@ bool ResourceManager::removeOutdatedObjectives(std::functiongetResourceAmount(); - myRes -= reservedResources(); //substract the value of reserved goals + myRes -= reservedResources(); //subtract the value of reserved goals for (auto & val : myRes) vstd::amax(val, 0); //never negative diff --git a/AI/VCAI/ResourceManager.h b/AI/VCAI/ResourceManager.h index af686081f..62ef1a404 100644 --- a/AI/VCAI/ResourceManager.h +++ b/AI/VCAI/ResourceManager.h @@ -24,15 +24,8 @@ struct DLL_EXPORT ResourceObjective ResourceObjective(const TResources &res, Goals::TSubgoal goal); bool operator < (const ResourceObjective &ro) const; - TResources resources; //how many resoures do we need + TResources resources; //how many resources do we need Goals::TSubgoal goal; //what for (build, gather army etc...) - - //TODO: register? - template void serializeInternal(Handler & h) - { - h & resources; - //h & goal; //FIXME: goal serialization is broken - } }; class DLL_EXPORT IResourceManager //: public: IAbstractManager @@ -86,7 +79,7 @@ public: bool notifyGoalCompleted(Goals::TSubgoal goal) override; protected: //not-const actions only for AI - virtual void reserveResoures(const TResources & res, Goals::TSubgoal goal = Goals::TSubgoal()); + virtual void reserveResources(const TResources & res, Goals::TSubgoal goal = Goals::TSubgoal()); virtual bool updateGoal(Goals::TSubgoal goal); //new goal must have same properties but different priority virtual bool tryPush(const ResourceObjective &o); @@ -103,11 +96,4 @@ private: boost::heap::binomial_heap queue; void dumpToLog() const; - - //TODO: register? - template void serializeInternal(Handler & h) - { - h & saving; - h & queue; - } }; diff --git a/AI/VCAI/VCAI.cbp b/AI/VCAI/VCAI.cbp deleted file mode 100644 index 5b0c0548e..000000000 --- a/AI/VCAI/VCAI.cbp +++ /dev/null @@ -1,191 +0,0 @@ - - - - - - diff --git a/AI/VCAI/VCAI.cpp b/AI/VCAI/VCAI.cpp index 686afbbba..8af203906 100644 --- a/AI/VCAI/VCAI.cpp +++ b/AI/VCAI/VCAI.cpp @@ -20,18 +20,16 @@ #include "../../lib/mapObjects/MapObjects.h" #include "../../lib/mapObjects/ObjectTemplate.h" #include "../../lib/CConfigHandler.h" -#include "../../lib/CHeroHandler.h" -#include "../../lib/GameSettings.h" +#include "../../lib/IGameSettings.h" #include "../../lib/gameState/CGameState.h" #include "../../lib/bonuses/Limiters.h" #include "../../lib/bonuses/Updaters.h" #include "../../lib/bonuses/Propagators.h" +#include "../../lib/entities/building/CBuilding.h" #include "../../lib/networkPacks/PacksForClient.h" #include "../../lib/networkPacks/PacksForClientBattle.h" #include "../../lib/networkPacks/PacksForServer.h" #include "../../lib/serializer/CTypeList.h" -#include "../../lib/serializer/BinarySerializer.h" -#include "../../lib/serializer/BinaryDeserializer.h" #include "AIhelper.h" @@ -571,7 +569,7 @@ void VCAI::objectPropertyChanged(const SetObjectProperty * sop) auto obj = myCb->getObj(sop->id, false); if(obj) { - addVisitableObj(obj); // TODO: Remove once save compatability broken. In past owned objects were removed from this set + addVisitableObj(obj); // TODO: Remove once save compatibility broken. In past owned objects were removed from this set vstd::erase_if_present(alreadyVisited, obj); } } @@ -682,7 +680,7 @@ void VCAI::showTeleportDialog(const CGHeroInstance * hero, TeleportChannelID cha status.addQuery(askID, boost::str(boost::format("Teleport dialog query with %d exits") % exits.size())); - int choosenExit = -1; + int chosenExit = -1; if(impassable) { knownTeleportChannels[channel]->passability = TeleportChannel::IMPASSABLE; @@ -691,14 +689,14 @@ void VCAI::showTeleportDialog(const CGHeroInstance * hero, TeleportChannelID cha { auto neededExit = std::make_pair(destinationTeleport, destinationTeleportPos); if(destinationTeleport != ObjectInstanceID() && vstd::contains(exits, neededExit)) - choosenExit = vstd::find_pos(exits, neededExit); + chosenExit = vstd::find_pos(exits, neededExit); } for(auto exit : exits) { if(status.channelProbing() && exit.first == destinationTeleport) { - choosenExit = vstd::find_pos(exits, exit); + chosenExit = vstd::find_pos(exits, exit); break; } else @@ -716,7 +714,7 @@ void VCAI::showTeleportDialog(const CGHeroInstance * hero, TeleportChannelID cha requestActionASAP([=]() { - answerQuery(askID, choosenExit); + answerQuery(askID, chosenExit); }); } @@ -733,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); @@ -747,31 +745,6 @@ void VCAI::showMapObjectSelectDialog(QueryID askID, const Component & icon, cons requestActionASAP([=](){ answerQuery(askID, selectedObject.getNum()); }); } -void VCAI::saveGame(BinarySerializer & h) -{ - NET_EVENT_HANDLER; - validateVisitableObjs(); - - #if 0 - //disabled due to issue 2890 - registerGoals(h); - #endif // 0 - CAdventureAI::saveGame(h); - serializeInternal(h); -} - -void VCAI::loadGame(BinaryDeserializer & h) -{ - //NET_EVENT_HANDLER; - - #if 0 - //disabled due to issue 2890 - registerGoals(h); - #endif // 0 - CAdventureAI::loadGame(h); - serializeInternal(h); -} - void makePossibleUpgrades(const CArmedInstance * obj) { if(!obj) @@ -798,7 +771,7 @@ void VCAI::makeTurn() auto day = cb->getDate(Date::DAY); logAi->info("Player %d (%s) starting turn, day %d", playerID, playerID.toString(), day); - boost::shared_lock gsLock(CGameState::mutex); + boost::shared_lock gsLock(CGameState::mutex); setThreadName("VCAI::makeTurn"); switch(cb->getDate(Date::DAY_OF_WEEK)) @@ -967,7 +940,7 @@ void VCAI::mainLoop() while (possibleGoals.size()) { //allow assign goals to heroes with 0 movement, but don't realize them - //maybe there are beter ones left + //maybe there are better ones left auto bestGoal = fh->chooseSolution(possibleGoals); if (bestGoal->hero) //lock this hero to fulfill goal @@ -1058,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: @@ -1207,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 { @@ -1220,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 @@ -1341,9 +1314,7 @@ 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() >= VLC->settings()->getInteger(EGameSettings::HEROES_PER_PLAYER_ON_MAP_CAP)) + if(cb->getHeroesInfo().size() >= cb->getSettings().getInteger(EGameSettings::HEROES_PER_PLAYER_ON_MAP_CAP)) return false; if(!cb->getAvailableHeroes(t).size()) return false; @@ -1443,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); @@ -1497,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()); @@ -1568,7 +1539,7 @@ void VCAI::completeGoal(Goals::TSubgoal goal) auto it = lockedHeroes.find(h); if(it != lockedHeroes.end()) { - if(it->second == goal || it->second->fulfillsMe(goal)) //FIXME this is overspecified, fulfillsMe shoudl be complete + if(it->second == goal || it->second->fulfillsMe(goal)) //FIXME this is overspecified, fulfillsMe should be complete { logAi->debug(goal->completeMessage()); lockedHeroes.erase(it); //goal fulfilled, free hero @@ -1590,7 +1561,7 @@ void VCAI::completeGoal(Goals::TSubgoal goal) } -void VCAI::battleStart(const BattleID & battleID, const CCreatureSet * army1, const CCreatureSet * army2, int3 tile, const CGHeroInstance * hero1, const CGHeroInstance * hero2, bool side, bool replayAllowed) +void VCAI::battleStart(const BattleID & battleID, const CCreatureSet * army1, const CCreatureSet * army2, int3 tile, const CGHeroInstance * hero1, const CGHeroInstance * hero2, BattleSide side, bool replayAllowed) { NET_EVENT_HANDLER; assert(!playerID.isValidPlayer() || status.getBattle() == UPCOMING_BATTLE); @@ -1760,7 +1731,7 @@ const CGObjectInstance * VCAI::lookForArt(ArtifactID aid) const return nullptr; - //TODO what if more than one artifact is available? return them all or some slection criteria + //TODO what if more than one artifact is available? return them all or some selection criteria } bool VCAI::isAccessible(const int3 & pos) const @@ -2020,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; } @@ -2107,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)); } @@ -2135,7 +2106,7 @@ void VCAI::tryRealize(Goals::Trade & g) //trade if(ah->freeResources()[g.resID] >= g.value) //goal is already fulfilled. Why we need this check, anyway? throw goalFulfilledException(sptr(g)); - int accquiredResources = 0; + int acquiredResources = 0; if(const CGObjectInstance * obj = cb->getObj(ObjectInstanceID(g.objid), false)) { if(const auto * m = dynamic_cast(obj)) @@ -2154,9 +2125,9 @@ void VCAI::tryRealize(Goals::Trade & g) //trade //TODO trade only as much as needed if (toGive) //don't try to sell 0 resources { - cb->trade(m, EMarketMode::RESOURCE_RESOURCE, res, GameResID(g.resID), toGive); - accquiredResources = static_cast(toGet * (it->resVal / toGive)); - logAi->debug("Traded %d of %s for %d of %s at %s", toGive, res, accquiredResources, g.resID, obj->getObjectName()); + cb->trade(m->getObjInstanceID(), EMarketMode::RESOURCE_RESOURCE, res, GameResID(g.resID), toGive); + acquiredResources = static_cast(toGet * (it->resVal / toGive)); + logAi->debug("Traded %d of %s for %d of %s at %s", toGive, res, acquiredResources, g.resID, obj->getObjectName()); } if (ah->freeResources()[g.resID] >= g.value) throw goalFulfilledException(sptr(g)); //we traded all we needed @@ -2504,7 +2475,7 @@ void VCAI::requestActionASAP(std::function whatToDo) { setThreadName("VCAI::requestActionASAP::whatToDo"); SET_GLOBAL_STATE(this); - boost::shared_lock gsLock(CGameState::mutex); + boost::shared_lock gsLock(CGameState::mutex); whatToDo(); }); @@ -2776,8 +2747,6 @@ bool isWeeklyRevisitable(const CGObjectInstance * obj) if(dynamic_cast(obj)) return true; - if(dynamic_cast(obj)) //banks tend to respawn often in mods - return true; switch(obj->ID) { @@ -2847,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; @@ -2878,12 +2847,12 @@ bool shouldVisit(HeroPtr h, const CGObjectInstance * obj) case Obj::MAGIC_WELL: return h->mana < h->manaLimit(); case Obj::PRISON: - return ai->myCb->getHeroesInfo().size() < VLC->settings()->getInteger(EGameSettings::HEROES_PER_PLAYER_ON_MAP_CAP); + return ai->myCb->getHeroesInfo().size() < cb->getSettings().getInteger(EGameSettings::HEROES_PER_PLAYER_ON_MAP_CAP); case Obj::TAVERN: { //TODO: make AI actually recruit heroes //TODO: only on request - if(ai->myCb->getHeroesInfo().size() >= VLC->settings()->getInteger(EGameSettings::HEROES_PER_PLAYER_ON_MAP_CAP)) + if(ai->myCb->getHeroesInfo().size() >= cb->getSettings().getInteger(EGameSettings::HEROES_PER_PLAYER_ON_MAP_CAP)) return false; else if(ai->ah->freeGold() < GameConstants::HERO_GOLD_COST) return false; diff --git a/AI/VCAI/VCAI.h b/AI/VCAI/VCAI.h index 310e4be77..57f68de19 100644 --- a/AI/VCAI/VCAI.h +++ b/AI/VCAI/VCAI.h @@ -18,9 +18,7 @@ #include "../../lib/GameConstants.h" #include "../../lib/VCMI_Lib.h" -#include "../../lib/CBuildingHandler.h" #include "../../lib/CCreatureHandler.h" -#include "../../lib/CTownHandler.h" #include "../../lib/mapObjects/MiscObjects.h" #include "../../lib/spells/CSpellHandler.h" #include "Pathfinding/AIPathfinder.h" @@ -65,15 +63,6 @@ public: void attemptedAnsweringQuery(QueryID queryID, int answerRequestID); void receivedAnswerConfirmation(int answerRequestID, int result); void heroVisit(const CGObjectInstance * obj, bool started); - - - template void serialize(Handler & h) - { - h & battle; - h & remainingQueries; - h & requestToQueryID; - h & havingTurn; - } }; class DLL_EXPORT VCAI : public CAdventureAI @@ -92,7 +81,7 @@ public: //std::vector visitedThisWeek; //only OPWs std::map> townVisitsThisWeek; - //part of mainLoop, but accessible from outisde + //part of mainLoop, but accessible from outside std::vector basicGoals; Goals::TGoalVec goalsToRemove; Goals::TGoalVec goalsToAdd; @@ -151,8 +140,6 @@ public: void showGarrisonDialog(const CArmedInstance * up, const CGHeroInstance * down, bool removableUnits, QueryID queryID) override; //all stacks operations between these objects become allowed, interface has to call onEnd when done void showTeleportDialog(const CGHeroInstance * hero, TeleportChannelID channel, TTeleportExitsList exits, bool impassable, QueryID askID) override; void showMapObjectSelectDialog(QueryID askID, const Component & icon, const MetaString & title, const MetaString & description, const std::vector & objects) override; - void saveGame(BinarySerializer & h) override; //saving - void loadGame(BinaryDeserializer & h) override; //loading void finish() override; void availableCreaturesChanged(const CGDwelling * town) override; @@ -200,7 +187,7 @@ public: void showMarketWindow(const IMarket * market, const CGHeroInstance * visitor, QueryID queryID) override; void showWorldViewEx(const std::vector & objectPositions, bool showTerrain) override; - void battleStart(const BattleID & battleID, const CCreatureSet * army1, const CCreatureSet * army2, int3 tile, const CGHeroInstance * hero1, const CGHeroInstance * hero2, bool side, bool replayAllowed) override; + void battleStart(const BattleID & battleID, const CCreatureSet * army1, const CCreatureSet * army2, int3 tile, const CGHeroInstance * hero1, const CGHeroInstance * hero2, BattleSide side, bool replayAllowed) override; void battleEnd(const BattleID & battleID, const BattleResult * br, QueryID queryID) override; std::optional makeSurrenderRetreatDecision(const BattleID & battleID, const BattleStateInfoForRetreat & battleState) override; @@ -272,92 +259,6 @@ public: void answerQuery(QueryID queryID, int selection); //special function that can be called ONLY from game events handling thread and will send request ASAP void requestActionASAP(std::function whatToDo); - - #if 0 - //disabled due to issue 2890 - template void registerGoals(Handler & h) - { - //h.template registerType(); - h.template registerType(); - h.template registerType(); - //h.template registerType(); - h.template registerType(); - h.template registerType(); - h.template registerType(); - h.template registerType(); - h.template registerType(); - h.template registerType(); - h.template registerType(); - h.template registerType(); - h.template registerType(); - h.template registerType(); - h.template registerType(); - //h.template registerType(); - h.template registerType(); - h.template registerType(); - h.template registerType(); - h.template registerType(); - } - #endif - - template void serializeInternal(Handler & h) - { - h & knownTeleportChannels; - h & knownSubterraneanGates; - h & destinationTeleport; - h & townVisitsThisWeek; - - #if 0 - //disabled due to issue 2890 - h & lockedHeroes; - #else - { - ui32 length = 0; - h & length; - if(!h.saving) - { - std::set loadedPointers; - lockedHeroes.clear(); - for(ui32 index = 0; index < length; index++) - { - HeroPtr ignored1; - h & ignored1; - - ui8 flag = 0; - h & flag; - - if(flag) - { - ui32 pid = 0xffffffff; - h & pid; - - if(!vstd::contains(loadedPointers, pid)) - { - loadedPointers.insert(pid); - - ui16 typeId = 0; - //this is the problem requires such hack - //we have to explicitly ignore invalid goal class type id - h & typeId; - Goals::AbstractGoal ignored2; - ignored2.serialize(h); - } - } - } - } - } - #endif - - h & reservedHeroesMap; //FIXME: cannot instantiate abstract class - h & visitableObjs; - h & alreadyVisited; - h & reservedObjs; - h & status; - h & battlename; - h & heroesUnableToExplore; - - //myCB is restored after load by init call - } }; class cannotFulfillGoalException : public std::exception diff --git a/AI/VCAI/VCAI.vcxproj b/AI/VCAI/VCAI.vcxproj deleted file mode 100644 index b3b127daa..000000000 --- a/AI/VCAI/VCAI.vcxproj +++ /dev/null @@ -1,246 +0,0 @@ - - - - - Debug - Win32 - - - Debug - x64 - - - RD - Win32 - - - RD - x64 - - - - {276C3DB0-7A6B-4417-8E5C-322B08633AAC} - StupidAI - 10.0 - - - - DynamicLibrary - true - MultiByte - v140_xp - - - DynamicLibrary - true - MultiByte - v140_xp - - - DynamicLibrary - false - true - MultiByte - v142 - - - DynamicLibrary - false - true - MultiByte - v140_xp - - - - - - - - - - - - - - - - - - - - - - - - - - - $(VCMI_Out)\AI\ - - - $(VCMI_Out)\AI\ - - - $(VCMI_Out)/AI - - - $(VCMI_Out)\AI\ - - - - $(FUZZYLITEDIR) - Use - StdInc.h - /Zm210 %(AdditionalOptions) - - - VCMI_lib.lib;FuzzyLite.lib;%(AdditionalDependencies) - ..\..\..\libs;..\..;.. - - - - - - - Use - StdInc.h - /Zm150 %(AdditionalOptions) - - - VCMI_lib.lib;FuzzyLite.lib;%(AdditionalDependencies) - $(VCMI_Out);$(OutDir);%(AdditionalLibraryDirectories) - - - - - ..\FuzzyLite\fuzzylite - Use - StdInc.h - true - Disabled - - - VCMI_lib.lib;FuzzyLite.lib;%(AdditionalDependencies) - $(VCMI_Out);$(SolutionDir)\AI - /d2:-notypeopt %(AdditionalOptions) - - - - - - - Use - StdInc.h - /Zm150 %(AdditionalOptions) - - - VCMI_lib.lib;FuzzyLite.lib;%(AdditionalDependencies) - $(VCMI_Out);$(OutDir);%(AdditionalLibraryDirectories) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Create - Create - Create - Create - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/AI/VCAI/VCAI.vcxproj.filters b/AI/VCAI/VCAI.vcxproj.filters deleted file mode 100644 index ca64f2922..000000000 --- a/AI/VCAI/VCAI.vcxproj.filters +++ /dev/null @@ -1,254 +0,0 @@ - - - - - - - - - - - - - - - - Pathfinding - - - Pathfinding - - - Pathfinding - - - Pathfinding - - - Goals - - - Goals - - - Goals - - - Goals - - - Goals - - - Goals - - - Goals - - - Goals - - - Goals - - - Goals - - - Goals - - - Goals - - - Goals - - - Goals - - - Goals - - - Goals - - - Goals - - - Goals - - - Goals - - - Goals - - - Goals - - - Goals - - - Pathfinding\Actions - - - Pathfinding\Actions - - - Pathfinding\Actions - - - Pathfinding\Rules - - - Pathfinding\Rules - - - Pathfinding\Rules - - - Pathfinding\Rules - - - - - - - - - - - - - - - - Pathfinding - - - Pathfinding - - - Pathfinding - - - Pathfinding - - - Goals - - - Goals - - - Goals - - - Goals - - - Goals - - - Goals - - - Goals - - - Goals - - - Goals - - - Goals - - - Goals - - - Goals - - - Goals - - - Goals - - - Goals - - - Goals - - - Goals - - - Goals - - - Goals - - - Goals - - - Goals - - - Goals - - - Goals - - - Goals - - - Goals - - - Pathfinding\Actions - - - Pathfinding\Actions - - - Pathfinding\Actions - - - Pathfinding\Actions - - - Pathfinding\Rules - - - Pathfinding\Rules - - - Pathfinding\Rules - - - Pathfinding\Rules - - - - - - {f0ef4866-37a3-4a10-a6bf-34460fcefab5} - - - {f97140a0-eee3-456f-b586-4b13265c01da} - - - {beabfdb9-2e76-4daa-8d1a-81086387f319} - - - {3ebb4852-a986-447a-b5cc-20992df76f0c} - - - \ No newline at end of file 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 7367c1edc..61c741cc8 100644 --- a/CCallback.cpp +++ b/CCallback.cpp @@ -17,33 +17,32 @@ #include "lib/mapping/CMap.h" #include "lib/mapObjects/CGHeroInstance.h" #include "lib/mapObjects/CGTownInstance.h" -#include "lib/CBuildingHandler.h" -#include "lib/CGeneralTextHandler.h" -#include "lib/CHeroHandler.h" +#include "lib/texts/CGeneralTextHandler.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) @@ -62,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) @@ -72,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) @@ -81,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; } @@ -96,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; } @@ -152,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; } @@ -161,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; } @@ -173,16 +172,16 @@ bool CCallback::swapArtifacts(const ArtifactLocation &l1, const ArtifactLocation * @param assembleTo If assemble is true, this represents the artifact ID of the combination * artifact to assemble to. Otherwise it's not used. */ -void CCallback::assembleArtifacts(const CGHeroInstance * hero, ArtifactPosition artifactSlot, bool assemble, ArtifactID assembleTo) +void CCallback::assembleArtifacts(const ObjectInstanceID & heroID, ArtifactPosition artifactSlot, bool assemble, ArtifactID assembleTo) { - AssembleArtifacts aa(hero->id, artifactSlot, assemble, assembleTo); - sendRequest(&aa); + AssembleArtifacts aa(heroID, artifactSlot, assemble, assembleTo); + 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) @@ -190,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) @@ -214,7 +231,17 @@ bool CCallback::buildBuilding(const CGTownInstance *town, BuildingID buildingID) return false; BuildStructure pack(town->id,buildingID); - sendRequest(&pack); + sendRequest(pack); + return true; +} + +bool CCallback::visitTownBuilding(const CGTownInstance *town, BuildingID buildingID) +{ + if(town->tempOwner!=player) + return false; + + VisitTownBuilding pack(town->id, buildingID); + sendRequest(pack); return true; } @@ -223,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) @@ -240,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); } } @@ -254,30 +287,30 @@ 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 IMarket * market, EMarketMode mode, TradeItemSell id1, TradeItemBuy id2, ui32 val1, const CGHeroInstance * hero) +void CCallback::trade(const ObjectInstanceID marketId, EMarketMode mode, TradeItemSell id1, TradeItemBuy id2, ui32 val1, const CGHeroInstance * hero) { - trade(market, mode, std::vector(1, id1), std::vector(1, id2), std::vector(1, val1), hero); + trade(marketId, mode, std::vector(1, id1), std::vector(1, id2), std::vector(1, val1), hero); } -void CCallback::trade(const IMarket * market, EMarketMode mode, const std::vector & id1, const std::vector & id2, const std::vector & val1, const CGHeroInstance * hero) +void CCallback::trade(const ObjectInstanceID marketId, EMarketMode mode, const std::vector & id1, const std::vector & id2, const std::vector & val1, const CGHeroInstance * hero) { TradeOnMarketplace pack; - pack.marketId = dynamic_cast(market)->id; + pack.marketId = marketId; pack.heroId = hero ? hero->id : ObjectInstanceID(); pack.mode = mode; 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) @@ -285,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 ) @@ -301,7 +343,7 @@ void CCallback::gamePause(bool pause) { GamePause pack; pack.player = *player; - sendRequest(&pack); + sendRequest(pack); } else { @@ -315,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) @@ -364,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) @@ -373,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) @@ -406,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 ) @@ -415,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 17cbf12fb..6e30299c6 100644 --- a/CCallback.h +++ b/CCallback.h @@ -34,7 +34,6 @@ class IBattleEventsReceiver; class IGameEventsReceiver; struct ArtifactLocation; class BattleStateInfoForRetreat; -class IMarket; VCMI_LIB_NAMESPACE_END @@ -69,19 +68,21 @@ public: //hero virtual void moveHero(const CGHeroInstance *h, const std::vector & path, bool transit) =0; //moves hero alongside provided path virtual void moveHero(const CGHeroInstance *h, const int3 & destination, bool transit) =0; //moves hero alongside provided path - virtual bool dismissHero(const CGHeroInstance * hero)=0; //dismisses given hero; true - successfuly, false - not successfuly + virtual bool dismissHero(const CGHeroInstance * hero)=0; //dismisses given hero; true - successfully, false - not successfully virtual void dig(const CGObjectInstance *hero)=0; virtual void castSpell(const CGHeroInstance *hero, SpellID spellID, const int3 &pos = int3(-1, -1, -1))=0; //cast adventure map spell //town virtual void recruitHero(const CGObjectInstance *townOrTavern, const CGHeroInstance *hero, const HeroTypeID & nextHero=HeroTypeID::NONE)=0; virtual bool buildBuilding(const CGTownInstance *town, BuildingID buildingID)=0; + 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 IMarket * market, EMarketMode mode, TradeItemSell id1, TradeItemBuy id2, ui32 val1, const CGHeroInstance * hero = nullptr)=0; //mode==0: sell val1 units of id1 resource for id2 resiurce - virtual void trade(const IMarket * market, EMarketMode mode, const std::vector & id1, const std::vector & id2, const std::vector & val1, const CGHeroInstance * hero = nullptr)=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 + virtual void trade(const ObjectInstanceID marketId, EMarketMode mode, const std::vector & id1, const std::vector & id2, const std::vector & val1, const CGHeroInstance * hero)=0; virtual int selectionMade(int selection, QueryID queryID) =0; virtual int sendQueryReply(std::optional reply, QueryID queryID) =0; @@ -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 CGHeroInstance * hero, ArtifactPosition artifactSlot, bool assemble, ArtifactID assembleTo)=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: @@ -176,20 +181,26 @@ public: int bulkMergeStacks(ObjectInstanceID armyId, SlotID srcSlot) override; bool dismissHero(const CGHeroInstance * hero) override; bool swapArtifacts(const ArtifactLocation &l1, const ArtifactLocation &l2) override; - void assembleArtifacts(const CGHeroInstance * hero, ArtifactPosition artifactSlot, bool assemble, ArtifactID assembleTo) override; + 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; + bool visitTownBuilding(const CGTownInstance *town, BuildingID buildingID) override; 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 IMarket * market, EMarketMode mode, TradeItemSell id1, TradeItemBuy id2, ui32 val1, const CGHeroInstance * hero = nullptr) override; - void trade(const IMarket * market, EMarketMode mode, const std::vector & id1, const std::vector & id2, const std::vector & val1, const CGHeroInstance * hero = nullptr) override; + void trade(const ObjectInstanceID marketId, EMarketMode mode, TradeItemSell id1, TradeItemBuy id2, ui32 val1, const CGHeroInstance * hero = nullptr) override; + void trade(const ObjectInstanceID marketId, EMarketMode mode, const std::vector & id1, const std::vector & id2, const std::vector & val1, const CGHeroInstance * hero = nullptr) override; void setFormation(const CGHeroInstance * hero, EArmyFormation mode) override; void recruitHero(const CGObjectInstance *townOrTavern, const CGHeroInstance *hero, const HeroTypeID & nextHero=HeroTypeID::NONE) override; void save(const std::string &fname) override; diff --git a/CI/NSIS.template.in b/CI/NSIS.template.in index 55b10a03d..0b283c827 100644 --- a/CI/NSIS.template.in +++ b/CI/NSIS.template.in @@ -486,15 +486,15 @@ Done: Exch $R1 FunctionEnd -Function ConditionalAddToRegisty +Function ConditionalAddToRegistry Pop $0 Pop $1 - StrCmp "$0" "" ConditionalAddToRegisty_EmptyString + StrCmp "$0" "" ConditionalAddToRegistry_EmptyString WriteRegStr SHCTX "Software\Microsoft\Windows\CurrentVersion\Uninstall\@CPACK_PACKAGE_INSTALL_REGISTRY_KEY@" \ "$1" "$0" ;MessageBox MB_OK "Set Registry: '$1' to '$0'" DetailPrint "Set install registry entry: '$1' to '$0'" - ConditionalAddToRegisty_EmptyString: + ConditionalAddToRegistry_EmptyString: FunctionEnd ;-------------------------------- @@ -567,56 +567,62 @@ FunctionEnd ;Languages !insertmacro MUI_LANGUAGE "English" ;first language is the default language - !insertmacro MUI_LANGUAGE "Albanian" - !insertmacro MUI_LANGUAGE "Arabic" - !insertmacro MUI_LANGUAGE "Basque" - !insertmacro MUI_LANGUAGE "Belarusian" - !insertmacro MUI_LANGUAGE "Bosnian" - !insertmacro MUI_LANGUAGE "Breton" - !insertmacro MUI_LANGUAGE "Bulgarian" - !insertmacro MUI_LANGUAGE "Croatian" !insertmacro MUI_LANGUAGE "Czech" - !insertmacro MUI_LANGUAGE "Danish" - !insertmacro MUI_LANGUAGE "Dutch" - !insertmacro MUI_LANGUAGE "Estonian" - !insertmacro MUI_LANGUAGE "Farsi" + !insertmacro MUI_LANGUAGE "SimpChinese" !insertmacro MUI_LANGUAGE "Finnish" !insertmacro MUI_LANGUAGE "French" !insertmacro MUI_LANGUAGE "German" - !insertmacro MUI_LANGUAGE "Greek" - !insertmacro MUI_LANGUAGE "Hebrew" !insertmacro MUI_LANGUAGE "Hungarian" - !insertmacro MUI_LANGUAGE "Icelandic" - !insertmacro MUI_LANGUAGE "Indonesian" - !insertmacro MUI_LANGUAGE "Irish" !insertmacro MUI_LANGUAGE "Italian" - !insertmacro MUI_LANGUAGE "Japanese" !insertmacro MUI_LANGUAGE "Korean" - !insertmacro MUI_LANGUAGE "Kurdish" - !insertmacro MUI_LANGUAGE "Latvian" - !insertmacro MUI_LANGUAGE "Lithuanian" - !insertmacro MUI_LANGUAGE "Luxembourgish" - !insertmacro MUI_LANGUAGE "Macedonian" - !insertmacro MUI_LANGUAGE "Malay" - !insertmacro MUI_LANGUAGE "Mongolian" - !insertmacro MUI_LANGUAGE "Norwegian" !insertmacro MUI_LANGUAGE "Polish" !insertmacro MUI_LANGUAGE "Portuguese" - !insertmacro MUI_LANGUAGE "PortugueseBR" - !insertmacro MUI_LANGUAGE "Romanian" !insertmacro MUI_LANGUAGE "Russian" - !insertmacro MUI_LANGUAGE "Serbian" - !insertmacro MUI_LANGUAGE "SerbianLatin" - !insertmacro MUI_LANGUAGE "SimpChinese" - !insertmacro MUI_LANGUAGE "Slovak" - !insertmacro MUI_LANGUAGE "Slovenian" !insertmacro MUI_LANGUAGE "Spanish" !insertmacro MUI_LANGUAGE "Swedish" - !insertmacro MUI_LANGUAGE "Thai" - !insertmacro MUI_LANGUAGE "TradChinese" !insertmacro MUI_LANGUAGE "Turkish" !insertmacro MUI_LANGUAGE "Ukrainian" - !insertmacro MUI_LANGUAGE "Welsh" + !insertmacro MUI_LANGUAGE "Vietnamese" + + ;!insertmacro MUI_LANGUAGE "Albanian" + ;!insertmacro MUI_LANGUAGE "Arabic" + ;!insertmacro MUI_LANGUAGE "Basque" + ;!insertmacro MUI_LANGUAGE "Belarusian" + ;!insertmacro MUI_LANGUAGE "Bosnian" + ;!insertmacro MUI_LANGUAGE "Breton" + ;!insertmacro MUI_LANGUAGE "Bulgarian" + ;!insertmacro MUI_LANGUAGE "Croatian" + ;!insertmacro MUI_LANGUAGE "Danish" + ;!insertmacro MUI_LANGUAGE "Dutch" + ;!insertmacro MUI_LANGUAGE "Estonian" + ;!insertmacro MUI_LANGUAGE "Farsi" + ;!insertmacro MUI_LANGUAGE "Greek" + ;!insertmacro MUI_LANGUAGE "Hebrew" + ;!insertmacro MUI_LANGUAGE "Icelandic" + ;!insertmacro MUI_LANGUAGE "Indonesian" + ;!insertmacro MUI_LANGUAGE "Irish" + ;!insertmacro MUI_LANGUAGE "Japanese" + ;!insertmacro MUI_LANGUAGE "Kurdish" + ;!insertmacro MUI_LANGUAGE "Latvian" + ;!insertmacro MUI_LANGUAGE "Lithuanian" + ;!insertmacro MUI_LANGUAGE "Luxembourgish" + ;!insertmacro MUI_LANGUAGE "Macedonian" + ;!insertmacro MUI_LANGUAGE "Malay" + ;!insertmacro MUI_LANGUAGE "Mongolian" + ;!insertmacro MUI_LANGUAGE "Norwegian" + ;!insertmacro MUI_LANGUAGE "PortugueseBR" + ;!insertmacro MUI_LANGUAGE "Romanian" + ;!insertmacro MUI_LANGUAGE "Serbian" + ;!insertmacro MUI_LANGUAGE "SerbianLatin" + ;!insertmacro MUI_LANGUAGE "Slovak" + ;!insertmacro MUI_LANGUAGE "Slovenian" + ;!insertmacro MUI_LANGUAGE "Thai" + ;!insertmacro MUI_LANGUAGE "TradChinese" + ;!insertmacro MUI_LANGUAGE "Welsh" + + +; Language Selection Dialog + !define MUI_LANGDLL_DISPLAY ;-------------------------------- @@ -646,44 +652,44 @@ Section "-Core installation" WriteUninstaller "$INSTDIR\Uninstall.exe" Push "DisplayName" Push "@CPACK_NSIS_DISPLAY_NAME@" - Call ConditionalAddToRegisty + Call ConditionalAddToRegistry Push "DisplayVersion" Push "@CPACK_PACKAGE_VERSION@" - Call ConditionalAddToRegisty + Call ConditionalAddToRegistry Push "Publisher" Push "@CPACK_PACKAGE_VENDOR@" - Call ConditionalAddToRegisty + Call ConditionalAddToRegistry Push "UninstallString" Push "$INSTDIR\Uninstall.exe" - Call ConditionalAddToRegisty + Call ConditionalAddToRegistry Push "NoRepair" Push "1" - Call ConditionalAddToRegisty + Call ConditionalAddToRegistry !ifdef CPACK_NSIS_ADD_REMOVE ;Create add/remove functionality Push "ModifyPath" Push "$INSTDIR\AddRemove.exe" - Call ConditionalAddToRegisty + Call ConditionalAddToRegistry !else Push "NoModify" Push "1" - Call ConditionalAddToRegisty + Call ConditionalAddToRegistry !endif ; Optional registration Push "DisplayIcon" Push "$INSTDIR\@CPACK_NSIS_INSTALLED_ICON_NAME@" - Call ConditionalAddToRegisty + Call ConditionalAddToRegistry Push "HelpLink" Push "@CPACK_NSIS_HELP_LINK@" - Call ConditionalAddToRegisty + Call ConditionalAddToRegistry Push "URLInfoAbout" Push "@CPACK_NSIS_URL_INFO_ABOUT@" - Call ConditionalAddToRegisty + Call ConditionalAddToRegistry Push "Contact" Push "@CPACK_NSIS_CONTACT@" - Call ConditionalAddToRegisty + Call ConditionalAddToRegistry !insertmacro MUI_INSTALLOPTIONS_READ $INSTALL_DESKTOP "NSIS.InstallOptions.ini" "Field 5" "State" !insertmacro MUI_STARTMENU_WRITE_BEGIN Application @@ -701,19 +707,19 @@ Section "-Core installation" ; Write special uninstall registry entries Push "StartMenu" Push "$STARTMENU_FOLDER" - Call ConditionalAddToRegisty + Call ConditionalAddToRegistry Push "DoNotAddToPath" Push "$DO_NOT_ADD_TO_PATH" - Call ConditionalAddToRegisty + Call ConditionalAddToRegistry Push "AddToPathAllUsers" Push "$ADD_TO_PATH_ALL_USERS" - Call ConditionalAddToRegisty + Call ConditionalAddToRegistry Push "AddToPathCurrentUser" Push "$ADD_TO_PATH_CURRENT_USER" - Call ConditionalAddToRegisty + Call ConditionalAddToRegistry Push "InstallToDesktop" Push "$INSTALL_DESKTOP" - Call ConditionalAddToRegisty + Call ConditionalAddToRegistry !insertmacro MUI_STARTMENU_WRITE_END @@ -848,7 +854,7 @@ Section "Uninstall" @CPACK_NSIS_DELETE_ICONS@ @CPACK_NSIS_DELETE_ICONS_EXTRA@ - ;Delete empty start menu parent diretories + ;Delete empty start menu parent directories StrCpy $MUI_TEMP "$SMPROGRAMS\$MUI_TEMP" startMenuDeleteLoop: @@ -867,7 +873,7 @@ Section "Uninstall" Delete "$SMPROGRAMS\$MUI_TEMP\Uninstall.lnk" @CPACK_NSIS_DELETE_ICONS_EXTRA@ - ;Delete empty start menu parent diretories + ;Delete empty start menu parent directories StrCpy $MUI_TEMP "$SMPROGRAMS\$MUI_TEMP" secondStartMenuDeleteLoop: @@ -899,6 +905,9 @@ SectionEnd ; "Program Files" for AllUsers, "My Documents" for JustMe... Function .onInit + + !insertmacro MUI_LANGDLL_DISPLAY + StrCmp "@CPACK_NSIS_ENABLE_UNINSTALL_BEFORE_INSTALL@" "ON" 0 inst ReadRegStr $0 HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\@CPACK_PACKAGE_INSTALL_REGISTRY_KEY@" "UninstallString" diff --git a/CI/android-32/before_install.sh b/CI/android-32/before_install.sh deleted file mode 100755 index 12adadd89..000000000 --- a/CI/android-32/before_install.sh +++ /dev/null @@ -1,4 +0,0 @@ -#!/usr/bin/env bash - -DEPS_FILENAME=armeabi-v7a -. 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 b26082d29..000000000 --- a/CI/android-64/before_install.sh +++ /dev/null @@ -1,4 +0,0 @@ -#!/usr/bin/env bash - -DEPS_FILENAME=aarch64-v8a -. CI/android/before_install.sh diff --git a/CI/android/before_install.sh b/CI/android/before_install.sh deleted file mode 100755 index 8a13382d7..000000000 --- a/CI/android/before_install.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/usr/bin/env bash - -echo "ANDROID_NDK_ROOT=$ANDROID_HOME/ndk/25.2.9519653" >> $GITHUB_ENV - -brew install ninja - -mkdir ~/.conan ; cd ~/.conan -curl -L "https://github.com/vcmi/vcmi-dependencies/releases/download/android-1.1/$DEPS_FILENAME.txz" \ - | tar -xf - --xz 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/before_install/macos.sh b/CI/before_install/macos.sh new file mode 100644 index 000000000..0664cc910 --- /dev/null +++ b/CI/before_install/macos.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + +echo DEVELOPER_DIR=/Applications/Xcode_14.2.app >> $GITHUB_ENV + +brew install ninja 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..a9cfda2cb --- /dev/null +++ b/CI/example.markdownlint-cli2.jsonc @@ -0,0 +1,278 @@ +{ + "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": { + // List of languages + "allowed_languages": [ "cpp", "json", "sh", "text", "nix", "powershell", "lua" ], + // 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 new file mode 100644 index 000000000..593f96e30 --- /dev/null +++ b/CI/install_conan_dependencies.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +RELEASE_TAG="1.3" +FILENAME="$1" +DOWNLOAD_URL="https://github.com/vcmi/vcmi-dependencies/releases/download/$RELEASE_TAG/$FILENAME.txz" + +mkdir ~/.conan +cd ~/.conan +curl -L "$DOWNLOAD_URL" | tar -xf - --xz 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 350730cb4..000000000 --- a/CI/ios/before_install.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/usr/bin/env bash - -echo DEVELOPER_DIR=/Applications/Xcode_14.2.app >> $GITHUB_ENV - -mkdir ~/.conan ; cd ~/.conan -curl -L 'https://github.com/vcmi/vcmi-ios-deps/releases/download/1.2.1/ios-arm64.txz' \ - | tar -xf - 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 c90b6a1b1..000000000 --- a/CI/mac-arm/before_install.sh +++ /dev/null @@ -1,4 +0,0 @@ -#!/usr/bin/env bash - -DEPS_FILENAME=intel-cross-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 fcbcea328..000000000 --- a/CI/mac-intel/before_install.sh +++ /dev/null @@ -1,4 +0,0 @@ -#!/usr/bin/env bash - -DEPS_FILENAME=intel -. CI/mac/before_install.sh diff --git a/CI/mac/before_install.sh b/CI/mac/before_install.sh deleted file mode 100755 index e1f53b145..000000000 --- a/CI/mac/before_install.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/usr/bin/env bash - -echo DEVELOPER_DIR=/Applications/Xcode_14.2.app >> $GITHUB_ENV - -brew install ninja - -mkdir ~/.conan ; cd ~/.conan -curl -L "https://github.com/vcmi/vcmi-deps-macos/releases/download/1.2.1/$DEPS_FILENAME.txz" \ - | tar -xf - diff --git a/CI/mingw-32/before_install.sh b/CI/mingw-32/before_install.sh deleted file mode 100644 index 3b41ed86d..000000000 --- a/CI/mingw-32/before_install.sh +++ /dev/null @@ -1,16 +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; - -mkdir ~/.conan ; cd ~/.conan -curl -L "https://github.com/vcmi/vcmi-deps-windows-conan/releases/download/1.2/vcmi-deps-windows-conan-w32.tgz" \ - | tar -xzf - diff --git a/CI/mingw/before_install.sh b/CI/mingw/before_install.sh deleted file mode 100755 index 09318d85b..000000000 --- a/CI/mingw/before_install.sh +++ /dev/null @@ -1,16 +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; - -mkdir ~/.conan ; cd ~/.conan -curl -L "https://github.com/vcmi/vcmi-deps-windows-conan/releases/download/1.2/vcmi-deps-windows-conan-w64.tgz" \ - | tar -xzf - 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 f63641de8..0629cf1ad 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,6 +1,6 @@ # Minimum required version greatly affect CMake behavior # So cmake_minimum_required must be called before the project() -# 3.16.0 is used since it's used by our currently oldest suppored system: Ubuntu-20.04 +# 3.16.0 is used since it's used by our currently oldest supported system: Ubuntu-20.04 cmake_minimum_required(VERSION 3.16.0) project(VCMI) @@ -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) @@ -403,7 +390,10 @@ if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU" OR NOT WIN32) if(CMAKE_BUILD_TYPE STREQUAL "Debug") if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU" AND NOT WIN32) # For gcc 14+ we can use -fhardened instead - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -D_GLIBCXX_ASSERTIONS -fstack-protector-strong -fstack-clash-protection -fcf-protection=full") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -D_GLIBCXX_ASSERTIONS -fstack-protector-strong -fstack-clash-protection") + if (CMAKE_SYSTEM_PROCESSOR STREQUAL "AMD64" OR CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fcf-protection=full") + endif() endif() endif() @@ -483,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) @@ -519,7 +514,7 @@ if(ENABLE_LAUNCHER OR ENABLE_EDITOR) endif() endif() -if(ENABLE_CLIENT) +if(NOT ENABLE_MINIMAL_LIB) find_package(TBB REQUIRED) endif() @@ -663,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() @@ -677,6 +676,7 @@ endif() if (ENABLE_CLIENT) add_subdirectory(client) + add_subdirectory(clientapp) endif() if(ENABLE_SERVER) @@ -722,6 +722,10 @@ endif() if(WIN32) + if(TBB_FOUND AND MSVC) + install_vcpkg_imported_tgt(TBB::tbb) + endif() + if(USING_CONAN) #Conan imports enabled vcmi_install_conan_deps("\${CMAKE_INSTALL_PREFIX}") @@ -729,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} @@ -839,10 +845,10 @@ if(WIN32) endif() # set the install/unistall icon used for the installer itself # There is a bug in NSI that does not handle full unix paths properly. - set(CPACK_NSIS_MUI_ICON "${CMAKE_CURRENT_SOURCE_DIR}/client\\\\vcmi.ico") - set(CPACK_NSIS_MUI_UNIICON "${CMAKE_CURRENT_SOURCE_DIR}/client\\\\vcmi.ico") + set(CPACK_NSIS_MUI_ICON "${CMAKE_CURRENT_SOURCE_DIR}/clientapp/icons\\\\vcmi.ico") + set(CPACK_NSIS_MUI_UNIICON "${CMAKE_CURRENT_SOURCE_DIR}/clientapp/icons\\\\vcmi.ico") # set the package header icon for MUI - set(CPACK_PACKAGE_ICON "${CMAKE_SOURCE_DIR}/client\\\\vcmi.ico") + set(CPACK_PACKAGE_ICON "${CMAKE_SOURCE_DIR}/clientapp/icons\\\\vcmi.ico") set(CPACK_NSIS_MENU_LINKS "http://vcmi.eu/" "VCMI Web Site") 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 f9137d3cb..d9a584afa 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -1,4 +1,270 @@ -# 1.5.6 -> 1.5.7 +# VCMI Project Changelog + +## 1.5.7 -> 1.6.0 + +### Major changes + +* Greatly improved decision-making of NullkillerAI +* Implemented support for multiple mod presets allowing player to quickly switch between them in Launcher +* 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 +* Added support for high-resolution graphical assets +* 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 +* When playing in non-English language using English Heroes III data files, game will now load all maps and campaigns using player language +* Added `vcmiscrolls` cheat code that gives spell scrolls for every possible spells + +### 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 +* Fixed crash on dismissing hero after picking up an artifact from hero doll +* Fixed possible crash if creature with spell before attack bonus kills unit it was attacking with spell + +### 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 +* View Earth will no longer reveal position of enemy heroes and towns +* It is now possible to sell Grail, in line with Heroes III +* Jeddite is no longer female +* Mutare and Mutare Drake are now Overlord and not Warlock +* Elixir of Life no longer affects siege machines +* Banned skills known by hero now have minimal chance (1) instead of 0 to appear on levelup +* The Transport Artifact victory condition fulfilled by the enemy AI will no longer trigger a victory for human players if "standard victory" is enabled on the map + +### Video / Audio + +* 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 +* Some of the assets provided by VCMI are now available in higher resolution +* Implemented support for semi-transparent spell effects, such as Life Drain or Resurrection (and many others) + +### Interface + +* It is now possible to search for a map object using Ctrl+F hotkey +* Holding Alt while using move unit button on Exchange screen will now move entire army except for single unit in this slot +* 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. +* Creature window now displays source of all bonuses that creature has, such as creature ability, hero skill, hero artifact, etc. +* Reduced font used for creature abilities description to reduce text clipping +* 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 +* Added scrollbars for selection of starting town, starting hero, and tavern invite if number of objects is too large to fit into the screen +* Fixed incorrect battle turn queue displaying incorrect turn order when all units have waited +* Semi-transparent shadows now correctly update their transparency during fading effects, such as resource pickups +* Fixed swapped Overlord and Warlock models on adventure map +* Fixed Heroes III bug - swapped icons of View Earth and View Air +* 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 +* 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 +* 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 +* Fixed bug leading to inability to select larger number of "CPU only players" in random map generation menu +* It is now possible to delete saved games +* Game will now promt to delete saves from no longer supported versions of VCMI +* It is now possible to use scroll in touch popup windows +* Name of spell provided by Shrine is now displayed in yellow color +* Fixed right-click popup on hero in town placed outside of screen boundaries on low resolutions +* Fixed misaligned button in 2-player alliance selector in random map generation menu +* Damage range of Ballista in unit window now accounts for hero attack skill, in line with Heroes III +* Changed format of automatic autosave to more human-readable version +* Names of autosave folders are now left-aligned in save game 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 +* Roads placed on the maps will now be curved a little bit to improve the look of the maps. +* 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 +* Added sealed zone types, for entirely unpassable zones. +* It is now possible to connect two zones with multiple connections of same or different types + +### 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 + +### Adventure AI + +* Greatly improved decision-making of NullkillerAI +* NullkillerAI will now act differently based on difficulty level +* 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 +* Nullkiller now uses whirlpools for map movement +* Fixed possible crash on AI attempting to visit town that is already being visited by this hero +* It is now possible to configure NullkillerAI parameters separately for each game difficulty +* Extended hardcoded logic of AI not taking creatures from Garrisons to all Heroes III: Restoration of Erathia campaigns, in line with original game + +### Combat 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 a bug causing BattleAI to focus on unreachable targets and ignoring reachable enemies +* AI can now correctly estimate effect of Dragon Breath and other similar abilities +* Battle AI should now avoid ending turn on the moat +* 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 + +### Launcher + +* Implemented support for multiple mod presets allowing player to quickly switch between them +* Added new Start Game page to Launcher which is now used when starting the game +* Added option to create empty mod preset to quickly disable all mods +* Added button to update all installed mods to Start Game page +* Added diagnostics to detect common issues with Heroes III data files +* Added built-in help descriptions for functionalities such as data files import to better explain them to players +* It is now always possible to disable or uninstall a mod. Any mods that depend on this mod will be automatically disabled +* It is now always possible to update a mod, even if there are mods that depend on this mod. +* It is now possible to enable mod that conflicts with already active mod. Conflicting mods will be automatically disabled +* If main mod is disabled, all its submods will have their active or inactive status shown as greyed-out for clarity +* If mod depends or conflicts with a submod, Launcher will now also show name of parent mod in list of dependencies / conflicts +* Game will now cache result of mod repository checkout and restore it immediately on next start. This removes flickering when game fills list of available mods. +* Screenshot and Changelog tabs in mod description are now disabled for mods that do not have them. +* Launcher will now correctly show conflicts on both sides - if mod A is marked as conflicting with B, then information on this conflict will be shown in description of both mod A and mod B (instead of only in mod B) +* Added Swedish translation +* Added better diagnostics for gog installer extraction errors +* It is no longer possible to start installation or update for a mod that is already being downloaded +* Fixed detection of existing Heroes III Complete or Shadow of Death data files during import + +### Map Editor + +* It is now possible to remove any map object as part of timed event +* Implemented tracking of building requirements for Building Dialog +* Added build/demolish/enable/disable all buildings options to Building Dialog in town properties +* Added support for customization of heroes artifacts +* 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 +* Added Recent Files to File Menu and Toolbar +* Fixed crash on attempting to save map with random dwelling + +### Modding + +* Json configuration files from different mods no longer can override each other to reduce possibility of a file name clash +* Game will now load high-resolution assets when xbrz upscaler is in use from Data2x, Data3x, Data4x, or Sprites2x, Sprites3x, Sprites4x directories. +* Game will now load high-resolution movies when xbrz upscaler is in use from Video2x, Video3x, Video4x directories +* 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. +* Added support for explicitly visitable town buildings that will activate only on click and not on construction or on hero visit (Mana Vortex from HotA) +* 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 +* It is now possible to add description to an object with "market" handler +* 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 +* Fixed several cases where vcmi will report errors in json without specifying filename of invalid file +* Fixed selection of gendered sprites for heroes on adventure map +* It is now possible to change minimal values of primary skills for heroes +* Added support for HotA Bank building from Factory +* Added support for HotA Grotto buiding from Cove +* Added support for HotA-style 8th creature in town +* Town building can now define war machine produced in this building (Blacksmith or Ballista Yard) +* Town building can now define provided fortifications - health of walls, towers, presence of moat, identifier of creature shooter on tower +* War Machines Factory no longer unconditionally contain war machines from the original game, allowing mods to define list of war machines from scratch +* Added MECHANICAL bonus +* 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 +* 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 +* Added support for text subtitiles for video files +* Added validation of objects with "market" and "flaggable" handlers +* Added "special" property for secondary skills + +## 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 @@ -10,14 +276,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 @@ -25,21 +293,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 @@ -49,20 +320,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 @@ -70,16 +344,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 @@ -89,16 +366,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. @@ -111,8 +391,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. @@ -121,6 +402,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 @@ -133,12 +415,13 @@ * 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 * Mod list no longer shows mod version column. Version is now only shown in the mod description. * Launcher will now skip the Heroes 3 data import step if data has been found automatically -* Fixed inport of existing data files on iOS. This option now requires iOS 13 or later +* Fixed import of existing data files on iOS. This option now requires iOS 13 or later * Fixed import using offline installer on iOS. * Buttons to open data directories in the Help tab are now hidden on mobile systems if they can't be opened with file browser * Added the configuration files directory to the Help tab as it is located separately on Linux systems @@ -146,15 +429,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 @@ -163,19 +447,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 @@ -189,6 +477,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 @@ -197,11 +486,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 @@ -213,37 +504,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 @@ -251,24 +543,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 @@ -281,6 +578,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 @@ -294,10 +592,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 @@ -312,23 +612,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 @@ -337,6 +641,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 @@ -353,6 +658,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 @@ -365,6 +671,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. @@ -402,6 +709,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 @@ -419,14 +727,16 @@ * 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 (shotcut: 'e') +* 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 * Battles against AI players can now be done using quick combat * Disabling battle queue will now correctly reposition hero statistics preview popup * 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 @@ -449,6 +759,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 @@ -465,6 +776,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 @@ -474,6 +786,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 @@ -483,6 +796,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 @@ -493,15 +807,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 @@ -510,9 +825,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 @@ -520,24 +836,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 @@ -545,6 +866,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 @@ -554,12 +876,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. @@ -570,10 +894,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 @@ -581,11 +906,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. @@ -596,6 +923,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. @@ -603,15 +931,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) @@ -623,9 +954,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 @@ -639,10 +971,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 @@ -653,6 +987,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 @@ -669,6 +1004,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 @@ -681,11 +1017,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 @@ -693,6 +1030,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 @@ -700,12 +1038,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 @@ -722,15 +1062,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 @@ -754,6 +1096,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 @@ -763,10 +1106,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 @@ -775,6 +1120,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 @@ -786,6 +1132,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 @@ -793,14 +1140,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 @@ -809,6 +1157,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 @@ -817,11 +1166,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) @@ -836,6 +1187,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 @@ -851,6 +1203,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 @@ -868,6 +1221,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 @@ -875,7 +1229,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 @@ -887,6 +1241,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 @@ -897,6 +1252,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 @@ -917,9 +1273,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 @@ -931,6 +1288,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 @@ -939,9 +1297,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 @@ -954,6 +1314,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 @@ -971,20 +1332,23 @@ * 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 campaing scenario will now correctly reapply starting bonus +* Restarting loaded campaign scenario will now correctly reapply starting bonus * Reverted FPS limit on mobile systems back to 60 fps * Fixed loading of translations for maps and campaigns * Fixed loading of preconfigured starting army for heroes with preconfigured spells @@ -993,7 +1357,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 @@ -1001,13 +1366,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 @@ -1027,9 +1394,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 @@ -1047,7 +1415,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 @@ -1058,7 +1427,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 @@ -1066,14 +1436,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 @@ -1082,6 +1454,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 @@ -1097,7 +1470,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 @@ -1107,7 +1481,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 @@ -1122,7 +1497,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 @@ -1143,19 +1519,21 @@ * 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 * Battle opening sound can now be skipped with mouse click * Fixed movement through moat of double-hexed units * Fixed removal of Land Mines and Fire Walls -* Obstacles will now corectly show up either below or above unit +* Obstacles will now correctly show up either below or above unit * It is now possible to teleport a unit through destroyed walls * 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 @@ -1178,9 +1556,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 @@ -1201,9 +1580,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 @@ -1214,7 +1594,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 @@ -1230,14 +1610,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 @@ -1250,7 +1632,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 @@ -1266,11 +1649,12 @@ * 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 * Fixed potential game hang on generation of random map -* RMG will now generate addditional monolith pairs to create required number of zone connections +* RMG will now generate additional monolith pairs to create required number of zone connections * RMG will try to place Subterranean Gates as far away from other objects (including each other) as possible * RMG will now try to place objects as far as possible in both zones sharing a guard, not only the first one. * Use only one template for an object in zone @@ -1283,7 +1667,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 @@ -1294,7 +1679,7 @@ * Added option to show amount of creatures as numeric range rather than adjective * Added option to show map grid * Map swipe is no longer exclusive for phones and can be enabled on desktop platforms -* Added more graduated settigns for hero movement speed +* Added more graduated settings for hero movement speed * Map scrolling is now more graduated and scrolls with pixel-level precision * Hero movement speed now matches H3 * Improved performance of adventure map rendering @@ -1308,24 +1693,27 @@ * 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 * Buttons in battle window now correctly show tooltip in status bar * Fixed cursor image during enemy turn in combat -* Game will no longer promt to assemble artifacts if they fall into backpack +* Game will no longer prompt to assemble artifacts if they fall into backpack * It is now possible to use in-game console for vcmi commands * Stacks sized 1000-9999 units will not be displayed as "1k" * It is now possible to select destination town for Town Portal via double-click * 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 @@ -1335,7 +1723,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 @@ -1354,7 +1743,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 @@ -1362,7 +1751,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 @@ -1371,7 +1760,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 @@ -1392,23 +1782,25 @@ * 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 * Dragon Breath attack now correctly uses different attack animation if multiple targets are hit * Petrification: implemented visual effect * Paralyze: added visual effect -* Blind: Stacks will no longer retailate on attack that blinds them +* 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. @@ -1429,10 +1821,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 @@ -1443,27 +1836,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 @@ -1471,7 +1869,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 @@ -1480,7 +1879,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 @@ -1489,27 +1889,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 @@ -1517,19 +1921,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 @@ -1537,30 +1944,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 @@ -1569,19 +1979,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: -* Fixed many mising or wrong pickup and visit sounds for map objects +### 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 @@ -1592,7 +2004,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 @@ -1601,50 +2014,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 - supress 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 @@ -1652,126 +2072,148 @@ * 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 -* Implemented CURE spell negative dispell effect + * 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::asign -* * 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 * Zone borders and connections, fractalized paths inside zones * Guard generation -* Treasue piles generation (so far only few removable objects) +* 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. @@ -1780,26 +2222,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 @@ -1810,12 +2256,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 @@ -1834,13 +2282,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 @@ -1848,15 +2298,17 @@ * 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. -* Mods can access only ID's from dependenies, virtual "core" mod and itself (optional for some mods compatibility) +* Mods can access only ID's from dependencies, virtual "core" mod and itself (optional for some mods compatibility) * 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 @@ -1865,34 +2317,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 @@ -1900,55 +2357,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 @@ -1956,9 +2419,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. @@ -1968,13 +2432,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: -* It is possible to select a battle AI module used by VCMI by typing into the console "setBattleAI ". The names of avaialble 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. +### 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 @@ -1986,11 +2452,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. @@ -2007,22 +2475,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 @@ -2031,7 +2502,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 @@ -2040,202 +2511,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 -* * Dispell 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) -* * Resorce 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 @@ -2243,107 +2738,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 higlighted. 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 unavaliable spells anymore. +* 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 @@ -2355,13 +2866,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: -* Cursor shows if tile is accesible and how many turns away +### 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) @@ -2371,72 +2884,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 @@ -2455,13 +2973,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 @@ -2470,49 +2990,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) @@ -2525,10 +3050,11 @@ http://bugs.vcmi.eu/changelog_page.php?version_id=2 * Diplomacy secondary skill support * timed events won't cause resources amount to be negative * support for sorcery secondary skill -* reduntant quotation marks from artifact descriptions are removed +* 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 @@ -2536,7 +3062,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 @@ -2546,41 +3073,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 @@ -2594,46 +3125,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 @@ -2647,82 +3181,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 @@ -2731,39 +3273,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 @@ -2775,39 +3320,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 @@ -2816,49 +3365,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 @@ -2867,7 +3421,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) @@ -2875,20 +3430,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 @@ -2901,20 +3459,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 @@ -2930,7 +3491,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 @@ -2940,7 +3502,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 @@ -2953,7 +3515,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 @@ -2973,9 +3535,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 @@ -2984,21 +3547,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 @@ -3011,7 +3577,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 @@ -3024,12 +3591,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 @@ -3046,18 +3613,18 @@ 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 * [feature] picked artifacts are added to hero's backpack * [feature] possibility of choosing player to play * [bugfix] ZELP.TXT file *should* be handled correctly even it is non-english -* [bugfix] fixed crashbug in reading defs with negativ left/right margins +* [bugfix] fixed crashbug in reading defs with negative left/right margins * [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 @@ -3070,7 +3637,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 57c4466ef..5e9b2c681 100644 --- a/Global.h +++ b/Global.h @@ -102,6 +102,12 @@ static_assert(sizeof(bool) == 1, "Bool needs to be 1 byte in size."); # define STRONG_INLINE inline #endif +// Required for building boost::stacktrace on macOS. +// See https://github.com/boostorg/stacktrace/issues/88 +#if defined(VCMI_APPLE) +#define _GNU_SOURCE +#endif + #define _USE_MATH_DEFINES #include @@ -148,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 @@ -700,6 +709,33 @@ namespace vstd return a + (b - a) * f; } + /// Divides dividend by divisor and rounds result up + /// For use with integer-only arithmetic + template + Integer1 divideAndCeil(const Integer1 & dividend, const Integer2 & divisor) + { + static_assert(std::is_integral_v && std::is_integral_v, "This function should only be used with integral types"); + return (dividend + divisor - 1) / divisor; + } + + /// Divides dividend by divisor and rounds result to nearest + /// For use with integer-only arithmetic + template + Integer1 divideAndRound(const Integer1 & dividend, const Integer2 & divisor) + { + static_assert(std::is_integral_v && std::is_integral_v, "This function should only be used with integral types"); + return (dividend + divisor / 2 - 1) / divisor; + } + + /// Divides dividend by divisor and rounds result down + /// For use with integer-only arithmetic + template + Integer1 divideAndFloor(const Integer1 & dividend, const Integer2 & divisor) + { + static_assert(std::is_integral_v && std::is_integral_v, "This function should only be used with integral types"); + return dividend / divisor; + } + template bool isAlmostZero(const Floating & value) { diff --git a/Mods/vcmi/Content/Data/NotoSans-Medium.ttf b/Mods/vcmi/Content/Data/NotoSans-Medium.ttf new file mode 100644 index 000000000..4c5069f50 Binary files /dev/null and b/Mods/vcmi/Content/Data/NotoSans-Medium.ttf differ diff --git a/Mods/vcmi/Content/Data/NotoSerif-Black.ttf b/Mods/vcmi/Content/Data/NotoSerif-Black.ttf new file mode 100644 index 000000000..cc9ebad47 Binary files /dev/null and b/Mods/vcmi/Content/Data/NotoSerif-Black.ttf differ diff --git a/Mods/vcmi/Content/Data/NotoSerif-Bold.ttf b/Mods/vcmi/Content/Data/NotoSerif-Bold.ttf new file mode 100644 index 000000000..13eb2a0a8 Binary files /dev/null and b/Mods/vcmi/Content/Data/NotoSerif-Bold.ttf differ diff --git a/Mods/vcmi/Content/Data/NotoSerif-Medium.ttf b/Mods/vcmi/Content/Data/NotoSerif-Medium.ttf new file mode 100644 index 000000000..ae77bd049 Binary files /dev/null and b/Mods/vcmi/Content/Data/NotoSerif-Medium.ttf differ 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/Content/Sprites/battle/rangeHighlights/rangeHighlightsGreen.json b/Mods/vcmi/Content/Sprites/battle/rangeHighlights/rangeHighlightsGreen.json new file mode 100644 index 000000000..fff27bb58 --- /dev/null +++ b/Mods/vcmi/Content/Sprites/battle/rangeHighlights/rangeHighlightsGreen.json @@ -0,0 +1,34 @@ +{ + "basepath" : "battle/rangeHighlights/green/", + "images" : + [ + { "frame" : 0, "file" : "empty.png"}, // 000001 -> 00 empty frame + + // load single edges + { "frame" : 1, "file" : "topLeft.png"}, //000001 -> 01 topLeft + { "frame" : 2, "file" : "topLeft.png", "verticalFlip" : true }, //000010 -> 02 topRight + { "frame" : 3, "file" : "left.png", "verticalFlip" : true }, //000100 -> 04 right + { "frame" : 4, "file" : "topLeft.png", "verticalFlip" : true, "horizontalFlip" : true }, //001000 -> 08 bottomRight + { "frame" : 5, "file" : "topLeft.png", "horizontalFlip" : true }, //010000 -> 16 bottomLeft + { "frame" : 6, "file" : "left.png"}, //100000 -> 32 left + + // load double edges + { "frame" : 7, "file" : "top.png"}, //000011 -> 03 top + { "frame" : 8, "file" : "top.png", "horizontalFlip" : true }, //011000 -> 24 bottom + { "frame" : 9, "file" : "topLeftHalfCorner.png", "verticalFlip" : true }, //000110 -> 06 topRightHalfCorner + { "frame" : 10, "file" : "topLeftHalfCorner.png", "verticalFlip" : true, "horizontalFlip" : true }, //001100 -> 12 bottomRightHalfCorner + { "frame" : 11, "file" : "topLeftHalfCorner.png", "horizontalFlip" : true }, //110000 -> 48 bottomLeftHalfCorner + { "frame" : 12, "file" : "topLeftHalfCorner.png"}, //100001 -> 33 topLeftHalfCorner + + // load halves + { "frame" : 13, "file" : "leftHalf.png", "verticalFlip" : true}, //001110 -> 14 rightHalf + { "frame" : 14, "file" : "leftHalf.png"}, //110001 -> 49 leftHalf + + // load corners + { "frame" : 15, "file" : "topLeftCorner.png", "verticalFlip" : true }, //000111 -> 07 topRightCorner + { "frame" : 16, "file" : "topLeftCorner.png", "verticalFlip" : true, "horizontalFlip" : true }, //011100 -> 28 bottomRightCorner + { "frame" : 17, "file" : "topLeftCorner.png", "horizontalFlip" : true }, //111000 -> 56 bottomLeftCorner + { "frame" : 18, "file" : "topLeftCorner.png"} //100011 -> 35 topLeftCorner + ] +} + diff --git a/Mods/vcmi/Content/Sprites/battle/rangeHighlights/rangeHighlightsRed.json b/Mods/vcmi/Content/Sprites/battle/rangeHighlights/rangeHighlightsRed.json new file mode 100644 index 000000000..68908f0ce --- /dev/null +++ b/Mods/vcmi/Content/Sprites/battle/rangeHighlights/rangeHighlightsRed.json @@ -0,0 +1,34 @@ +{ + "basepath" : "battle/rangeHighlights/red/", + "images" : + [ + { "frame" : 0, "file" : "empty.png"}, // 000001 -> 00 empty frame + + // load single edges + { "frame" : 1, "file" : "topLeft.png"}, //000001 -> 01 topLeft + { "frame" : 2, "file" : "topLeft.png", "verticalFlip" : true }, //000010 -> 02 topRight + { "frame" : 3, "file" : "left.png", "verticalFlip" : true }, //000100 -> 04 right + { "frame" : 4, "file" : "topLeft.png", "verticalFlip" : true, "horizontalFlip" : true }, //001000 -> 08 bottomRight + { "frame" : 5, "file" : "topLeft.png", "horizontalFlip" : true }, //010000 -> 16 bottomLeft + { "frame" : 6, "file" : "left.png"}, //100000 -> 32 left + + // load double edges + { "frame" : 7, "file" : "top.png"}, //000011 -> 03 top + { "frame" : 8, "file" : "top.png", "horizontalFlip" : true }, //011000 -> 24 bottom + { "frame" : 9, "file" : "topLeftHalfCorner.png", "verticalFlip" : true }, //000110 -> 06 topRightHalfCorner + { "frame" : 10, "file" : "topLeftHalfCorner.png", "verticalFlip" : true, "horizontalFlip" : true }, //001100 -> 12 bottomRightHalfCorner + { "frame" : 11, "file" : "topLeftHalfCorner.png", "horizontalFlip" : true }, //110000 -> 48 bottomLeftHalfCorner + { "frame" : 12, "file" : "topLeftHalfCorner.png"}, //100001 -> 33 topLeftHalfCorner + + // load halves + { "frame" : 13, "file" : "leftHalf.png", "verticalFlip" : true}, //001110 -> 14 rightHalf + { "frame" : 14, "file" : "leftHalf.png"}, //110001 -> 49 leftHalf + + // load corners + { "frame" : 15, "file" : "topLeftCorner.png", "verticalFlip" : true }, //000111 -> 07 topRightCorner + { "frame" : 16, "file" : "topLeftCorner.png", "verticalFlip" : true, "horizontalFlip" : true }, //011100 -> 28 bottomRightCorner + { "frame" : 17, "file" : "topLeftCorner.png", "horizontalFlip" : true }, //111000 -> 56 bottomLeftCorner + { "frame" : 18, "file" : "topLeftCorner.png"} //100011 -> 35 topLeftCorner + ] +} + 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 78% rename from Mods/vcmi/config/vcmi/chinese.json rename to Mods/vcmi/Content/config/chinese.json index a3fedda51..671011845 100644 --- a/Mods/vcmi/config/vcmi/chinese.json +++ b/Mods/vcmi/Content/config/chinese.json @@ -11,7 +11,12 @@ "vcmi.adventureMap.monsterThreat.levels.8" : "挑战性的", "vcmi.adventureMap.monsterThreat.levels.9" : "压倒性的", "vcmi.adventureMap.monsterThreat.levels.10" : "致命的", - "vcmi.adventureMap.monsterThreat.levels.11" : "无法取胜", + "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" : "没有足够的市场。", @@ -20,8 +25,16 @@ "vcmi.adventureMap.playerAttacked" : "玩家遭受攻击: %s", "vcmi.adventureMap.moveCostDetails" : "移动点数 - 花费: %TURNS 轮 + %POINTS 点移动力, 剩余移动力: %REMAINING", "vcmi.adventureMap.moveCostDetailsNoTurns" : "移动点数 - 花费: %POINTS 点移动力, 剩余移动力: %REMAINING", + "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" : "褐色", @@ -36,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" : "分割单个生物", @@ -54,9 +73,27 @@ "vcmi.radialWheel.moveUp" : "上移", "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" : "连接失败", @@ -72,6 +109,55 @@ "vcmi.lobby.noUnderground" : "无地下部分", "vcmi.lobby.sortDate" : "以修改时间排序地图", "vcmi.lobby.backToLobby" : "返回大厅", + "vcmi.lobby.author" : "作者", + "vcmi.lobby.handicap" : "障碍", + "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.broadcast.failedLoadGame" : "加载游戏失败", + "vcmi.broadcast.command" : "输入'!help'来列举可用命令", + "vcmi.broadcast.simturn.end" : "同步回合已结束", + "vcmi.broadcast.simturn.endBetween" : "在玩家 %s和%s之间的同步回合已结束", + "vcmi.broadcast.serverProblem" : "服务器遇到了一个问题", + "vcmi.broadcast.gameTerminated" : "游戏已终止", + "vcmi.broadcast.gameSavedAs" : "游戏另存为", + "vcmi.broadcast.noCheater" : "没有注册作弊者!", + "vcmi.broadcast.playerCheater" : "玩家%s是作弊者!", + "vcmi.broadcast.statisticFile" : "统计文件可以在目录%s中找到", + "vcmi.broadcast.help.commands" : "主机可用命令:", + "vcmi.broadcast.help.exit" : "'!exit' - 立即结束当前游戏", + "vcmi.broadcast.help.kick" : "'!kick <玩家>' - 从游戏中踢除特定玩家", + "vcmi.broadcast.help.save" : "'!save <文件名>' - 以指定文件名保存游戏", + "vcmi.broadcast.help.statistic" : "'!statistic' - 将游戏统计信息保存为csv文件", + "vcmi.broadcast.help.commandsAll" : "所有玩家可用命令:", + "vcmi.broadcast.help.help" : "'!help' - 显示此帮助", + "vcmi.broadcast.help.cheaters" : "'!cheaters' - 列出在游戏中使用作弊命令的玩家", + "vcmi.broadcast.help.vote" : "'!vote' - 如果所有玩家投票通过,允许更改一些游戏设置", + "vcmi.broadcast.vote.allow" : "'!vote simturns allow X' - 允许进行指定天数的同步回合,发生接触解除", + "vcmi.broadcast.vote.force" : "'!vote simturns force X' - 强制进行指定天数的同步回合,阻止玩家接触", + "vcmi.broadcast.vote.abort" : "'!vote simturns abort' - 本回合结束后中止同步回合", + "vcmi.broadcast.vote.timer" : "'!vote timer prolong X' - 将所有玩家的基础计时器延长指定的秒数", + "vcmi.broadcast.vote.noActive" : "没有正在进行的投票!", + "vcmi.broadcast.vote.yes" : "是", + "vcmi.broadcast.vote.no" : "否", + "vcmi.broadcast.vote.notRecognized" : "投票命令无法识别!", + "vcmi.broadcast.vote.success.untilContacts" : "投票成功,同步回合将继续进行%s天,发生接触解除", + "vcmi.broadcast.vote.success.contactsBlocked" : "投票成功,同步回合将继续进行%s天,阻止玩家接触", + "vcmi.broadcast.vote.success.nextDay" : "投票成功,将于第二天结束同步回合", + "vcmi.broadcast.vote.success.timer" : "投票成功,所有玩家的计时器已延长 %s 秒。", + "vcmi.broadcast.vote.aborted" : "玩家投票反对更改,投票已中止。", + "vcmi.broadcast.vote.start.untilContacts" : "开始投票,允许同步回合再进行 %s 天", + "vcmi.broadcast.vote.start.contactsBlocked" : "开始投票, 允许同步回合再强制进行 %s 天", + "vcmi.broadcast.vote.start.nextDay" : "开始投票,从第二天起结束同布回合", + "vcmi.broadcast.vote.start.timer" : "开始投票,将所有玩家的计时器延长 %s 秒。", + "vcmi.broadcast.vote.hint" : "输入'!vote yes'来同意这项改动或输入'!vote no'来投票反对它。", "vcmi.lobby.login.title" : "VCMI大厅", "vcmi.lobby.login.username" : "用户名:", @@ -80,6 +166,7 @@ "vcmi.lobby.login.create" : "新账号", "vcmi.lobby.login.login" : "登录", "vcmi.lobby.login.as" : "以 %s 身份登录", + "vcmi.lobby.login.spectator" : "旁观者", "vcmi.lobby.header.rooms" : "游戏房间 - %d", "vcmi.lobby.header.channels" : "聊天频道", "vcmi.lobby.header.chat.global" : "全局游戏聊天 - %s", // %s -> language name @@ -136,12 +223,13 @@ "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.server.errors.wrongIdentified" : "你被识别为玩家%s,但预期是玩家%s。", + "vcmi.server.errors.notAllowed" : "你无权执行此操作!", "vcmi.dimensionDoor.seaToLandError" : "无法在陆地与海洋之间使用异次元之门传送。", @@ -157,6 +245,38 @@ "vcmi.systemOptions.otherGroup" : "其他设置", // unused right now "vcmi.systemOptions.townsGroup" : "城镇画面", + "vcmi.statisticWindow.statistics" : "统计", + "vcmi.statisticWindow.tsvCopy" : "复制到剪切板", + "vcmi.statisticWindow.selectView" : "选择视角", + "vcmi.statisticWindow.value" : "值", + "vcmi.statisticWindow.title.overview" : "概况", + "vcmi.statisticWindow.title.resources" : "资源", + "vcmi.statisticWindow.title.income" : "收入", + "vcmi.statisticWindow.title.numberOfHeroes" : "英雄数量", + "vcmi.statisticWindow.title.numberOfTowns" : "城镇数量", + "vcmi.statisticWindow.title.numberOfArtifacts" : "宝物数量", + "vcmi.statisticWindow.title.numberOfDwellings" : "野外巢穴数量", + "vcmi.statisticWindow.title.numberOfMines" : "矿井数量", + "vcmi.statisticWindow.title.armyStrength" : "部队强度", + "vcmi.statisticWindow.title.experience" : "经验", + "vcmi.statisticWindow.title.resourcesSpentArmy" : "部队花费", + "vcmi.statisticWindow.title.resourcesSpentBuildings" : "建造花费", + "vcmi.statisticWindow.title.mapExplored" : "地图探索比例", + "vcmi.statisticWindow.param.playerName" : "玩家名称", + "vcmi.statisticWindow.param.daysSurvived" : "存活天数", + "vcmi.statisticWindow.param.maxHeroLevel" : "最大英雄等级", + "vcmi.statisticWindow.param.battleWinRatioHero" : "胜率(对英雄)", + "vcmi.statisticWindow.param.battleWinRatioNeutral" : "胜率(对中立生物)", + "vcmi.statisticWindow.param.battlesHero" : "战斗(对英雄)", + "vcmi.statisticWindow.param.battlesNeutral" : "战斗(对中立生物)", + "vcmi.statisticWindow.param.maxArmyStrength" : "最大部队强度", + "vcmi.statisticWindow.param.tradeVolume" : "交易量", + "vcmi.statisticWindow.param.obeliskVisited" : "方尖塔访问", + "vcmi.statisticWindow.icon.townCaptured" : "占领城镇", + "vcmi.statisticWindow.icon.strongestHeroDefeated" : "击败对手最强英雄", + "vcmi.statisticWindow.icon.grailFound" : "找到神器", + "vcmi.statisticWindow.icon.defeated" : "被击败", + "vcmi.systemOptions.fullscreenBorderless.hover" : "全屏 (无边框)", "vcmi.systemOptions.fullscreenBorderless.help" : "{全屏}\n\n选中时,VCMI将以无边框全屏模式运行。该模式下,游戏会始终和桌面分辨率保持一致,无视设置的分辨率。 ", "vcmi.systemOptions.fullscreenExclusive.hover" : "全屏 (独占)", @@ -199,6 +319,8 @@ "vcmi.adventureOptions.infoBarCreatureManagement.help" : "{信息面板生物管理}\n\n允许在信息面板中重新排列生物,而不是在默认组件之间循环。", "vcmi.adventureOptions.leftButtonDrag.hover" : "左键拖动地图", "vcmi.adventureOptions.leftButtonDrag.help" : "{左键拖动地图}\n\n启用后,按住左键移动鼠标将拖动冒险地图视图。", + "vcmi.adventureOptions.rightButtonDrag.hover" : "右键拖动地图", + "vcmi.adventureOptions.rightButtonDrag.help" : "{右键拖动地图}\n\n启用后,按住右键移动鼠标将拖动冒险地图视图。", "vcmi.adventureOptions.smoothDragging.hover" : "平滑地图拖动", "vcmi.adventureOptions.smoothDragging.help" : "{平滑地图拖动}\n\n启用后,地图拖动会产生柔和的羽化效果。", "vcmi.adventureOptions.skipAdventureMapAnimations.hover" : "关闭淡入淡出特效", @@ -220,7 +342,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": "", @@ -237,6 +359,8 @@ "vcmi.battleOptions.skipBattleIntroMusic.help": "{跳过战斗开始音乐}\n\n战斗开始音乐播放期间,你也能够进行操作。", "vcmi.battleOptions.endWithAutocombat.hover": "结束战斗", "vcmi.battleOptions.endWithAutocombat.help": "{结束战斗}\n\n以自动战斗立即结束剩余战斗过程", + "vcmi.battleOptions.showQuickSpell.hover": "展示快捷法术面板", + "vcmi.battleOptions.showQuickSpell.help": "{展示快捷法术面板}\n\n展示快捷选择法术的面板。", "vcmi.adventureMap.revisitObject.hover" : "重新访问", "vcmi.adventureMap.revisitObject.help" : "{重新访问}\n\n让当前英雄重新访问地图建筑或城镇。", @@ -285,17 +409,9 @@ "vcmi.townHall.missingBase" : "必须先建造基础建筑 %s", "vcmi.townHall.noCreaturesToRecruit" : "没有可供招募的生物。", - "vcmi.townHall.greetingManaVortex" : "接近%s时,你会全身充满活力,并且你的魔法值会加倍。", - "vcmi.townHall.greetingKnowledge" : "你研究了%s的浮雕,洞察了魔法的秘密(知识+1)。", - "vcmi.townHall.greetingSpellPower" : "%s教你如何运用魔法力量(力量+1)。", - "vcmi.townHall.greetingExperience" : "参观%s可以让你学会许多新的技能(经验值+1000)。", - "vcmi.townHall.greetingAttack" : "在%s中稍待片刻可以让你学会更有效的战斗技巧(攻击力+1)。", - "vcmi.townHall.greetingDefence" : "在%s中稍待片刻,富有战斗经验的战士会教你防御技巧(防御力+1)。", - "vcmi.townHall.hasNotProduced" : "本周%s并没有产生什么资源。", - "vcmi.townHall.hasProduced" : "本周%s产生了%d个%s。", - "vcmi.townHall.greetingCustomBonus" : "%s 给予英雄 +%d %s%s。", - "vcmi.townHall.greetingCustomUntil" : "直到下一场战斗。", - "vcmi.townHall.greetingInTownMagicWell" : "%s使你的魔法值恢复到最大值。", + + "vcmi.townStructure.bank.borrow" : "你走进银行。一位银行职员看到你,说道:“我们为您提供了一个特别优惠。您可以向我们借2500金币,期限为5天。您每天需要偿还500金币。”", + "vcmi.townStructure.bank.payBack" : "你走进银行。一位银行职员看到你,说道:“您已经获得了贷款。还清之前,不能再申请新的贷款。”", "vcmi.logicalExpressions.anyOf" : "以下任一前提:", "vcmi.logicalExpressions.allOf" : "以下所有前提:", @@ -305,6 +421,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" : "邀请英雄", @@ -482,6 +605,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": "额外反击", @@ -544,7 +669,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": "可以治疗友军单位", @@ -629,5 +754,35 @@ "core.bonus.WATER_IMMUNITY.name": "水系免疫", "core.bonus.WATER_IMMUNITY.description": "免疫水系魔法", "core.bonus.WIDE_BREATH.name": "弧形焰息", - "core.bonus.WIDE_BREATH.description": "大范围喷吐攻击(目标左右以及后方共6格)" + "core.bonus.WIDE_BREATH.description": "大范围喷吐攻击(目标左右以及后方共6格)", + "core.bonus.DISINTEGRATE.name": "解体", + "core.bonus.DISINTEGRATE.description": "死亡后不会留下尸体", + "core.bonus.INVINCIBLE.name": "无敌", + "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..0494436fe --- /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 artefakty", + + "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 80% rename from Mods/vcmi/config/vcmi/english.json rename to Mods/vcmi/Content/config/english.json index 47f490262..f70cad6f5 100644 --- a/Mods/vcmi/config/vcmi/english.json +++ b/Mods/vcmi/Content/config/english.json @@ -12,6 +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 %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!", @@ -20,8 +25,16 @@ "vcmi.adventureMap.playerAttacked" : "Player has been attacked: %s", "vcmi.adventureMap.moveCostDetails" : "Movement points - Cost: %TURNS turns + %POINTS points, Remaining points: %REMAINING", "vcmi.adventureMap.moveCostDetailsNoTurns" : "Movement points - Cost: %POINTS points, Remaining points: %REMAINING", + "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", @@ -36,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 Artifacts", + "vcmi.radialWheel.mergeSameUnit" : "Merge same creatures", "vcmi.radialWheel.fillSingleUnit" : "Fill with single creatures", "vcmi.radialWheel.splitSingleUnit" : "Split off single creature", @@ -54,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", @@ -72,7 +109,56 @@ "vcmi.lobby.noUnderground" : "no underground", "vcmi.lobby.sortDate" : "Sorts maps by change date", "vcmi.lobby.backToLobby" : "Return to lobby", - + "vcmi.lobby.author" : "Author", + "vcmi.lobby.handicap" : "Handicap", + "vcmi.lobby.handicap.resource" : "Gives players appropriate resources to start with in addition to the normal starting resources. Negative values are allowed, but are limited to 0 in total (the player never starts with negative resources).", + "vcmi.lobby.handicap.income" : "Changes the player's various incomes by the percentage. Is rounded up.", + "vcmi.lobby.handicap.growth" : "Changes the growth rate of creatures in the towns owned by the player. Is rounded up.", + "vcmi.lobby.deleteUnsupportedSave" : "{Unsupported saves found}\n\nVCMI has found %d saved games that are no longer supported, possibly due to differences in VCMI versions.\n\nDo you want to delete them?", + "vcmi.lobby.deleteSaveGameTitle" : "Select a Saved Game to delete", + "vcmi.lobby.deleteMapTitle" : "Select a Scenario to delete", + "vcmi.lobby.deleteFile" : "Do you want to delete following file?", + "vcmi.lobby.deleteFolder" : "Do you want to delete following folder?", + "vcmi.lobby.deleteMode" : "Switch to delete mode and back", + + "vcmi.broadcast.failedLoadGame" : "Failed to load game", + "vcmi.broadcast.command" : "Use '!help' to list available commands", + "vcmi.broadcast.simturn.end" : "Simultaneous turns have ended", + "vcmi.broadcast.simturn.endBetween" : "Simultaneous turns between players %s and %s have ended", + "vcmi.broadcast.serverProblem" : "Server encountered a problem", + "vcmi.broadcast.gameTerminated" : "game was terminated", + "vcmi.broadcast.gameSavedAs" : "game saved as", + "vcmi.broadcast.noCheater" : "No cheaters registered!", + "vcmi.broadcast.playerCheater" : "Player %s is cheater!", + "vcmi.broadcast.statisticFile" : "Statistic files can be found in %s directory", + "vcmi.broadcast.help.commands" : "Available commands to host:", + "vcmi.broadcast.help.exit" : "'!exit' - immediately ends current game", + "vcmi.broadcast.help.kick" : "'!kick ' - kick specified player from the game", + "vcmi.broadcast.help.save" : "'!save ' - save game under specified filename", + "vcmi.broadcast.help.statistic" : "'!statistic' - save game statistics as csv file", + "vcmi.broadcast.help.commandsAll" : "Available commands to all players:", + "vcmi.broadcast.help.help" : "'!help' - display this help", + "vcmi.broadcast.help.cheaters" : "'!cheaters' - list players that entered cheat command during game", + "vcmi.broadcast.help.vote" : "'!vote' - allows to change some game settings if all players vote for it", + "vcmi.broadcast.vote.allow" : "'!vote simturns allow X' - allow simultaneous turns for specified number of days, or until contact", + "vcmi.broadcast.vote.force" : "'!vote simturns force X' - force simultaneous turns for specified number of days, blocking player contacts", + "vcmi.broadcast.vote.abort" : "'!vote simturns abort' - abort simultaneous turns once this turn ends", + "vcmi.broadcast.vote.timer" : "'!vote timer prolong X' - prolong base timer for all players by specified number of seconds", + "vcmi.broadcast.vote.noActive" : "No active voting!", + "vcmi.broadcast.vote.yes" : "yes", + "vcmi.broadcast.vote.no" : "no", + "vcmi.broadcast.vote.notRecognized" : "Voting command not recognized!", + "vcmi.broadcast.vote.success.untilContacts" : "Voting successful. Simultaneous turns will run for %s more days, or until contact", + "vcmi.broadcast.vote.success.contactsBlocked" : "Voting successful. Simultaneous turns will run for %s more days. Contacts are blocked", + "vcmi.broadcast.vote.success.nextDay" : "Voting successful. Simultaneous turns will end on next day", + "vcmi.broadcast.vote.success.timer" : "Voting successful. Timer for all players has been prolonger for %s seconds", + "vcmi.broadcast.vote.aborted" : "Player voted against change. Voting aborted", + "vcmi.broadcast.vote.start.untilContacts" : "Started voting to allow simultaneous turns for %s more days", + "vcmi.broadcast.vote.start.contactsBlocked" : "Started voting to force simultaneous turns for %s more days", + "vcmi.broadcast.vote.start.nextDay" : "Started voting to end simultaneous turns starting from next day", + "vcmi.broadcast.vote.start.timer" : "Started voting to prolong timer for all players by %s seconds", + "vcmi.broadcast.vote.hint" : "Type '!vote yes' to agree to this change or '!vote no' to vote against it", + "vcmi.lobby.login.title" : "VCMI Online Lobby", "vcmi.lobby.login.username" : "Username:", "vcmi.lobby.login.connecting" : "Connecting...", @@ -80,6 +166,7 @@ "vcmi.lobby.login.create" : "New Account", "vcmi.lobby.login.login" : "Login", "vcmi.lobby.login.as" : "Login as %s", + "vcmi.lobby.login.spectator" : "Spectator", "vcmi.lobby.header.rooms" : "Game Rooms - %d", "vcmi.lobby.header.channels" : "Chat Channels", "vcmi.lobby.header.chat.global" : "Global Game Chat - %s", // %s -> language name @@ -136,12 +223,13 @@ "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.server.errors.wrongIdentified" : "You were identified as player %s while expecting %s", + "vcmi.server.errors.notAllowed" : "You are not allowed to perform this action!", "vcmi.dimensionDoor.seaToLandError" : "It's not possible to teleport from sea to land or vice versa with a Dimension Door.", @@ -157,6 +245,38 @@ "vcmi.systemOptions.otherGroup" : "Other Settings", // unused right now "vcmi.systemOptions.townsGroup" : "Town Screen", + "vcmi.statisticWindow.statistics" : "Statistics", + "vcmi.statisticWindow.tsvCopy" : "Data to clipboard", + "vcmi.statisticWindow.selectView" : "Select view", + "vcmi.statisticWindow.value" : "Value", + "vcmi.statisticWindow.title.overview" : "Overview", + "vcmi.statisticWindow.title.resources" : "Resources", + "vcmi.statisticWindow.title.income" : "Income", + "vcmi.statisticWindow.title.numberOfHeroes" : "No. of heroes", + "vcmi.statisticWindow.title.numberOfTowns" : "No. of towns", + "vcmi.statisticWindow.title.numberOfArtifacts" : "No. of artifacts", + "vcmi.statisticWindow.title.numberOfDwellings" : "No. of dwellings", + "vcmi.statisticWindow.title.numberOfMines" : "No. of mines", + "vcmi.statisticWindow.title.armyStrength" : "Army strength", + "vcmi.statisticWindow.title.experience" : "Experience", + "vcmi.statisticWindow.title.resourcesSpentArmy" : "Army costs", + "vcmi.statisticWindow.title.resourcesSpentBuildings" : "Building costs", + "vcmi.statisticWindow.title.mapExplored" : "Map explore ratio", + "vcmi.statisticWindow.param.playerName" : "Player name", + "vcmi.statisticWindow.param.daysSurvived" : "Days survived", + "vcmi.statisticWindow.param.maxHeroLevel" : "Max hero level", + "vcmi.statisticWindow.param.battleWinRatioHero" : "Win ratio (vs. hero)", + "vcmi.statisticWindow.param.battleWinRatioNeutral" : "Win ratio (vs. neutral)", + "vcmi.statisticWindow.param.battlesHero" : "Battles (vs. hero)", + "vcmi.statisticWindow.param.battlesNeutral" : "Battles (vs. neutral)", + "vcmi.statisticWindow.param.maxArmyStrength" : "Max total army strength", + "vcmi.statisticWindow.param.tradeVolume" : "Trade volume", + "vcmi.statisticWindow.param.obeliskVisited" : "Obelisk visited", + "vcmi.statisticWindow.icon.townCaptured" : "Town captured", + "vcmi.statisticWindow.icon.strongestHeroDefeated" : "Strongest hero of opponent defeated", + "vcmi.statisticWindow.icon.grailFound" : "Grail found", + "vcmi.statisticWindow.icon.defeated" : "Defeated", + "vcmi.systemOptions.fullscreenBorderless.hover" : "Fullscreen (borderless)", "vcmi.systemOptions.fullscreenBorderless.help" : "{Borderless Fullscreen}\n\nIf selected, VCMI will run in borderless fullscreen mode. In this mode, game will always use same resolution as desktop, ignoring selected resolution.", "vcmi.systemOptions.fullscreenExclusive.hover" : "Fullscreen (exclusive)", @@ -197,8 +317,10 @@ "vcmi.adventureOptions.borderScroll.help" : "{Border Scrolling}\n\nScroll adventure map when cursor is adjacent to window edge. Can be disabled by holding down CTRL key.", "vcmi.adventureOptions.infoBarCreatureManagement.hover" : "Info Panel Creature Management", "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" : "Left Click Drag Map", - "vcmi.adventureOptions.leftButtonDrag.help" : "{Left Click Drag Map}\n\nWhen enabled, moving mouse with left button pressed will drag adventure map view.", + "vcmi.adventureOptions.leftButtonDrag.hover" : "Left Click Drag", + "vcmi.adventureOptions.leftButtonDrag.help" : "{Left Click Drag}\n\nWhen enabled, moving mouse with left button pressed will drag adventure map view.", + "vcmi.adventureOptions.rightButtonDrag.hover" : "Right Click Drag", + "vcmi.adventureOptions.rightButtonDrag.help" : "{Right Click Drag}\n\nWhen enabled, moving mouse with right button pressed will drag adventure map view.", "vcmi.adventureOptions.smoothDragging.hover" : "Smooth Map Dragging", "vcmi.adventureOptions.smoothDragging.help" : "{Smooth Map Dragging}\n\nWhen enabled, map dragging has a modern run out effect.", "vcmi.adventureOptions.skipAdventureMapAnimations.hover" : "Skip fading effects", @@ -237,6 +359,8 @@ "vcmi.battleOptions.skipBattleIntroMusic.help": "{Skip Intro Music}\n\nAllow actions during the intro music that plays at the beginning of each battle.", "vcmi.battleOptions.endWithAutocombat.hover": "Ends battle", "vcmi.battleOptions.endWithAutocombat.help": "{Ends battle}\n\nAuto-Combat plays battle to end instant", + "vcmi.battleOptions.showQuickSpell.hover": "Show Quickspell panel", + "vcmi.battleOptions.showQuickSpell.help": "{Show Quickspell panel}\n\nShow panel for quick selecting spells", "vcmi.adventureMap.revisitObject.hover" : "Revisit Object", "vcmi.adventureMap.revisitObject.help" : "{Revisit Object}\n\nIf a hero currently stands on a Map Object, he can revisit the location.", @@ -285,17 +409,9 @@ "vcmi.townHall.missingBase" : "Base building %s must be built first", "vcmi.townHall.noCreaturesToRecruit" : "There are no creatures to recruit!", - "vcmi.townHall.greetingManaVortex" : "As you near the %s your body is filled with new energy. You have doubled your normal spell points.", - "vcmi.townHall.greetingKnowledge" : "You study the glyphs on the %s and gain insight into the workings of various magics (+1 Knowledge).", - "vcmi.townHall.greetingSpellPower" : "The %s teaches you new ways to focus your magical powers (+1 Power).", - "vcmi.townHall.greetingExperience" : "A visit to the %s teaches you many new skills (+1000 Experience).", - "vcmi.townHall.greetingAttack" : "Some time spent at the %s allows you to learn more effective combat skills (+1 Attack Skill).", - "vcmi.townHall.greetingDefence" : "Spending time in the %s, the experienced warriors therein teach you additional defensive skills (+1 Defense).", - "vcmi.townHall.hasNotProduced" : "The %s has not produced anything yet.", - "vcmi.townHall.hasProduced" : "The %s produced %d %s this week.", - "vcmi.townHall.greetingCustomBonus" : "%s gives you +%d %s%s", - "vcmi.townHall.greetingCustomUntil" : " until next battle.", - "vcmi.townHall.greetingInTownMagicWell" : "%s has restored your spell points to maximum.", + + "vcmi.townStructure.bank.borrow" : "You enter the bank. A banker sees you and says: \"We have made a special offer for you. You can take a loan of 2500 gold from us for 5 days. You will have to repay 500 gold every day.\"", + "vcmi.townStructure.bank.payBack" : "You enter the bank. A banker sees you and says: \"You have already got your loan. Pay it back before taking a new one.\"", "vcmi.logicalExpressions.anyOf" : "Any of the following:", "vcmi.logicalExpressions.allOf" : "All of the following:", @@ -305,6 +421,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", @@ -481,7 +604,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", @@ -629,5 +754,13 @@ "core.bonus.WATER_IMMUNITY.name": "Water immunity", "core.bonus.WATER_IMMUNITY.description": "Immune to all spells from the school of Water magic", "core.bonus.WIDE_BREATH.name": "Wide breath", - "core.bonus.WIDE_BREATH.description": "Wide breath attack (multiple hexes)" + "core.bonus.WIDE_BREATH.description": "Wide breath attack (multiple hexes)", + "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.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 95% rename from Mods/vcmi/config/vcmi/french.json rename to Mods/vcmi/Content/config/french.json index 377f3da83..5e1e1e067 100644 --- a/Mods/vcmi/config/vcmi/french.json +++ b/Mods/vcmi/Content/config/french.json @@ -135,17 +135,6 @@ "vcmi.townHall.missingBase" : "Le bâtiment de base %s doit être construit avant", "vcmi.townHall.noCreaturesToRecruit" : "Il n'y a aucune créature à recruter !", - "vcmi.townHall.greetingManaVortex" : "Alors que vous approchez du %s, votre corps est rempli d'une nouvelle énergie. Vous avez doublé vos points de sort normaux.", - "vcmi.townHall.greetingKnowledge" : "Vous étudiez les glyphes sur le %s et découvrez le fonctionnement de diverses magies (+1 Connaissance).", - "vcmi.townHall.greetingSpellPower" : "Le %s vous apprend de nouvelles façons de concentrer vos pouvoirs magiques (+1 Pouvoir).", - "vcmi.townHall.greetingExperience" : "Une visite au %s vous apprend de nombreuses nouvelles compétences (+1000 Expérience).", - "vcmi.townHall.greetingAttack" : "Un peu de temps passé au %s vous permet d'apprendre des compétences de combat plus efficaces (+1 compétence d'attaque).", - "vcmi.townHall.greetingDefence" : "En passant du temps dans le %s, les guerriers expérimentés qui s'y trouvent vous enseignent des compétences défensives supplémentaires (+1 Défense).", - "vcmi.townHall.hasNotProduced" : "Le %s n'a encore rien produit.", - "vcmi.townHall.hasProduced" : "Le %s a produit %d %s cette semaine.", - "vcmi.townHall.greetingCustomBonus" : "%s vous offre +%d %s%s", - "vcmi.townHall.greetingCustomUntil" : " jusqu'à la prochaine bataille.", - "vcmi.townHall.greetingInTownMagicWell" : "%s a restauré vos points de sort au maximum.", "vcmi.logicalExpressions.anyOf" : "L'un des éléments suivants :", "vcmi.logicalExpressions.allOf" : "Tous les éléments suivants :", diff --git a/Mods/vcmi/config/vcmi/german.json b/Mods/vcmi/Content/config/german.json similarity index 79% rename from Mods/vcmi/config/vcmi/german.json rename to Mods/vcmi/Content/config/german.json index 8aabb4e29..25776beef 100644 --- a/Mods/vcmi/config/vcmi/german.json +++ b/Mods/vcmi/Content/config/german.json @@ -12,6 +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 (%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!", @@ -20,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", @@ -36,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", @@ -54,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", @@ -72,7 +109,56 @@ "vcmi.lobby.noUnderground" : "Kein Untergrund", "vcmi.lobby.sortDate" : "Ordnet Karten nach Änderungsdatum", "vcmi.lobby.backToLobby" : "Zur Lobby zurückkehren", - + "vcmi.lobby.author" : "Author", + "vcmi.lobby.handicap" : "Handicap", + "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.broadcast.failedLoadGame" : "Spiel konnte nicht geladen werden", + "vcmi.broadcast.command" : "Benutze '!help' um alle verfügbaren Befehle aufzulisten", + "vcmi.broadcast.simturn.end" : "Simultane Züge wurden beendet", + "vcmi.broadcast.simturn.endBetween" : "Simultane Züge zwischen den Spielern %s und %s wurden beendet", + "vcmi.broadcast.serverProblem" : "Server hat ein Problem festgestellt", + "vcmi.broadcast.gameTerminated" : "Spiel wurde abgebrochen", + "vcmi.broadcast.gameSavedAs" : "Spiel gespeichert als", + "vcmi.broadcast.noCheater" : "Keine Betrüger registriert!", + "vcmi.broadcast.playerCheater" : "Spieler %s ist ein Betrüger!", + "vcmi.broadcast.statisticFile" : "Die Statistikdateien befinden sich im Verzeichnis %s", + "vcmi.broadcast.help.commands" : "Verfügbare Befehle für den Host:", + "vcmi.broadcast.help.exit" : "'!exit' - beendet sofort das aktuelle Spiel", + "vcmi.broadcast.help.kick" : "'!kick ' - den angegebenen Spieler aus dem Spiel werfen", + "vcmi.broadcast.help.save" : "'!save ' - Spiel unter dem angegebenen Dateinamen speichern", + "vcmi.broadcast.help.statistic" : "'!statistic' - Spielstatistiken als csv-Datei speichern", + "vcmi.broadcast.help.commandsAll" : "Verfügbare Befehle für alle Spieler:", + "vcmi.broadcast.help.help" : "'!help' - diese Hilfe anzeigen", + "vcmi.broadcast.help.cheaters" : "'!cheaters' - Liste der Spieler, die während des Spiels einen Cheat-Befehl eingegeben haben", + "vcmi.broadcast.help.vote" : "'!vote' - erlaubt es, einige Spieleinstellungen zu ändern, wenn alle Spieler dafür stimmen", + "vcmi.broadcast.vote.allow" : "'!vote simturns allow X' - erlaubt simultane Züge für eine bestimmte Anzahl von Tagen oder bis zum Kontakt", + "vcmi.broadcast.vote.force" : "'!vote simturns force X' - erzwingt simultane Züge für die angegebene Anzahl von Tagen und blockiert Spielerkontakte", + "vcmi.broadcast.vote.abort" : "'!vote simturns abort' - Simultane Züge abbrechen, sobald dieser Zug endet", + "vcmi.broadcast.vote.timer" : "'!vote timer prolong X' - verlängert den Basis-Timer für alle Spieler um die angegebene Anzahl von Sekunden", + "vcmi.broadcast.vote.noActive" : "Keine aktive Abstimmung!", + "vcmi.broadcast.vote.yes" : "ja", + "vcmi.broadcast.vote.no" : "nein", + "vcmi.broadcast.vote.notRecognized" : "Abstimmungsbefehl nicht erkannt!", + "vcmi.broadcast.vote.success.untilContacts" : "Abstimmung erfolgreich. Simultane Züge laufen für %s weitere Tage, oder bis zum Kontakt", + "vcmi.broadcast.vote.success.contactsBlocked" : "Abstimmung erfolgreich. Simultane Züge werden für %s weitere Tage laufen. Kontakte sind blockiert", + "vcmi.broadcast.vote.success.nextDay" : "Abstimmung erfolgreich. Simultane Züge werden am nächsten Tag beendet", + "vcmi.broadcast.vote.success.timer" : "Abstimmung erfolgreich. Der Timer für alle Spieler wurde um %s Sekunden verlängert.", + "vcmi.broadcast.vote.aborted" : "Spieler haben gegen die Änderung gestimmt. Abstimmung abgebrochen", + "vcmi.broadcast.vote.start.untilContacts" : "Abstimmung gestartet, um simultane Züge für weitere %s Tage zu erlauben", + "vcmi.broadcast.vote.start.contactsBlocked" : "Abstimmung über die Erzwingung simultaner Züge für weitere %s-Tage eingeleitet", + "vcmi.broadcast.vote.start.nextDay" : "Beginn der Abstimmung zur Beendigung der simultanen Züge ab dem nächsten Tag", + "vcmi.broadcast.vote.start.timer" : "Abstimmung gestartet, um den Timer für alle Spieler um %s Sekunden zu verlängern", + "vcmi.broadcast.vote.hint" : "Gib '!vote yes' ein, um dieser Änderung zuzustimmen oder '!vote no', um dagegen zu stimmen", + "vcmi.lobby.login.title" : "VCMI Online Lobby", "vcmi.lobby.login.username" : "Benutzername:", "vcmi.lobby.login.connecting" : "Verbinde...", @@ -80,6 +166,7 @@ "vcmi.lobby.login.create" : "Neuer Account", "vcmi.lobby.login.login" : "Login", "vcmi.lobby.login.as" : "Login als %s", + "vcmi.lobby.login.spectator" : "Beobachter", "vcmi.lobby.header.rooms" : "Spielräume - %d", "vcmi.lobby.header.channels" : "Chat Kanäle", "vcmi.lobby.header.chat.global" : "Globaler Spiele-Chat - %s", // %s -> language name @@ -136,12 +223,13 @@ "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.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.server.errors.wrongIdentified" : "Ihr wurdet als Spieler %s identifiziert, während %s erwartet wurde", + "vcmi.server.errors.notAllowed" : "Ihr dürft diese Aktion nicht durchführen!", "vcmi.dimensionDoor.seaToLandError" : "Es ist nicht möglich, mit einer Dimensionstür vom Meer zum Land oder umgekehrt zu teleportieren.", @@ -157,6 +245,38 @@ "vcmi.systemOptions.otherGroup" : "Andere Einstellungen", // unused right now "vcmi.systemOptions.townsGroup" : "Stadt-Bildschirm", + "vcmi.statisticWindow.statistics" : "Statistik", + "vcmi.statisticWindow.tsvCopy" : "In Zwischenablage", + "vcmi.statisticWindow.selectView" : "Ansicht wählen", + "vcmi.statisticWindow.value" : "Wert", + "vcmi.statisticWindow.title.overview" : "Überblick", + "vcmi.statisticWindow.title.resources" : "Ressourcen", + "vcmi.statisticWindow.title.income" : "Einkommen", + "vcmi.statisticWindow.title.numberOfHeroes" : "Nr. der Helden", + "vcmi.statisticWindow.title.numberOfTowns" : "Nr. der Städte", + "vcmi.statisticWindow.title.numberOfArtifacts" : "Nr. der Artefakte", + "vcmi.statisticWindow.title.numberOfDwellings" : "Nr. der Behausungen", + "vcmi.statisticWindow.title.numberOfMines" : "Nr. der Minen", + "vcmi.statisticWindow.title.armyStrength" : "Armeestärke", + "vcmi.statisticWindow.title.experience" : "Erfahrung", + "vcmi.statisticWindow.title.resourcesSpentArmy" : "Armeekosten", + "vcmi.statisticWindow.title.resourcesSpentBuildings" : "Gebäudekosten", + "vcmi.statisticWindow.title.mapExplored" : "Maperkundungsrate", + "vcmi.statisticWindow.param.playerName" : "Spielername", + "vcmi.statisticWindow.param.daysSurvived" : "Tage überlebt", + "vcmi.statisticWindow.param.maxHeroLevel" : "Max Heldenlevel", + "vcmi.statisticWindow.param.battleWinRatioHero" : "Sieg Verh. (Helden)", + "vcmi.statisticWindow.param.battleWinRatioNeutral" : "Sieg Verh. (Neutral)", + "vcmi.statisticWindow.param.battlesHero" : "Kämpfe (Helden)", + "vcmi.statisticWindow.param.battlesNeutral" : "Kämpfe (Neutral)", + "vcmi.statisticWindow.param.maxArmyStrength" : "Max Gesamt-Armeestärke", + "vcmi.statisticWindow.param.tradeVolume" : "Handelsvol.", + "vcmi.statisticWindow.param.obeliskVisited" : "Obelisk besucht", + "vcmi.statisticWindow.icon.townCaptured" : "Stadt erobert", + "vcmi.statisticWindow.icon.strongestHeroDefeated" : "Stärksten Helden eines Gegners besiegt", + "vcmi.statisticWindow.icon.grailFound" : "Gral gefunden", + "vcmi.statisticWindow.icon.defeated" : "Besiegt", + "vcmi.systemOptions.fullscreenBorderless.hover" : "Vollbild (randlos)", "vcmi.systemOptions.fullscreenBorderless.help" : "{Randloses Vollbild}\n\nWenn diese Option ausgewählt ist, wird VCMI im randlosen Vollbildmodus ausgeführt. In diesem Modus wird das Spiel immer dieselbe Auflösung wie der Desktop verwenden und die gewählte Auflösung ignorieren.", "vcmi.systemOptions.fullscreenExclusive.hover" : "Vollbild (exklusiv)", @@ -166,13 +286,13 @@ "vcmi.systemOptions.resolutionMenu.hover" : "Wähle Auflösung", "vcmi.systemOptions.resolutionMenu.help" : "Ändere die Spielauflösung.", "vcmi.systemOptions.scalingButton.hover" : "Interface-Skalierung: %p%", - "vcmi.systemOptions.scalingButton.help" : "{Interface-Skalierung}\n\nÄndern der Skalierung des Interfaces im Spiel", + "vcmi.systemOptions.scalingButton.help" : "{Interface-Skalierung}\n\nÄndern der Interface-Skalierung im Spiel", "vcmi.systemOptions.scalingMenu.hover" : "Skalierung auswählen", - "vcmi.systemOptions.scalingMenu.help" : "Ändern der Skalierung des Interfaces im Spiel.", - "vcmi.systemOptions.longTouchButton.hover" : "Berührungsdauer für langer Touch: %d ms", // Translation note: "ms" = "milliseconds" - "vcmi.systemOptions.longTouchButton.help" : "{Berührungsdauer für langer Touch}\n\nBei Verwendung des Touchscreens erscheinen Popup-Fenster nach Berührung des Bildschirms für die angegebene Dauer (in Millisekunden)", - "vcmi.systemOptions.longTouchMenu.hover" : "Wähle Berührungsdauer für langer Touch", - "vcmi.systemOptions.longTouchMenu.help" : "Ändere die Berührungsdauer für den langen Touch", + "vcmi.systemOptions.scalingMenu.help" : "Ändern der Interface-Skalierung im Spiel.", + "vcmi.systemOptions.longTouchButton.hover" : "Dauer für langer Touch: %d ms", // Translation note: "ms" = "milliseconds" + "vcmi.systemOptions.longTouchButton.help" : "{Dauer für langer Touch}\n\nBei Verwendung des Touchscreens erscheinen Popup-Fenster nach Berührung des Bildschirms für die angegebene Dauer (in Millisekunden)", + "vcmi.systemOptions.longTouchMenu.hover" : "Wähle Dauer für Touch", + "vcmi.systemOptions.longTouchMenu.help" : "Ändere die Dauer für den langen Touch", "vcmi.systemOptions.longTouchMenu.entry" : "%d Millisekunden", "vcmi.systemOptions.framerateButton.hover" : "FPS anzeigen", "vcmi.systemOptions.framerateButton.help" : "{FPS anzeigen}\n\n Schaltet die Sichtbarkeit des Zählers für die Bilder pro Sekunde in der Ecke des Spielfensters um.", @@ -197,8 +317,10 @@ "vcmi.adventureOptions.borderScroll.help" : "{Scrollen am Rand}\n\nScrollt die Abenteuerkarte, wenn sich der Cursor neben dem Fensterrand befindet. Kann mit gedrückter STRG-Taste deaktiviert werden.", "vcmi.adventureOptions.infoBarCreatureManagement.hover" : "Info-Panel Kreaturenmanagement", "vcmi.adventureOptions.infoBarCreatureManagement.help" : "{Info-Panel Kreaturenmanagement}\n\nErmöglicht die Neuanordnung von Kreaturen im Info-Panel, anstatt zwischen den Standardkomponenten zu wechseln", - "vcmi.adventureOptions.leftButtonDrag.hover" : "Ziehen der Karte mit Links", - "vcmi.adventureOptions.leftButtonDrag.help" : "{Ziehen der Karte mit Links}\n\nWenn aktiviert, wird die Maus bei gedrückter linker Taste in die Kartenansicht gezogen", + "vcmi.adventureOptions.leftButtonDrag.hover" : "Ziehen mit Links", + "vcmi.adventureOptions.leftButtonDrag.help" : "{Ziehen mit Links}\n\nWenn aktiviert, kann mit gedrückter linker Taste die Kartenansicht gezogen werden", + "vcmi.adventureOptions.rightButtonDrag.hover" : "Ziehen mit Rechts", + "vcmi.adventureOptions.rightButtonDrag.help" : "{Ziehen mit Rechts}\n\nWenn aktiviert, kann mit gedrückter rechter Taste die Kartenansicht gezogen werden", "vcmi.adventureOptions.smoothDragging.hover" : "Nahtloses Ziehen der Karte", "vcmi.adventureOptions.smoothDragging.help" : "{Nahtloses Ziehen der Karte}\n\nWenn aktiviert hat das Ziehen der Karte einen sanften Auslaufeffekt.", "vcmi.adventureOptions.skipAdventureMapAnimations.hover" : "Fading-Effekte überspringen", @@ -237,6 +359,8 @@ "vcmi.battleOptions.skipBattleIntroMusic.help": "{Intro-Musik überspringen}\n\n Überspringe die kurze Musik, die zu Beginn eines jeden Kampfes gespielt wird, bevor die Action beginnt. Kann auch durch Drücken der ESC-Taste übersprungen werden.", "vcmi.battleOptions.endWithAutocombat.hover": "Kampf beenden", "vcmi.battleOptions.endWithAutocombat.help": "{Kampf beenden}\n\nAutokampf spielt den Kampf sofort zu Ende", + "vcmi.battleOptions.showQuickSpell.hover": "Schnellzauber-Panel anzeigen", + "vcmi.battleOptions.showQuickSpell.help": "{Schnellzauber-Panel anzeigen}\n\nZeigt ein Panel, auf dem schnell Zauber ausgewählt werden können", "vcmi.adventureMap.revisitObject.hover" : "Objekt erneut besuchen", "vcmi.adventureMap.revisitObject.help" : "{Objekt erneut besuchen}\n\nSteht ein Held gerade auf einem Kartenobjekt, kann er den Ort erneut aufsuchen.", @@ -285,17 +409,9 @@ "vcmi.townHall.missingBase" : "Basis Gebäude %s muss als erstes gebaut werden", "vcmi.townHall.noCreaturesToRecruit" : "Es gibt keine Kreaturen zu rekrutieren!", - "vcmi.townHall.greetingManaVortex" : "Wenn Ihr Euch den %s nähert, wird Euer Körper mit neuer Energie gefüllt. Ihr habt Eure normalen Zauberpunkte verdoppelt.", - "vcmi.townHall.greetingKnowledge" : "Ihr studiert die Glyphen auf dem %s und erhaltet Einblick in die Funktionsweise verschiedener Magie (+1 Wissen).", - "vcmi.townHall.greetingSpellPower" : "Der %s lehrt Euch neue Wege, Eure magischen Kräfte zu bündeln (+1 Kraft).", - "vcmi.townHall.greetingExperience" : "Ein Besuch bei den %s bringt Euch viele neue Fähigkeiten bei (+1000 Erfahrung).", - "vcmi.townHall.greetingAttack" : "Nach einiger Zeit im %s könnt Ihr effizientere Kampffertigkeiten erlernen (+1 Angriffsfertigkeit).", - "vcmi.townHall.greetingDefence" : "Wenn Ihr Zeit im %s verbringt, bringen Euch die erfahrenen Krieger dort zusätzliche Verteidigungsfähigkeiten bei (+1 Verteidigung).", - "vcmi.townHall.hasNotProduced" : "Die %s hat noch nichts produziert.", - "vcmi.townHall.hasProduced" : "Die %s hat diese Woche %d %s produziert.", - "vcmi.townHall.greetingCustomBonus" : "%s gibt Ihnen +%d %s%s", - "vcmi.townHall.greetingCustomUntil" : " bis zur nächsten Schlacht.", - "vcmi.townHall.greetingInTownMagicWell" : "%s hat Eure Zauberpunkte wieder auf das Maximum erhöht.", + + "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:", @@ -305,6 +421,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", @@ -481,7 +604,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", @@ -629,5 +754,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 75% rename from Mods/vcmi/config/vcmi/polish.json rename to Mods/vcmi/Content/config/polish.json index ceac0bd9a..15cb5828e 100644 --- a/Mods/vcmi/config/vcmi/polish.json +++ b/Mods/vcmi/Content/config/polish.json @@ -12,16 +12,67 @@ "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-go 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!", "vcmi.adventureMap.noTownWithTavern" : "Brak dostępnego miasta z karczmą!", "vcmi.adventureMap.spellUnknownProblem" : "Nieznany problem z zaklęciem, brak dodatkowych informacji.", "vcmi.adventureMap.playerAttacked" : "Gracz został zaatakowany: %s", - "vcmi.adventureMap.moveCostDetails" : "Punkty ruchu - Koszt: %TURNS tury + %POINTS punkty, Pozostałe punkty: %REMAINING", - "vcmi.adventureMap.moveCostDetailsNoTurns" : "Punkty ruchu - Koszt: %POINTS punkty, Pozostałe punkty: %REMAINING", + "vcmi.adventureMap.moveCostDetails" : "Punkty ruchu - Koszt: %TURNS tury + %POINTS punktów, Pozostanie: %REMAINING punktów", + "vcmi.adventureMap.moveCostDetailsNoTurns" : "Punkty ruchu - Koszt: %POINTS punktów, Pozostanie: %REMAINING punktów", + "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.broadcast.command" : "Wpisz '!help' aby zobaczyć listę dostępnych komend.", + "vcmi.broadcast.failedLoadGame" : "Nie udało się wczytać gry", + "vcmi.broadcast.gameSavedAs" : "gra zapisana jako", + "vcmi.broadcast.gameTerminated" : "gra została zamknięta", + "vcmi.broadcast.help.cheaters" : "'!cheaters' - wyświetla listę graczy, którzy użyli 'kodów' w trakcie gry", + "vcmi.broadcast.help.commands" : "Dostępne komendy dla hosta:", + "vcmi.broadcast.help.commandsAll" : "Dostępne komendy dla wszystkich graczy:", + "vcmi.broadcast.help.exit" : "'!exit' - natychmiast kończy bieżącą grę", + "vcmi.broadcast.help.help" : "'!help' - wyświetla tę pomoc", + "vcmi.broadcast.help.kick" : "'!kick ' - wyrzuca określonego gracza z gry", + "vcmi.broadcast.help.save" : "'!save ' - zapisz grę pod określoną nazwą", + "vcmi.broadcast.help.statistic" : "'!statistic' - zapisz statystyki gry do pliku csv", + "vcmi.broadcast.help.vote" : "'!vote' - pozwala na zmianę ustawień gry jeśli wszyscy gracze zagłosują 'za'", + "vcmi.broadcast.noCheater" : "Nie zarejestrowano oszustw!", + "vcmi.broadcast.playerCheater" : "Gracz %s to oszust!", + "vcmi.broadcast.serverProblem" : "Serwer napotkał problem", + "vcmi.broadcast.simturn.end" : "Tury symultaniczne zostały zakończone", + "vcmi.broadcast.simturn.endBetween" : "Tury symultaniczne pomiędzy graczami %s i %s dobiegły końca", + "vcmi.broadcast.statisticFile" : "Pliki statystyk są dostępne w folderze %s", + "vcmi.broadcast.vote.abort" : "'!vote simturns abort' - przerwanie tur symultanicznych po zakończeniu tej tury", + "vcmi.broadcast.vote.aborted" : "Gracz zagłosował przeciwko. Głosowanie anulowane.", + "vcmi.broadcast.vote.allow" : "'!vote simturns allow X' - tury symultaniczne przez określoną ilość dni, albo do pierwszego kontaktu", + "vcmi.broadcast.vote.force" : "'!vote simturns force X' - tury symultaniczne przez określoną ilość dni, niezależnie od kontaktu", + "vcmi.broadcast.vote.hint" : "Wpisz '!vote yes' żeby zagłosować na tak lub '!vote no' - przeciwko tej zmianie", + "vcmi.broadcast.vote.no" : "nie", + "vcmi.broadcast.vote.noActive" : "Żadne głosowanie nie jest aktywne!", + "vcmi.broadcast.vote.notRecognized" : "Nieznana komenda do głosowania!", + "vcmi.broadcast.vote.start.contactsBlocked" : "Rozpoczęto głosowanie: tury symultaniczne przez %s kolejnych dni", + "vcmi.broadcast.vote.start.nextDay" : "Rozpoczęto głosowanie: zakończenie tur symultanicznych od następnego dnia", + "vcmi.broadcast.vote.start.timer" : "Rozpoczęto głosowanie: wydłużenie czasu dla wszystkich graczy o %s sekund", + "vcmi.broadcast.vote.start.untilContacts" : "Rozpoczęto głosowanie: przedłużenie trwania tur symultanicznych o kolejne %s dni", + "vcmi.broadcast.vote.success.contactsBlocked" : "Głosowanie zakończone pomyślnie. Tury symultaniczne będą trwały jeszcze przez kolejne %s dni, niezależnie od kontaktu.", + "vcmi.broadcast.vote.success.nextDay" : "Głosowanie zakończone pomyślnie. Tury symultaniczne zostaną wyłączone w kolejnym dniu", + "vcmi.broadcast.vote.success.timer" : "Głosowanie zakończone pomyślnie. Czas dla wszystkich graczy został wydłużony o %s sekund", + "vcmi.broadcast.vote.success.untilContacts" : "Głosowanie zakończone pomyślnie. Tury symultaniczne będą trwały jeszcze przez kolejne %s dni lub do pierwszego kontaktu.", + "vcmi.broadcast.vote.timer" : "'!vote timer prolong X' - wydłużenie bazowego czasu dla wszystkich graczy o X sekund", + "vcmi.broadcast.vote.yes" : "tak", + "vcmi.capitalColors.0" : "Czerwony", "vcmi.capitalColors.1" : "Niebieski", "vcmi.capitalColors.2" : "Brązowy", @@ -36,6 +87,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", @@ -55,8 +112,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ę", @@ -72,6 +147,11 @@ "vcmi.lobby.noUnderground" : "brak podziemi", "vcmi.lobby.sortDate" : "Sortuj mapy według daty modyfikacji", "vcmi.lobby.backToLobby" : "Wróc do lobby", + "vcmi.lobby.author" : "Autor", + "vcmi.lobby.handicap" : "Balans", + "vcmi.lobby.handicap.resource" : "Przyznaje graczom zasoby na start, oprócz normalnych zasobów początkowych. Wartości ujemne są dozwolone, ale łącznie gracz nie może zaczynać na minus.", + "vcmi.lobby.handicap.income" : "Ustala procentowy współczynnik przychodu gracza. Zaokrąglone w górę.", + "vcmi.lobby.handicap.growth" : "Ustala współczynnik populacji stworzeń w miastach należących do gracza. Zaokrąglone w górę.", "vcmi.lobby.login.title" : "Lobby sieciowe VCMI", "vcmi.lobby.login.username" : "Użytkownik:", @@ -79,7 +159,7 @@ "vcmi.lobby.login.error" : "Błąd połączenia: %s", "vcmi.lobby.login.create" : "Nowe konto", "vcmi.lobby.login.login" : "Login", - "vcmi.lobby.login.as" : "Zaloguj jako %s", + "vcmi.lobby.login.as" : "Logowanie jako %s", "vcmi.lobby.header.rooms" : "Pokoje - %d", "vcmi.lobby.header.channels" : "Kanały tekstowe", "vcmi.lobby.header.chat.global" : "Chat globalny - %s", // %s -> language name @@ -88,7 +168,7 @@ "vcmi.lobby.header.history" : "Twoje poprzednie gry", "vcmi.lobby.header.players" : "Graczy online - %d", "vcmi.lobby.match.solo" : "Gra jednoosobowa", - "vcmi.lobby.match.duel" : "Graj z %s", // %s -> nickname of another player + "vcmi.lobby.match.duel" : "vs. %s", // %s -> nickname of another player "vcmi.lobby.match.multi" : "%d graczy", "vcmi.lobby.room.create" : "Stwórz nowy pokój", "vcmi.lobby.room.players.limit" : "Limit graczy", @@ -125,24 +205,32 @@ "vcmi.lobby.mod.state.version" : "Niepoprawna wersja", "vcmi.lobby.mod.state.excessive" : "Musi być wyłączony", "vcmi.lobby.mod.state.missing" : "Nie zainstalowany", - "vcmi.lobby.pvp.coin.hover" : "Moneta", - "vcmi.lobby.pvp.coin.help" : "Rzut monetą", - "vcmi.lobby.pvp.randomTown.hover" : "Losowe miasto", - "vcmi.lobby.pvp.randomTown.help" : "Wyświetli nazwę wylosowanego miasta na czacie", - "vcmi.lobby.pvp.randomTownVs.hover" : "Losowe miasto vs.", - "vcmi.lobby.pvp.randomTownVs.help" : "Wyświetli nazwę 2 wylosowanych miast na czacie", + "vcmi.lobby.pvp.coin.hover" : "Rzut monetą", + "vcmi.lobby.pvp.coin.help" : "Wyświetli symulację rzutu monetą na czacie, wskazując 0 lub 1 w zależności od rezultatu rzutu.", + "vcmi.lobby.pvp.randomTown.hover" : "Wylosuj miasto", + "vcmi.lobby.pvp.randomTown.help" : "Wyświetli nazwę wylosowanego miasta na czacie, które nie zostało zablokowane na liście", + "vcmi.lobby.pvp.randomTownVs.hover" : "Wylosuj 2 miasta", + "vcmi.lobby.pvp.randomTownVs.help" : "Wyświetli nazwę 2 wylosowanych miast na czacie, które nie zostały zablokowane na liście", "vcmi.lobby.pvp.versus" : "vs.", + "vcmi.lobby.deleteFile" : "Czy chcesz usunąć ten plik ?", + "vcmi.lobby.deleteFolder" : "Czy chcesz usunąć ten folder ?", + "vcmi.lobby.deleteMapTitle" : "Wskaż tytuł, który chcesz usunąć", + "vcmi.lobby.deleteMode" : "Przełącza tryb na usuwanie i spowrotem", + "vcmi.lobby.deleteSaveGameTitle" : "Wskaż zapis gry do usunięcia", + "vcmi.lobby.deleteUnsupportedSave" : "{Znaleziono niekompatybilne zapisy gry}\n\nVCMI wykrył %d zapisów gry, które nie są już wspierane. Prawdopodobnie ze względu na różne wersje gry.\n\nCzy chcesz je usunąć ?", "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.confirmReconnect" : "Połączyć ponownie z ostatnią sesją?", - "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.notAllowed" : "To działanie nie jest dozwolone!", "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.server.errors.wrongIdentified" : "Zostałeś zidentyfikowany jako gracz %s natomiast powinieneś być %s", + "vcmi.dimensionDoor.seaToLandError" : "Nie jest możliwa teleportacja przez drzwi wymiarów z wód na ląd i na odwrót.", "vcmi.settingsMainWindow.generalTab.hover" : "Ogólne", @@ -157,6 +245,38 @@ "vcmi.systemOptions.otherGroup" : "Inne ustawienia", // unused right now "vcmi.systemOptions.townsGroup" : "Ekran miasta", + "vcmi.statisticWindow.statistics" : "Statystyki", + "vcmi.statisticWindow.tsvCopy" : "Kopiuj do schowka", + "vcmi.statisticWindow.selectView" : "Tryb widoku", + "vcmi.statisticWindow.value" : "Wartość", + "vcmi.statisticWindow.title.overview" : "Przegląd", + "vcmi.statisticWindow.title.resources" : "Surowce", + "vcmi.statisticWindow.title.income" : "Przychód", + "vcmi.statisticWindow.title.numberOfHeroes" : "Lb. bohaterów", + "vcmi.statisticWindow.title.numberOfTowns" : "Lb. miast", + "vcmi.statisticWindow.title.numberOfArtifacts" : "Lb. artefaktów", + "vcmi.statisticWindow.title.numberOfDwellings" : "Lb. siedlisk", + "vcmi.statisticWindow.title.numberOfMines" : "Lb. kopalni", + "vcmi.statisticWindow.title.armyStrength" : "Siła armii", + "vcmi.statisticWindow.title.experience" : "Doświadczenie", + "vcmi.statisticWindow.title.resourcesSpentArmy" : "Koszty armii", + "vcmi.statisticWindow.title.resourcesSpentBuildings" : "Koszty budowy", + "vcmi.statisticWindow.title.mapExplored" : "Odkrycie mapy", + "vcmi.statisticWindow.param.playerName" : "Imię", + "vcmi.statisticWindow.param.daysSurvived" : "Przeżytych dni", + "vcmi.statisticWindow.param.maxHeroLevel" : "Maks. poziom bohatera", + "vcmi.statisticWindow.param.battleWinRatioHero" : "Zwycięstw (vs. bohaterom)", + "vcmi.statisticWindow.param.battleWinRatioNeutral" : "Zwycięstw (vs. neutralnym)", + "vcmi.statisticWindow.param.battlesHero" : "Walk (vs. bohaterom)", + "vcmi.statisticWindow.param.battlesNeutral" : "Walk (vs. neutralnym)", + "vcmi.statisticWindow.param.maxArmyStrength" : "Maks. siła armii", + "vcmi.statisticWindow.param.tradeVolume" : "Wielkość handlu", + "vcmi.statisticWindow.param.obeliskVisited" : "Lb. obelisków", + "vcmi.statisticWindow.icon.townCaptured" : "Miasto zdobyte", + "vcmi.statisticWindow.icon.strongestHeroDefeated" : "Najsilniejszy bohater przeciwnika pokonany", + "vcmi.statisticWindow.icon.grailFound" : "Graal znaleziony", + "vcmi.statisticWindow.icon.defeated" : "Pokonany", + "vcmi.systemOptions.fullscreenBorderless.hover" : "Pełny ekran (bez ramek)", "vcmi.systemOptions.fullscreenBorderless.help" : "{Pełny ekran w trybie okna}\n\nVCMI będzie działać w trybie okna pełnoekranowego. W tym trybie gra będzie zawsze używać rozdzielczości pulpitu, ignorując wybraną rozdzielczość.", "vcmi.systemOptions.fullscreenExclusive.hover" : "Pełny ekran (tradycyjny)", @@ -197,8 +317,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", @@ -237,6 +359,8 @@ "vcmi.battleOptions.skipBattleIntroMusic.help": "{Pomiń czekanie startowe}\n\n Pomija konieczność czekania podczas muzyki startowej, która jest odtwarzana na początku każdej bitwy przed rozpoczęciem akcji.", "vcmi.battleOptions.endWithAutocombat.hover": "Natychmiastowe auto-walki", "vcmi.battleOptions.endWithAutocombat.help": "{Natychmiastowe auto-walki}\n\nAuto-walka natychmiastowo toczy walkę do samego końca", + "vcmi.battleOptions.showQuickSpell.hover": "Szybki dostęp do magii", + "vcmi.battleOptions.showQuickSpell.help": "{Szybki dostęp do magii}\n\nPokazuje panel szybkiego dostępu do czarów", "vcmi.adventureMap.revisitObject.hover" : "Odwiedź obiekt ponownie", "vcmi.adventureMap.revisitObject.help" : "{Odwiedź obiekt ponownie}\n\nJeżeli bohater aktualnie stoi na polu odwiedzającym obiekt za pomocą tego przycisku może go odwiedzić ponownie.", @@ -285,17 +409,9 @@ "vcmi.townHall.missingBase" : "Podstawowy budynek %s musi zostać najpierw wybudowany", "vcmi.townHall.noCreaturesToRecruit" : "Brak stworzeń do rekrutacji!", - "vcmi.townHall.greetingManaVortex" : "Zbliżając się do %s czujesz jak twoje ciało wypełnia energia. Ilość pkt. magii, które posiadasz, zwiększa się dwukrotnie.", - "vcmi.townHall.greetingKnowledge" : "Studiując napisy na %s odkrywasz nowe aspekty stosowania magii (wiedza +1).", - "vcmi.townHall.greetingSpellPower" : "Odwiedzając %s dowiadujesz się, jak zwiększyć potęgę swojej mocy magicznej (moc +1).", - "vcmi.townHall.greetingExperience" : "Wizyta w %s zwiększa twoje doświadczenie (doświadczenie +1000).", - "vcmi.townHall.greetingAttack" : "Krótka wizyka w %s umożliwia ci polepszenie technik walki (atak +1).", - "vcmi.townHall.greetingDefence" : "Odwiedzasz %s. Doświadczeni żołnierze, którzy tam przebywają, uczą cię sztuki skutecznej obrony (obrona +1).", - "vcmi.townHall.hasNotProduced" : "%s nic jeszcze nie wyprodukował.", - "vcmi.townHall.hasProduced" : "%s wyprodukował w tym tygodniu: %d %s.", - "vcmi.townHall.greetingCustomBonus" : "%s daje tobie +%d %s%s", - "vcmi.townHall.greetingCustomUntil" : " do następnej bitwy.", - "vcmi.townHall.greetingInTownMagicWell" : "%s przywraca ci wszystkie punkty magii.", + + "vcmi.townStructure.bank.borrow" : "Wchodzisz do banku. Bankier cię widzi i mówi: \"Złożyliśmy ci specjalną ofertę. Możesz wziąć od nas pożyczkę w wysokości 2500 złota na 5 dni. Będziesz musiał spłacać 500 złota każdego dnia.\"", + "vcmi.townStructure.bank.payBack" : "Wchodzisz do banku. Bankier cię widzi i mówi: „Już dostałeś pożyczkę. Spłać ją zanim weźmiesz nową.\"", "vcmi.logicalExpressions.anyOf" : "Dowolne spośród:", "vcmi.logicalExpressions.allOf" : "Wszystkie spośród:", @@ -305,6 +421,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", @@ -414,11 +537,33 @@ "vcmi.stackExperience.rank.4" : "Udowodniony", "vcmi.stackExperience.rank.5" : "Weteran", "vcmi.stackExperience.rank.6" : "Adept", - "vcmi.stackExperience.rank.7" : "Expert", + "vcmi.stackExperience.rank.7" : "Ekspert", "vcmi.stackExperience.rank.8" : "Elitarny", - "vcmi.stackExperience.rank.9" : "Master", + "vcmi.stackExperience.rank.9" : "Mistrz", "vcmi.stackExperience.rank.10" : "As", + "spell.core.castleMoat.name" : "Fosa", + "spell.core.castleMoatTrigger.name" : "Fosa", + "spell.core.catapultShot.name" : "Strzał z katapulty", + "spell.core.cyclopsShot.name" : "Strzał oblężniczy", + "spell.core.dungeonMoat.name" : "Wrzący olej", + "spell.core.dungeonMoatTrigger.name" : "Wrzący olej", + "spell.core.fireWallTrigger.name" : "Ściana Ognia", + "spell.core.firstAid.name" : "Pierwsza pomoc", + "spell.core.fortressMoat.name" : "Wrząca smoła", + "spell.core.fortressMoatTrigger.name" : "Wrząca smoła", + "spell.core.infernoMoat.name" : "Lawa", + "spell.core.infernoMoatTrigger.name" : "Lawa", + "spell.core.landMineTrigger.name" : "Mina", + "spell.core.necropolisMoat.name" : "Fosa z kości", + "spell.core.necropolisMoatTrigger.name" : "Fosa z kości", + "spell.core.rampartMoat.name" : "Ciernisko", + "spell.core.rampartMoatTrigger.name" : "Ciernisko", + "spell.core.strongholdMoat.name" : "Palisada obronna", + "spell.core.strongholdMoatTrigger.name" : "Palisada obronna", + "spell.core.summonDemons.name" : "Przyzwanie Demonów", + "spell.core.towerMoat.name" : "Pole minowe", + // Strings for HotA Seer Hut / Quest Guards "core.seerhut.quest.heroClass.complete.0" : "Ah, ty jesteś %s. Oto prezent dla ciebie. Czy go przyjmiesz?", "core.seerhut.quest.heroClass.complete.1" : "Ah, ty jesteś %s. Oto prezent dla ciebie. Czy go przyjmiesz?", @@ -481,13 +626,15 @@ "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", "core.bonus.ADDITIONAL_RETALIATION.description": "${val} dodatkowy kontratak", "core.bonus.AIR_IMMUNITY.name": "Odporność: Powietrze", - "core.bonus.AIR_IMMUNITY.description": "Odporny na wszystkie czary szkoły powietrza", + "core.bonus.AIR_IMMUNITY.description": "Odporny na magię powietrza", "core.bonus.ATTACKS_ALL_ADJACENT.name": "Obrotowy atak", "core.bonus.ATTACKS_ALL_ADJACENT.description": "Atakuje wszystkich sąsiadujących wrogów", "core.bonus.BLOCKS_RETALIATION.name": "Bez kontrataku", @@ -509,13 +656,13 @@ "core.bonus.DEFENSIVE_STANCE.name": "Bonus do obrony", "core.bonus.DEFENSIVE_STANCE.description": "+${val} Obrony kiedy broni", "core.bonus.DESTRUCTION.name": "Destrukcja", - "core.bonus.DESTRUCTION.description": "Ma ${val}% szans na zabicie dodatkowych jednostek po ataku", + "core.bonus.DESTRUCTION.description": "${val}% szans na zabicie dodatkowych jednostek po ataku", "core.bonus.DOUBLE_DAMAGE_CHANCE.name": "Uderzenie Śmierci", "core.bonus.DOUBLE_DAMAGE_CHANCE.description": "${val}% szans na podwójne obrażenia", "core.bonus.DRAGON_NATURE.name": "Smok", "core.bonus.DRAGON_NATURE.description": "Stworzenie posiada smoczą naturę", "core.bonus.EARTH_IMMUNITY.name": "Odporność: Ziemia", - "core.bonus.EARTH_IMMUNITY.description": "Odporny na wszystkie czary szkoły ziemi", + "core.bonus.EARTH_IMMUNITY.description": "Odporny na magię ziemi", "core.bonus.ENCHANTER.name": "Czarodziej", "core.bonus.ENCHANTER.description": "Rzuca czar ${subtype.spell}", "core.bonus.ENCHANTED.name": "Zaczarowany", @@ -525,7 +672,7 @@ "core.bonus.ENEMY_DEFENCE_REDUCTION.name": "Osłabienie Obrony (${val}%)", "core.bonus.ENEMY_DEFENCE_REDUCTION.description": "Osłabia obronę wroga podczas ataku", "core.bonus.FIRE_IMMUNITY.name": "Odporność: Ogień", - "core.bonus.FIRE_IMMUNITY.description": "Odporny na wszystkie czary szkoły ognia", + "core.bonus.FIRE_IMMUNITY.description": "Odporny na magię ognia", "core.bonus.FIRE_SHIELD.name": "Ognista tarcza (${val}%)", "core.bonus.FIRE_SHIELD.description": "Odbija część obrażeń z walki wręcz", "core.bonus.FIRST_STRIKE.name": "Pierwsze Uderzenie", @@ -543,7 +690,7 @@ "core.bonus.GARGOYLE.name": "Gargulec", "core.bonus.GARGOYLE.description": "Nie może się wskrzesić i uleczyć", "core.bonus.GENERAL_DAMAGE_REDUCTION.name": "Redukcja obrażeń (${val}%)", - "core.bonus.GENERAL_DAMAGE_REDUCTION.description": "Zmiejsza obrażenia fizyczne z dystansu lub walki wręcz", + "core.bonus.GENERAL_DAMAGE_REDUCTION.description": "Zmiejsza obr. fizyczne z dystansu lub wręcz", "core.bonus.HATE.name": "${subtype.creature}", "core.bonus.HATE.description": "+${val}% dodatkowych obrażeń", "core.bonus.HEALER.name": "Uzdrowiciel", @@ -556,8 +703,8 @@ "core.bonus.KING.description": "czar POGROMCA stopnia ${val}+", "core.bonus.LEVEL_SPELL_IMMUNITY.name": "Odporność: Czary 1-${val}", "core.bonus.LEVEL_SPELL_IMMUNITY.description": "Odporny na czary 1-${val} poz.", - "core.bonus.LIMITED_SHOOTING_RANGE.name" : "Ograniczony zasięg strzelania", - "core.bonus.LIMITED_SHOOTING_RANGE.description" : "Nie może strzelać do celów będących dalej niż ${val} heksów", + "core.bonus.LIMITED_SHOOTING_RANGE.name" : "Nie może strzelać do", + "core.bonus.LIMITED_SHOOTING_RANGE.description" : "celów będących dalej niż ${val} heksów", "core.bonus.LIFE_DRAIN.name": "Wysysa życie (${val}%)", "core.bonus.LIFE_DRAIN.description": "Wysysa ${val}% zadanych obrażeń", "core.bonus.MANA_CHANNELING.name": "Transfer many ${val}%", @@ -578,7 +725,7 @@ "core.bonus.NO_MORALE.description": "Odporność na efekty morale", "core.bonus.NO_WALL_PENALTY.name": "Bez przeszkód", "core.bonus.NO_WALL_PENALTY.description": "Pełne obrażenia podczas oblężenia", - "core.bonus.NON_LIVING.name": "Nie żyjący", + "core.bonus.NON_LIVING.name": "Nieżyjący", "core.bonus.NON_LIVING.description": "Niewrażliwość na wiele efektów", "core.bonus.RANDOM_SPELLCASTER.name": "Losowy czarodziej", "core.bonus.RANDOM_SPELLCASTER.description": "Może rzucić losowy czar", @@ -591,13 +738,13 @@ "core.bonus.RETURN_AFTER_STRIKE.name": "Atak i Powrót", "core.bonus.RETURN_AFTER_STRIKE.description": "Wraca po ataku wręcz", "core.bonus.REVENGE.name": "Odwet", - "core.bonus.REVENGE.description": "Zadaje dodatkowe obrażenia zależne od strat własnych oddziału", + "core.bonus.REVENGE.description": "Zadaje dodat. obr. zależne od strat własnych oddziału", "core.bonus.SHOOTER.name": "Dystansowy", "core.bonus.SHOOTER.description": "Stworzenie może strzelać", "core.bonus.SHOOTS_ALL_ADJACENT.name": "Ostrzeliwuje wszystko dookoła", - "core.bonus.SHOOTS_ALL_ADJACENT.description": "Ataki dystansowe tego stworzenia uderzają we wszystkie cele na małym obszarze", + "core.bonus.SHOOTS_ALL_ADJACENT.description": "Ataki dyst. tego stworzenia uderzają we wszystkie cele na małym obszarze", "core.bonus.SOUL_STEAL.name": "Kradzież dusz", - "core.bonus.SOUL_STEAL.description": "Zdobywa ${val} nowych stworzeń za każdego zabitego wroga", + "core.bonus.SOUL_STEAL.description": "+${val} nowych stworzeń za każdego zabitego wroga", "core.bonus.SPELLCASTER.name": "Czarodziej", "core.bonus.SPELLCASTER.description": "Może rzucić ${subtype.spell}", "core.bonus.SPELL_AFTER_ATTACK.name": "${val}% szans na czar", @@ -629,5 +776,13 @@ "core.bonus.WATER_IMMUNITY.name": "Odporność: Woda", "core.bonus.WATER_IMMUNITY.description": "Odporny na wszystkie czary szkoły wody", "core.bonus.WIDE_BREATH.name": "Szerokie zionięcie", - "core.bonus.WIDE_BREATH.description": "Szeroki atak zionięciem (wiele heksów)" + "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.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 73% rename from Mods/vcmi/config/vcmi/portuguese.json rename to Mods/vcmi/Content/config/portuguese.json index 0e5391e17..025375d4c 100644 --- a/Mods/vcmi/config/vcmi/portuguese.json +++ b/Mods/vcmi/Content/config/portuguese.json @@ -3,15 +3,20 @@ "vcmi.adventureMap.monsterThreat.levels.0" : "Sem Esforço", "vcmi.adventureMap.monsterThreat.levels.1" : "Muito Fraca", "vcmi.adventureMap.monsterThreat.levels.2" : "Fraca", - "vcmi.adventureMap.monsterThreat.levels.3" : "Um pouco mais fraca", + "vcmi.adventureMap.monsterThreat.levels.3" : "Um Pouco Mais Fraca", "vcmi.adventureMap.monsterThreat.levels.4" : "Igual", - "vcmi.adventureMap.monsterThreat.levels.5" : "Um pouco mais forte", + "vcmi.adventureMap.monsterThreat.levels.5" : "Um Pouco Mais Forte", "vcmi.adventureMap.monsterThreat.levels.6" : "Forte", "vcmi.adventureMap.monsterThreat.levels.7" : "Muito Forte", "vcmi.adventureMap.monsterThreat.levels.8" : "Desafiante", - "vcmi.adventureMap.monsterThreat.levels.9" : "Dominante", + "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 %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!", @@ -20,8 +25,16 @@ "vcmi.adventureMap.playerAttacked" : "O jogador foi atacado: %s", "vcmi.adventureMap.moveCostDetails" : "Pontos de movimento - Custo: %TURNS turnos + %POINTS pontos, Pontos restantes: %REMAINING", "vcmi.adventureMap.moveCostDetailsNoTurns" : "Pontos de movimento - Custo: %POINTS pontos, Pontos restantes: %REMAINING", + "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", @@ -36,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", @@ -54,32 +73,100 @@ "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", "vcmi.mainMenu.serverClosing" : "Fechando...", "vcmi.mainMenu.hostTCP" : "Hospedar jogo TCP/IP", "vcmi.mainMenu.joinTCP" : "Entrar em jogo TCP/IP", - + "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 @@ -136,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.", @@ -157,16 +245,48 @@ "vcmi.systemOptions.otherGroup" : "Outras Configurações", // não utilizado no momento "vcmi.systemOptions.townsGroup" : "Tela da Cidade", + "vcmi.statisticWindow.statistics" : "Estatísticas", + "vcmi.statisticWindow.tsvCopy" : "Para a área de transf.", + "vcmi.statisticWindow.selectView" : "Selec. visualização", + "vcmi.statisticWindow.value" : "Valor", + "vcmi.statisticWindow.title.overview" : "Visão geral", + "vcmi.statisticWindow.title.resources" : "Recursos", + "vcmi.statisticWindow.title.income" : "Renda", + "vcmi.statisticWindow.title.numberOfHeroes" : "Nº de heróis", + "vcmi.statisticWindow.title.numberOfTowns" : "Nº de cidades", + "vcmi.statisticWindow.title.numberOfArtifacts" : "Nº de artefatos", + "vcmi.statisticWindow.title.numberOfDwellings" : "Nº de moradias", + "vcmi.statisticWindow.title.numberOfMines" : "Nº de minas", + "vcmi.statisticWindow.title.armyStrength" : "Força do exército", + "vcmi.statisticWindow.title.experience" : "Experiência", + "vcmi.statisticWindow.title.resourcesSpentArmy" : "Custo do exército", + "vcmi.statisticWindow.title.resourcesSpentBuildings" : "Custo de construção", + "vcmi.statisticWindow.title.mapExplored" : "Mapa explorado", + "vcmi.statisticWindow.param.playerName" : "Nome do jogador", + "vcmi.statisticWindow.param.daysSurvived" : "Dias sobrevividos", + "vcmi.statisticWindow.param.maxHeroLevel" : "Nível máximo do herói", + "vcmi.statisticWindow.param.battleWinRatioHero" : "Taxa de vitória (vs. herói)", + "vcmi.statisticWindow.param.battleWinRatioNeutral" : "Taxa de vitória (vs. neutro)", + "vcmi.statisticWindow.param.battlesHero" : "Batalhas (vs. herói)", + "vcmi.statisticWindow.param.battlesNeutral" : "Batalhas (vs. neutro)", + "vcmi.statisticWindow.param.maxArmyStrength" : "Força máxima do exército", + "vcmi.statisticWindow.param.tradeVolume" : "Volume de comércio", + "vcmi.statisticWindow.param.obeliskVisited" : "Obelisco visitado", + "vcmi.statisticWindow.icon.townCaptured" : "Cidade capturada", + "vcmi.statisticWindow.icon.strongestHeroDefeated" : "Herói mais forte do oponente derrotado", + "vcmi.statisticWindow.icon.grailFound" : "Graal encontrado", + "vcmi.statisticWindow.icon.defeated" : "Derrotado", + "vcmi.systemOptions.fullscreenBorderless.hover" : "Tela Cheia (sem bordas)", "vcmi.systemOptions.fullscreenBorderless.help" : "{Tela Cheia sem Bordas}\n\nSe selecionado, o VCMI será executado em modo de tela cheia sem bordas. Neste modo, o jogo sempre usará a mesma resolução que a área de trabalho, ignorando a resolução selecionada.", "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" @@ -175,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.", @@ -192,17 +312,19 @@ "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.", "vcmi.adventureOptions.infoBarCreatureManagement.help" : "{Gerencia as Criaturas no Painel de Informações}\n\nPermite reorganizar criaturas no painel de informações em vez de alternar entre os componentes padrão.", - "vcmi.adventureOptions.leftButtonDrag.hover" : "Arrastar Mapa com o Botão Esquerdo", + "vcmi.adventureOptions.leftButtonDrag.hover" : "Botão Esq. Arrasta", "vcmi.adventureOptions.leftButtonDrag.help" : "{Arrastar Mapa com o Botão Esquerdo}\n\nQuando ativado, mover o mouse com o botão esquerdo pressionado irá arrastar a visualização do mapa de aventura.", + "vcmi.adventureOptions.rightButtonDrag.hover" : "Botão Dir. Arrasta", + "vcmi.adventureOptions.rightButtonDrag.help" : "{Arrastar Mapa com o Botão Direito}\n\nQuando ativado, mover o mouse com o botão direito pressionado irá arrastar a visualização do mapa de aventura.", "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": "", @@ -210,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.", @@ -218,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": "", @@ -230,13 +352,15 @@ "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", "vcmi.battleOptions.skipBattleIntroMusic.help": "{Pula a Música de Introdução}\n\nPermite ações durante a música de introdução que toca no início de cada batalha.", "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": "{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.", @@ -281,21 +405,13 @@ "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.noCreaturesToRecruit" : "Não há criaturas para recrutar!", - "vcmi.townHall.greetingManaVortex" : "Ao se aproximar de %s, seu corpo é preenchido com nova energia. Você dobrou seus pontos de mana normais.", - "vcmi.townHall.greetingKnowledge" : "Estudando os glifos de %s, você adquire uma visão dos segredos sobre o funcionamento de várias magias (+1 de Conhecimento).", - "vcmi.townHall.greetingSpellPower" : "%s ensina novas maneiras de concentrar seus poderes mágicos (+1 de Força).", - "vcmi.townHall.greetingExperience" : "Uma visita em %s ensina muitas habilidades novas (+1000 de Experiência).", - "vcmi.townHall.greetingAttack" : "Algum tempo passado em %s permite que você aprenda habilidades de combate mais eficazes (+1 de Ataque).", - "vcmi.townHall.greetingDefence" : "Ao passar um tempo em %s, os guerreiros experientes lá dentro te ensinam habilidades defensivas adicionais (+1 de Defesa).", - "vcmi.townHall.hasNotProduced" : "%s ainda não produziu nada.", - "vcmi.townHall.hasProduced" : "%s produziu %d %s nesta semana.", - "vcmi.townHall.greetingCustomBonus" : "%s dá +%d %s%s", - "vcmi.townHall.greetingCustomUntil" : " até a próxima batalha.", - "vcmi.townHall.greetingInTownMagicWell" : "%s restaurou seus pontos de mana para o máximo.", + "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.\"", + "vcmi.townStructure.bank.payBack" : "Você entra no banco. Um banqueiro o vê e diz: \"Você já pegou um empréstimo. Pague-o antes de pegar um novo.\"", "vcmi.logicalExpressions.anyOf" : "Qualquer um dos seguintes:", "vcmi.logicalExpressions.allOf" : "Todos os seguintes:", @@ -305,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", @@ -315,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.", @@ -327,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", @@ -337,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", @@ -469,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.", @@ -481,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", @@ -519,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}%)", @@ -538,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", @@ -557,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}%", @@ -610,8 +735,8 @@ "core.bonus.SPELL_IMMUNITY.description" : "Imune a ${subtype.spell}", "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 a Feitiços", - "core.bonus.SPELL_RESISTANCE_AURA.description" : "Pilhas próximas ganham ${val}% de resistência a magia", + "core.bonus.SPELL_RESISTANCE_AURA.name" : "Aura de Resistência", + "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", @@ -624,10 +749,40 @@ "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", - "core.bonus.WIDE_BREATH.description" : "Ataque de sopro amplo (vários hexágonos)" + "core.bonus.WIDE_BREATH.description" : "Ataque de sopro amplo (vários hexágonos)", + "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.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 96% rename from Mods/vcmi/config/vcmi/russian.json rename to Mods/vcmi/Content/config/russian.json index 636d36d7c..8417255fd 100644 --- a/Mods/vcmi/config/vcmi/russian.json +++ b/Mods/vcmi/Content/config/russian.json @@ -162,17 +162,6 @@ "vcmi.townHall.missingBase" : "Сначала необходимо построить: %s", "vcmi.townHall.noCreaturesToRecruit" : "Нет существ для найма!", - "vcmi.townHall.greetingManaVortex" : "Близ %s ваше тело наполняется новой силой. Ваша обычная магическая энергия ныне удвоена.", - "vcmi.townHall.greetingKnowledge" : "Вы изучили знаки %s, на вас снизошло прозрение в деле магии (+1 Знания).", - "vcmi.townHall.greetingSpellPower" : "В %s вас научили новым способам концентрации магической силы (+1 Силы)", - "vcmi.townHall.greetingExperience" : "Посетив %s, вы узнали много нового (+1000 опыта).", - "vcmi.townHall.greetingAttack" : "Пребывание в %s позволило вам лучше использовать боевые навыки (+1 Атаки).", - "vcmi.townHall.greetingDefence" : "В %s искушенные воины преподали вам свои защитные умения (+1 Защиты).", - "vcmi.townHall.hasNotProduced" : "В %s еще ничего не произведено.", - "vcmi.townHall.hasProduced" : "В %s на этой неделе произведено: %d %s", - "vcmi.townHall.greetingCustomBonus" : "%s дает вам +%d %s%s", - "vcmi.townHall.greetingCustomUntil" : " до следующей битвы.", - "vcmi.townHall.greetingInTownMagicWell" : "%s восстанавливает ваши очки заклинаний до максимума.", "vcmi.logicalExpressions.anyOf" : "Любое из:", "vcmi.logicalExpressions.allOf" : "Все перечисленное:", @@ -214,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 96% rename from Mods/vcmi/config/vcmi/spanish.json rename to Mods/vcmi/Content/config/spanish.json index c9d6b9a31..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", @@ -219,17 +217,6 @@ "vcmi.townHall.missingBase" : "Primero se debe construir el edificio base %s", "vcmi.townHall.noCreaturesToRecruit" : "¡No hay criaturas para reclutar!", - "vcmi.townHall.greetingManaVortex" : "Al acercarte a %s, tu cuerpo se llena de nueva energía. Has duplicado tus puntos de hechizo normales.", - "vcmi.townHall.greetingKnowledge" : "Estudias los glifos en %s y obtienes una visión de los entresijos de varias magias (+1 conocimiento).", - "vcmi.townHall.greetingSpellPower" : "El %s te enseña nuevas formas de enfocar tus poderes mágicos (+1 Poder).", - "vcmi.townHall.greetingExperience" : "Una visita a %s te enseña muchas habilidades nuevas (+1000 Experiencia).", - "vcmi.townHall.greetingAttack" : "El tiempo dedicado en %s te permite aprender habilidades de combate más efectivas (+1 habilidad de ataque).", - "vcmi.townHall.greetingDefence" : "Pasando tiempo en %s, los guerreros experimentados allí te enseñan habilidades defensivas adicionales (+1 Defensa).", - "vcmi.townHall.hasNotProduced" : "%s aún no ha producido nada.", - "vcmi.townHall.hasProduced" : "%s ha producido %d %s esta semana.", - "vcmi.townHall.greetingCustomBonus" : "%s te da +%d %s%s", - "vcmi.townHall.greetingCustomUntil" : " hasta la próxima batalla.", - "vcmi.townHall.greetingInTownMagicWell" : "%s ha restaurado tus puntos de hechizo al máximo.", "vcmi.logicalExpressions.anyOf" : "Cualquiera de lo siguiente:", "vcmi.logicalExpressions.allOf" : "Todo lo siguiente:", 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..868d39b87 --- /dev/null +++ b/Mods/vcmi/Content/config/swedish.json @@ -0,0 +1,766 @@ +{ + "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.broadcast.failedLoadGame" : "Misslyckades med att ladda spelet", + "vcmi.broadcast.command" : "Använd '!help' för att lista tillgängliga kommandon", + "vcmi.broadcast.simturn.end" : "Simultana turomgångar har avslutats", + "vcmi.broadcast.simturn.endBetween" : "De samtidiga turomgångarna mellan spelarna %s och %s har avslutats", + "vcmi.broadcast.serverProblem" : "Servern stötte på ett problem", + "vcmi.broadcast.gameTerminated" : "Spelet avslutades", + "vcmi.broadcast.gameSavedAs" : "Spelet sparades som", + "vcmi.broadcast.noCheater" : "Inga fuskare registrerade!", + "vcmi.broadcast.playerCheater" : "Spelare %s är fuskare!", + "vcmi.broadcast.statisticFile" : "Statistikfiler finns i %s-katalogen", + "vcmi.broadcast.help.commands" : "Tillgängliga kommandon till värden:", + "vcmi.broadcast.help.exit" : "'!exit' - avslutar omedelbart det aktuella spelet", + "vcmi.broadcast.help.kick" : "'!kick ' - sparkar ut angiven spelare från spelet", + "vcmi.broadcast.help.save" : "'!save ' - sparar spelet under angivet filnamn", + "vcmi.broadcast.help.statistic" : "'!statistic' - spara spelstatistik som csv-fil", + "vcmi.broadcast.help.commandsAll" : "Tillgängliga kommandon för alla spelare:", + "vcmi.broadcast.help.help" : "'!help' - visa den här hjälpen", + "vcmi.broadcast.help.cheaters" : "'!cheaters' - visa lista över spelare som angav fuskkommando under spelet", + "vcmi.broadcast.help.vote" : "'!vote' - gör det möjligt att ändra vissa spelinställningar om alla spelare röstar för det", + "vcmi.broadcast.vote.allow" : "'!vote simturns allow X' - tillåter simultana turomgångar under angivet antal dagar, eller tills spelarnas hjältar kommer för nära varandra", + "vcmi.broadcast.vote.force" : "'!vote simturns force X' - tvingar fram simultana turomgångar under ett angivet antal dagar (blockerar spelarkontakter)", + "vcmi.broadcast.vote.abort" : "'!vote simturns abort' - avbryter samtidiga turomgångar när denna turomgång avslutas", + "vcmi.broadcast.vote.timer" : "'!vote timer prolong X' - förlänger bastimern för alla spelare med angivet antal sekunder", + "vcmi.broadcast.vote.noActive" : "Ingen aktiv röstning!", + "vcmi.broadcast.vote.yes" : "ja", + "vcmi.broadcast.vote.no" : "nej", + "vcmi.broadcast.vote.notRecognized" : "Röstningskommando känns inte igen!", + "vcmi.broadcast.vote.success.untilContacts" : "Omröstningen lyckades. Simultana turomgångar kommer att pågå i %s dagar eller tills spelarnas hjältar kommer för nära varandra", + "vcmi.broadcast.vote.success.contactsBlocked" : "Omröstningen lyckades. Simultana turomgångar kommer att pågå i %s fler dagar. Närkontakter är blockerade", + "vcmi.broadcast.vote.success.nextDay" : "Omröstningen lyckades. Simultana turomgångar kommer att avslutas nästa dag", + "vcmi.broadcast.vote.success.timer" : "Omröstningen lyckades. Timern för alla spelare har förlängts med %s sekunder", + "vcmi.broadcast.vote.aborted" : "Spelare röstade mot förändring. Omröstningen avbröts", + "vcmi.broadcast.vote.start.untilContacts" : "Började rösta för att tillåta samtidiga turomgångar i ytterligare %s dagar", + "vcmi.broadcast.vote.start.contactsBlocked" : "Började rösta för att tvinga fram simultana turomgångar i ytterligare %s fler dagar", + "vcmi.broadcast.vote.start.nextDay" : "Började rösta för att avsluta samtidiga turomgångar från och med nästa dag", + "vcmi.broadcast.vote.start.timer" : "Började rösta för att förlänga timern för alla spelare med %s sekunder", + "vcmi.broadcast.vote.hint" : "Skriv '!vote yes' för att godkänna denna ändring eller '!vote no' för att rösta emot den", + + "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.login.spectator" : "Åskådare", + "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.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.server.errors.wrongIdentified" : "Du identifierades som spelare %s när du förväntade dig %s", + "vcmi.server.errors.notAllowed" : "Du får inte utföra denna åtgärd!", + + "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 78% rename from Mods/vcmi/config/vcmi/ukrainian.json rename to Mods/vcmi/Content/config/ukrainian.json index 4a50e65ff..e806e15f7 100644 --- a/Mods/vcmi/config/vcmi/ukrainian.json +++ b/Mods/vcmi/Content/config/ukrainian.json @@ -12,6 +12,11 @@ "vcmi.adventureMap.monsterThreat.levels.9" : "Нездоланна", "vcmi.adventureMap.monsterThreat.levels.10" : "Смертельна", "vcmi.adventureMap.monsterThreat.levels.11" : "Неможлива", + "vcmi.adventureMap.monsterLevel" : "\n\n%TOWN, істота%ATTACK_TYPE, %LEVELго рівня", + "vcmi.adventureMap.monsterMeleeType" : " ближнього бою", + "vcmi.adventureMap.monsterRangedType" : "-стрілок", + "vcmi.adventureMap.search.hover" : "Шукати об'єкт мапи", + "vcmi.adventureMap.search.help" : "Оберіть об'єкт для пошуку на мапі.", "vcmi.adventureMap.confirmRestartGame" : "Ви впевнені, що хочете перезапустити гру?", "vcmi.adventureMap.noTownWithMarket" : "Немає доступних ринків!", @@ -20,8 +25,16 @@ "vcmi.adventureMap.playerAttacked" : "Гравця атаковано: %s", "vcmi.adventureMap.moveCostDetails" : "Очки руху - Вартість: %TURNS ходів + %POINTS очок. Залишок очок: %REMAINING", "vcmi.adventureMap.moveCostDetailsNoTurns" : "Очки руху - Вартість: %POINTS очок, Залишок очок: %REMAINING", + "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" : "Сірий", @@ -36,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" : "Відділити одну істоту", @@ -55,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" : "Помилка з'єднання", @@ -73,6 +110,56 @@ "vcmi.lobby.sortDate" : "Сортувати мапи за датою зміни", "vcmi.lobby.backToLobby" : "Назад до лобі", + "vcmi.lobby.author" : "Автор", + "vcmi.lobby.handicap" : "Гандикап", + "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.broadcast.failedLoadGame" : "Не вдалося завантажити гру", + "vcmi.broadcast.command" : "Введіть '!help' у чаті гри, щоб переглянути список доступних команд", + "vcmi.broadcast.simturn.end" : "Одночасні ходи закінчилися", + "vcmi.broadcast.simturn.endBetween" : "Одночасні ходи між гравцями %s та %s завершилися", + "vcmi.broadcast.serverProblem" : "Сервер зіткнувся з проблемою", + "vcmi.broadcast.gameTerminated" : "гру було завершено", + "vcmi.broadcast.gameSavedAs" : "гру збережено як", + "vcmi.broadcast.noCheater" : "Читерів не зареєстровано!", + "vcmi.broadcast.playerCheater" : "Гравець %s - шахрай!", + "vcmi.broadcast.statisticFile" : "Файли статистики можна знайти в каталозі %s", + "vcmi.broadcast.help.commands" : "Команди доступні для хоста:", + "vcmi.broadcast.help.exit" : "'!exit' - негайно завершує поточну гру", + "vcmi.broadcast.help.kick" : "'!kick ' - вигнати вказаного гравця з гри", + "vcmi.broadcast.help.save" : "'!save ' - зберегти гру під вказаним ім'ям", + "vcmi.broadcast.help.statistic" : "'!statistic' - зберегти статистику гри у форматі csv", + "vcmi.broadcast.help.commandsAll" : "Команди доступні всім гравцям:", + "vcmi.broadcast.help.help" : "'!help' - відобразити цю довідку", + "vcmi.broadcast.help.cheaters" : "'!cheaters' - список гравців, які вводили чит-команду під час гри", + "vcmi.broadcast.help.vote" : "'!vote' - дозволяє змінити деякі налаштування гри, якщо всі гравці проголосують за це", + "vcmi.broadcast.vote.allow" : "'!vote simturns allow X' - дозволяти одночасні ходи на визначену кількість днів або до контакту", + "vcmi.broadcast.vote.force" : "'!vote simturns force X' - увімкнути одночасні ходи на визначену кількість днів, блокуючи контакти гравців", + "vcmi.broadcast.vote.abort" : "'!vote simturns abort' - завершити одночасні ходи, як тільки цей хід закінчиться", + "vcmi.broadcast.vote.timer" : "'!vote timer prolong X' - подовжити базовий таймер для всіх гравців на вказану кількість секунд", + "vcmi.broadcast.vote.noActive" : "Активне голосування відсутнє!", + "vcmi.broadcast.vote.yes" : "так", + "vcmi.broadcast.vote.no" : "ні", + "vcmi.broadcast.vote.notRecognized" : "Команда для голосування не розпізнана!", + "vcmi.broadcast.vote.success.untilContacts" : "Голосування пройшло успішно. Одночасні ходи триватимуть ще %s днів, або до контакту", + "vcmi.broadcast.vote.success.contactsBlocked" : "Голосування пройшло успішно. Одночасні ходи триватимуть ще %s днів. Контакти між гравцями заблоковані", + "vcmi.broadcast.vote.success.nextDay" : "Голосування пройшло успішно. Одночасні ходи закінчаться на наступний день", + "vcmi.broadcast.vote.success.timer" : "Голосування пройшло успішно. Таймер для всіх гравців було подовжено на %s секунд", + "vcmi.broadcast.vote.aborted" : "Гравець проголосував проти змін. Голосування перервано", + "vcmi.broadcast.vote.start.untilContacts" : "Розпочато голосування, за одночасні ходи на %s більше днів або до контакту гравців", + "vcmi.broadcast.vote.start.contactsBlocked" : "Розпочато голосування за безумовні одночасні ходи на %s більше днів", + "vcmi.broadcast.vote.start.nextDay" : "Розпочато голосування за припинення одночасних ходів з наступного дня", + "vcmi.broadcast.vote.start.timer" : "Розпочато голосування за продовження таймера для всіх гравців на %s секунд", + "vcmi.broadcast.vote.hint" : "Введіть \"!vote yes\", щоб погодитися з цією зміною, або \"!vote no\", щоб проголосувати проти неї", + "vcmi.lobby.login.title" : "Онлайн лобі VCMI", "vcmi.lobby.login.username" : "Логін:", "vcmi.lobby.login.connecting" : "Підключення...", @@ -80,6 +167,7 @@ "vcmi.lobby.login.create" : "Створити акаунт", "vcmi.lobby.login.login" : "Увійти", "vcmi.lobby.login.as" : "Увійти як %s", + "vcmi.lobby.login.spectator" : "Спостерігач", "vcmi.lobby.header.rooms" : "Активні кімнати - %d", "vcmi.lobby.header.channels" : "Канали чату", "vcmi.lobby.header.chat.global" : "Глобальний ігровий чат - %s", // %s -> language name @@ -135,13 +223,14 @@ "vcmi.client.errors.invalidMap" : "{Пошкоджена карта або кампанія}\n\nНе вдалося запустити гру! Вибрана карта або кампанія може бути невірною або пошкодженою. Причина:\n%s", "vcmi.client.errors.missingCampaigns" : "{Не вистачає файлів даних}\n\nФайли даних кампаній не знайдено! Можливо, ви використовуєте неповні або пошкоджені файли даних Heroes 3. Будь ласка, перевстановіть дані гри.", + "vcmi.server.errors.disconnected" : "{Помилка мережі}\n\nВтрачено зв'язок з сервером гри!", + "vcmi.server.errors.playerLeft" : "{Гравець покинув гру}\n\n%s гравець від'єднався від гри!", //%s -> player color "vcmi.server.errors.existingProcess" : "Працює інший процес vcmiserver, будь ласка, спочатку завершіть його", "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.server.errors.wrongIdentified" : "Ви були ідентифіковані як гравець %s, хоча очікували %s", + "vcmi.server.errors.notAllowed" : "Ви не можете виконати цю дію!", "vcmi.dimensionDoor.seaToLandError" : "Неможливо телепортуватися з моря на сушу або навпаки за допомогою просторової брами", @@ -157,6 +246,38 @@ "vcmi.systemOptions.otherGroup" : "Інші налаштування", "vcmi.systemOptions.townsGroup" : "Екран міста", + "vcmi.statisticWindow.statistics" : "Статистика", + "vcmi.statisticWindow.tsvCopy" : "Дані до буфера обміну", + "vcmi.statisticWindow.selectView" : "Оберіть представлення", + "vcmi.statisticWindow.value" : "Цінність", + "vcmi.statisticWindow.title.overview" : "Загальний огляд", + "vcmi.statisticWindow.title.resources" : "Ресурси", + "vcmi.statisticWindow.title.income" : "Прибуток", + "vcmi.statisticWindow.title.numberOfHeroes" : "К-сть героїв", + "vcmi.statisticWindow.title.numberOfTowns" : "К-сть міст", + "vcmi.statisticWindow.title.numberOfArtifacts" : "К-сть артефактів", + "vcmi.statisticWindow.title.numberOfDwellings" : "К-сть помешкань", + "vcmi.statisticWindow.title.numberOfMines" : "К-сть шахт", + "vcmi.statisticWindow.title.armyStrength" : "Сила армії", + "vcmi.statisticWindow.title.experience" : "Досвід", + "vcmi.statisticWindow.title.resourcesSpentArmy" : "Витрати на армію", + "vcmi.statisticWindow.title.resourcesSpentBuildings" : "Витрати на будівництво", + "vcmi.statisticWindow.title.mapExplored" : "Ступінь вивченості карти", + "vcmi.statisticWindow.param.playerName" : "Ім'я гравця", + "vcmi.statisticWindow.param.daysSurvived" : "Прожиті дні", + "vcmi.statisticWindow.param.maxHeroLevel" : "Макс. рівень героя", + "vcmi.statisticWindow.param.battleWinRatioHero" : "Частка перемог (проти героя)", + "vcmi.statisticWindow.param.battleWinRatioNeutral" : "Частка перемог (проти нейтральних)", + "vcmi.statisticWindow.param.battlesHero" : "Боїв (проти героя)", + "vcmi.statisticWindow.param.battlesNeutral" : "Боїв (проти нейтральних)", + "vcmi.statisticWindow.param.maxArmyStrength" : "Макс. сила армії", + "vcmi.statisticWindow.param.tradeVolume" : "Обсяг торгівлі", + "vcmi.statisticWindow.param.obeliskVisited" : "Відвідано обеліск", + "vcmi.statisticWindow.icon.townCaptured" : "Місто захоплено", + "vcmi.statisticWindow.icon.strongestHeroDefeated" : "Перемогли сильного героя суперника", + "vcmi.statisticWindow.icon.grailFound" : "Грааль знайдено", + "vcmi.statisticWindow.icon.defeated" : "Переможений", + "vcmi.systemOptions.fullscreenBorderless.hover" : "На весь екран (безрамкове вікно)", "vcmi.systemOptions.fullscreenBorderless.help" : "{На весь екран (безрамкове вікно)}\n\nЯкщо обрано, VCMI працюватиме у режимі безрамкового вікна на весь екран. У цьому режимі гра завжди використовує ту саму роздільну здатність, що й робочий стіл, ігноруючи вибрану роздільну здатність", "vcmi.systemOptions.fullscreenExclusive.hover" : "На весь екран (ексклюзивний режим)", @@ -197,8 +318,10 @@ "vcmi.adventureOptions.borderScroll.help" : "{{Прокрутка по краю}\n\nПрокручувати мапу пригод, коли курсор знаходиться біля краю вікна. Цю функцію можна вимкнути, утримуючи клавішу CTRL.", "vcmi.adventureOptions.infoBarCreatureManagement.hover" : "Керування істотами у вікні статусу", "vcmi.adventureOptions.infoBarCreatureManagement.help" : "{Керування істотами у вікні статусу}\n\nДозволяє впорядковувати істот у вікні статусу замість циклічного перемикання між типовими компонентами", - "vcmi.adventureOptions.leftButtonDrag.hover" : "Переміщення мапи лівою кнопкою", - "vcmi.adventureOptions.leftButtonDrag.help" : "{Переміщення мапи лівою кнопкою}\n\nЯкщо увімкнено, переміщення миші з натиснутою лівою кнопкою буде перетягувати мапу пригод", + "vcmi.adventureOptions.leftButtonDrag.hover" : "Переміщення мапи ЛКМ", + "vcmi.adventureOptions.leftButtonDrag.help" : "{Переміщення мапи ЛКМ}\n\nЯкщо увімкнено, переміщення миші з натиснутою лівою кнопкою буде перетягувати мапу пригод", + "vcmi.adventureOptions.rightButtonDrag.hover" : "Переміщення мапи ПКМ", + "vcmi.adventureOptions.rightButtonDrag.help" : "{Переміщення мапи ПКМ}\n\nЯкщо увімкнено, переміщення миші з натиснутою правою кнопкою буде перетягувати мапу пригод", "vcmi.adventureOptions.smoothDragging.hover" : "Плавне перетягування мапи", "vcmi.adventureOptions.smoothDragging.help" : "{Плавне перетягування мапи}\n\nЯкщо увімкнено, перетягування мапи має сучасний ефект завершення.", "vcmi.adventureOptions.skipAdventureMapAnimations.hover" : "Вимкнути ефекти зникнення", @@ -237,6 +360,8 @@ "vcmi.battleOptions.skipBattleIntroMusic.help": "{Пропускати вступну музику}\n\n Пропускати коротку музику, яка грає на початку кожної битви перед початком дії. Також можна пропустити, натиснувши клавішу ESC.", "vcmi.battleOptions.endWithAutocombat.hover": "Завершує бій", "vcmi.battleOptions.endWithAutocombat.help": "{Завершує бій}\n\nАвто-бій миттєво завершує бій", + "vcmi.battleOptions.showQuickSpell.hover": "Панель швидкого чарування", + "vcmi.battleOptions.showQuickSpell.help": "{Панель швидкого чарування}\n\nПоказати панель для швидкого вибору заклять.", "vcmi.adventureMap.revisitObject.hover" : "Відвідати Об'єкт", "vcmi.adventureMap.revisitObject.help" : "{Відвідати Об'єкт}\n\nЯкщо герой в даний момент стоїть на об'єкті мапи, він може знову відвідати цю локацію.", @@ -285,17 +410,9 @@ "vcmi.townHall.missingBase" : "Спочатку необхідно звести початкову будівлю: %s", "vcmi.townHall.noCreaturesToRecruit" : "Немає істот, яких можна завербувати!", - "vcmi.townHall.greetingManaVortex" : "Неподалік %s ваше тіло наповнюється новою силою. Ваша звична магічна енергія сьогодні подвоєна.", - "vcmi.townHall.greetingKnowledge" : "Ви вивчили знаки на %s, і на вас зійшло прозріння у справах магії. (+1 Knowledge).", - "vcmi.townHall.greetingSpellPower" : "В %s вас навчили новим методам концентрації магічної сили. (+1 Power).", - "vcmi.townHall.greetingExperience" : "Відвідавши %s, ви дізналися багато нового. (+1000 Experience).", - "vcmi.townHall.greetingAttack" : "Перебування у %s дозволило вам краще використовувати бойові навички (+1 Attack Skill).", - "vcmi.townHall.greetingDefence" : "У %s досвідчені воїни виклали вам свої захисні вміння. (+1 Defense).", - "vcmi.townHall.hasNotProduced" : "Поки що %s нічого не створило.", - "vcmi.townHall.hasProduced" : "Цього тижня %s створило %d одиниць, цього разу це %s.", - "vcmi.townHall.greetingCustomBonus" : "%s дає вам +%d %s%s", - "vcmi.townHall.greetingCustomUntil" : " до наступної битви.", - "vcmi.townHall.greetingInTownMagicWell" : "%s повністю відновлює ваш запас очків магії.", + + "vcmi.townStructure.bank.borrow" : "Ви заходите в банк. Вас бачить банкір і каже: 'Ми зробили для вас спеціальну пропозицію. Ви можете взяти у нас позику в розмірі 2500 золотих на 5 днів. Але щодня ви повинні будете повертати по 500 золотих'.", + "vcmi.townStructure.bank.payBack" : "Ви заходите в банк. Банкір бачить вас і каже: 'Ви вже отримали позику. Погасіть її, перш ніж брати нову позику'.", "vcmi.logicalExpressions.anyOf" : "Будь-що з перерахованого:", "vcmi.logicalExpressions.allOf" : "Все з перерахованого:", @@ -305,6 +422,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" : "Запросити героя", @@ -482,6 +606,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" : "Додаткові відплати", @@ -624,17 +750,18 @@ "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.DISINTEGRATE.description" : "Після смерті не залишається трупа", + "core.bonus.DISINTEGRATE.name" : "Розпад", + "core.bonus.ENEMY_ATTACK_REDUCTION.description" : "При атаці ігнорується ${val}% атаки нападника", + "core.bonus.ENEMY_ATTACK_REDUCTION.name" : "Ігнорування атаки (${val}%)", + "core.bonus.FEROCITY.description" : "Атакує ${val} більше разів, якщо вбиває когось", + "core.bonus.FEROCITY.name" : "Лютість", + "core.bonus.INVINCIBLE.description" : "На нього ніщо не може вплинути", + "core.bonus.INVINCIBLE.name" : "Невразливий", + "core.bonus.MECHANICAL.description" : "Імунітет до багатьох ефектів, можна ремонтувати", + "core.bonus.MECHANICAL.name" : "Механічний", + "core.bonus.PRISM_HEX_ATTACK_BREATH.description" : "Атака подихом у трьох напрямах", + "core.bonus.PRISM_HEX_ATTACK_BREATH.name" : "Призматична атака", + "core.bonus.REVENGE.description" : "Завдає додаткової шкоди залежно від втраченого здоров'я в бою", + "core.bonus.REVENGE.name" : "Помста" } diff --git a/Mods/vcmi/config/vcmi/vietnamese.json b/Mods/vcmi/Content/config/vietnamese.json similarity index 95% rename from Mods/vcmi/config/vcmi/vietnamese.json rename to Mods/vcmi/Content/config/vietnamese.json index 5939c1db0..9b08b5e2b 100644 --- a/Mods/vcmi/config/vcmi/vietnamese.json +++ b/Mods/vcmi/Content/config/vietnamese.json @@ -159,17 +159,6 @@ "vcmi.townHall.missingBase": "Căn cứ %s phải được xây trước", "vcmi.townHall.noCreaturesToRecruit": "Không có quái để chiêu mộ!", - "vcmi.townHall.greetingManaVortex": "%s giúp cơ thể bạn tràn đầy năng lượng mới. Bạn được gấp đôi năng lượng tối đa.", - "vcmi.townHall.greetingKnowledge": "Bạn học chữ khắc trên %s và thấu hiểu cách vận hành của nhiều ma thuật (+1 Trí).", - "vcmi.townHall.greetingSpellPower": "%s dạy bạn hướng mới tập trung sức mạnh ma thuật (+1 Lực).", - "vcmi.townHall.greetingExperience": "Viếng thăm %s dạy bạn nhiều kĩ năng mới (+1000 Kinh nghiệm).", - "vcmi.townHall.greetingAttack": "Thời gian ở %s giúp bạn học nhiều kĩ năng chiến đấu hiệu quả (+1 Công).", - "vcmi.townHall.greetingDefence": "Thời gian ở %s, các chiến binh lão luyện tại đó dạy bạn nhiều kĩ năng phòng thủ (+1 Thủ).", - "vcmi.townHall.hasNotProduced": "%s chưa tạo được cái gì.", - "vcmi.townHall.hasProduced": "%s tạo %d %s tuần này.", - "vcmi.townHall.greetingCustomBonus": "%s cho bạn +%d %s%s", - "vcmi.townHall.greetingCustomUntil": " đến trận đánh tiếp theo.", - "vcmi.townHall.greetingInTownMagicWell": "%s đã hồi phục năng lượng tối đa của bạn.", "vcmi.logicalExpressions.anyOf": "Bất kì cái sau:", "vcmi.logicalExpressions.allOf": "Tất cả cái sau:", 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/battle/rangeHighlights/rangeHighlightsGreen.json b/Mods/vcmi/Sprites/battle/rangeHighlights/rangeHighlightsGreen.json deleted file mode 100644 index 1c6b60d36..000000000 --- a/Mods/vcmi/Sprites/battle/rangeHighlights/rangeHighlightsGreen.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "basepath" : "battle/rangeHighlights/green/", - "images" : - [ - { "frame" : 0, "file" : "empty.png"}, // 000001 -> 00 empty frame - - // load single edges - { "frame" : 1, "file" : "topLeft.png"}, //000001 -> 01 topLeft - { "frame" : 2, "file" : "topLeft.png"}, //000010 -> 02 topRight - { "frame" : 3, "file" : "left.png"}, //000100 -> 04 right - { "frame" : 4, "file" : "topLeft.png"}, //001000 -> 08 bottomRight - { "frame" : 5, "file" : "topLeft.png"}, //010000 -> 16 bottomLeft - { "frame" : 6, "file" : "left.png"}, //100000 -> 32 left - - // load double edges - { "frame" : 7, "file" : "top.png"}, //000011 -> 03 top - { "frame" : 8, "file" : "top.png"}, //011000 -> 24 bottom - { "frame" : 9, "file" : "topLeftHalfCorner.png"}, //000110 -> 06 topRightHalfCorner - { "frame" : 10, "file" : "topLeftHalfCorner.png"}, //001100 -> 12 bottomRightHalfCorner - { "frame" : 11, "file" : "topLeftHalfCorner.png"}, //110000 -> 48 bottomLeftHalfCorner - { "frame" : 12, "file" : "topLeftHalfCorner.png"}, //100001 -> 33 topLeftHalfCorner - - // load halves - { "frame" : 13, "file" : "leftHalf.png"}, //001110 -> 14 rightHalf - { "frame" : 14, "file" : "leftHalf.png"}, //110001 -> 49 leftHalf - - // load corners - { "frame" : 15, "file" : "topLeftCorner.png"}, //000111 -> 07 topRightCorner - { "frame" : 16, "file" : "topLeftCorner.png"}, //011100 -> 28 bottomRightCorner - { "frame" : 17, "file" : "topLeftCorner.png"}, //111000 -> 56 bottomLeftCorner - { "frame" : 18, "file" : "topLeftCorner.png"} //100011 -> 35 topLeftCorner - ] -} - diff --git a/Mods/vcmi/Sprites/battle/rangeHighlights/rangeHighlightsRed.json b/Mods/vcmi/Sprites/battle/rangeHighlights/rangeHighlightsRed.json deleted file mode 100644 index 42350c073..000000000 --- a/Mods/vcmi/Sprites/battle/rangeHighlights/rangeHighlightsRed.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "basepath" : "battle/rangeHighlights/red/", - "images" : - [ - { "frame" : 0, "file" : "empty.png"}, // 000001 -> 00 empty frame - - // load single edges - { "frame" : 1, "file" : "topLeft.png"}, //000001 -> 01 topLeft - { "frame" : 2, "file" : "topLeft.png"}, //000010 -> 02 topRight - { "frame" : 3, "file" : "left.png"}, //000100 -> 04 right - { "frame" : 4, "file" : "topLeft.png"}, //001000 -> 08 bottomRight - { "frame" : 5, "file" : "topLeft.png"}, //010000 -> 16 bottomLeft - { "frame" : 6, "file" : "left.png"}, //100000 -> 32 left - - // load double edges - { "frame" : 7, "file" : "top.png"}, //000011 -> 03 top - { "frame" : 8, "file" : "top.png"}, //011000 -> 24 bottom - { "frame" : 9, "file" : "topLeftHalfCorner.png"}, //000110 -> 06 topRightHalfCorner - { "frame" : 10, "file" : "topLeftHalfCorner.png"}, //001100 -> 12 bottomRightHalfCorner - { "frame" : 11, "file" : "topLeftHalfCorner.png"}, //110000 -> 48 bottomLeftHalfCorner - { "frame" : 12, "file" : "topLeftHalfCorner.png"}, //100001 -> 33 topLeftHalfCorner - - // load halves - { "frame" : 13, "file" : "leftHalf.png"}, //001110 -> 14 rightHalf - { "frame" : 14, "file" : "leftHalf.png"}, //110001 -> 49 leftHalf - - // load corners - { "frame" : 15, "file" : "topLeftCorner.png"}, //000111 -> 07 topRightCorner - { "frame" : 16, "file" : "topLeftCorner.png"}, //011100 -> 28 bottomRightCorner - { "frame" : 17, "file" : "topLeftCorner.png"}, //111000 -> 56 bottomLeftCorner - { "frame" : 18, "file" : "topLeftCorner.png"} //100011 -> 35 topLeftCorner - ] -} - 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 3851bfb5c..000000000 --- a/Mods/vcmi/config/vcmi/czech.json +++ /dev/null @@ -1,559 +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.townHall.greetingManaVortex" : "Při pobytu u místa %s se vaše tělo naplnilo novou energií. Máte dvojnásobné množství maximální magické energie.", - "vcmi.townHall.greetingKnowledge" : "Studujete glyfy na the %s a porozumíte fungování různých magií (+1 Znalosti).", - "vcmi.townHall.greetingSpellPower" : "%s vás učí nové cesty zaměření vaší magické síly (+1 Síla kouzel).", - "vcmi.townHall.greetingExperience" : "Návštěva %s vás naučila spoustu nových dovedností (+1000 zkušeností).", - "vcmi.townHall.greetingAttack" : "Čas strávený poblíž místa zvaného %s vám dovolil se naučit efektivnější bojové dovednosti (+1 Útočná síla).", - "vcmi.townHall.greetingDefence" : "Trávíte čas na místě zvaném %s, zkušení bojovníci vás u toho naučili nové metody obrany (+1 Obranná síla).", - "vcmi.townHall.hasNotProduced" : "%s - zatím nic nevyrobeno.", - "vcmi.townHall.hasProduced" : "%s - vyrobeno %d %s tento týden.", - "vcmi.townHall.greetingCustomBonus" : "%s vám dává +%d %s%s", - "vcmi.townHall.greetingCustomUntil" : " do další bitvy.", - "vcmi.townHall.greetingInTownMagicWell" : "%s - obnoveno na maximum vaši magickou energii.", - - "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/VCMI_VS15.sln b/VCMI_VS15.sln deleted file mode 100644 index 7eea8ae08..000000000 --- a/VCMI_VS15.sln +++ /dev/null @@ -1,144 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 15 -VisualStudioVersion = 15.0.27130.2003 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "VCMI_client", "client\VCMI_client.vcxproj", "{8355EBA8-65C2-44A4-BC2D-78053E1BF2D6}" -EndProject -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "VCMI_lib", "lib\VCMI_lib.vcxproj", "{B952FFC5-3039-4DE1-9F08-90ACDA483D8F}" -EndProject -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "VCMI_server", "server\VCMI_server.vcxproj", "{8AF697C3-465E-4910-B31B-576A9ECDB309}" -EndProject -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "StupidAI", "AI\StupidAI\StupidAI.vcxproj", "{15DABC90-234A-4B6B-9EEB-777C4768B82B}" - ProjectSection(ProjectDependencies) = postProject - {B952FFC5-3039-4DE1-9F08-90ACDA483D8F} = {B952FFC5-3039-4DE1-9F08-90ACDA483D8F} - EndProjectSection -EndProject -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "ERM", "scripting\erm\ERM.vcxproj", "{8F202F43-106D-4F63-AD9D-B1D43E803E8C}" - ProjectSection(ProjectDependencies) = postProject - {B952FFC5-3039-4DE1-9F08-90ACDA483D8F} = {B952FFC5-3039-4DE1-9F08-90ACDA483D8F} - EndProjectSection -EndProject -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "VCAI", "AI\VCAI\VCAI.vcxproj", "{276C3DB0-7A6B-4417-8E5C-322B08633AAC}" - ProjectSection(ProjectDependencies) = postProject - {B952FFC5-3039-4DE1-9F08-90ACDA483D8F} = {B952FFC5-3039-4DE1-9F08-90ACDA483D8F} - EndProjectSection -EndProject -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "BattleAI", "AI\BattleAI\BattleAI.vcxproj", "{C0300513-E845-43B4-9A4F-E8817EAEF57C}" - ProjectSection(ProjectDependencies) = postProject - {B952FFC5-3039-4DE1-9F08-90ACDA483D8F} = {B952FFC5-3039-4DE1-9F08-90ACDA483D8F} - EndProjectSection -EndProject -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "EmptyAI", "AI\EmptyAI\EmptyAI.vcxproj", "{C41C4EB6-6F74-4F37-9FB0-9FA6BF377837}" - ProjectSection(ProjectDependencies) = postProject - {B952FFC5-3039-4DE1-9F08-90ACDA483D8F} = {B952FFC5-3039-4DE1-9F08-90ACDA483D8F} - EndProjectSection -EndProject -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Test", "test\Test.vcxproj", "{BA25F3F0-EB87-4164-AAB9-073C50A3557A}" -EndProject -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "minizip", "lib\minizip\minizip.vcxproj", "{AA3CC588-9D08-4178-A1E8-C71561E99723}" -EndProject -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "VCMI_launcher", "launcher\VCMI_launcher.vcxproj", "{5B6946C8-A24F-4223-8415-5E16A238ACED}" - ProjectSection(ProjectDependencies) = postProject - {B952FFC5-3039-4DE1-9F08-90ACDA483D8F} = {B952FFC5-3039-4DE1-9F08-90ACDA483D8F} - EndProjectSection -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Win32 = Debug|Win32 - Debug|x64 = Debug|x64 - RD|Win32 = RD|Win32 - RD|x64 = RD|x64 - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {8355EBA8-65C2-44A4-BC2D-78053E1BF2D6}.Debug|Win32.ActiveCfg = RD|Win32 - {8355EBA8-65C2-44A4-BC2D-78053E1BF2D6}.Debug|Win32.Build.0 = RD|Win32 - {8355EBA8-65C2-44A4-BC2D-78053E1BF2D6}.Debug|x64.ActiveCfg = Debug|x64 - {8355EBA8-65C2-44A4-BC2D-78053E1BF2D6}.Debug|x64.Build.0 = Debug|x64 - {8355EBA8-65C2-44A4-BC2D-78053E1BF2D6}.RD|Win32.ActiveCfg = RD|Win32 - {8355EBA8-65C2-44A4-BC2D-78053E1BF2D6}.RD|Win32.Build.0 = RD|Win32 - {8355EBA8-65C2-44A4-BC2D-78053E1BF2D6}.RD|x64.ActiveCfg = RD|x64 - {8355EBA8-65C2-44A4-BC2D-78053E1BF2D6}.RD|x64.Build.0 = RD|x64 - {B952FFC5-3039-4DE1-9F08-90ACDA483D8F}.Debug|Win32.ActiveCfg = RD|Win32 - {B952FFC5-3039-4DE1-9F08-90ACDA483D8F}.Debug|Win32.Build.0 = RD|Win32 - {B952FFC5-3039-4DE1-9F08-90ACDA483D8F}.Debug|x64.ActiveCfg = Debug|x64 - {B952FFC5-3039-4DE1-9F08-90ACDA483D8F}.Debug|x64.Build.0 = Debug|x64 - {B952FFC5-3039-4DE1-9F08-90ACDA483D8F}.RD|Win32.ActiveCfg = RD|Win32 - {B952FFC5-3039-4DE1-9F08-90ACDA483D8F}.RD|Win32.Build.0 = RD|Win32 - {B952FFC5-3039-4DE1-9F08-90ACDA483D8F}.RD|x64.ActiveCfg = RD|x64 - {B952FFC5-3039-4DE1-9F08-90ACDA483D8F}.RD|x64.Build.0 = RD|x64 - {8AF697C3-465E-4910-B31B-576A9ECDB309}.Debug|Win32.ActiveCfg = RD|Win32 - {8AF697C3-465E-4910-B31B-576A9ECDB309}.Debug|Win32.Build.0 = RD|Win32 - {8AF697C3-465E-4910-B31B-576A9ECDB309}.Debug|x64.ActiveCfg = Debug|x64 - {8AF697C3-465E-4910-B31B-576A9ECDB309}.Debug|x64.Build.0 = Debug|x64 - {8AF697C3-465E-4910-B31B-576A9ECDB309}.RD|Win32.ActiveCfg = RD|Win32 - {8AF697C3-465E-4910-B31B-576A9ECDB309}.RD|Win32.Build.0 = RD|Win32 - {8AF697C3-465E-4910-B31B-576A9ECDB309}.RD|x64.ActiveCfg = RD|x64 - {8AF697C3-465E-4910-B31B-576A9ECDB309}.RD|x64.Build.0 = RD|x64 - {15DABC90-234A-4B6B-9EEB-777C4768B82B}.Debug|Win32.ActiveCfg = RD|Win32 - {15DABC90-234A-4B6B-9EEB-777C4768B82B}.Debug|Win32.Build.0 = RD|Win32 - {15DABC90-234A-4B6B-9EEB-777C4768B82B}.Debug|x64.ActiveCfg = Debug|x64 - {15DABC90-234A-4B6B-9EEB-777C4768B82B}.Debug|x64.Build.0 = Debug|x64 - {15DABC90-234A-4B6B-9EEB-777C4768B82B}.RD|Win32.ActiveCfg = RD|Win32 - {15DABC90-234A-4B6B-9EEB-777C4768B82B}.RD|Win32.Build.0 = RD|Win32 - {15DABC90-234A-4B6B-9EEB-777C4768B82B}.RD|x64.ActiveCfg = RD|x64 - {15DABC90-234A-4B6B-9EEB-777C4768B82B}.RD|x64.Build.0 = RD|x64 - {8F202F43-106D-4F63-AD9D-B1D43E803E8C}.Debug|Win32.ActiveCfg = RD|Win32 - {8F202F43-106D-4F63-AD9D-B1D43E803E8C}.Debug|Win32.Build.0 = RD|Win32 - {8F202F43-106D-4F63-AD9D-B1D43E803E8C}.Debug|x64.ActiveCfg = Debug|x64 - {8F202F43-106D-4F63-AD9D-B1D43E803E8C}.Debug|x64.Build.0 = Debug|x64 - {8F202F43-106D-4F63-AD9D-B1D43E803E8C}.RD|Win32.ActiveCfg = RD|Win32 - {8F202F43-106D-4F63-AD9D-B1D43E803E8C}.RD|Win32.Build.0 = RD|Win32 - {8F202F43-106D-4F63-AD9D-B1D43E803E8C}.RD|x64.ActiveCfg = RD|x64 - {8F202F43-106D-4F63-AD9D-B1D43E803E8C}.RD|x64.Build.0 = RD|x64 - {276C3DB0-7A6B-4417-8E5C-322B08633AAC}.Debug|Win32.ActiveCfg = RD|Win32 - {276C3DB0-7A6B-4417-8E5C-322B08633AAC}.Debug|Win32.Build.0 = RD|Win32 - {276C3DB0-7A6B-4417-8E5C-322B08633AAC}.Debug|x64.ActiveCfg = Debug|x64 - {276C3DB0-7A6B-4417-8E5C-322B08633AAC}.Debug|x64.Build.0 = Debug|x64 - {276C3DB0-7A6B-4417-8E5C-322B08633AAC}.RD|Win32.ActiveCfg = RD|Win32 - {276C3DB0-7A6B-4417-8E5C-322B08633AAC}.RD|Win32.Build.0 = RD|Win32 - {276C3DB0-7A6B-4417-8E5C-322B08633AAC}.RD|x64.ActiveCfg = RD|x64 - {276C3DB0-7A6B-4417-8E5C-322B08633AAC}.RD|x64.Build.0 = RD|x64 - {C0300513-E845-43B4-9A4F-E8817EAEF57C}.Debug|Win32.ActiveCfg = RD|Win32 - {C0300513-E845-43B4-9A4F-E8817EAEF57C}.Debug|Win32.Build.0 = RD|Win32 - {C0300513-E845-43B4-9A4F-E8817EAEF57C}.Debug|x64.ActiveCfg = Debug|x64 - {C0300513-E845-43B4-9A4F-E8817EAEF57C}.Debug|x64.Build.0 = Debug|x64 - {C0300513-E845-43B4-9A4F-E8817EAEF57C}.RD|Win32.ActiveCfg = RD|Win32 - {C0300513-E845-43B4-9A4F-E8817EAEF57C}.RD|Win32.Build.0 = RD|Win32 - {C0300513-E845-43B4-9A4F-E8817EAEF57C}.RD|x64.ActiveCfg = RD|x64 - {C0300513-E845-43B4-9A4F-E8817EAEF57C}.RD|x64.Build.0 = RD|x64 - {C41C4EB6-6F74-4F37-9FB0-9FA6BF377837}.Debug|Win32.ActiveCfg = RD|Win32 - {C41C4EB6-6F74-4F37-9FB0-9FA6BF377837}.Debug|Win32.Build.0 = RD|Win32 - {C41C4EB6-6F74-4F37-9FB0-9FA6BF377837}.Debug|x64.ActiveCfg = Debug|x64 - {C41C4EB6-6F74-4F37-9FB0-9FA6BF377837}.Debug|x64.Build.0 = Debug|x64 - {C41C4EB6-6F74-4F37-9FB0-9FA6BF377837}.RD|Win32.ActiveCfg = RD|Win32 - {C41C4EB6-6F74-4F37-9FB0-9FA6BF377837}.RD|Win32.Build.0 = RD|Win32 - {C41C4EB6-6F74-4F37-9FB0-9FA6BF377837}.RD|x64.ActiveCfg = RD|x64 - {C41C4EB6-6F74-4F37-9FB0-9FA6BF377837}.RD|x64.Build.0 = RD|x64 - {BA25F3F0-EB87-4164-AAB9-073C50A3557A}.Debug|Win32.ActiveCfg = RD|Win32 - {BA25F3F0-EB87-4164-AAB9-073C50A3557A}.Debug|Win32.Build.0 = RD|Win32 - {BA25F3F0-EB87-4164-AAB9-073C50A3557A}.Debug|x64.ActiveCfg = Debug|x64 - {BA25F3F0-EB87-4164-AAB9-073C50A3557A}.Debug|x64.Build.0 = Debug|x64 - {BA25F3F0-EB87-4164-AAB9-073C50A3557A}.RD|Win32.ActiveCfg = RD|Win32 - {BA25F3F0-EB87-4164-AAB9-073C50A3557A}.RD|Win32.Build.0 = RD|Win32 - {BA25F3F0-EB87-4164-AAB9-073C50A3557A}.RD|x64.ActiveCfg = RD|x64 - {BA25F3F0-EB87-4164-AAB9-073C50A3557A}.RD|x64.Build.0 = RD|x64 - {AA3CC588-9D08-4178-A1E8-C71561E99723}.Debug|Win32.ActiveCfg = RD|Win32 - {AA3CC588-9D08-4178-A1E8-C71561E99723}.Debug|Win32.Build.0 = RD|Win32 - {AA3CC588-9D08-4178-A1E8-C71561E99723}.Debug|x64.ActiveCfg = Debug|x64 - {AA3CC588-9D08-4178-A1E8-C71561E99723}.Debug|x64.Build.0 = Debug|x64 - {AA3CC588-9D08-4178-A1E8-C71561E99723}.RD|Win32.ActiveCfg = RD|Win32 - {AA3CC588-9D08-4178-A1E8-C71561E99723}.RD|Win32.Build.0 = RD|Win32 - {AA3CC588-9D08-4178-A1E8-C71561E99723}.RD|x64.ActiveCfg = RD|x64 - {AA3CC588-9D08-4178-A1E8-C71561E99723}.RD|x64.Build.0 = RD|x64 - {5B6946C8-A24F-4223-8415-5E16A238ACED}.Debug|Win32.ActiveCfg = RD|Win32 - {5B6946C8-A24F-4223-8415-5E16A238ACED}.Debug|Win32.Build.0 = RD|Win32 - {5B6946C8-A24F-4223-8415-5E16A238ACED}.Debug|x64.ActiveCfg = Debug|Win32 - {5B6946C8-A24F-4223-8415-5E16A238ACED}.RD|Win32.ActiveCfg = RD|Win32 - {5B6946C8-A24F-4223-8415-5E16A238ACED}.RD|Win32.Build.0 = RD|Win32 - {5B6946C8-A24F-4223-8415-5E16A238ACED}.RD|x64.ActiveCfg = RD|Win32 - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection -EndGlobal diff --git a/VCMI_global.props b/VCMI_global.props deleted file mode 100644 index 0411f080b..000000000 --- a/VCMI_global.props +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - - - <_PropertySheetDisplayName>VCMI_global - $(SolutionDir)\deps\libs;$(ProjectDir);$(LibraryPath) - $(SolutionDir)\deps\include;$(SolutionDir)\include;$(ProjectDir);$(IncludePath) - $(VCMI_Out)\ - - - - - Console - - - 4275 - - - - \ No newline at end of file diff --git a/VCMI_global_debug.props b/VCMI_global_debug.props deleted file mode 100644 index 1c498039f..000000000 --- a/VCMI_global_debug.props +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - MultiThreadedDebugDLL - - - true - - - \ No newline at end of file diff --git a/VCMI_global_release.props b/VCMI_global_release.props deleted file mode 100644 index de329b5a8..000000000 --- a/VCMI_global_release.props +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - - MaxSpeed - - - - - true - - - - - Speed - - - - - true - MultiThreadedDLL - - - true - - - \ No newline at end of file diff --git a/VCMI_global_user.props b/VCMI_global_user.props deleted file mode 100644 index 33f02d813..000000000 --- a/VCMI_global_user.props +++ /dev/null @@ -1,19 +0,0 @@ - - - - - D:\Program files co nie\Qt\5.9.1\msvc2015\ - D:\VCMI\vcmi_devversion - - - - - - $(QTDIR) - - - $(VCMI_Out) - true - - - \ No newline at end of file diff --git a/android/AndroidManifest.xml b/android/AndroidManifest.xml index c3b1d13da..cf5f041d0 100644 --- a/android/AndroidManifest.xml +++ b/android/AndroidManifest.xml @@ -76,7 +76,7 @@ diff --git a/android/vcmi-app/build.gradle b/android/vcmi-app/build.gradle index ae5907dc8..c8518ab92 100644 --- a/android/vcmi-app/build.gradle +++ b/android/vcmi-app/build.gradle @@ -26,8 +26,8 @@ android { minSdk = qtMinSdkVersion as Integer targetSdk = qtTargetSdkVersion as Integer // ANDROID_TARGET_SDK_VERSION in the CMake project - versionCode 1572 - versionName "1.5.7" + versionCode 1610 + versionName "1.6.0" setProperty("archivesBaseName", "vcmi") } diff --git a/android/vcmi-app/src/main/java/org/libsdl/app/SDLActivity.java b/android/vcmi-app/src/main/java/org/libsdl/app/SDLActivity.java index 9bc864fc2..649fb1e74 100644 --- a/android/vcmi-app/src/main/java/org/libsdl/app/SDLActivity.java +++ b/android/vcmi-app/src/main/java/org/libsdl/app/SDLActivity.java @@ -89,7 +89,7 @@ public class SDLActivity extends Activity implements View.OnSystemUiVisibilityCh | InputDevice.SOURCE_CLASS_POSITION | InputDevice.SOURCE_CLASS_TRACKBALL); - if (s2 != 0) cls += "Some_Unkown"; + if (s2 != 0) cls += "Some_Unknown"; s2 = s_copy & InputDevice.SOURCE_ANY; // keep source only, no class; @@ -163,7 +163,7 @@ public class SDLActivity extends Activity implements View.OnSystemUiVisibilityCh if (s == FLAG_TAINTED) src += " FLAG_TAINTED"; s2 &= ~FLAG_TAINTED; - if (s2 != 0) src += " Some_Unkown"; + if (s2 != 0) src += " Some_Unknown"; Log.v(TAG, prefix + "int=" + s_copy + " CLASS={" + cls + " } source(s):" + src); } diff --git a/client/ArtifactsUIController.cpp b/client/ArtifactsUIController.cpp new file mode 100644 index 000000000..7f018c208 --- /dev/null +++ b/client/ArtifactsUIController.cpp @@ -0,0 +1,170 @@ +/* + * ArtifactsUIController.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 "ArtifactsUIController.h" +#include "CGameInfo.h" +#include "CPlayerInterface.h" + +#include "../CCallback.h" +#include "../lib/ArtifactUtils.h" +#include "../lib/texts/CGeneralTextHandler.h" +#include "../lib/mapObjects/CGHeroInstance.h" + +#include "gui/CGuiHandler.h" +#include "gui/WindowHandler.h" +#include "widgets/CComponent.h" +#include "windows/CWindowWithArtifacts.h" + +ArtifactsUIController::ArtifactsUIController() +{ + numOfMovedArts = 0; + numOfArtsAskAssembleSession = 0; +} + +bool ArtifactsUIController::askToAssemble(const ArtifactLocation & al, const bool onlyEquipped, const bool checkIgnored) +{ + if(auto hero = LOCPLINT->cb->getHero(al.artHolder)) + { + if(hero->getArt(al.slot) == nullptr) + { + logGlobal->error("artifact location %d points to nothing", al.slot.num); + return false; + } + return askToAssemble(hero, al.slot, onlyEquipped, checkIgnored); + } + return false; +} + +bool ArtifactsUIController::askToAssemble(const CGHeroInstance * hero, const ArtifactPosition & slot, + const bool onlyEquipped, const bool checkIgnored) +{ + assert(hero); + const auto art = hero->getArt(slot); + assert(art); + + if(hero->tempOwner != LOCPLINT->playerID) + return false; + + if(numOfArtsAskAssembleSession != 0) + numOfArtsAskAssembleSession--; + auto assemblyPossibilities = ArtifactUtils::assemblyPossibilities(hero, art->getTypeId(), onlyEquipped); + if(!assemblyPossibilities.empty()) + { + auto askThread = new boost::thread([this, hero, art, slot, assemblyPossibilities, checkIgnored]() -> void + { + boost::mutex::scoped_lock askLock(askAssembleArtifactMutex); + for(const auto combinedArt : assemblyPossibilities) + { + boost::mutex::scoped_lock interfaceLock(GH.interfaceMutex); + if(checkIgnored) + { + if(vstd::contains(ignoredArtifacts, combinedArt->getId())) + continue; + ignoredArtifacts.emplace(combinedArt->getId()); + } + + bool assembleConfirmed = false; + MetaString message = MetaString::createFromTextID(art->getType()->getDescriptionTextID()); + message.appendEOL(); + message.appendEOL(); + 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]() + { + assembleConfirmed = true; + LOCPLINT->cb.get()->assembleArtifacts(hero->id, slot, true, combinedArt->getId()); + }, nullptr, {std::make_shared(ComponentType::ARTIFACT, combinedArt->getId())}); + + LOCPLINT->waitWhileDialog(); + if(assembleConfirmed) + break; + } + }); + askThread->detach(); + return true; + } + return false; +} + +bool ArtifactsUIController::askToDisassemble(const CGHeroInstance * hero, const ArtifactPosition & slot) +{ + assert(hero); + const auto art = hero->getArt(slot); + assert(art); + + if(hero->tempOwner != LOCPLINT->playerID) + return false; + + if(art->hasParts()) + { + if(ArtifactUtils::isSlotBackpack(slot) && !ArtifactUtils::isBackpackFreeSlots(hero, art->getType()->getConstituents().size() - 1)) + return false; + + MetaString message = MetaString::createFromTextID(art->getType()->getDescriptionTextID()); + message.appendEOL(); + message.appendEOL(); + message.appendRawString(CGI->generaltexth->allTexts[733]); // Do you wish to disassemble this artifact? + LOCPLINT->showYesNoDialog(message.toString(), [hero, slot]() + { + LOCPLINT->cb->assembleArtifacts(hero->id, slot, false, ArtifactID()); + }, nullptr); + return true; + } + return false; +} + +void ArtifactsUIController::artifactRemoved() +{ + for(const auto & artWin : GH.windows().findWindows()) + artWin->update(); + LOCPLINT->waitWhileDialog(); +} + +void ArtifactsUIController::artifactMoved() +{ + // If a bulk transfer has arrived, then redrawing only the last art movement. + if(numOfMovedArts != 0) + numOfMovedArts--; + + if(numOfMovedArts == 0) + for(const auto & artWin : GH.windows().findWindows()) + { + artWin->update(); + } + LOCPLINT->waitWhileDialog(); +} + +void ArtifactsUIController::bulkArtMovementStart(size_t totalNumOfArts, size_t possibleAssemblyNumOfArts) +{ + assert(totalNumOfArts >= possibleAssemblyNumOfArts); + numOfMovedArts = totalNumOfArts; + if(numOfArtsAskAssembleSession == 0) + { + // Do not start the next session until the previous one is finished + numOfArtsAskAssembleSession = possibleAssemblyNumOfArts; + ignoredArtifacts.clear(); + } +} + +void ArtifactsUIController::artifactAssembled() +{ + for(const auto & artWin : GH.windows().findWindows()) + artWin->update(); +} + +void ArtifactsUIController::artifactDisassembled() +{ + for(const auto & artWin : GH.windows().findWindows()) + artWin->update(); +} diff --git a/client/ArtifactsUIController.h b/client/ArtifactsUIController.h new file mode 100644 index 000000000..94f209399 --- /dev/null +++ b/client/ArtifactsUIController.h @@ -0,0 +1,42 @@ +/* + * ArtifactsUIController.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 "../lib/constants/EntityIdentifiers.h" +#include "../lib/networkPacks/ArtifactLocation.h" + +VCMI_LIB_NAMESPACE_BEGIN + +class CGHeroInstance; + +VCMI_LIB_NAMESPACE_END + +class ArtifactsUIController +{ + size_t numOfMovedArts; + size_t numOfArtsAskAssembleSession; + std::set ignoredArtifacts; + + boost::mutex askAssembleArtifactMutex; + +public: + ArtifactsUIController(); + bool askToAssemble(const ArtifactLocation & al, const bool onlyEquipped = false, const bool checkIgnored = false); + bool askToAssemble(const CGHeroInstance * hero, const ArtifactPosition & slot, const bool onlyEquipped = false, + const bool checkIgnored = false); + bool askToDisassemble(const CGHeroInstance * hero, const ArtifactPosition & slot); + + void artifactRemoved(); + void artifactMoved(); + void bulkArtMovementStart(size_t totalNumOfArts, size_t possibleAssemblyNumOfArts); + void artifactAssembled(); + void artifactDisassembled(); +}; + \ No newline at end of file diff --git a/client/CGameInfo.cpp b/client/CGameInfo.cpp index 3d025118f..dd7f66436 100644 --- a/client/CGameInfo.cpp +++ b/client/CGameInfo.cpp @@ -92,14 +92,9 @@ const ObstacleService * CGameInfo::obstacles() const return globalServices->obstacles(); } -const IGameSettings * CGameInfo::settings() const +const IGameSettings * CGameInfo::engineSettings() const { - return globalServices->settings(); -} - -void CGameInfo::updateEntity(Metatype metatype, int32_t index, const JsonNode & data) -{ - logGlobal->error("CGameInfo::updateEntity call is not expected."); + return globalServices->engineSettings(); } spells::effects::Registry * CGameInfo::spellEffects() diff --git a/client/CGameInfo.h b/client/CGameInfo.h index c07bb3c7d..4f6d6592b 100644 --- a/client/CGameInfo.h +++ b/client/CGameInfo.h @@ -20,7 +20,6 @@ class CHeroHandler; class CCreatureHandler; class CSpellHandler; class CSkillHandler; -class CBuildingHandler; class CObjectHandler; class CObjectClassesHandler; class CTownHandler; @@ -36,26 +35,26 @@ class CMap; VCMI_LIB_NAMESPACE_END class CMapHandler; -class CSoundHandler; -class CMusicHandler; +class ISoundPlayer; +class IMusicPlayer; class CursorHandler; -class IMainVideoPlayer; +class IVideoPlayer; class CServerHandler; //a class for non-mechanical client GUI classes class CClientState { public: - CSoundHandler * soundh; - CMusicHandler * musich; + ISoundPlayer * soundh; + IMusicPlayer * musich; CConsoleHandler * consoleh; CursorHandler * curh; - IMainVideoPlayer * videoh; + IVideoPlayer * videoh; }; extern CClientState * CCS; /// CGameInfo class -/// for allowing different functions for accessing game informations +/// for allowing different functions for accessing game information class CGameInfo final : public Services { public: @@ -71,9 +70,7 @@ public: const SkillService * skills() const override; const BattleFieldService * battlefields() const override; const ObstacleService * obstacles() const override; - const IGameSettings * settings() const override; - - void updateEntity(Metatype metatype, int32_t index, const JsonNode & data) override; + const IGameSettings * engineSettings() const override; const spells::effects::Registry * spellEffects() const override; spells::effects::Registry * spellEffects() override; diff --git a/client/CMT.h b/client/CMT.h index 28d877875..abd6596f5 100644 --- a/client/CMT.h +++ b/client/CMT.h @@ -20,8 +20,8 @@ extern SDL_Surface *screen; // main screen surface extern SDL_Surface *screen2; // and hlp surface (used to store not-active interfaces layer) extern SDL_Surface *screenBuf; // points to screen (if only advmapint is present) or screen2 (else) - should be used when updating controls which are not regularly redrawed -void handleQuit(bool ask = true); - -/// Notify user about encoutered fatal error and terminate the game +/// Notify user about encountered fatal error and terminate the game +/// Defined in clientapp EntryPoint /// TODO: decide on better location for this method [[noreturn]] void handleFatalError(const std::string & message, bool terminate); +void handleQuit(bool ask = true); diff --git a/client/CMakeLists.txt b/client/CMakeLists.txt index 93d5ab564..e44f6ec5e 100644 --- a/client/CMakeLists.txt +++ b/client/CMakeLists.txt @@ -1,4 +1,4 @@ -set(client_SRCS +set(vcmiclientcommon_SRCS StdInc.cpp ../CCallback.cpp @@ -27,6 +27,7 @@ set(client_SRCS battle/BattleStacksController.cpp battle/BattleWindow.cpp battle/CreatureAnimation.cpp + battle/BattleOverlayLogVisualizer.cpp eventsSDL/NotificationHandler.cpp eventsSDL/InputHandler.cpp @@ -64,6 +65,7 @@ set(client_SRCS mainmenu/CPrologEpilogVideo.cpp mainmenu/CreditsScreen.cpp mainmenu/CHighScoreScreen.cpp + mainmenu/CStatisticScreen.cpp mapView/MapRenderer.cpp mapView/MapRendererContext.cpp @@ -74,7 +76,14 @@ set(client_SRCS mapView/MapViewController.cpp mapView/MapViewModel.cpp mapView/mapHandler.cpp + mapView/MapOverlayLogVisualizer.cpp + media/CAudioBase.cpp + media/CMusicHandler.cpp + media/CSoundHandler.cpp + media/CVideoHandler.cpp + + render/AssetGenerator.cpp render/CAnimation.cpp render/CBitmapHandler.cpp render/CDefFile.cpp @@ -83,12 +92,14 @@ set(client_SRCS render/Colors.cpp render/Graphics.cpp render/IFont.cpp + render/ImageLocator.cpp renderSDL/CBitmapFont.cpp - renderSDL/CBitmapHanFont.cpp renderSDL/CTrueTypeFont.cpp renderSDL/CursorHardware.cpp renderSDL/CursorSoftware.cpp + renderSDL/FontChain.cpp + renderSDL/ImageScaled.cpp renderSDL/RenderHandler.cpp renderSDL/SDLImage.cpp renderSDL/SDLImageLoader.cpp @@ -105,8 +116,8 @@ set(client_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 @@ -126,6 +137,7 @@ set(client_SRCS widgets/CArtifactsOfHeroMarket.cpp widgets/CArtifactsOfHeroBackpack.cpp widgets/RadialMenu.cpp + widgets/VideoWidget.cpp widgets/markets/CAltarArtifacts.cpp widgets/markets/CAltarCreatures.cpp widgets/markets/CArtifactsBuying.cpp @@ -162,13 +174,13 @@ set(client_SRCS windows/settings/BattleOptionsTab.cpp windows/settings/AdventureOptionsTab.cpp + xBRZ/xbrz.cpp + + ArtifactsUIController.cpp CGameInfo.cpp - CMT.cpp - CMusicHandler.cpp CPlayerInterface.cpp PlayerLocalState.cpp CServerHandler.cpp - CVideoHandler.cpp Client.cpp ClientCommandManager.cpp GameChatHandler.cpp @@ -178,7 +190,7 @@ set(client_SRCS ServerRunner.cpp ) -set(client_HEADERS +set(vcmiclientcommon_HEADERS StdInc.h adventureMap/AdventureMapInterface.h @@ -208,6 +220,7 @@ set(client_HEADERS battle/BattleStacksController.h battle/BattleWindow.h battle/CreatureAnimation.h + battle/BattleOverlayLogVisualizer.h eventsSDL/NotificationHandler.h eventsSDL/InputHandler.h @@ -248,6 +261,7 @@ set(client_HEADERS mainmenu/CPrologEpilogVideo.h mainmenu/CreditsScreen.h mainmenu/CHighScoreScreen.h + mainmenu/CStatisticScreen.h mapView/IMapRendererContext.h mapView/IMapRendererObserver.h @@ -260,7 +274,18 @@ set(client_HEADERS mapView/MapViewController.h mapView/MapViewModel.h mapView/mapHandler.h + mapView/MapOverlayLogVisualizer.h + media/CAudioBase.h + media/CEmptyVideoPlayer.h + media/CMusicHandler.h + media/CSoundHandler.h + media/CVideoHandler.h + media/IMusicPlayer.h + media/ISoundPlayer.h + media/IVideoPlayer.h + + render/AssetGenerator.h render/CAnimation.h render/CBitmapHandler.h render/CDefFile.h @@ -273,14 +298,16 @@ set(client_HEADERS render/IFont.h render/IImage.h render/IImageLoader.h + render/ImageLocator.h render/IRenderHandler.h render/IScreenHandler.h renderSDL/CBitmapFont.h - renderSDL/CBitmapHanFont.h renderSDL/CTrueTypeFont.h renderSDL/CursorHardware.h renderSDL/CursorSoftware.h + renderSDL/FontChain.h + renderSDL/ImageScaled.h renderSDL/RenderHandler.h renderSDL/SDLImage.h renderSDL/SDLImageLoader.h @@ -300,8 +327,8 @@ set(client_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 @@ -320,7 +347,9 @@ set(client_HEADERS widgets/CArtifactsOfHeroAltar.h widgets/CArtifactsOfHeroMarket.h widgets/CArtifactsOfHeroBackpack.h + widgets/IVideoHolder.h widgets/RadialMenu.h + widgets/VideoWidget.h widgets/markets/CAltarArtifacts.h widgets/markets/CAltarCreatures.h widgets/markets/CArtifactsBuying.h @@ -357,13 +386,15 @@ set(client_HEADERS windows/settings/BattleOptionsTab.h windows/settings/AdventureOptionsTab.h + xBRZ/xbrz.h + xBRZ/xbrz_tools.h + + ArtifactsUIController.h CGameInfo.h CMT.h - CMusicHandler.h CPlayerInterface.h PlayerLocalState.h CServerHandler.h - CVideoHandler.h Client.h ClientCommandManager.h ClientNetPackVisitors.h @@ -376,76 +407,50 @@ set(client_HEADERS ) if(APPLE_IOS) - set(client_SRCS ${client_SRCS} - CFocusableHelper.cpp - ios/GameChatKeyboardHandler.m - ios/main.m - ios/startSDL.mm + set(vcmiclientcommon_SRCS ${vcmiclientcommon_SRCS} ios/utils.mm ) - set(client_HEADERS ${client_HEADERS} - CFocusableHelper.h - ios/GameChatKeyboardHandler.h - ios/startSDL.h + set(vcmiclientcommon_HEADERS ${vcmiclientcommon_HEADERS} ios/utils.h ) endif() -assign_source_group(${client_SRCS} ${client_HEADERS} VCMI_client.rc) - -if(ANDROID) - add_library(vcmiclient SHARED ${client_SRCS} ${client_HEADERS}) - set_target_properties(vcmiclient PROPERTIES - OUTPUT_NAME "vcmiclient_${ANDROID_ABI}" # required by Qt - ) -else() - add_executable(vcmiclient ${client_SRCS} ${client_HEADERS}) -endif() +assign_source_group(${vcmiclientcommon_SRCS} ${vcmiclientcommon_HEADERS}) +add_library(vcmiclientcommon STATIC ${vcmiclientcommon_SRCS} ${vcmiclientcommon_HEADERS}) if(NOT ENABLE_STATIC_LIBS) - add_dependencies(vcmiclient + add_dependencies(vcmiclientcommon BattleAI EmptyAI StupidAI VCAI ) if(ENABLE_NULLKILLER_AI) - add_dependencies(vcmiclient Nullkiller) + add_dependencies(vcmiclientcommon Nullkiller) endif() endif() if(APPLE_IOS) if(ENABLE_ERM) - add_dependencies(vcmiclient vcmiERM) + add_dependencies(vcmiclientcommon vcmiERM) endif() if(ENABLE_LUA) - add_dependencies(vcmiclient vcmiLua) + add_dependencies(vcmiclientcommon vcmiLua) endif() endif() if(WIN32) - target_sources(vcmiclient PRIVATE "VCMI_client.rc") - set_target_properties(vcmiclient + set_target_properties(vcmiclientcommon PROPERTIES - OUTPUT_NAME "VCMI_client" - PROJECT_LABEL "VCMI_client" + OUTPUT_NAME "VCMI_vcmiclientcommon" + PROJECT_LABEL "VCMI_vcmiclientcommon" ) - set_property(DIRECTORY ${CMAKE_SOURCE_DIR} PROPERTY VS_STARTUP_PROJECT vcmiclient) + set_property(DIRECTORY ${CMAKE_SOURCE_DIR} PROPERTY VS_STARTUP_PROJECT vcmiclientcommon) if(NOT ENABLE_DEBUG_CONSOLE) - set_target_properties(vcmiclient PROPERTIES WIN32_EXECUTABLE) - target_link_libraries(vcmiclient SDL2::SDL2main) - endif() - target_compile_definitions(vcmiclient PRIVATE WINDOWS_IGNORE_PACKING_MISMATCH) - -# TODO: very hacky, find proper solution to copy AI dlls into bin dir - if(MSVC) - add_custom_command(TARGET vcmiclient POST_BUILD - WORKING_DIRECTORY "$" - COMMAND ${CMAKE_COMMAND} -E copy AI/fuzzylite.dll fuzzylite.dll - COMMAND ${CMAKE_COMMAND} -E copy AI/tbb12.dll tbb12.dll - ) + target_link_libraries(vcmiclientcommon SDL2::SDL2main) endif() + target_compile_definitions(vcmiclientcommon PRIVATE WINDOWS_IGNORE_PACKING_MISMATCH) elseif(APPLE_IOS) - target_link_libraries(vcmiclient PRIVATE + target_link_libraries(vcmiclientcommon PRIVATE iOS_utils # FFmpeg @@ -457,101 +462,31 @@ elseif(APPLE_IOS) "-framework CoreMedia" "-framework VideoToolbox" ) - - set_target_properties(vcmiclient PROPERTIES - MACOSX_BUNDLE_INFO_PLIST "${CMAKE_CURRENT_LIST_DIR}/ios/Info.plist" - XCODE_ATTRIBUTE_LD_RUNPATH_SEARCH_PATHS "@executable_path/Frameworks" - XCODE_ATTRIBUTE_CODE_SIGNING_ALLOWED "$(CODE_SIGNING_ALLOWED_FOR_APPS)" - XCODE_ATTRIBUTE_ASSETCATALOG_COMPILER_APPICON_NAME AppIcon - ) - - foreach(XCODE_RESOURCE LaunchScreen.storyboard Images.xcassets Settings.bundle vcmi_logo.png) - set(XCODE_RESOURCE_PATH ios/${XCODE_RESOURCE}) - target_sources(vcmiclient PRIVATE ${XCODE_RESOURCE_PATH}) - set_source_files_properties(${XCODE_RESOURCE_PATH} PROPERTIES MACOSX_PACKAGE_LOCATION Resources) - - # workaround to prevent CMAKE_SKIP_PRECOMPILE_HEADERS being added as compile flag - if(CMAKE_VERSION VERSION_GREATER_EQUAL "3.22.0" AND CMAKE_VERSION VERSION_LESS "3.25.0") - set_source_files_properties(${XCODE_RESOURCE_PATH} PROPERTIES LANGUAGE CXX) - endif() - endforeach() - - set(CMAKE_EXE_LINKER_FLAGS "-Wl,-e,_client_main") endif() -target_link_libraries(vcmiclient PRIVATE vcmiservercommon) -if(ENABLE_SINGLE_APP_BUILD AND ENABLE_LAUNCHER) - target_link_libraries(vcmiclient PRIVATE vcmilauncher) -endif() +target_link_libraries(vcmiclientcommon PRIVATE vcmiservercommon) -target_link_libraries(vcmiclient PRIVATE +target_link_libraries(vcmiclientcommon PUBLIC vcmi SDL2::SDL2 SDL2::Image SDL2::Mixer SDL2::TTF ) if(ffmpeg_LIBRARIES) - target_link_libraries(vcmiclient PRIVATE + target_link_libraries(vcmiclientcommon PRIVATE ${ffmpeg_LIBRARIES} ) else() - target_compile_definitions(vcmiclient PRIVATE DISABLE_VIDEO) + target_compile_definitions(vcmiclientcommon PRIVATE DISABLE_VIDEO) endif() -target_include_directories(vcmiclient PUBLIC +target_include_directories(vcmiclientcommon PUBLIC ${CMAKE_CURRENT_SOURCE_DIR} ) if (ffmpeg_INCLUDE_DIRS) - target_include_directories(vcmiclient PRIVATE + target_include_directories(vcmiclientcommon PRIVATE ${ffmpeg_INCLUDE_DIRS} ) endif() -vcmi_set_output_dir(vcmiclient "") -enable_pch(vcmiclient) - -if(APPLE_IOS) - vcmi_install_conan_deps("\${CMAKE_INSTALL_PREFIX}") - add_custom_command(TARGET vcmiclient POST_BUILD - COMMAND ios/set_build_version.sh "$" - COMMAND ${CMAKE_COMMAND} --install "${CMAKE_BINARY_DIR}" --component "${CMAKE_INSTALL_DEFAULT_COMPONENT_NAME}" --config "$" --prefix "$" - COMMAND ios/rpath_remove_symlinks.sh - COMMAND ios/codesign.sh - WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} - ) - install(TARGETS vcmiclient DESTINATION Payload COMPONENT app) # for ipa generation with cpack -elseif(ANDROID) - find_program(androidDeployQt androiddeployqt - PATHS "${qtBinDir}" - ) - vcmi_install_conan_deps("\${CMAKE_INSTALL_PREFIX}/${LIB_DIR}") - - add_custom_target(android_deploy ALL - COMMAND ${CMAKE_COMMAND} --install "${CMAKE_BINARY_DIR}" --config "$" --prefix "${androidQtBuildDir}" - COMMAND "${androidDeployQt}" --input "${CMAKE_BINARY_DIR}/androiddeployqt.json" --output "${androidQtBuildDir}" --android-platform "android-${ANDROID_TARGET_SDK_VERSION}" --verbose $<$>:--release> ${ANDROIDDEPLOYQT_OPTIONS} - COMMAND_EXPAND_LISTS - VERBATIM - COMMENT "Create android package" - ) - add_dependencies(android_deploy vcmiclient) -else() - install(TARGETS vcmiclient DESTINATION ${BIN_DIR}) -endif() - -#install icons and desktop file on Linux -if(NOT WIN32 AND NOT APPLE AND NOT ANDROID) - #FIXME: move to client makefile? - foreach(iconSize 16 22 32 48 64 128 256 512 1024 2048) - install(FILES "icons/vcmiclient.${iconSize}x${iconSize}.png" - DESTINATION "share/icons/hicolor/${iconSize}x${iconSize}/apps" - RENAME vcmiclient.png - ) - endforeach() - - install(FILES icons/vcmiclient.svg - DESTINATION share/icons/hicolor/scalable/apps - RENAME vcmiclient.svg - ) - install(FILES icons/vcmiclient.desktop - DESTINATION share/applications - ) -endif() +vcmi_set_output_dir(vcmiclientcommon "") +enable_pch(vcmiclientcommon) diff --git a/client/CMusicHandler.cpp b/client/CMusicHandler.cpp deleted file mode 100644 index 00edf149c..000000000 --- a/client/CMusicHandler.cpp +++ /dev/null @@ -1,753 +0,0 @@ -/* - * CMusicHandler.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 "CMusicHandler.h" -#include "CGameInfo.h" -#include "renderSDL/SDLRWwrapper.h" -#include "eventsSDL/InputHandler.h" -#include "gui/CGuiHandler.h" - -#include "../lib/GameConstants.h" -#include "../lib/filesystem/Filesystem.h" -#include "../lib/constants/StringConstants.h" -#include "../lib/CRandomGenerator.h" -#include "../lib/VCMIDirs.h" -#include "../lib/TerrainHandler.h" - - -#define VCMI_SOUND_NAME(x) -#define VCMI_SOUND_FILE(y) #y, - -// sounds mapped to soundBase enum -static const std::string sounds[] = { - "", // invalid - "", // todo - VCMI_SOUND_LIST -}; -#undef VCMI_SOUND_NAME -#undef VCMI_SOUND_FILE - -void CAudioBase::init() -{ - if (initialized) - return; - - if (Mix_OpenAudio(44100, MIX_DEFAULT_FORMAT, 2, 1024)==-1) - { - logGlobal->error("Mix_OpenAudio error: %s", Mix_GetError()); - return; - } - - initialized = true; -} - -void CAudioBase::release() -{ - if(!(CCS->soundh->initialized && CCS->musich->initialized)) - Mix_CloseAudio(); - - initialized = false; -} - -void CAudioBase::setVolume(ui32 percent) -{ - if (percent > 100) - percent = 100; - - volume = percent; -} - -void CSoundHandler::onVolumeChange(const JsonNode &volumeNode) -{ - setVolume((ui32)volumeNode.Float()); -} - -CSoundHandler::CSoundHandler(): - listener(settings.listen["general"]["sound"]), - ambientConfig(JsonPath::builtin("config/ambientSounds.json")) -{ - listener(std::bind(&CSoundHandler::onVolumeChange, this, _1)); - - battleIntroSounds = - { - soundBase::battle00, soundBase::battle01, - soundBase::battle02, soundBase::battle03, soundBase::battle04, - soundBase::battle05, soundBase::battle06, soundBase::battle07 - }; -} - -void CSoundHandler::init() -{ - CAudioBase::init(); - if(ambientConfig["allocateChannels"].isNumber()) - Mix_AllocateChannels((int)ambientConfig["allocateChannels"].Integer()); - - if (initialized) - { - Mix_ChannelFinished([](int channel) - { - CCS->soundh->soundFinishedCallback(channel); - }); - } -} - -void CSoundHandler::release() -{ - if (initialized) - { - Mix_HaltChannel(-1); - - for (auto &chunk : soundChunks) - { - if (chunk.second.first) - Mix_FreeChunk(chunk.second.first); - } - } - - CAudioBase::release(); -} - -// Allocate an SDL chunk and cache it. -Mix_Chunk *CSoundHandler::GetSoundChunk(const AudioPath & sound, bool cache) -{ - try - { - if (cache && soundChunks.find(sound) != soundChunks.end()) - return soundChunks[sound].first; - - auto data = CResourceHandler::get()->load(sound.addPrefix("SOUNDS/"))->readAll(); - SDL_RWops *ops = SDL_RWFromMem(data.first.get(), (int)data.second); - Mix_Chunk *chunk = Mix_LoadWAV_RW(ops, 1); // will free ops - - if (cache) - soundChunks.insert({sound, std::make_pair (chunk, std::move (data.first))}); - - return chunk; - } - catch(std::exception &e) - { - logGlobal->warn("Cannot get sound %s chunk: %s", sound.getOriginalName(), e.what()); - return nullptr; - } -} - -Mix_Chunk *CSoundHandler::GetSoundChunk(std::pair, si64> & data, bool cache) -{ - try - { - std::vector startBytes = std::vector(data.first.get(), data.first.get() + std::min((si64)100, data.second)); - - if (cache && soundChunksRaw.find(startBytes) != soundChunksRaw.end()) - return soundChunksRaw[startBytes].first; - - SDL_RWops *ops = SDL_RWFromMem(data.first.get(), (int)data.second); - Mix_Chunk *chunk = Mix_LoadWAV_RW(ops, 1); // will free ops - - if (cache) - soundChunksRaw.insert({startBytes, std::make_pair (chunk, std::move (data.first))}); - - return chunk; - } - catch(std::exception &e) - { - logGlobal->warn("Cannot get sound chunk: %s", e.what()); - return nullptr; - } -} - -int CSoundHandler::ambientDistToVolume(int distance) const -{ - const auto & distancesVector = ambientConfig["distances"].Vector(); - - if(distance >= distancesVector.size()) - return 0; - - int volume = static_cast(distancesVector[distance].Integer()); - return volume * (int)ambientConfig["volume"].Integer() / 100; -} - -void CSoundHandler::ambientStopSound(const AudioPath & soundId) -{ - stopSound(ambientChannels[soundId]); - setChannelVolume(ambientChannels[soundId], volume); -} - -uint32_t CSoundHandler::getSoundDurationMilliseconds(const AudioPath & sound) -{ - if (!initialized || sound.empty()) - return 0; - - auto resourcePath = sound.addPrefix("SOUNDS/"); - - if (!CResourceHandler::get()->existsResource(resourcePath)) - return 0; - - auto data = CResourceHandler::get()->load(resourcePath)->readAll(); - - SDL_AudioSpec spec; - uint32_t audioLen; - uint8_t *audioBuf; - uint32_t miliseconds = 0; - - if(SDL_LoadWAV_RW(SDL_RWFromMem(data.first.get(), (int)data.second), 1, &spec, &audioBuf, &audioLen) != nullptr) - { - SDL_FreeWAV(audioBuf); - uint32_t sampleSize = SDL_AUDIO_BITSIZE(spec.format) / 8; - uint32_t sampleCount = audioLen / sampleSize; - uint32_t sampleLen = sampleCount / spec.channels; - miliseconds = 1000 * sampleLen / spec.freq; - } - - return miliseconds ; -} - -// Plays a sound, and return its channel so we can fade it out later -int CSoundHandler::playSound(soundBase::soundID soundID, int repeats) -{ - assert(soundID < soundBase::sound_after_last); - auto sound = AudioPath::builtin(sounds[soundID]); - logGlobal->trace("Attempt to play sound %d with file name %s with cache", soundID, sound.getOriginalName()); - - return playSound(sound, repeats, true); -} - -int CSoundHandler::playSound(const AudioPath & sound, int repeats, bool cache) -{ - if (!initialized || sound.empty()) - return -1; - - int channel; - Mix_Chunk *chunk = GetSoundChunk(sound, cache); - - if (chunk) - { - channel = Mix_PlayChannel(-1, chunk, repeats); - if (channel == -1) - { - logGlobal->error("Unable to play sound file %s , error %s", sound.getOriginalName(), Mix_GetError()); - if (!cache) - Mix_FreeChunk(chunk); - } - else if (cache) - initCallback(channel); - else - initCallback(channel, [chunk](){ Mix_FreeChunk(chunk);}); - } - else - channel = -1; - - return channel; -} - -int CSoundHandler::playSound(std::pair, si64> & data, int repeats, bool cache) -{ - int channel = -1; - if (Mix_Chunk *chunk = GetSoundChunk(data, cache)) - { - channel = Mix_PlayChannel(-1, chunk, repeats); - if (channel == -1) - { - logGlobal->error("Unable to play sound, error %s", Mix_GetError()); - if (!cache) - Mix_FreeChunk(chunk); - } - else if (cache) - initCallback(channel); - else - initCallback(channel, [chunk](){ Mix_FreeChunk(chunk);}); - } - return channel; -} - -// Helper. Randomly select a sound from an array and play it -int CSoundHandler::playSoundFromSet(std::vector &sound_vec) -{ - return playSound(*RandomGeneratorUtil::nextItem(sound_vec, CRandomGenerator::getDefault())); -} - -void CSoundHandler::stopSound(int handler) -{ - if (initialized && handler != -1) - Mix_HaltChannel(handler); -} - -// Sets the sound volume, from 0 (mute) to 100 -void CSoundHandler::setVolume(ui32 percent) -{ - CAudioBase::setVolume(percent); - - if (initialized) - { - setChannelVolume(-1, volume); - - for (auto const & channel : channelVolumes) - updateChannelVolume(channel.first); - } -} - -void CSoundHandler::updateChannelVolume(int channel) -{ - if (channelVolumes.count(channel)) - setChannelVolume(channel, getVolume() * channelVolumes[channel] / 100); - else - setChannelVolume(channel, getVolume()); -} - -// Sets the sound volume, from 0 (mute) to 100 -void CSoundHandler::setChannelVolume(int channel, ui32 percent) -{ - Mix_Volume(channel, (MIX_MAX_VOLUME * percent)/100); -} - -void CSoundHandler::setCallback(int channel, std::function function) -{ - boost::mutex::scoped_lock lockGuard(mutexCallbacks); - - auto iter = callbacks.find(channel); - - //channel not found. It may have finished so fire callback now - if(iter == callbacks.end()) - function(); - else - iter->second.push_back(function); -} - -void CSoundHandler::resetCallback(int channel) -{ - boost::mutex::scoped_lock lockGuard(mutexCallbacks); - - callbacks.erase(channel); -} - -void CSoundHandler::soundFinishedCallback(int channel) -{ - boost::mutex::scoped_lock lockGuard(mutexCallbacks); - - if (callbacks.count(channel) == 0) - return; - - // store callbacks from container locally - SDL might reuse this channel for another sound - // but do actualy execution in separate thread, to avoid potential deadlocks in case if callback requires locks of its own - auto callback = callbacks.at(channel); - callbacks.erase(channel); - - if (!callback.empty()) - { - GH.dispatchMainThread([callback](){ - for (auto entry : callback) - entry(); - }); - } -} - -void CSoundHandler::initCallback(int channel) -{ - boost::mutex::scoped_lock lockGuard(mutexCallbacks); - assert(callbacks.count(channel) == 0); - callbacks[channel] = {}; -} - -void CSoundHandler::initCallback(int channel, const std::function & function) -{ - boost::mutex::scoped_lock lockGuard(mutexCallbacks); - assert(callbacks.count(channel) == 0); - callbacks[channel].push_back(function); -} - -int CSoundHandler::ambientGetRange() const -{ - return static_cast(ambientConfig["range"].Integer()); -} - -void CSoundHandler::ambientUpdateChannels(std::map soundsArg) -{ - boost::mutex::scoped_lock guard(mutex); - - std::vector stoppedSounds; - for(auto & pair : ambientChannels) - { - const auto & soundId = pair.first; - const int channel = pair.second; - - if(!vstd::contains(soundsArg, soundId)) - { - ambientStopSound(soundId); - stoppedSounds.push_back(soundId); - } - else - { - int volume = ambientDistToVolume(soundsArg[soundId]); - channelVolumes[channel] = volume; - updateChannelVolume(channel); - } - } - for(auto soundId : stoppedSounds) - { - channelVolumes.erase(ambientChannels[soundId]); - ambientChannels.erase(soundId); - } - - for(auto & pair : soundsArg) - { - const auto & soundId = pair.first; - const int distance = pair.second; - - if(!vstd::contains(ambientChannels, soundId)) - { - int channel = playSound(soundId, -1); - int volume = ambientDistToVolume(distance); - channelVolumes[channel] = volume; - - updateChannelVolume(channel); - ambientChannels[soundId] = channel; - } - } -} - -void CSoundHandler::ambientStopAllChannels() -{ - boost::mutex::scoped_lock guard(mutex); - - for(auto ch : ambientChannels) - { - ambientStopSound(ch.first); - } - channelVolumes.clear(); - ambientChannels.clear(); -} - -void CMusicHandler::onVolumeChange(const JsonNode &volumeNode) -{ - setVolume((ui32)volumeNode.Float()); -} - -CMusicHandler::CMusicHandler(): - listener(settings.listen["general"]["music"]) -{ - listener(std::bind(&CMusicHandler::onVolumeChange, this, _1)); - - auto mp3files = CResourceHandler::get()->getFilteredFiles([](const ResourcePath & id) -> bool - { - if(id.getType() != EResType::SOUND) - return false; - - if(!boost::algorithm::istarts_with(id.getName(), "MUSIC/")) - return false; - - logGlobal->trace("Found music file %s", id.getName()); - return true; - }); - - for(const ResourcePath & file : mp3files) - { - if(boost::algorithm::istarts_with(file.getName(), "MUSIC/Combat")) - addEntryToSet("battle", AudioPath::fromResource(file)); - else if(boost::algorithm::istarts_with(file.getName(), "MUSIC/AITheme")) - addEntryToSet("enemy-turn", AudioPath::fromResource(file)); - } - -} - -void CMusicHandler::loadTerrainMusicThemes() -{ - for (const auto & terrain : CGI->terrainTypeHandler->objects) - { - addEntryToSet("terrain_" + terrain->getJsonKey(), terrain->musicFilename); - } -} - -void CMusicHandler::addEntryToSet(const std::string & set, const AudioPath & musicURI) -{ - musicsSet[set].push_back(musicURI); -} - -void CMusicHandler::init() -{ - CAudioBase::init(); - - if (initialized) - { - Mix_HookMusicFinished([]() - { - CCS->musich->musicFinishedCallback(); - }); - } -} - -void CMusicHandler::release() -{ - if (initialized) - { - boost::mutex::scoped_lock guard(mutex); - - Mix_HookMusicFinished(nullptr); - current->stop(); - - current.reset(); - next.reset(); - } - - CAudioBase::release(); -} - -void CMusicHandler::playMusic(const AudioPath & musicURI, bool loop, bool fromStart) -{ - boost::mutex::scoped_lock guard(mutex); - - if (current && current->isPlaying() && current->isTrack(musicURI)) - return; - - queueNext(this, "", musicURI, loop, fromStart); -} - -void CMusicHandler::playMusicFromSet(const std::string & musicSet, const std::string & entryID, bool loop, bool fromStart) -{ - playMusicFromSet(musicSet + "_" + entryID, loop, fromStart); -} - -void CMusicHandler::playMusicFromSet(const std::string & whichSet, bool loop, bool fromStart) -{ - boost::mutex::scoped_lock guard(mutex); - - auto selectedSet = musicsSet.find(whichSet); - if (selectedSet == musicsSet.end()) - { - logGlobal->error("Error: playing music from non-existing set: %s", whichSet); - return; - } - - if (current && current->isPlaying() && current->isSet(whichSet)) - return; - - // in this mode - play random track from set - queueNext(this, whichSet, AudioPath(), loop, fromStart); -} - -void CMusicHandler::queueNext(std::unique_ptr queued) -{ - if (!initialized) - return; - - next = std::move(queued); - - if (current.get() == nullptr || !current->stop(1000)) - { - current.reset(next.release()); - current->play(); - } -} - -void CMusicHandler::queueNext(CMusicHandler *owner, const std::string & setName, const AudioPath & musicURI, bool looped, bool fromStart) -{ - queueNext(std::make_unique(owner, setName, musicURI, looped, fromStart)); -} - -void CMusicHandler::stopMusic(int fade_ms) -{ - if (!initialized) - return; - - boost::mutex::scoped_lock guard(mutex); - - if (current.get() != nullptr) - current->stop(fade_ms); - next.reset(); -} - -void CMusicHandler::setVolume(ui32 percent) -{ - CAudioBase::setVolume(percent); - - if (initialized) - Mix_VolumeMusic((MIX_MAX_VOLUME * volume)/100); -} - -void CMusicHandler::musicFinishedCallback() -{ - // call music restart in separate thread to avoid deadlock in some cases - // It is possible for: - // 1) SDL thread to call this method on end of playback - // 2) VCMI code to call queueNext() method to queue new file - // this leads to: - // 1) SDL thread waiting to acquire music lock in this method (while keeping internal SDL mutex locked) - // 2) VCMI thread waiting to acquire internal SDL mutex (while keeping music mutex locked) - - GH.dispatchMainThread([this]() - { - boost::unique_lock lockGuard(mutex); - if (current.get() != nullptr) - { - // if music is looped, play it again - if (current->play()) - return; - else - current.reset(); - } - - if (current.get() == nullptr && next.get() != nullptr) - { - current.reset(next.release()); - current->play(); - } - }); -} - -MusicEntry::MusicEntry(CMusicHandler *owner, std::string setName, const AudioPath & musicURI, bool looped, bool fromStart): - owner(owner), - music(nullptr), - playing(false), - startTime(uint32_t(-1)), - startPosition(0), - loop(looped ? -1 : 1), - fromStart(fromStart), - setName(std::move(setName)) -{ - if (!musicURI.empty()) - load(std::move(musicURI)); -} -MusicEntry::~MusicEntry() -{ - if (playing && loop > 0) - { - assert(0); - logGlobal->error("Attempt to delete music while playing!"); - Mix_HaltMusic(); - } - - if (loop == 0 && Mix_FadingMusic() != MIX_NO_FADING) - { - assert(0); - logGlobal->error("Attempt to delete music while fading out!"); - Mix_HaltMusic(); - } - - logGlobal->trace("Del-ing music file %s", currentName.getOriginalName()); - if (music) - Mix_FreeMusic(music); -} - -void MusicEntry::load(const AudioPath & musicURI) -{ - if (music) - { - logGlobal->trace("Del-ing music file %s", currentName.getOriginalName()); - Mix_FreeMusic(music); - music = nullptr; - } - - if (CResourceHandler::get()->existsResource(musicURI)) - currentName = musicURI; - else - currentName = musicURI.addPrefix("MUSIC/"); - - music = nullptr; - - logGlobal->trace("Loading music file %s", currentName.getOriginalName()); - - try - { - auto musicFile = MakeSDLRWops(CResourceHandler::get()->load(currentName)); - music = Mix_LoadMUS_RW(musicFile, SDL_TRUE); - } - catch(std::exception &e) - { - logGlobal->error("Failed to load music. setName=%s\tmusicURI=%s", setName, currentName.getOriginalName()); - logGlobal->error("Exception: %s", e.what()); - } - - if(!music) - { - logGlobal->warn("Warning: Cannot open %s: %s", currentName.getOriginalName(), Mix_GetError()); - return; - } -} - -bool MusicEntry::play() -{ - if (!(loop--) && music) //already played once - return - return false; - - if (!setName.empty()) - { - const auto & set = owner->musicsSet[setName]; - const auto & iter = RandomGeneratorUtil::nextItem(set, CRandomGenerator::getDefault()); - load(*iter); - } - - logGlobal->trace("Playing music file %s", currentName.getOriginalName()); - - if (!fromStart && owner->trackPositions.count(currentName) > 0 && owner->trackPositions[currentName] > 0) - { - float timeToStart = owner->trackPositions[currentName]; - startPosition = std::round(timeToStart * 1000); - - // erase stored position: - // if music track will be interrupted again - new position will be written in stop() method - // if music track is not interrupted and will finish by timeout/end of file - it will restart from begginning as it should - owner->trackPositions.erase(owner->trackPositions.find(currentName)); - - if (Mix_FadeInMusicPos(music, 1, 1000, timeToStart) == -1) - { - logGlobal->error("Unable to play music (%s)", Mix_GetError()); - return false; - } - } - else - { - startPosition = 0; - - if(Mix_PlayMusic(music, 1) == -1) - { - logGlobal->error("Unable to play music (%s)", Mix_GetError()); - return false; - } - } - - startTime = GH.input().getTicks(); - - playing = true; - return true; -} - -bool MusicEntry::stop(int fade_ms) -{ - if (Mix_PlayingMusic()) - { - playing = false; - loop = 0; - uint32_t endTime = GH.input().getTicks(); - assert(startTime != uint32_t(-1)); - float playDuration = (endTime - startTime + startPosition) / 1000.f; - owner->trackPositions[currentName] = playDuration; - logGlobal->trace("Stopping music file %s at %f", currentName.getOriginalName(), playDuration); - - Mix_FadeOutMusic(fade_ms); - return true; - } - return false; -} - -bool MusicEntry::isPlaying() -{ - return playing; -} - -bool MusicEntry::isSet(std::string set) -{ - return !setName.empty() && set == setName; -} - -bool MusicEntry::isTrack(const AudioPath & track) -{ - return setName.empty() && track == currentName; -} diff --git a/client/CMusicHandler.h b/client/CMusicHandler.h deleted file mode 100644 index 4a024c450..000000000 --- a/client/CMusicHandler.h +++ /dev/null @@ -1,170 +0,0 @@ -/* - * CMusicHandler.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 "../lib/CConfigHandler.h" -#include "../lib/CSoundBase.h" - -struct _Mix_Music; -struct SDL_RWops; -using Mix_Music = struct _Mix_Music; -struct Mix_Chunk; - -class CAudioBase { -protected: - boost::mutex mutex; - bool initialized; - int volume; // from 0 (mute) to 100 - - CAudioBase(): initialized(false), volume(0) {}; - ~CAudioBase() = default; -public: - virtual void init() = 0; - virtual void release() = 0; - - virtual void setVolume(ui32 percent); - ui32 getVolume() const { return volume; }; -}; - -class CSoundHandler final : public CAudioBase -{ -private: - //update volume on configuration change - SettingsListener listener; - void onVolumeChange(const JsonNode &volumeNode); - - using CachedChunk = std::pair>; - std::map soundChunks; - std::map, CachedChunk> soundChunksRaw; - - Mix_Chunk *GetSoundChunk(const AudioPath & sound, bool cache); - Mix_Chunk *GetSoundChunk(std::pair, si64> & data, bool cache); - - /// have entry for every currently active channel - /// vector will be empty if callback was not set - std::map> > callbacks; - - /// Protects access to callbacks member to avoid data races: - /// SDL calls sound finished callbacks from audio thread - boost::mutex mutexCallbacks; - - int ambientDistToVolume(int distance) const; - void ambientStopSound(const AudioPath & soundId); - void updateChannelVolume(int channel); - - const JsonNode ambientConfig; - - std::map ambientChannels; - std::map channelVolumes; - - void initCallback(int channel, const std::function & function); - void initCallback(int channel); - -public: - CSoundHandler(); - - void init() override; - void release() override; - - void setVolume(ui32 percent) override; - void setChannelVolume(int channel, ui32 percent); - - // Sounds - uint32_t getSoundDurationMilliseconds(const AudioPath & sound); - int playSound(soundBase::soundID soundID, int repeats=0); - int playSound(const AudioPath & sound, int repeats=0, bool cache=false); - int playSound(std::pair, si64> & data, int repeats=0, bool cache=false); - int playSoundFromSet(std::vector &sound_vec); - void stopSound(int handler); - - void setCallback(int channel, std::function function); - void resetCallback(int channel); - void soundFinishedCallback(int channel); - - int ambientGetRange() const; - void ambientUpdateChannels(std::map currentSounds); - void ambientStopAllChannels(); - - // Sets - std::vector battleIntroSounds; -}; - -class CMusicHandler; - -//Class for handling one music file -class MusicEntry -{ - CMusicHandler *owner; - Mix_Music *music; - - int loop; // -1 = indefinite - bool fromStart; - bool playing; - uint32_t startTime; - uint32_t startPosition; - //if not null - set from which music will be randomly selected - std::string setName; - AudioPath currentName; - - void load(const AudioPath & musicURI); - -public: - MusicEntry(CMusicHandler *owner, std::string setName, const AudioPath & musicURI, bool looped, bool fromStart); - ~MusicEntry(); - - bool isSet(std::string setName); - bool isTrack(const AudioPath & trackName); - bool isPlaying(); - - bool play(); - bool stop(int fade_ms=0); -}; - -class CMusicHandler final: public CAudioBase -{ -private: - //update volume on configuration change - SettingsListener listener; - void onVolumeChange(const JsonNode &volumeNode); - - std::unique_ptr current; - std::unique_ptr next; - - void queueNext(CMusicHandler *owner, const std::string & setName, const AudioPath & musicURI, bool looped, bool fromStart); - void queueNext(std::unique_ptr queued); - void musicFinishedCallback(); - - /// map -> - std::map> musicsSet; - /// stored position, in seconds at which music player should resume playing this track - std::map trackPositions; - -public: - CMusicHandler(); - - /// add entry with URI musicURI in set. Track will have ID musicID - void addEntryToSet(const std::string & set, const AudioPath & musicURI); - - void init() override; - void loadTerrainMusicThemes(); - void release() override; - void setVolume(ui32 percent) override; - - /// play track by URI, if loop = true music will be looped - void playMusic(const AudioPath & musicURI, bool loop, bool fromStart); - /// play random track from this set - void playMusicFromSet(const std::string & musicSet, bool loop, bool fromStart); - /// play random track from set (musicSet, entryID) - void playMusicFromSet(const std::string & musicSet, const std::string & entryID, bool loop, bool fromStart); - /// stops currently playing music by fading out it over fade_ms and starts next scheduled track, if any - void stopMusic(int fade_ms=1000); - - friend class MusicEntry; -}; diff --git a/client/CPlayerInterface.cpp b/client/CPlayerInterface.cpp index 9a690d502..1462e7977 100644 --- a/client/CPlayerInterface.cpp +++ b/client/CPlayerInterface.cpp @@ -13,8 +13,6 @@ #include #include "CGameInfo.h" -#include "CMT.h" -#include "CMusicHandler.h" #include "CServerHandler.h" #include "HeroMovementController.h" #include "PlayerLocalState.h" @@ -41,6 +39,9 @@ #include "mapView/mapHandler.h" +#include "media/IMusicPlayer.h" +#include "media/ISoundPlayer.h" + #include "render/CAnimation.h" #include "render/IImage.h" #include "render/IRenderHandler.h" @@ -64,20 +65,17 @@ #include "../CCallback.h" -#include "../lib/CArtHandler.h" #include "../lib/CConfigHandler.h" -#include "../lib/CGeneralTextHandler.h" -#include "../lib/CHeroHandler.h" +#include "../lib/texts/CGeneralTextHandler.h" #include "../lib/CPlayerState.h" +#include "../lib/CRandomGenerator.h" #include "../lib/CStack.h" #include "../lib/CStopWatch.h" #include "../lib/CThreadHelper.h" -#include "../lib/CTownHandler.h" #include "../lib/GameConstants.h" #include "../lib/RoadHandler.h" #include "../lib/StartInfo.h" #include "../lib/TerrainHandler.h" -#include "../lib/TextOperations.h" #include "../lib/UnlockGuard.h" #include "../lib/VCMIDirs.h" @@ -100,12 +98,13 @@ #include "../lib/pathfinder/CGPathNode.h" -#include "../lib/serializer/BinaryDeserializer.h" -#include "../lib/serializer/BinarySerializer.h" #include "../lib/serializer/CTypeList.h" +#include "../lib/serializer/ESerializationVersion.h" #include "../lib/spells/CSpellHandler.h" +#include "../lib/texts/TextOperations.h" + // The macro below is used to mark functions that are called by client when game state changes. // They all assume that interface mutex is locked. #define EVENT_HANDLER_CALLED_BY_CLIENT @@ -130,10 +129,11 @@ struct HeroObjectRetriever CPlayerInterface::CPlayerInterface(PlayerColor Player): localState(std::make_unique(*this)), - movementController(std::make_unique()) + movementController(std::make_unique()), + artifactController(std::make_unique()) + { logGlobal->trace("\tHuman player interface for player %s being constructed", Player.toString()); - GH.defActionsDef = 0; LOCPLINT = this; playerID=Player; human=true; @@ -142,12 +142,10 @@ CPlayerInterface::CPlayerInterface(PlayerColor Player): makingTurn = false; showingDialog = new ConditionalWait(); cingconsole = new CInGameConsole(); - firstCall = 1; //if loading will be overwritten in serialize autosaveCount = 0; isAutoFightOn = false; isAutoFightEndBattle = false; ignoreEvents = false; - numOfMovedArts = 0; } CPlayerInterface::~CPlayerInterface() @@ -172,10 +170,11 @@ void CPlayerInterface::initGameInterface(std::shared_ptr ENV, std:: void CPlayerInterface::closeAllDialogs() { // remove all active dialogs that do not expect query answer - for (;;) + while(true) { auto adventureWindow = GH.windows().topWindow(); auto infoWindow = GH.windows().topWindow(); + auto topWindow = GH.windows().topWindow(); if(adventureWindow != nullptr) break; @@ -183,16 +182,8 @@ void CPlayerInterface::closeAllDialogs() if(infoWindow && infoWindow->ID != QueryID::NONE) break; - if (infoWindow) - infoWindow->close(); - else - GH.windows().popWindows(1); + topWindow->close(); } - - if(castleInt) - castleInt->close(); - - castleInt = nullptr; } void CPlayerInterface::playerEndsTurn(PlayerColor player) @@ -247,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("\\/:*?\"<>| "); @@ -258,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 + "/"; } } @@ -412,13 +403,26 @@ void CPlayerInterface::heroKilled(const CGHeroInstance* hero) localState->erasePath(hero); } +void CPlayerInterface::townRemoved(const CGTownInstance* town) +{ + EVENT_HANDLER_CALLED_BY_CLIENT; + + if(town->tempOwner == playerID) + { + localState->removeOwnedTown(town); + adventureInt->onTownChanged(town); + } +} + + void CPlayerInterface::heroVisit(const CGHeroInstance * visitor, const CGObjectInstance * visitedObj, bool start) { EVENT_HANDLER_CALLED_BY_CLIENT; if(start && visitedObj) { - if(visitedObj->getVisitSound()) - CCS->soundh->playSound(visitedObj->getVisitSound().value()); + auto visitSound = visitedObj->getVisitSound(CRandomGenerator::getDefault()); + if (visitSound) + CCS->soundh->playSound(visitSound.value()); } } @@ -427,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) { @@ -444,18 +450,20 @@ void CPlayerInterface::heroPrimarySkillChanged(const CGHeroInstance * hero, Prim EVENT_HANDLER_CALLED_BY_CLIENT; if (which == PrimarySkill::EXPERIENCE) { - for(auto ctw : GH.windows().findWindows()) - ctw->updateHero(); + for(auto ctw : GH.windows().findWindows()) + ctw->updateExperience(); } else + { adventureInt->onHeroChanged(hero); + } } void CPlayerInterface::heroSecondarySkillChanged(const CGHeroInstance * hero, int which, int val) { EVENT_HANDLER_CALLED_BY_CLIENT; - for (auto cuw : GH.windows().findWindows()) - cuw->redraw(); + for (auto cuw : GH.windows().findWindows()) + cuw->updateSecondarySkills(); } void CPlayerInterface::heroManaPointsChanged(const CGHeroInstance * hero) @@ -474,8 +482,8 @@ void CPlayerInterface::heroMovePointsChanged(const CGHeroInstance * hero) void CPlayerInterface::receivedResource() { EVENT_HANDLER_CALLED_BY_CLIENT; - for (auto mw : GH.windows().findWindows()) - mw->updateResource(); + for (auto mw : GH.windows().findWindows()) + mw->updateResources(); GH.windows().totalRedraw(); } @@ -623,7 +631,7 @@ void CPlayerInterface::battleStartBefore(const BattleID & battleID, const CCreat waitForAllDialogs(); } -void CPlayerInterface::battleStart(const BattleID & battleID, const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool side, bool replayAllowed) +void CPlayerInterface::battleStart(const BattleID & battleID, const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, BattleSide side, bool replayAllowed) { EVENT_HANDLER_CALLED_BY_CLIENT; @@ -935,7 +943,7 @@ void CPlayerInterface::battleAttack(const BattleID & battleID, const BattleAttac info.secondaryDefender.push_back(cb->getBattle(battleID)->battleGetStackByID(elem.stackAttacked)); } } - assert(info.defender != nullptr); + assert(info.defender != nullptr || (info.spellEffect != SpellID::NONE && info.indirectAttack)); assert(info.attacker != nullptr); battleInt->stackAttacking(info); @@ -1125,7 +1133,7 @@ void CPlayerInterface::showMapObjectSelectDialog(QueryID askID, const Component std::vector tempList; tempList.reserve(objectGuiOrdered.size()); - for(auto item : objectGuiOrdered) + for(const auto & item : objectGuiOrdered) tempList.push_back(item.getNum()); CComponent localIconC(icon); @@ -1134,16 +1142,16 @@ void CPlayerInterface::showMapObjectSelectDialog(QueryID askID, const Component localIconC.removeChild(localIcon.get(), false); std::vector> images; - for(auto & obj : objectGuiOrdered) + for(const auto & obj : objectGuiOrdered) { if(!settings["general"]["enableUiEnhancements"].Bool()) break; const CGTownInstance * t = dynamic_cast(cb->getObj(obj)); if(t) { - std::shared_ptr a = GH.renderHandler().loadAnimation(AnimationPath::builtin("ITPA")); - a->preload(); - images.push_back(a->getImage(t->town->clientInfo.icons[t->hasFort()][false] + 2)->scaleFast(Point(35, 23))); + 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); } } @@ -1211,19 +1219,6 @@ void CPlayerInterface::heroBonusChanged( const CGHeroInstance *hero, const Bonus } } -void CPlayerInterface::saveGame( BinarySerializer & h ) -{ - EVENT_HANDLER_CALLED_BY_CLIENT; - localState->serialize(h); -} - -void CPlayerInterface::loadGame( BinaryDeserializer & h ) -{ - EVENT_HANDLER_CALLED_BY_CLIENT; - localState->serialize(h); - firstCall = -1; -} - void CPlayerInterface::moveHero( const CGHeroInstance *h, const CGPath& path ) { LOG_TRACE(logGlobal); @@ -1265,35 +1260,6 @@ void CPlayerInterface::showGarrisonDialog( const CArmedInstance *up, const CGHer GH.windows().pushWindow(cgw); } -/** - * Shows the dialog that appears when right-clicking an artifact that can be assembled - * into a combinational one on an artifact screen. Does not require the combination of - * artifacts to be legal. - */ -void CPlayerInterface::showArtifactAssemblyDialog(const Artifact * artifact, const Artifact * assembledArtifact, CFunctionList onYes) -{ - std::string text = artifact->getDescriptionTranslated(); - text += "\n\n"; - std::vector> scs; - - if(assembledArtifact) - { - // You possess all of the components to... - text += boost::str(boost::format(CGI->generaltexth->allTexts[732]) % assembledArtifact->getNameTranslated()); - - // Picture of assembled artifact at bottom. - auto sc = std::make_shared(ComponentType::ARTIFACT, assembledArtifact->getId()); - scs.push_back(sc); - } - else - { - // Do you wish to disassemble this artifact? - text += CGI->generaltexth->allTexts[733]; - } - - showYesNoDialog(text, onYes, nullptr, scs); -} - void CPlayerInterface::requestRealized( PackageApplied *pa ) { if(pa->packType == CTypeList::getInstance().getTypeID(nullptr)) @@ -1378,6 +1344,8 @@ void CPlayerInterface::initializeHeroTownList() localState->addOwnedTown(town); } + localState->deserialize(*cb->getPlayerState(playerID)->playerLocalSettings); + if(adventureInt) adventureInt->onHeroChanged(nullptr); } @@ -1452,10 +1420,14 @@ void CPlayerInterface::centerView (int3 pos, int focusTime) void CPlayerInterface::objectRemoved(const CGObjectInstance * obj, const PlayerColor & initiator) { EVENT_HANDLER_CALLED_BY_CLIENT; - if(playerID == initiator && obj->getRemovalSound()) + if(playerID == initiator) { - waitWhileDialog(); - CCS->soundh->playSound(obj->getRemovalSound().value()); + auto removalSound = obj->getRemovalSound(CRandomGenerator::getDefault()); + if (removalSound) + { + waitWhileDialog(); + CCS->soundh->playSound(removalSound.value()); + } } CGI->mh->waitForOngoingAnimations(); @@ -1464,6 +1436,12 @@ void CPlayerInterface::objectRemoved(const CGObjectInstance * obj, const PlayerC const CGHeroInstance * h = static_cast(obj); heroKilled(h); } + + if(obj->ID == Obj::TOWN && obj->tempOwner == playerID) + { + const CGTownInstance * t = static_cast(obj); + townRemoved(t); + } GH.fakeMouseMove(); } @@ -1496,6 +1474,7 @@ void CPlayerInterface::playerBlocked(int reason, bool start) cmp.push_back(std::make_shared(ComponentType::FLAG, playerID)); makingTurn = true; //workaround for stiff showInfoDialog implementation showInfoDialog(msg, cmp); + waitWhileDialog(); makingTurn = false; } } @@ -1504,7 +1483,7 @@ void CPlayerInterface::playerBlocked(int reason, bool start) void CPlayerInterface::update() { // Make sure that gamestate won't change when GUI objects may obtain its parts on event processing or drawing request - boost::shared_lock gsLock(CGameState::mutex); + boost::shared_lock gsLock(CGameState::mutex); // While mutexes were locked away we may be have stopped being the active interface if (LOCPLINT != this) @@ -1667,30 +1646,30 @@ void CPlayerInterface::battleNewRoundFirst(const BattleID & battleID) battleInt->newRoundFirst(); } -void CPlayerInterface::showMarketWindow(const IMarket *market, const CGHeroInstance *visitor, QueryID queryID) +void CPlayerInterface::showMarketWindow(const IMarket * market, const CGHeroInstance * visitor, QueryID queryID) { EVENT_HANDLER_CALLED_BY_CLIENT; auto onWindowClosed = [this, queryID](){ cb->selectionMade(0, queryID); }; - if (market->allowsTrade(EMarketMode::ARTIFACT_EXP) && dynamic_cast(market) == nullptr) - { - // compatibility check, safe to remove for 1.6 - // 1.4 saves loaded in 1.5 will not be able to visit Altar of Sacrifice due to Altar now requiring different map object class - static_assert(ESerializationVersion::RELEASE_143 < ESerializationVersion::CURRENT, "Please remove this compatibility check once it no longer needed"); - onWindowClosed(); - return; - } - if(market->allowsTrade(EMarketMode::ARTIFACT_EXP) && visitor->getAlignment() != EAlignment::EVIL) GH.windows().createAndPushWindow(market, visitor, onWindowClosed, EMarketMode::ARTIFACT_EXP); else if(market->allowsTrade(EMarketMode::CREATURE_EXP) && visitor->getAlignment() != EAlignment::GOOD) GH.windows().createAndPushWindow(market, visitor, onWindowClosed, EMarketMode::CREATURE_EXP); else if(market->allowsTrade(EMarketMode::CREATURE_UNDEAD)) GH.windows().createAndPushWindow(market, visitor, onWindowClosed); - else if(!market->availableModes().empty()) - GH.windows().createAndPushWindow(market, visitor, onWindowClosed, market->availableModes().front()); + else if (!market->availableModes().empty()) + for(auto mode = EMarketMode::RESOURCE_RESOURCE; mode != EMarketMode::MARKET_AFTER_LAST_PLACEHOLDER; mode = vstd::next(mode, 1)) + { + if(vstd::contains(market->availableModes(), mode)) + { + GH.windows().createAndPushWindow(market, visitor, onWindowClosed, mode); + break; + } + } + else + onWindowClosed(); } void CPlayerInterface::showUniversityWindow(const IMarket *market, const CGHeroInstance *visitor, QueryID queryID) @@ -1699,7 +1678,7 @@ void CPlayerInterface::showUniversityWindow(const IMarket *market, const CGHeroI auto onWindowClosed = [this, queryID](){ cb->selectionMade(0, queryID); }; - GH.windows().createAndPushWindow(visitor, market, onWindowClosed); + GH.windows().createAndPushWindow(visitor, BuildingID::NONE, market, onWindowClosed); } void CPlayerInterface::showHillFortWindow(const CGObjectInstance *object, const CGHeroInstance *visitor) @@ -1711,7 +1690,7 @@ void CPlayerInterface::showHillFortWindow(const CGObjectInstance *object, const void CPlayerInterface::availableArtifactsChanged(const CGBlackMarket * bm) { EVENT_HANDLER_CALLED_BY_CLIENT; - for (auto cmw : GH.windows().findWindows()) + for (auto cmw : GH.windows().findWindows()) cmw->updateArtifacts(); } @@ -1751,17 +1730,7 @@ void CPlayerInterface::showShipyardDialogOrProblemPopup(const IShipyard *obj) void CPlayerInterface::askToAssembleArtifact(const ArtifactLocation &al) { - if(auto hero = cb->getHero(al.artHolder)) - { - auto art = hero->getArt(al.slot); - if(art == nullptr) - { - logGlobal->error("artifact location %d points to nothing", - al.slot.num); - return; - } - ArtifactUtilsClient::askToAssemble(hero, al.slot); - } + artifactController->askToAssemble(al, true, true); } void CPlayerInterface::artifactPut(const ArtifactLocation &al) @@ -1774,54 +1743,33 @@ void CPlayerInterface::artifactRemoved(const ArtifactLocation &al) { EVENT_HANDLER_CALLED_BY_CLIENT; adventureInt->onHeroChanged(cb->getHero(al.artHolder)); - - for(auto artWin : GH.windows().findWindows()) - artWin->artifactRemoved(al); - - waitWhileDialog(); + artifactController->artifactRemoved(); } void CPlayerInterface::artifactMoved(const ArtifactLocation &src, const ArtifactLocation &dst) { EVENT_HANDLER_CALLED_BY_CLIENT; adventureInt->onHeroChanged(cb->getHero(dst.artHolder)); - - bool redraw = true; - // If a bulk transfer has arrived, then redrawing only the last art movement. - if(numOfMovedArts != 0) - { - numOfMovedArts--; - if(numOfMovedArts != 0) - redraw = false; - } - - for(auto artWin : GH.windows().findWindows()) - artWin->artifactMoved(src, dst, redraw); - - waitWhileDialog(); + artifactController->artifactMoved(); } -void CPlayerInterface::bulkArtMovementStart(size_t numOfArts) +void CPlayerInterface::bulkArtMovementStart(size_t totalNumOfArts, size_t possibleAssemblyNumOfArts) { - numOfMovedArts = numOfArts; + artifactController->bulkArtMovementStart(totalNumOfArts, possibleAssemblyNumOfArts); } void CPlayerInterface::artifactAssembled(const ArtifactLocation &al) { EVENT_HANDLER_CALLED_BY_CLIENT; adventureInt->onHeroChanged(cb->getHero(al.artHolder)); - - for(auto artWin : GH.windows().findWindows()) - artWin->artifactAssembled(al); + artifactController->artifactAssembled(); } void CPlayerInterface::artifactDisassembled(const ArtifactLocation &al) { EVENT_HANDLER_CALLED_BY_CLIENT; adventureInt->onHeroChanged(cb->getHero(al.artHolder)); - - for(auto artWin : GH.windows().findWindows()) - artWin->artifactDisassembled(al); + artifactController->artifactDisassembled(); } void CPlayerInterface::waitForAllDialogs() @@ -1844,7 +1792,6 @@ void CPlayerInterface::proposeLoadingGame() []() { CSH->endGameplay(); - GH.defActionsDef = 63; CMM->menu->switchToTab("load"); }, nullptr diff --git a/client/CPlayerInterface.h b/client/CPlayerInterface.h index 1d1dcd01f..af32faf6e 100644 --- a/client/CPlayerInterface.h +++ b/client/CPlayerInterface.h @@ -9,6 +9,8 @@ */ #pragma once +#include "ArtifactsUIController.h" + #include "../lib/FunctionList.h" #include "../lib/CGameInterface.h" #include "gui/CIntObject.h" @@ -16,9 +18,7 @@ VCMI_LIB_NAMESPACE_BEGIN class Artifact; - struct TryMoveHero; -class CGHeroInstance; class CStack; class CCreature; struct CGPath; @@ -59,16 +59,13 @@ namespace boost class CPlayerInterface : public CGameInterface, public IUpdateable { bool ignoreEvents; - size_t numOfMovedArts; - - // -1 - just loaded game; 1 - just started game; 0 otherwise - int firstCall; int autosaveCount; std::list> dialogs; //queue of dialogs awaiting to be shown (not currently shown!) std::unique_ptr movementController; public: // TODO: make private + std::unique_ptr artifactController; std::shared_ptr env; std::unique_ptr localState; @@ -100,7 +97,7 @@ protected: // Call-ins from server, should not be called directly, but only via void artifactPut(const ArtifactLocation &al) override; void artifactRemoved(const ArtifactLocation &al) override; void artifactMoved(const ArtifactLocation &src, const ArtifactLocation &dst) override; - void bulkArtMovementStart(size_t numOfArts) override; + void bulkArtMovementStart(size_t totalNumOfArts, size_t possibleAssemblyNumOfArts) override; void artifactAssembled(const ArtifactLocation &al) override; void askToAssembleArtifact(const ArtifactLocation & dst) override; void artifactDisassembled(const ArtifactLocation &al) override; @@ -123,7 +120,7 @@ protected: // Call-ins from server, should not be called directly, but only via void showTeleportDialog(const CGHeroInstance * hero, TeleportChannelID channel, TTeleportExitsList exits, bool impassable, QueryID askID) override; void showGarrisonDialog(const CArmedInstance *up, const CGHeroInstance *down, bool removableUnits, QueryID queryID) override; void showMapObjectSelectDialog(QueryID askID, const Component & icon, const MetaString & title, const MetaString & description, const std::vector & objects) override; - void showMarketWindow(const IMarket *market, const CGHeroInstance *visitor, QueryID queryID) override; + void showMarketWindow(const IMarket * market, const CGHeroInstance * visitor, QueryID queryID) override; void showUniversityWindow(const IMarket *market, const CGHeroInstance *visitor, QueryID queryID) override; void showHillFortWindow(const CGObjectInstance *object, const CGHeroInstance *visitor) override; void advmapSpellCast(const CGHeroInstance * caster, SpellID spellID) override; //called when a hero casts a spell @@ -144,10 +141,8 @@ protected: // Call-ins from server, should not be called directly, but only via void objectRemovedAfter() override; void playerBlocked(int reason, bool start) override; void gameOver(PlayerColor player, const EVictoryLossCheckResult & victoryLossCheckResult) override; - void playerStartsTurn(PlayerColor player) override; //called before yourTurn on active itnerface + void playerStartsTurn(PlayerColor player) override; //called before yourTurn on active interface void playerEndsTurn(PlayerColor player) override; - void saveGame(BinarySerializer & h) override; //saving - void loadGame(BinaryDeserializer & h) override; //loading void showWorldViewEx(const std::vector & objectPositions, bool showTerrain) override; //for battles @@ -165,7 +160,7 @@ protected: // Call-ins from server, should not be called directly, but only via void battleTriggerEffect(const BattleID & battleID, const BattleTriggerEffect & bte) override; //various one-shot effect void battleStacksAttacked(const BattleID & battleID, const std::vector & bsa, bool ranged) override; void battleStartBefore(const BattleID & battleID, const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2) override; //called by engine just before battle starts; side=0 - left, side=1 - right - void battleStart(const BattleID & battleID, const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool side, bool replayAllowed) override; //called by engine when battle starts; side=0 - left, side=1 - right + void battleStart(const BattleID & battleID, const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, BattleSide side, bool replayAllowed) override; //called by engine when battle starts; side=0 - left, side=1 - right void battleUnitsChanged(const BattleID & battleID, const std::vector & units) override; void battleObstaclesChanged(const BattleID & battleID, const std::vector & obstacles) override; void battleCatapultAttacked(const BattleID & battleID, const CatapultAttack & ca) override; //called when catapult makes an attack @@ -184,7 +179,6 @@ public: // public interface for use by client via LOCPLINT access void showShipyardDialog(const IShipyard *obj) override; //obj may be town or shipyard; void showHeroExchange(ObjectInstanceID hero1, ObjectInstanceID hero2); - void showArtifactAssemblyDialog(const Artifact * artifact, const Artifact * assembledArtifact, CFunctionList onYes); void waitWhileDialog(); void waitForAllDialogs(); void openTownWindow(const CGTownInstance * town); //shows townscreen @@ -226,6 +220,7 @@ private: }; void heroKilled(const CGHeroInstance* hero); + void townRemoved(const CGTownInstance* town); void garrisonsChanged(std::vector objs); void requestReturningToMainMenu(bool won); void acceptTurn(QueryID queryID, bool hotseatWait); //used during hot seat after your turn message is close diff --git a/client/CServerHandler.cpp b/client/CServerHandler.cpp index b162e4c89..4a3dcd690 100644 --- a/client/CServerHandler.cpp +++ b/client/CServerHandler.cpp @@ -21,20 +21,27 @@ #include "globalLobby/GlobalLobbyClient.h" #include "lobby/CSelectionBase.h" #include "lobby/CLobbyScreen.h" +#include "lobby/CBonusSelection.h" #include "windows/InfoWindows.h" +#include "windows/GUIClasses.h" +#include "media/CMusicHandler.h" +#include "media/IVideoPlayer.h" + #include "mainmenu/CMainMenu.h" #include "mainmenu/CPrologEpilogVideo.h" #include "mainmenu/CHighScoreScreen.h" #include "../lib/CConfigHandler.h" -#include "../lib/CGeneralTextHandler.h" +#include "../lib/texts/CGeneralTextHandler.h" #include "ConditionalWait.h" #include "../lib/CThreadHelper.h" #include "../lib/StartInfo.h" #include "../lib/TurnTimerInfo.h" #include "../lib/VCMIDirs.h" #include "../lib/campaign/CampaignState.h" +#include "../lib/gameState/CGameState.h" +#include "../lib/gameState/HighScore.h" #include "../lib/CPlayerState.h" #include "../lib/mapping/CMapInfo.h" #include "../lib/mapObjects/CGTownInstance.h" @@ -43,73 +50,16 @@ #include "../lib/rmg/CMapGenOptions.h" #include "../lib/serializer/Connection.h" #include "../lib/filesystem/Filesystem.h" -#include "../lib/registerTypes/RegisterTypesLobbyPacks.h" #include "../lib/serializer/CMemorySerializer.h" #include "../lib/UnlockGuard.h" #include #include #include -#include "../lib/serializer/Cast.h" #include "LobbyClientNetPackVisitors.h" #include -template class CApplyOnLobby; - -class CBaseForLobbyApply -{ -public: - virtual bool applyOnLobbyHandler(CServerHandler * handler, CPackForLobby & pack) const = 0; - virtual void applyOnLobbyScreen(CLobbyScreen * lobby, CServerHandler * handler, CPackForLobby & pack) const = 0; - virtual ~CBaseForLobbyApply(){}; - template static CBaseForLobbyApply * getApplier(const U * t = nullptr) - { - return new CApplyOnLobby(); - } -}; - -template class CApplyOnLobby : public CBaseForLobbyApply -{ -public: - bool applyOnLobbyHandler(CServerHandler * handler, CPackForLobby & pack) const override - { - auto & ref = static_cast(pack); - ApplyOnLobbyHandlerNetPackVisitor visitor(*handler); - - logNetwork->trace("\tImmediately apply on lobby: %s", typeid(ref).name()); - ref.visit(visitor); - - return visitor.getResult(); - } - - void applyOnLobbyScreen(CLobbyScreen * lobby, CServerHandler * handler, CPackForLobby & pack) const override - { - auto & ref = static_cast(pack); - ApplyOnLobbyScreenNetPackVisitor visitor(*handler, lobby); - - logNetwork->trace("\tApply on lobby from queue: %s", typeid(ref).name()); - ref.visit(visitor); - } -}; - -template<> class CApplyOnLobby: public CBaseForLobbyApply -{ -public: - bool applyOnLobbyHandler(CServerHandler * handler, CPackForLobby & pack) const override - { - logGlobal->error("Cannot apply plain CPack!"); - assert(0); - return false; - } - - void applyOnLobbyScreen(CLobbyScreen * lobby, CServerHandler * handler, CPackForLobby & pack) const override - { - logGlobal->error("Cannot apply plain CPack!"); - assert(0); - } -}; - CServerHandler::~CServerHandler() { if (serverRunner) @@ -147,7 +97,6 @@ CServerHandler::CServerHandler() : networkHandler(INetworkHandler::createHandler()) , lobbyClient(std::make_unique()) , gameChat(std::make_unique()) - , applier(std::make_unique>()) , threadNetwork(&CServerHandler::threadRunNetwork, this) , state(EClientState::NONE) , serverPort(0) @@ -158,12 +107,6 @@ CServerHandler::CServerHandler() , client(nullptr) { uuid = boost::uuids::to_string(boost::uuids::random_generator()()); - registerTypesLobbyPacks(*applier); -} - -void CServerHandler::setHighScoreCalc(const std::shared_ptr &newHighScoreCalc) -{ - campaignScoreCalculator = newHighScoreCalc; } void CServerHandler::threadRunNetwork() @@ -173,7 +116,7 @@ void CServerHandler::threadRunNetwork() try { networkHandler->run(); } - catch (const TerminationRequestedException & e) + catch (const TerminationRequestedException &) { logGlobal->info("Terminating network thread"); return; @@ -198,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(); @@ -238,9 +186,9 @@ void CServerHandler::startLocalServerAndConnect(bool connectToLobby) si->difficulty = lastDifficulty.Integer(); logNetwork->trace("\tStarting local server"); - serverRunner->start(getLocalPort(), connectToLobby, si); + uint16_t srvport = serverRunner->start(getLocalPort(), connectToLobby, si); logNetwork->trace("\tConnecting to local server"); - connectToServer(getLocalHostname(), getLocalPort()); + connectToServer(getLocalHostname(), srvport); logNetwork->trace("\tWaiting for connection"); } @@ -324,8 +272,8 @@ void CServerHandler::onConnectionEstablished(const NetworkConnectionPtr & netCon void CServerHandler::applyPackOnLobbyScreen(CPackForLobby & pack) { - const CBaseForLobbyApply * apply = applier->getApplier(CTypeList::getInstance().getTypeID(&pack)); //find the applier - apply->applyOnLobbyScreen(dynamic_cast(SEL), this, pack); + ApplyOnLobbyScreenNetPackVisitor visitor(*this, dynamic_cast(SEL)); + pack.visit(visitor); GH.windows().totalRedraw(); } @@ -497,6 +445,14 @@ void CServerHandler::setPlayerName(PlayerColor color, const std::string & name) sendLobbyPack(lspn); } +void CServerHandler::setPlayerHandicap(PlayerColor color, Handicap handicap) const +{ + LobbySetPlayerHandicap lsph; + lsph.color = color; + lsph.handicap = handicap; + sendLobbyPack(lsph); +} + void CServerHandler::setPlayerOption(ui8 what, int32_t value, PlayerColor player) const { LobbyChangePlayerOption lcpo; @@ -585,7 +541,10 @@ void CServerHandler::sendGuiAction(ui8 action) const void CServerHandler::sendRestartGame() const { - GH.windows().createAndPushWindow(); + if(si->campState && !si->campState->getLoadingBackground().empty()) + GH.windows().createAndPushWindow(si->campState->getLoadingBackground()); + else + GH.windows().createAndPushWindow(); LobbyRestartGame endGame; sendLobbyPack(endGame); @@ -629,7 +588,12 @@ void CServerHandler::sendStartGame(bool allowOnlyAI) const verifyStateBeforeStart(allowOnlyAI ? true : settings["session"]["onlyai"].Bool()); if(!settings["session"]["headless"].Bool()) - GH.windows().createAndPushWindow(); + { + if(si->campState && !si->campState->getLoadingBackground().empty()) + GH.windows().createAndPushWindow(si->campState->getLoadingBackground()); + else + GH.windows().createAndPushWindow(); + } LobbyPrepareStartGame lpsg; sendLobbyPack(lpsg); @@ -655,7 +619,7 @@ void CServerHandler::startGameplay(VCMI_LIB_WRAP_NAMESPACE(CGameState) * gameSta break; case EStartMode::CAMPAIGN: if(si->campState->conqueredScenarios().empty()) - campaignScoreCalculator.reset(); + si->campState->highscoreParameters.clear(); client->newGame(gameState); break; case EStartMode::LOAD_GAME: @@ -669,43 +633,13 @@ void CServerHandler::startGameplay(VCMI_LIB_WRAP_NAMESPACE(CGameState) * gameSta setState(EClientState::GAMEPLAY); } -HighScoreParameter CServerHandler::prepareHighScores(PlayerColor player, bool victory) +void CServerHandler::showHighScoresAndEndGameplay(PlayerColor player, bool victory, const StatisticDataSet & statistic) { - const auto * gs = client->gameState(); - const auto * playerState = gs->getPlayerState(player); - - HighScoreParameter param; - param.difficulty = gs->getStartInfo()->difficulty; - param.day = gs->getDate(); - param.townAmount = gs->howManyTowns(player); - param.usedCheat = gs->getPlayerState(player)->cheated; - param.hasGrail = false; - for(const CGHeroInstance * h : playerState->heroes) - if(h->hasArt(ArtifactID::GRAIL)) - param.hasGrail = true; - for(const CGTownInstance * t : playerState->towns) - if(t->builtBuildings.count(BuildingID::GRAIL)) - param.hasGrail = true; - param.allDefeated = true; - for (PlayerColor otherPlayer(0); otherPlayer < PlayerColor::PLAYER_LIMIT; ++otherPlayer) - { - auto ps = gs->getPlayerState(otherPlayer, false); - if(ps && otherPlayer != player && !ps->checkVanquished()) - param.allDefeated = false; - } - param.scenarioName = gs->getMapHeader()->name.toString(); - param.playerName = gs->getStartInfo()->playerInfos.find(player)->second.name; - - return param; -} - -void CServerHandler::showHighScoresAndEndGameplay(PlayerColor player, bool victory) -{ - HighScoreParameter param = prepareHighScores(player, victory); + HighScoreParameter param = HighScore::prepareHighScores(client->gameState(), player, victory); if(victory && client->gameState()->getStartInfo()->campState) { - startCampaignScenario(param, client->gameState()->getStartInfo()->campState); + startCampaignScenario(param, client->gameState()->getStartInfo()->campState, statistic); } else { @@ -714,9 +648,8 @@ void CServerHandler::showHighScoresAndEndGameplay(PlayerColor player, bool victo scenarioHighScores.isCampaign = false; endGameplay(); - GH.defActionsDef = 63; CMM->menu->switchToTab("main"); - GH.windows().createAndPushWindow(victory, scenarioHighScores); + GH.windows().createAndPushWindow(victory, scenarioHighScores, statistic); } } @@ -734,10 +667,13 @@ void CServerHandler::endGameplay() { GH.curInt = CMM.get(); CMM->enable(); + CMM->playMusic(); } else { - GH.curInt = CMainMenu::create().get(); + auto mainMenu = CMainMenu::create(); + GH.curInt = mainMenu.get(); + mainMenu->playMusic(); } } @@ -749,26 +685,23 @@ void CServerHandler::restartGameplay() logicConnection->enterLobbyConnectionMode(); } -void CServerHandler::startCampaignScenario(HighScoreParameter param, std::shared_ptr cs) +void CServerHandler::startCampaignScenario(HighScoreParameter param, std::shared_ptr cs, const StatisticDataSet & statistic) { std::shared_ptr ourCampaign = cs; if (!cs) ourCampaign = si->campState; - if(campaignScoreCalculator == nullptr) - { - campaignScoreCalculator = std::make_shared(); - campaignScoreCalculator->isCampaign = true; - campaignScoreCalculator->parameters.clear(); - } param.campaignName = cs->getNameTranslated(); - campaignScoreCalculator->parameters.push_back(param); + cs->highscoreParameters.push_back(param); + auto campaignScoreCalculator = std::make_shared(); + campaignScoreCalculator->isCampaign = true; + campaignScoreCalculator->parameters = cs->highscoreParameters; endGameplay(); auto & epilogue = ourCampaign->scenario(*ourCampaign->lastScenario()).epilog; - auto finisher = [this, ourCampaign]() + auto finisher = [ourCampaign, campaignScoreCalculator, statistic]() { if(ourCampaign->campaignSet != "" && ourCampaign->isCampaignFinished()) { @@ -784,7 +717,15 @@ void CServerHandler::startCampaignScenario(HighScoreParameter param, std::shared else { CMM->openCampaignScreen(ourCampaign->campaignSet); - GH.windows().createAndPushWindow(true, *campaignScoreCalculator); + if(!ourCampaign->getOutroVideo().empty() && CCS->videoh->open(ourCampaign->getOutroVideo(), 1)) + { + CCS->musich->stopMusic(); + GH.windows().createAndPushWindow(ourCampaign->getOutroVideo(), ourCampaign->getVideoRim().empty() ? ImagePath::builtin("INTRORIM") : ourCampaign->getVideoRim(), false, 1, [campaignScoreCalculator, statistic](bool skipped){ + GH.windows().createAndPushWindow(true, *campaignScoreCalculator, statistic); + }); + } + else + GH.windows().createAndPushWindow(true, *campaignScoreCalculator, statistic); } }; @@ -917,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); } @@ -948,7 +889,6 @@ void CServerHandler::onDisconnected(const std::shared_ptr & if(client) { endGameplay(); - GH.defActionsDef = 63; CMM->menu->switchToTab("main"); showServerError(CGI->generaltexth->translate("vcmi.server.errors.disconnected")); } @@ -991,7 +931,10 @@ void CServerHandler::waitForServerShutdown() void CServerHandler::visitForLobby(CPackForLobby & lobbyPack) { - if(applier->getApplier(CTypeList::getInstance().getTypeID(&lobbyPack))->applyOnLobbyHandler(this, lobbyPack)) + ApplyOnLobbyHandlerNetPackVisitor visitor(*this); + lobbyPack.visit(visitor); + + if(visitor.getResult()) { if(!settings["session"]["headless"].Bool()) applyPackOnLobbyScreen(lobbyPack); @@ -1000,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 85825b2e8..a62aa8a45 100644 --- a/client/CServerHandler.h +++ b/client/CServerHandler.h @@ -13,6 +13,7 @@ #include "../lib/network/NetworkInterface.h" #include "../lib/StartInfo.h" +#include "../lib/gameState/GameStatistics.h" VCMI_LIB_NAMESPACE_BEGIN @@ -24,11 +25,10 @@ struct TurnTimerInfo; class CMapInfo; class CGameState; struct ClientPlayer; -struct CPack; struct CPackForLobby; struct CPackForClient; -template class CApplier; +class HighScoreParameter; VCMI_LIB_NAMESPACE_END @@ -38,9 +38,6 @@ class GlobalLobbyClient; class GameChatHandler; class IServerRunner; -class HighScoreCalculation; -class HighScoreParameter; - enum class ESelectionScreen : ui8; enum class ELoadMode : ui8; @@ -81,6 +78,7 @@ public: virtual void setMapInfo(std::shared_ptr to, std::shared_ptr mapGenOpts = {}) const = 0; virtual void setPlayer(PlayerColor color) const = 0; virtual void setPlayerName(PlayerColor color, const std::string & name) const = 0; + virtual void setPlayerHandicap(PlayerColor color, Handicap handicap) const = 0; virtual void setPlayerOption(ui8 what, int32_t value, PlayerColor player) const = 0; virtual void setDifficulty(int to) const = 0; virtual void setTurnTimerInfo(const TurnTimerInfo &) const = 0; @@ -101,11 +99,9 @@ class CServerHandler final : public IServerAPI, public LobbyInfo, public INetwor std::shared_ptr networkConnection; std::unique_ptr lobbyClient; std::unique_ptr gameChat; - std::unique_ptr> applier; std::unique_ptr serverRunner; std::shared_ptr mapToStart; std::vector localPlayerNames; - std::shared_ptr campaignScoreCalculator; boost::thread threadNetwork; @@ -127,8 +123,6 @@ class CServerHandler final : public IServerAPI, public LobbyInfo, public INetwor bool isServerLocal() const; - HighScoreParameter prepareHighScores(PlayerColor player, bool victory); - public: /// High-level connection overlay that is capable of (de)serializing network data std::shared_ptr logicConnection; @@ -191,6 +185,7 @@ public: void setMapInfo(std::shared_ptr to, std::shared_ptr mapGenOpts = {}) const override; void setPlayer(PlayerColor color) const override; void setPlayerName(PlayerColor color, const std::string & name) const override; + void setPlayerHandicap(PlayerColor color, Handicap handicap) const override; void setPlayerOption(ui8 what, int32_t value, PlayerColor player) const override; void setDifficulty(int to) const override; void setTurnTimerInfo(const TurnTimerInfo &) const override; @@ -206,11 +201,11 @@ public: void debugStartTest(std::string filename, bool save = false); void startGameplay(VCMI_LIB_WRAP_NAMESPACE(CGameState) * gameState = nullptr); - void showHighScoresAndEndGameplay(PlayerColor player, bool victory); + void showHighScoresAndEndGameplay(PlayerColor player, bool victory, const StatisticDataSet & statistic); void endNetwork(); void endGameplay(); void restartGameplay(); - void startCampaignScenario(HighScoreParameter param, std::shared_ptr cs = {}); + void startCampaignScenario(HighScoreParameter param, std::shared_ptr cs, const StatisticDataSet & statistic); void showServerError(const std::string & txt) const; // TODO: LobbyState must be updated within game so we should always know how many player interfaces our client handle @@ -219,7 +214,6 @@ public: void visitForLobby(CPackForLobby & lobbyPack); void visitForClient(CPackForClient & clientPack); - void setHighScoreCalc(const std::shared_ptr &newHighScoreCalc); }; extern CServerHandler * CSH; diff --git a/client/CVideoHandler.cpp b/client/CVideoHandler.cpp deleted file mode 100644 index 4595e5977..000000000 --- a/client/CVideoHandler.cpp +++ /dev/null @@ -1,713 +0,0 @@ -/* - * CVideoHandler.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 "CVideoHandler.h" - -#include "CMT.h" -#include "gui/CGuiHandler.h" -#include "eventsSDL/InputHandler.h" -#include "gui/FramerateManager.h" -#include "renderSDL/SDL_Extensions.h" -#include "CPlayerInterface.h" -#include "../lib/filesystem/Filesystem.h" -#include "../lib/filesystem/CInputStream.h" - -#include - -#ifndef DISABLE_VIDEO - -extern "C" { -#include -#include -#include -#include -} - -#ifdef _MSC_VER -#pragma comment(lib, "avcodec.lib") -#pragma comment(lib, "avutil.lib") -#pragma comment(lib, "avformat.lib") -#pragma comment(lib, "swscale.lib") -#endif // _MSC_VER - -// Define a set of functions to read data -static int lodRead(void* opaque, uint8_t* buf, int size) -{ - auto video = reinterpret_cast(opaque); - int bytes = static_cast(video->data->read(buf, size)); - if(bytes == 0) - return AVERROR_EOF; - - return bytes; -} - -static si64 lodSeek(void * opaque, si64 pos, int whence) -{ - auto video = reinterpret_cast(opaque); - - if (whence & AVSEEK_SIZE) - return video->data->getSize(); - - return video->data->seek(pos); -} - -// Define a set of functions to read data -static int lodReadAudio(void* opaque, uint8_t* buf, int size) -{ - auto video = reinterpret_cast(opaque); - int bytes = static_cast(video->dataAudio->read(buf, size)); - if(bytes == 0) - return AVERROR_EOF; - - return bytes; -} - -static si64 lodSeekAudio(void * opaque, si64 pos, int whence) -{ - auto video = reinterpret_cast(opaque); - - if (whence & AVSEEK_SIZE) - return video->dataAudio->getSize(); - - return video->dataAudio->seek(pos); -} - -CVideoPlayer::CVideoPlayer() - : stream(-1) - , format (nullptr) - , codecContext(nullptr) - , codec(nullptr) - , frame(nullptr) - , sws(nullptr) - , context(nullptr) - , texture(nullptr) - , dest(nullptr) - , destRect(0,0,0,0) - , pos(0,0,0,0) - , frameTime(0) - , doLoop(false) -{} - -bool CVideoPlayer::open(const VideoPath & fname, bool scale) -{ - return open(fname, true, false); -} - -// loop = to loop through the video -// overlay = directly write to the screen. -bool CVideoPlayer::open(const VideoPath & videoToOpen, bool loop, bool overlay, bool scale) -{ - close(); - - doLoop = loop; - frameTime = 0; - - if (CResourceHandler::get()->existsResource(videoToOpen)) - fname = videoToOpen; - else - fname = videoToOpen.addPrefix("VIDEO/"); - - if (!CResourceHandler::get()->existsResource(fname)) - { - logGlobal->error("Error: video %s was not found", fname.getName()); - return false; - } - - data = CResourceHandler::get()->load(fname); - - static const int BUFFER_SIZE = 4096; - - unsigned char * buffer = (unsigned char *)av_malloc(BUFFER_SIZE);// will be freed by ffmpeg - context = avio_alloc_context( buffer, BUFFER_SIZE, 0, (void *)this, lodRead, nullptr, lodSeek); - - format = avformat_alloc_context(); - format->pb = context; - // filename is not needed - file was already open and stored in this->data; - int avfopen = avformat_open_input(&format, "dummyFilename", nullptr, nullptr); - - if (avfopen != 0) - { - return false; - } - // Retrieve stream information - if (avformat_find_stream_info(format, nullptr) < 0) - return false; - - // Find the first video stream - stream = -1; - for(ui32 i=0; inb_streams; i++) - { - if (format->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) - { - stream = i; - break; - } - } - - if (stream < 0) - // No video stream in that file - return false; - - // Find the decoder for the video stream - codec = avcodec_find_decoder(format->streams[stream]->codecpar->codec_id); - - if (codec == nullptr) - { - // Unsupported codec - return false; - } - - codecContext = avcodec_alloc_context3(codec); - if(!codecContext) - return false; - // Get a pointer to the codec context for the video stream - int ret = avcodec_parameters_to_context(codecContext, format->streams[stream]->codecpar); - if (ret < 0) - { - //We cannot get codec from parameters - avcodec_free_context(&codecContext); - return false; - } - - // Open codec - if ( avcodec_open2(codecContext, codec, nullptr) < 0 ) - { - // Could not open codec - codec = nullptr; - return false; - } - // Allocate video frame - frame = av_frame_alloc(); - - //setup scaling - if(scale) - { - pos.w = screen->w; - pos.h = screen->h; - } - else - { - pos.w = codecContext->width; - pos.h = codecContext->height; - } - - // Allocate a place to put our YUV image on that screen - if (overlay) - { - texture = SDL_CreateTexture( mainRenderer, SDL_PIXELFORMAT_IYUV, SDL_TEXTUREACCESS_STATIC, pos.w, pos.h); - } - else - { - dest = CSDL_Ext::newSurface(pos.w, pos.h); - destRect.x = destRect.y = 0; - destRect.w = pos.w; - destRect.h = pos.h; - } - - if (texture == nullptr && dest == nullptr) - return false; - - if (texture) - { // Convert the image into YUV format that SDL uses - sws = sws_getContext(codecContext->width, codecContext->height, codecContext->pix_fmt, - pos.w, pos.h, - AV_PIX_FMT_YUV420P, - SWS_BICUBIC, nullptr, nullptr, nullptr); - } - else - { - AVPixelFormat screenFormat = AV_PIX_FMT_NONE; - if (screen->format->Bshift > screen->format->Rshift) - { - // this a BGR surface - switch (screen->format->BytesPerPixel) - { - case 2: screenFormat = AV_PIX_FMT_BGR565; break; - case 3: screenFormat = AV_PIX_FMT_BGR24; break; - case 4: screenFormat = AV_PIX_FMT_BGR32; break; - default: return false; - } - } - else - { - // this a RGB surface - switch (screen->format->BytesPerPixel) - { - case 2: screenFormat = AV_PIX_FMT_RGB565; break; - case 3: screenFormat = AV_PIX_FMT_RGB24; break; - case 4: screenFormat = AV_PIX_FMT_RGB32; break; - default: return false; - } - } - - sws = sws_getContext(codecContext->width, codecContext->height, codecContext->pix_fmt, - pos.w, pos.h, screenFormat, - SWS_BICUBIC, nullptr, nullptr, nullptr); - } - - if (sws == nullptr) - return false; - - return true; -} - -// Read the next frame. Return false on error/end of file. -bool CVideoPlayer::nextFrame() -{ - AVPacket packet; - int frameFinished = 0; - bool gotError = false; - - if (sws == nullptr) - return false; - - while(!frameFinished) - { - int ret = av_read_frame(format, &packet); - if (ret < 0) - { - // Error. It's probably an end of file. - if (doLoop && !gotError) - { - // Rewind - frameTime = 0; - if (av_seek_frame(format, stream, 0, AVSEEK_FLAG_BYTE) < 0) - break; - gotError = true; - } - else - { - break; - } - } - else - { - // Is this a packet from the video stream? - if (packet.stream_index == stream) - { - // Decode video frame - int rc = avcodec_send_packet(codecContext, &packet); - if (rc >=0) - packet.size = 0; - rc = avcodec_receive_frame(codecContext, frame); - if (rc >= 0) - frameFinished = 1; - // Did we get a video frame? - if (frameFinished) - { - uint8_t *data[4]; - int linesize[4]; - - if (texture) { - av_image_alloc(data, linesize, pos.w, pos.h, AV_PIX_FMT_YUV420P, 1); - - sws_scale(sws, frame->data, frame->linesize, - 0, codecContext->height, data, linesize); - - SDL_UpdateYUVTexture(texture, nullptr, data[0], linesize[0], - data[1], linesize[1], - data[2], linesize[2]); - av_freep(&data[0]); - } - else - { - /* Avoid buffer overflow caused by sws_scale(): - * http://trac.ffmpeg.org/ticket/9254 - * Currently (ffmpeg-4.4 with SSE3 enabled) sws_scale() - * has a few requirements for target data buffers on rescaling: - * 1. buffer has to be aligned to be usable for SIMD instructions - * 2. buffer has to be padded to allow small overflow by SIMD instructions - * Unfortunately SDL_Surface does not provide these guarantees. - * This means that atempt to rescale directly into SDL surface causes - * memory corruption. Usually it happens on campaign selection screen - * where short video moves start spinning on mouse hover. - * - * To fix [1.] we use av_malloc() for memory allocation. - * To fix [2.] we add an `ffmpeg_pad` that provides plenty of space. - * We have to use intermdiate buffer and then use memcpy() to land it - * to SDL_Surface. - */ - size_t pic_bytes = dest->pitch * dest->h; - size_t ffmped_pad = 1024; /* a few bytes of overflow will go here */ - void * for_sws = av_malloc (pic_bytes + ffmped_pad); - data[0] = (ui8 *)for_sws; - linesize[0] = dest->pitch; - - sws_scale(sws, frame->data, frame->linesize, - 0, codecContext->height, data, linesize); - memcpy(dest->pixels, for_sws, pic_bytes); - av_free(for_sws); - } - } - } - - av_packet_unref(&packet); - } - } - - return frameFinished != 0; -} - -void CVideoPlayer::show( int x, int y, SDL_Surface *dst, bool update ) -{ - if (sws == nullptr) - return; - - pos.x = x; - pos.y = y; - CSDL_Ext::blitSurface(dest, destRect, dst, pos.topLeft()); - - if (update) - CSDL_Ext::updateRect(dst, pos); -} - -void CVideoPlayer::redraw( int x, int y, SDL_Surface *dst, bool update ) -{ - show(x, y, dst, update); -} - -void CVideoPlayer::update( int x, int y, SDL_Surface *dst, bool forceRedraw, bool update, std::function onVideoRestart) -{ - if (sws == nullptr) - return; - -#if (LIBAVUTIL_VERSION_MAJOR < 58) - auto packet_duration = frame->pkt_duration; -#else - auto packet_duration = frame->duration; -#endif - double frameEndTime = (frame->pts + packet_duration) * av_q2d(format->streams[stream]->time_base); - frameTime += GH.framerate().getElapsedMilliseconds() / 1000.0; - - if (frameTime >= frameEndTime ) - { - if (nextFrame()) - show(x,y,dst,update); - else - { - if(onVideoRestart) - onVideoRestart(); - VideoPath filenameToReopen = fname; // create copy to backup this->fname - open(filenameToReopen); - nextFrame(); - - // The y position is wrong at the first frame. - // Note: either the windows player or the linux player is - // broken. Compensate here until the bug is found. - show(x, y--, dst, update); - } - } - else - { - redraw(x, y, dst, update); - } -} - -void CVideoPlayer::close() -{ - fname = VideoPath(); - - if (sws) - { - sws_freeContext(sws); - sws = nullptr; - } - - if (texture) - { - SDL_DestroyTexture(texture); - texture = nullptr; - } - - if (dest) - { - SDL_FreeSurface(dest); - dest = nullptr; - } - - if (frame) - { - av_frame_free(&frame);//will be set to null - } - - if (codec) - { - avcodec_close(codecContext); - codec = nullptr; - } - if (codecContext) - { - avcodec_free_context(&codecContext); - } - - if (format) - { - avformat_close_input(&format); - } - - if (context) - { - av_free(context); - context = nullptr; - } -} - -std::pair, si64> CVideoPlayer::getAudio(const VideoPath & videoToOpen) -{ - std::pair, si64> dat(std::make_pair(nullptr, 0)); - - VideoPath fnameAudio; - - if (CResourceHandler::get()->existsResource(videoToOpen)) - fnameAudio = videoToOpen; - else - fnameAudio = videoToOpen.addPrefix("VIDEO/"); - - if (!CResourceHandler::get()->existsResource(fnameAudio)) - { - logGlobal->error("Error: video %s was not found", fnameAudio.getName()); - return dat; - } - - dataAudio = CResourceHandler::get()->load(fnameAudio); - - static const int BUFFER_SIZE = 4096; - - unsigned char * bufferAudio = (unsigned char *)av_malloc(BUFFER_SIZE);// will be freed by ffmpeg - AVIOContext * contextAudio = avio_alloc_context( bufferAudio, BUFFER_SIZE, 0, (void *)this, lodReadAudio, nullptr, lodSeekAudio); - - AVFormatContext * formatAudio = avformat_alloc_context(); - formatAudio->pb = contextAudio; - // filename is not needed - file was already open and stored in this->data; - int avfopen = avformat_open_input(&formatAudio, "dummyFilename", nullptr, nullptr); - - if (avfopen != 0) - { - return dat; - } - // Retrieve stream information - if (avformat_find_stream_info(formatAudio, nullptr) < 0) - return dat; - - // Find the first audio stream - int streamAudio = -1; - for(ui32 i = 0; i < formatAudio->nb_streams; i++) - { - if (formatAudio->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_AUDIO) - { - streamAudio = i; - break; - } - } - - if(streamAudio < 0) - return dat; - - const AVCodec *codecAudio = avcodec_find_decoder(formatAudio->streams[streamAudio]->codecpar->codec_id); - - AVCodecContext *codecContextAudio; - if (codecAudio != nullptr) - codecContextAudio = avcodec_alloc_context3(codecAudio); - - // Get a pointer to the codec context for the audio stream - if (streamAudio > -1) - { - int ret = avcodec_parameters_to_context(codecContextAudio, formatAudio->streams[streamAudio]->codecpar); - if (ret < 0) - { - //We cannot get codec from parameters - avcodec_free_context(&codecContextAudio); - } - } - - // Open codec - AVFrame *frameAudio; - if (codecAudio != nullptr) - { - if ( avcodec_open2(codecContextAudio, codecAudio, nullptr) < 0 ) - { - // Could not open codec - codecAudio = nullptr; - } - // Allocate audio frame - frameAudio = av_frame_alloc(); - } - - AVPacket packet; - - std::vector samples; - - while (av_read_frame(formatAudio, &packet) >= 0) - { - if(packet.stream_index == streamAudio) - { - int rc = avcodec_send_packet(codecContextAudio, &packet); - if (rc >= 0) - packet.size = 0; - rc = avcodec_receive_frame(codecContextAudio, frameAudio); - int bytesToRead = (frameAudio->nb_samples * 2 * (formatAudio->streams[streamAudio]->codecpar->bits_per_coded_sample / 8)); - if (rc >= 0) - for (int s = 0; s < bytesToRead; s += sizeof(ui8)) - { - ui8 value; - memcpy(&value, &frameAudio->data[0][s], sizeof(ui8)); - samples.push_back(value); - } - } - - av_packet_unref(&packet); - } - - typedef struct WAV_HEADER { - ui8 RIFF[4] = {'R', 'I', 'F', 'F'}; - ui32 ChunkSize; - ui8 WAVE[4] = {'W', 'A', 'V', 'E'}; - ui8 fmt[4] = {'f', 'm', 't', ' '}; - ui32 Subchunk1Size = 16; - ui16 AudioFormat = 1; - ui16 NumOfChan = 2; - ui32 SamplesPerSec = 22050; - ui32 bytesPerSec = 22050 * 2; - ui16 blockAlign = 2; - ui16 bitsPerSample = 16; - ui8 Subchunk2ID[4] = {'d', 'a', 't', 'a'}; - ui32 Subchunk2Size; - } wav_hdr; - - wav_hdr wav; - wav.ChunkSize = samples.size() + sizeof(wav_hdr) - 8; - wav.Subchunk2Size = samples.size() + sizeof(wav_hdr) - 44; - wav.SamplesPerSec = formatAudio->streams[streamAudio]->codecpar->sample_rate; - wav.bitsPerSample = formatAudio->streams[streamAudio]->codecpar->bits_per_coded_sample; - auto wavPtr = reinterpret_cast(&wav); - - dat = std::make_pair(std::make_unique(samples.size() + sizeof(wav_hdr)), samples.size() + sizeof(wav_hdr)); - std::copy(wavPtr, wavPtr + sizeof(wav_hdr), dat.first.get()); - std::copy(samples.begin(), samples.end(), dat.first.get() + sizeof(wav_hdr)); - - if (frameAudio) - av_frame_free(&frameAudio); - - if (codecAudio) - { - avcodec_close(codecContextAudio); - codecAudio = nullptr; - } - if (codecContextAudio) - avcodec_free_context(&codecContextAudio); - - if (formatAudio) - avformat_close_input(&formatAudio); - - if (contextAudio) - { - av_free(contextAudio); - contextAudio = nullptr; - } - - return dat; -} - -Point CVideoPlayer::size() -{ - if(frame) - return Point(frame->width, frame->height); - else - return Point(0, 0); -} - -// Plays a video. Only works for overlays. -bool CVideoPlayer::playVideo(int x, int y, bool stopOnKey, bool overlay) -{ - // Note: either the windows player or the linux player is - // broken. Compensate here until the bug is found. - y--; - - pos.x = x; - pos.y = y; - frameTime = 0.0; - - auto lastTimePoint = boost::chrono::steady_clock::now(); - - while(nextFrame()) - { - if(stopOnKey) - { - GH.input().fetchEvents(); - if(GH.input().ignoreEventsUntilInput()) - return false; - } - - SDL_Rect rect = CSDL_Ext::toSDL(pos); - - if(overlay) - { - SDL_RenderFillRect(mainRenderer, &rect); - } - else - { - SDL_RenderClear(mainRenderer); - } - SDL_RenderCopy(mainRenderer, texture, nullptr, &rect); - SDL_RenderPresent(mainRenderer); - -#if (LIBAVUTIL_VERSION_MAJOR < 58) - auto packet_duration = frame->pkt_duration; -#else - auto packet_duration = frame->duration; -#endif - // Framerate delay - double targetFrameTimeSeconds = packet_duration * av_q2d(format->streams[stream]->time_base); - 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; -} - -bool CVideoPlayer::openAndPlayVideo(const VideoPath & name, int x, int y, EVideoType videoType) -{ - bool scale; - bool stopOnKey; - bool overlay; - - switch(videoType) - { - case EVideoType::INTRO: - stopOnKey = true; - scale = true; - overlay = false; - break; - case EVideoType::SPELLBOOK: - default: - stopOnKey = false; - scale = false; - overlay = true; - } - open(name, false, true, scale); - bool ret = playVideo(x, y, stopOnKey, overlay); - close(); - return ret; -} - -CVideoPlayer::~CVideoPlayer() -{ - close(); -} - -#endif - diff --git a/client/CVideoHandler.h b/client/CVideoHandler.h deleted file mode 100644 index 89f8016a2..000000000 --- a/client/CVideoHandler.h +++ /dev/null @@ -1,131 +0,0 @@ -/* - * CVideoHandler.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 "../lib/Rect.h" -#include "../lib/filesystem/ResourcePath.h" - -struct SDL_Surface; -struct SDL_Texture; - -enum class EVideoType : ui8 -{ - INTRO = 0, // use entire window: stopOnKey = true, scale = true, overlay = false - SPELLBOOK // overlay video: stopOnKey = false, scale = false, overlay = true -}; - -class IVideoPlayer : boost::noncopyable -{ -public: - virtual bool open(const VideoPath & name, bool scale = false)=0; //true - succes - virtual void close()=0; - virtual bool nextFrame()=0; - virtual void show(int x, int y, SDL_Surface *dst, bool update = true)=0; - virtual void redraw(int x, int y, SDL_Surface *dst, bool update = true)=0; //reblits buffer - virtual bool wait()=0; - virtual int curFrame() const =0; - virtual int frameCount() const =0; -}; - -class IMainVideoPlayer : public IVideoPlayer -{ -public: - virtual ~IMainVideoPlayer() = default; - virtual void update(int x, int y, SDL_Surface *dst, bool forceRedraw, bool update = true, std::function restart = nullptr){} - virtual bool openAndPlayVideo(const VideoPath & name, int x, int y, EVideoType videoType) - { - return false; - } - virtual std::pair, si64> getAudio(const VideoPath & videoToOpen) { return std::make_pair(nullptr, 0); }; - virtual Point size() { return Point(0, 0); }; -}; - -class CEmptyVideoPlayer final : public IMainVideoPlayer -{ -public: - int curFrame() const override {return -1;}; - int frameCount() const override {return -1;}; - void redraw( int x, int y, SDL_Surface *dst, bool update = true ) override {}; - void show( int x, int y, SDL_Surface *dst, bool update = true ) override {}; - bool nextFrame() override {return false;}; - void close() override {}; - bool wait() override {return false;}; - bool open(const VideoPath & name, bool scale = false) override {return false;}; -}; - -#ifndef DISABLE_VIDEO - -struct AVFormatContext; -struct AVCodecContext; -struct AVCodec; -struct AVFrame; -struct AVIOContext; - -VCMI_LIB_NAMESPACE_BEGIN -class CInputStream; -VCMI_LIB_NAMESPACE_END - -class CVideoPlayer final : public IMainVideoPlayer -{ - int stream; // stream index in video - AVFormatContext *format; - AVCodecContext *codecContext; // codec context for stream - const AVCodec *codec; - AVFrame *frame; - struct SwsContext *sws; - - AVIOContext * context; - - VideoPath fname; //name of current video file (empty if idle) - - // Destination. Either overlay or dest. - - SDL_Texture *texture; - SDL_Surface *dest; - Rect destRect; // valid when dest is used - Rect pos; // destination on screen - - /// video playback currnet progress, in seconds - double frameTime; - bool doLoop; // loop through video - - bool playVideo(int x, int y, bool stopOnKey, bool overlay); - bool open(const VideoPath & fname, bool loop, bool useOverlay = false, bool scale = false); -public: - CVideoPlayer(); - ~CVideoPlayer(); - - bool init(); - bool open(const VideoPath & fname, bool scale = false) override; - void close() override; - bool nextFrame() override; // display next frame - - void show(int x, int y, SDL_Surface *dst, bool update = true) override; //blit current frame - void redraw(int x, int y, SDL_Surface *dst, bool update = true) override; //reblits buffer - void update(int x, int y, SDL_Surface *dst, bool forceRedraw, bool update = true, std::function onVideoRestart = nullptr) override; //moves to next frame if appropriate, and blits it or blits only if redraw parameter is set true - - // Opens video, calls playVideo, closes video; returns playVideo result (if whole video has been played) - bool openAndPlayVideo(const VideoPath & name, int x, int y, EVideoType videoType) override; - - std::pair, si64> getAudio(const VideoPath & videoToOpen) override; - - Point size() override; - - //TODO: - bool wait() override {return false;}; - int curFrame() const override {return -1;}; - int frameCount() const override {return -1;}; - - // public to allow access from ffmpeg IO functions - std::unique_ptr data; - std::unique_ptr dataAudio; -}; - -#endif diff --git a/client/Client.cpp b/client/Client.cpp index 238297fee..0f55cbd52 100644 --- a/client/Client.cpp +++ b/client/Client.cpp @@ -13,6 +13,7 @@ #include "CGameInfo.h" #include "CPlayerInterface.h" +#include "PlayerLocalState.h" #include "CServerHandler.h" #include "ClientNetPackVisitors.h" #include "adventureMap/AdventureMapInterface.h" @@ -29,13 +30,10 @@ #include "../lib/VCMIDirs.h" #include "../lib/UnlockGuard.h" #include "../lib/battle/BattleInfo.h" -#include "../lib/serializer/BinaryDeserializer.h" -#include "../lib/serializer/BinarySerializer.h" #include "../lib/serializer/Connection.h" #include "../lib/mapping/CMapService.h" #include "../lib/pathfinder/CGPathNode.h" #include "../lib/filesystem/Filesystem.h" -#include "../lib/registerTypes/RegisterTypesClientPacks.h" #include #include @@ -50,53 +48,6 @@ ThreadSafeVector CClient::waitingRequest; -template class CApplyOnCL; - -class CBaseForCLApply -{ -public: - virtual void applyOnClAfter(CClient * cl, CPack * pack) const =0; - virtual void applyOnClBefore(CClient * cl, CPack * pack) const =0; - virtual ~CBaseForCLApply(){} - - template static CBaseForCLApply * getApplier(const U * t = nullptr) - { - return new CApplyOnCL(); - } -}; - -template class CApplyOnCL : public CBaseForCLApply -{ -public: - void applyOnClAfter(CClient * cl, CPack * pack) const override - { - T * ptr = static_cast(pack); - ApplyClientNetPackVisitor visitor(*cl, *cl->gameState()); - ptr->visit(visitor); - } - void applyOnClBefore(CClient * cl, CPack * pack) const override - { - T * ptr = static_cast(pack); - ApplyFirstClientNetPackVisitor visitor(*cl, *cl->gameState()); - ptr->visit(visitor); - } -}; - -template<> class CApplyOnCL: public CBaseForCLApply -{ -public: - void applyOnClAfter(CClient * cl, CPack * pack) const override - { - logGlobal->error("Cannot apply on CL plain CPack!"); - assert(0); - } - void applyOnClBefore(CClient * cl, CPack * pack) const override - { - logGlobal->error("Cannot apply on CL plain CPack!"); - assert(0); - } -}; - CPlayerEnvironment::CPlayerEnvironment(PlayerColor player_, CClient * cl_, std::shared_ptr mainCallback_) : player(player_), cl(cl_), @@ -130,12 +81,9 @@ const CPlayerEnvironment::GameCb * CPlayerEnvironment::game() const return mainCallback.get(); } - CClient::CClient() { waitingRequest.clear(); - applier = std::make_shared>(); - registerTypesClientPacks(*applier); gs = nullptr; } @@ -203,137 +151,9 @@ void CClient::loadGame(CGameState * initializedGameState) reinitScripting(); initPlayerEnvironments(); - - // Loading of client state - disabled for now - // Since client no longer writes or loads its own state and instead receives it from server - // client state serializer will serialize its own copies of all pointers, e.g. heroes/towns/objects - // and on deserialize will create its own copies (instead of using copies from state received from server) - // Potential solutions: - // 1) Use server gamestate to deserialize pointers, so any pointer to same object will point to server instance and not our copy - // 2) Remove all serialization of pointers with instance ID's and restore them on load (including AI deserializer code) - // 3) Completely remove client savegame and send all information, like hero paths and sleeping status to server (either in form of hero properties or as some generic "client options" message -#ifdef BROKEN_CLIENT_STATE_SERIALIZATION_HAS_BEEN_FIXED - // try to deserialize client data including sleepingHeroes - try - { - boost::filesystem::path clientSaveName = *CResourceHandler::get()->getResourceName(ResourcePath(CSH->si->mapname, EResType::CLIENT_SAVEGAME)); - - if(clientSaveName.empty()) - throw std::runtime_error("Cannot open client part of " + CSH->si->mapname); - - std::unique_ptr loader (new CLoadFile(clientSaveName)); - serialize(loader->serializer, loader->serializer.version); - - logNetwork->info("Client data loaded."); - } - catch(std::exception & e) - { - logGlobal->info("Cannot load client data for game %s. Error: %s", CSH->si->mapname, e.what()); - } -#endif - initPlayerInterfaces(); } -void CClient::serialize(BinarySerializer & h) -{ - assert(h.saving); - ui8 players = static_cast(playerint.size()); - h & players; - - for(auto i = playerint.begin(); i != playerint.end(); i++) - { - logGlobal->trace("Saving player %s interface", i->first); - assert(i->first == i->second->playerID); - h & i->first; - h & i->second->dllName; - h & i->second->human; - i->second->saveGame(h); - } - -#if SCRIPTING_ENABLED - JsonNode scriptsState; - clientScripts->serializeState(h.saving, scriptsState); - h & scriptsState; -#endif -} - -void CClient::serialize(BinaryDeserializer & h) -{ - assert(!h.saving); - ui8 players = 0; - h & players; - - for(int i = 0; i < players; i++) - { - std::string dllname; - PlayerColor pid; - bool isHuman = false; - auto prevInt = LOCPLINT; - - h & pid; - h & dllname; - h & isHuman; - assert(dllname.length() == 0 || !isHuman); - if(pid == PlayerColor::NEUTRAL) - { - logGlobal->trace("Neutral battle interfaces are not serialized."); - continue; - } - - logGlobal->trace("Loading player %s interface", pid); - std::shared_ptr nInt; - if(dllname.length()) - nInt = CDynLibHandler::getNewAI(dllname); - else - nInt = std::make_shared(pid); - - nInt->dllName = dllname; - nInt->human = isHuman; - nInt->playerID = pid; - - bool shouldResetInterface = true; - // Client no longer handle this player at all - if(!vstd::contains(CSH->getAllClientPlayers(CSH->logicConnection->connectionID), pid)) - { - logGlobal->trace("Player %s is not belong to this client. Destroying interface", pid); - } - else if(isHuman && !vstd::contains(CSH->getHumanColors(), pid)) - { - logGlobal->trace("Player %s is no longer controlled by human. Destroying interface", pid); - } - else if(!isHuman && vstd::contains(CSH->getHumanColors(), pid)) - { - logGlobal->trace("Player %s is no longer controlled by AI. Destroying interface", pid); - } - else - { - installNewPlayerInterface(nInt, pid); - shouldResetInterface = false; - } - - // loadGame needs to be called after initGameInterface to load paths correctly - // initGameInterface is called in installNewPlayerInterface - nInt->loadGame(h); - - if (shouldResetInterface) - { - nInt.reset(); - LOCPLINT = prevInt; - } - } - -#if SCRIPTING_ENABLED - { - JsonNode scriptsState; - h & scriptsState; - clientScripts->serializeState(h.saving, scriptsState); - } -#endif - - logNetwork->trace("Loaded client part of save %d ms", CSH->th->getDiff()); -} - void CClient::save(const std::string & fname) { if(!gs->currentBattles.empty()) @@ -343,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() @@ -476,7 +296,7 @@ void CClient::initPlayerInterfaces() logNetwork->trace("Initialized player interfaces %d ms", CSH->th->getDiff()); } -std::string CClient::aiNameForPlayer(const PlayerSettings & ps, bool battleAI, bool alliedToHuman) +std::string CClient::aiNameForPlayer(const PlayerSettings & ps, bool battleAI, bool alliedToHuman) const { if(ps.name.size()) { @@ -488,7 +308,7 @@ std::string CClient::aiNameForPlayer(const PlayerSettings & ps, bool battleAI, b return aiNameForPlayer(battleAI, alliedToHuman); } -std::string CClient::aiNameForPlayer(bool battleAI, bool alliedToHuman) +std::string CClient::aiNameForPlayer(bool battleAI, bool alliedToHuman) const { const int sensibleAILimit = settings["session"]["oneGoodAI"].Bool() ? 1 : PlayerColor::PLAYER_LIMIT_I; std::string goodAdventureAI = alliedToHuman ? settings["server"]["alliedAI"].String() : settings["server"]["playerAI"].String(); @@ -528,41 +348,35 @@ void CClient::installNewBattleInterface(std::shared_ptr ba } } -void CClient::handlePack(CPack * pack) +void CClient::handlePack(CPackForClient & pack) { - CBaseForCLApply * apply = applier->getApplier(CTypeList::getInstance().getTypeID(pack)); //find the applier - if(apply) + ApplyClientNetPackVisitor afterVisitor(*this, *gameState()); + ApplyFirstClientNetPackVisitor beforeVisitor(*this, *gameState()); + + pack.visit(beforeVisitor); + logNetwork->trace("\tMade first apply on cl: %s", typeid(pack).name()); { - apply->applyOnClBefore(this, pack); - 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()); - apply->applyOnClAfter(this, pack); - logNetwork->trace("\tMade second apply on cl: %s", typeid(*pack).name()); + boost::unique_lock lock(CGameState::mutex); + gs->apply(pack); } - else - { - logNetwork->error("Message %s cannot be applied, cannot find applier!", 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; } @@ -571,8 +385,8 @@ void CClient::battleStarted(const BattleInfo * info) { std::shared_ptr att; std::shared_ptr def; - auto & leftSide = info->sides[0]; - auto & rightSide = info->sides[1]; + const auto & leftSide = info->getSide(BattleSide::LEFT_SIDE); + const auto & rightSide = info->getSide(BattleSide::RIGHT_SIDE); for(auto & battleCb : battleCallbacks) { @@ -581,17 +395,17 @@ void CClient::battleStarted(const BattleInfo * info) } //If quick combat is not, do not prepare interfaces for battleint - auto callBattleStart = [&](PlayerColor color, ui8 side) + auto callBattleStart = [&](PlayerColor color, BattleSide side) { if(vstd::contains(battleints, color)) battleints[color]->battleStart(info->battleID, leftSide.armyObject, rightSide.armyObject, info->tile, leftSide.hero, rightSide.hero, side, info->replayAllowed); }; - callBattleStart(leftSide.color, 0); - callBattleStart(rightSide.color, 1); - callBattleStart(PlayerColor::UNFLAGGABLE, 1); + callBattleStart(leftSide.color, BattleSide::LEFT_SIDE); + callBattleStart(rightSide.color, BattleSide::RIGHT_SIDE); + callBattleStart(PlayerColor::UNFLAGGABLE, BattleSide::RIGHT_SIDE); if(settings["session"]["spectate"].Bool() && !settings["session"]["spectate-skip-battle"].Bool()) - callBattleStart(PlayerColor::SPECTATOR, 1); + callBattleStart(PlayerColor::SPECTATOR, BattleSide::RIGHT_SIDE); if(vstd::contains(playerint, leftSide.color) && playerint[leftSide.color]->human) att = std::dynamic_pointer_cast(playerint[leftSide.color]); @@ -608,9 +422,9 @@ void CClient::battleStarted(const BattleInfo * info) { auto side = interface->cb->getBattle(info->battleID)->playerToSide(interface->playerID); - if(interface->playerID == info->sides[info->tacticsSide].color) + if(interface->playerID == info->getSide(info->tacticsSide).color) { - auto action = BattleAction::makeEndOFTacticPhase(*side); + auto action = BattleAction::makeEndOFTacticPhase(side); interface->cb->battleMakeTacticAction(info->battleID, action); } } @@ -642,7 +456,7 @@ void CClient::battleStarted(const BattleInfo * info) if(info->tacticDistance) { - auto tacticianColor = info->sides[info->tacticsSide].color; + auto tacticianColor = info->getSide(info->tacticsSide).color; if (vstd::contains(battleints, tacticianColor)) battleints[tacticianColor]->yourTacticPhase(info->battleID, info->tacticDistance); @@ -651,9 +465,11 @@ void CClient::battleStarted(const BattleInfo * info) void CClient::battleFinished(const BattleID & battleID) { - for(auto & side : gs->getBattle(battleID)->sides) - if(battleCallbacks.count(side.color)) - battleCallbacks[side.color]->onBattleEnded(battleID); + for(auto side : { BattleSide::ATTACKER, BattleSide::DEFENDER }) + { + if(battleCallbacks.count(gs->getBattle(battleID)->getSide(side).color)) + battleCallbacks[gs->getBattle(battleID)->getSide(side).color]->onBattleEnded(battleID); + } if(settings["session"]["spectate"].Bool() && !settings["session"]["spectate-skip-battle"].Bool()) battleCallbacks[PlayerColor::SPECTATOR]->onBattleEnded(battleID); @@ -678,12 +494,32 @@ void CClient::startPlayerBattleAction(const BattleID & battleID, PlayerColor col } } +void CClient::updatePath(const ObjectInstanceID & id) +{ + invalidatePaths(); + auto hero = getHero(id); + updatePath(hero); +} + +void CClient::updatePath(const CGHeroInstance * hero) +{ + if(LOCPLINT && hero) + LOCPLINT->localState->verifyPath(hero); +} + void CClient::invalidatePaths() { boost::unique_lock pathLock(pathCacheMutex); pathCache.clear(); } +vstd::RNG & CClient::getRandomGenerator() +{ + // Client should use CRandomGenerator::getDefault() for UI logic + // Gamestate should never call this method on client! + throw std::runtime_error("Illegal access to random number generator from client code!"); +} + std::shared_ptr CClient::getPathsInfo(const CGHeroInstance * h) { assert(h); diff --git a/client/Client.h b/client/Client.h index a04b9aa71..ce1276b06 100644 --- a/client/Client.h +++ b/client/Client.h @@ -16,17 +16,13 @@ VCMI_LIB_NAMESPACE_BEGIN -struct CPack; struct CPackForServer; class IBattleEventsReceiver; class CBattleGameInterface; class CGameInterface; -class BinaryDeserializer; -class BinarySerializer; class BattleAction; class BattleInfo; - -template class CApplier; +struct BankConfig; #if SCRIPTING_ENABLED namespace scripting @@ -131,8 +127,6 @@ public: void newGame(CGameState * gameState); void loadGame(CGameState * gameState); - void serialize(BinarySerializer & h); - void serialize(BinaryDeserializer & h); void save(const std::string & fname); void endNetwork(); @@ -141,35 +135,38 @@ public: void initMapHandler(); void initPlayerEnvironments(); void initPlayerInterfaces(); - std::string aiNameForPlayer(const PlayerSettings & ps, bool battleAI, bool alliedToHuman); //empty means no AI -> human - std::string aiNameForPlayer(bool battleAI, bool alliedToHuman); + std::string aiNameForPlayer(const PlayerSettings & ps, bool battleAI, bool alliedToHuman) const; //empty means no AI -> human + std::string aiNameForPlayer(bool battleAI, bool alliedToHuman) const; void installNewPlayerInterface(std::shared_ptr gameInterface, PlayerColor color, bool battlecb = false); void installNewBattleInterface(std::shared_ptr battleInterface, PlayerColor color, bool needCallback = true); static ThreadSafeVector waitingRequest; //FIXME: make this normal field (need to join all threads before client destruction) - void handlePack(CPack * 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); void startPlayerBattleAction(const BattleID & battleID, PlayerColor color); - void invalidatePaths(); + void invalidatePaths(); // clears this->pathCache() + void updatePath(const ObjectInstanceID & heroID); // invalidatePaths and update displayed hero path + void updatePath(const CGHeroInstance * hero); std::shared_ptr getPathsInfo(const CGHeroInstance * h); friend class CCallback; //handling players actions 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 createObject(const int3 & visitablePosition, const PlayerColor & initiator, MapObjectID type, MapObjectSubID subtype) override {}; + void createBoat(const int3 & visitablePosition, BoatId type, PlayerColor initiator) override {}; void setOwner(const CGObjectInstance * obj, PlayerColor owner) override {}; void giveExperience(const CGHeroInstance * hero, TExpType val) override {}; void changePrimSkill(const CGHeroInstance * hero, PrimarySkill which, si64 val, bool abs = false) override {}; void changeSecSkill(const CGHeroInstance * hero, SecondarySkill which, int val, bool abs = false) override {}; - void showBlockingDialog(BlockingDialog * iw) override {}; + void showBlockingDialog(const IObjectInterface * caller, BlockingDialog * iw) override {}; void showGarrisonDialog(ObjectInstanceID upobj, ObjectInstanceID hid, bool removableUnits) override {}; void showTeleportDialog(TeleportDialog * iw) override {}; void showObjectWindow(const CGObjectInstance * object, EOpenWindowMode window, const CGHeroInstance * visitor, bool addQuery) override {}; @@ -189,17 +186,17 @@ public: void removeAfterVisit(const CGObjectInstance * object) override {}; bool swapGarrisonOnSiege(ObjectInstanceID tid) override {return false;}; - bool giveHeroNewArtifact(const CGHeroInstance * h, const CArtifact * artType, ArtifactPosition pos) override {return false;} - bool putArtifact(const ArtifactLocation & al, const CArtifactInstance * art, std::optional askAssemble) 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 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;}; void heroVisitCastle(const CGTownInstance * obj, const CGHeroInstance * hero) override {}; void visitCastleObjects(const CGTownInstance * obj, const CGHeroInstance * hero) override {}; void stopHeroVisitCastle(const CGTownInstance * obj, const CGHeroInstance * hero) override {}; - void startBattlePrimary(const CArmedInstance * army1, const CArmedInstance * army2, int3 tile, const CGHeroInstance * hero1, const CGHeroInstance * hero2, bool creatureBank = false, const CGTownInstance * town = nullptr) override {}; //use hero=nullptr for no hero - void startBattleI(const CArmedInstance * army1, const CArmedInstance * army2, int3 tile, bool creatureBank = false) override {}; //if any of armies is hero, hero will be used - void startBattleI(const CArmedInstance * army1, const CArmedInstance * army2, bool creatureBank = false) override {}; //if any of armies is hero, hero will be used, visitable tile of second obj is place of battle + void startBattle(const CArmedInstance * army1, const CArmedInstance * army2, int3 tile, const CGHeroInstance * hero1, const CGHeroInstance * hero2, const BattleLayout & layout, const CGTownInstance * town) override {}; //use hero=nullptr for no hero + void startBattle(const CArmedInstance * army1, const CArmedInstance * army2) override {}; //if any of armies is hero, hero will be used bool moveHero(ObjectInstanceID hid, int3 dst, EMovementMode movementMode, bool transit = false, PlayerColor asker = PlayerColor::NEUTRAL) override {return false;}; void giveHeroBonus(GiveBonus * bonus) override {}; void setMovePoints(SetMovePoints * smp) override {}; @@ -207,19 +204,24 @@ 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 {}; void changeFogOfWar(int3 center, ui32 radius, PlayerColor player, ETileVisibility mode) override {} - void changeFogOfWar(std::unordered_set & tiles, PlayerColor player, ETileVisibility mode) override {} + void changeFogOfWar(const std::unordered_set & tiles, PlayerColor player, ETileVisibility mode) override {} void setObjPropertyValue(ObjectInstanceID objid, ObjProperty prop, int32_t value) override {}; void setObjPropertyID(ObjectInstanceID objid, ObjProperty prop, ObjPropertyID identifier) override {}; + void setBankObjectConfiguration(ObjectInstanceID objid, const BankConfig & configuration) override {}; + void setRewardableObjectConfiguration(ObjectInstanceID objid, const Rewardable::Configuration & configuration) override {}; + void setRewardableObjectConfiguration(ObjectInstanceID townInstanceID, BuildingID buildingID, const Rewardable::Configuration & configuration) override{}; void showInfoDialog(InfoWindow * iw) override {}; void removeGUI() const; + vstd::RNG & getRandomGenerator() override; + #if SCRIPTING_ENABLED scripting::Pool * getGlobalContextPool() const override; #endif @@ -233,8 +235,6 @@ private: #endif std::unique_ptr clientEventBus; - std::shared_ptr> applier; - mutable boost::mutex pathCacheMutex; std::map> pathCache; diff --git a/client/ClientCommandManager.cpp b/client/ClientCommandManager.cpp index 5bba85706..fc41ffab1 100644 --- a/client/ClientCommandManager.cpp +++ b/client/ClientCommandManager.cpp @@ -18,6 +18,7 @@ #include "gui/CGuiHandler.h" #include "gui/WindowHandler.h" #include "render/IRenderHandler.h" +#include "render/AssetGenerator.h" #include "ClientNetPackVisitors.h" #include "../lib/CConfigHandler.h" #include "../lib/gameState/CGameState.h" @@ -30,15 +31,14 @@ #include "../lib/mapObjects/CGHeroInstance.h" #include "render/CAnimation.h" #include "../CCallback.h" -#include "../lib/CGeneralTextHandler.h" +#include "../lib/texts/CGeneralTextHandler.h" #include "../lib/filesystem/Filesystem.h" #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 "CMT.h" +#include "../lib/serializer/Connection.h" #ifdef SCRIPTING_ENABLED #include "../lib/ScriptHandler.h" @@ -82,31 +82,37 @@ void ClientCommandManager::handleGoSoloCommand() printCommandMessage("Game is not in playing state"); return; } - PlayerColor color; + if(session["aiSolo"].Bool()) { - for(auto & elem : CSH->client->gameState()->players) + // unlikely it will work but just in case to be consistent + for(auto & color : CSH->getAllClientPlayers(CSH->logicConnection->connectionID)) { - if(elem.second.human) - CSH->client->installNewPlayerInterface(std::make_shared(elem.first), elem.first); + if(color.isValidPlayer() && CSH->client->getStartInfo()->playerInfos.at(color).isControlledByHuman()) + { + CSH->client->installNewPlayerInterface(std::make_shared(color), color); + } } } else { - color = LOCPLINT->playerID; + PlayerColor currentColor = LOCPLINT->playerID; CSH->client->removeGUI(); - for(auto & elem : CSH->client->gameState()->players) + + for(auto & color : CSH->getAllClientPlayers(CSH->logicConnection->connectionID)) { - if(elem.second.human) + if(color.isValidPlayer() && CSH->client->getStartInfo()->playerInfos.at(color).isControlledByHuman()) { - auto AiToGive = CSH->client->aiNameForPlayer(*CSH->client->getPlayerSettings(elem.first), false, false); - printCommandMessage("Player " + elem.first.toString() + " will be lead by " + AiToGive, ELogLevel::INFO); - CSH->client->installNewPlayerInterface(CDynLibHandler::getNewAI(AiToGive), elem.first); + auto AiToGive = CSH->client->aiNameForPlayer(*CSH->client->getPlayerSettings(color), false, false); + printCommandMessage("Player " + color.toString() + " will be lead by " + AiToGive, ELogLevel::INFO); + CSH->client->installNewPlayerInterface(CDynLibHandler::getNewAI(AiToGive), color); } } + GH.windows().totalRedraw(); - giveTurn(color); + giveTurn(currentColor); } + session["aiSolo"].Bool() = !session["aiSolo"].Bool(); } @@ -179,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) @@ -248,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); @@ -381,8 +394,7 @@ void ClientCommandManager::handleDef2bmpCommand(std::istringstream& singleWordBu { std::string URI; singleWordBuffer >> URI; - auto anim = GH.renderHandler().loadAnimation(AnimationPath::builtin(URI)); - anim->preload(); + auto anim = GH.renderHandler().loadAnimation(AnimationPath::builtin(URI), EImageBlitMode::SIMPLE); anim->exportBitmaps(VCMIDirs::get().userExtractedPath()); } @@ -447,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()); } @@ -496,6 +508,12 @@ void ClientCommandManager::handleVsLog(std::istringstream & singleWordBuffer) logVisual->setKey(key); } +void ClientCommandManager::handleGenerateAssets() +{ + AssetGenerator::generateAll(); + printCommandMessage("All assets generated"); +} + void ClientCommandManager::printCommandMessage(const std::string &commandMessage, ELogLevel::ELogLevel messageType) { switch(messageType) @@ -580,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(); @@ -618,6 +639,9 @@ void ClientCommandManager::processCommand(const std::string & message, bool call else if(commandName == "vslog") handleVsLog(singleWordBuffer); + else if(message=="generate assets") + handleGenerateAssets(); + else { if (!commandName.empty() && !vstd::iswithin(commandName[0], 0, ' ')) // filter-out debugger/IDE noise diff --git a/client/ClientCommandManager.h b/client/ClientCommandManager.h index 56bd4619e..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(); @@ -66,7 +66,7 @@ class ClientCommandManager //take mantis #2292 issue about account if thinking a // Export file into Extracted directory void handleExtractCommand(std::istringstream& singleWordBuffer); - // Print in console the current bonuses for curent army + // Print in console the current bonuses for current army void handleBonusesCommand(std::istringstream & singleWordBuffer); // Get what artifact is present on artifact slot with specified ID for hero with specified ID @@ -84,6 +84,9 @@ class ClientCommandManager //take mantis #2292 issue about account if thinking a // shows object graph void handleVsLog(std::istringstream & singleWordBuffer); + // generate all assets + void handleGenerateAssets(); + // Prints in Chat the given message void printCommandMessage(const std::string &commandMessage, ELogLevel::ELogLevel messageType = ELogLevel::NOT_SET); void giveTurn(const PlayerColor &color); diff --git a/client/ClientNetPackVisitors.h b/client/ClientNetPackVisitors.h index 51812dedc..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; @@ -47,8 +48,7 @@ public: void visitBulkRebalanceStacks(BulkRebalanceStacks & pack) override; void visitBulkSmartRebalanceStacks(BulkSmartRebalanceStacks & pack) override; void visitPutArtifact(PutArtifact & pack) override; - void visitEraseArtifact(EraseArtifact & pack) override; - void visitMoveArtifact(MoveArtifact & pack) override; + void visitEraseArtifact(BulkEraseArtifacts & pack) override; void visitBulkMoveArtifacts(BulkMoveArtifacts & pack) override; void visitAssembledArtifact(AssembledArtifact & pack) override; void visitDisassembledArtifact(DisassembledArtifact & pack) override; diff --git a/client/ConditionalWait.h b/client/ConditionalWait.h index 0df2de789..22262fade 100644 --- a/client/ConditionalWait.h +++ b/client/ConditionalWait.h @@ -33,7 +33,7 @@ class ConditionalWait void set(bool value) { - boost::unique_lock lock(mx); + std::unique_lock lock(mx); isBusyValue = value; } @@ -59,15 +59,14 @@ public: bool isBusy() { - std::unique_lock lock(mx); + std::unique_lock lock(mx); return isBusyValue; } void waitWhileBusy() { - std::unique_lock un(mx); - while(isBusyValue) - cond.wait(un); + std::unique_lock un(mx); + cond.wait(un, [this](){ return !isBusyValue;}); if (isTerminating) throw TerminationRequestedException(); diff --git a/client/GameChatHandler.cpp b/client/GameChatHandler.cpp index e93208bd9..34d5866e9 100644 --- a/client/GameChatHandler.cpp +++ b/client/GameChatHandler.cpp @@ -21,12 +21,11 @@ #include "../CCallback.h" #include "../lib/networkPacks/PacksForLobby.h" -#include "../lib/TextOperations.h" #include "../lib/mapObjects/CArmedInstance.h" #include "../lib/CConfigHandler.h" -#include "../lib/MetaString.h" #include "../lib/VCMI_Lib.h" -#include "../lib/CGeneralTextHandler.h" +#include "../lib/texts/CGeneralTextHandler.h" +#include "../lib/texts/TextOperations.h" const std::vector & GameChatHandler::getChatHistory() const { @@ -94,7 +93,7 @@ void GameChatHandler::onNewGameMessageReceived(PlayerColor sender, const std::st playerName = LOCPLINT->cb->getStartInfo()->playerInfos.at(sender).name; if (sender.isSpectator()) - playerName = "Spectator"; // FIXME: translate? Provide nickname somewhere? + playerName = VLC->generaltexth->translate("vcmi.lobby.login.spectator"); chatHistory.push_back({playerName, messageText, timeFormatted}); diff --git a/client/HeroMovementController.cpp b/client/HeroMovementController.cpp index ca6311c6b..8584ddb44 100644 --- a/client/HeroMovementController.cpp +++ b/client/HeroMovementController.cpp @@ -11,7 +11,6 @@ #include "HeroMovementController.h" #include "CGameInfo.h" -#include "CMusicHandler.h" #include "CPlayerInterface.h" #include "PlayerLocalState.h" #include "adventureMap/AdventureMapInterface.h" @@ -19,10 +18,13 @@ #include "gui/CGuiHandler.h" #include "gui/CursorHandler.h" #include "mapView/mapHandler.h" +#include "media/ISoundPlayer.h" #include "../CCallback.h" #include "ConditionalWait.h" +#include "../lib/CConfigHandler.h" +#include "../lib/CRandomGenerator.h" #include "../lib/pathfinder/CGPathNode.h" #include "../lib/mapObjects/CGHeroInstance.h" #include "../lib/networkPacks/PacksForClient.h" @@ -152,8 +154,12 @@ void HeroMovementController::onTryMoveHero(const CGHeroInstance * hero, const Tr if(details.result == TryMoveHero::EMBARK || details.result == TryMoveHero::DISEMBARK) { - if(hero->getRemovalSound() && hero->tempOwner == LOCPLINT->playerID) - CCS->soundh->playSound(hero->getRemovalSound().value()); + if (hero->tempOwner == LOCPLINT->playerID) + { + auto removalSound = hero->getRemovalSound(CRandomGenerator::getDefault()); + if (removalSound) + CCS->soundh->playSound(removalSound.value()); + } } bool directlyAttackingCreature = @@ -285,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) @@ -369,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 6789486d0..cf1c896a1 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" @@ -29,10 +30,8 @@ #include "../CCallback.h" #include "../lib/filesystem/Filesystem.h" #include "../lib/filesystem/FileInfo.h" -#include "../lib/serializer/BinarySerializer.h" #include "../lib/serializer/Connection.h" -#include "../lib/CGeneralTextHandler.h" -#include "../lib/CHeroHandler.h" +#include "../lib/texts/CGeneralTextHandler.h" #include "../lib/VCMI_Lib.h" #include "../lib/mapping/CMap.h" #include "../lib/VCMIDirs.h" @@ -102,14 +101,14 @@ void callBattleInterfaceIfPresentForBothSides(CClient & cl, const BattleID & bat { assert(cl.gameState()->getBattle(battleID)); - if (!cl.gameState()->getBattle(battleID)) + if(!cl.gameState()->getBattle(battleID)) { logGlobal->error("Attempt to call battle interface without ongoing battle!"); return; } - callOnlyThatBattleInterface(cl, cl.gameState()->getBattle(battleID)->sides[0].color, ptr, std::forward(args)...); - callOnlyThatBattleInterface(cl, cl.gameState()->getBattle(battleID)->sides[1].color, ptr, std::forward(args)...); + callOnlyThatBattleInterface(cl, cl.gameState()->getBattle(battleID)->getSide(BattleSide::ATTACKER).color, ptr, std::forward(args)...); + callOnlyThatBattleInterface(cl, cl.gameState()->getBattle(battleID)->getSide(BattleSide::DEFENDER).color, ptr, std::forward(args)...); if(settings["session"]["spectate"].Bool() && !settings["session"]["spectate-skip-battle"].Bool() && LOCPLINT->battleInt) { callOnlyThatBattleInterface(cl, PlayerColor::SPECTATOR, ptr, std::forward(args)...); @@ -118,7 +117,7 @@ void callBattleInterfaceIfPresentForBothSides(CClient & cl, const BattleID & bat void ApplyClientNetPackVisitor::visitSetResources(SetResources & pack) { - //todo: inform on actual resource set transfered + //todo: inform on actual resource set transferred callInterfaceIfPresent(cl, pack.player, &IGameEventsReceiver::receivedResource); } @@ -162,17 +161,23 @@ void ApplyClientNetPackVisitor::visitSetMana(SetMana & pack) if(settings["session"]["headless"].Bool()) return; - for (auto window : GH.windows().findWindows()) + for(auto window : GH.windows().findWindows()) window->heroManaPointsChanged(h); } void ApplyClientNetPackVisitor::visitSetMovePoints(SetMovePoints & pack) { const CGHeroInstance *h = cl.getHero(pack.hid); - cl.invalidatePaths(); + cl.updatePath(h); 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) @@ -230,7 +235,7 @@ void ApplyClientNetPackVisitor::visitSetStackType(SetStackType & pack) void ApplyClientNetPackVisitor::visitEraseStack(EraseStack & pack) { dispatchGarrisonChange(cl, pack.army, ObjectInstanceID()); - cl.invalidatePaths(); //it is possible to remove last non-native unit for current terrain and lose movement penalty + cl.updatePath(pack.army); //it is possible to remove last non-native unit for current terrain and lose movement penalty } void ApplyClientNetPackVisitor::visitSwapStacks(SwapStacks & pack) @@ -238,15 +243,14 @@ void ApplyClientNetPackVisitor::visitSwapStacks(SwapStacks & pack) dispatchGarrisonChange(cl, pack.srcArmy, pack.dstArmy); if(pack.srcArmy != pack.dstArmy) - cl.invalidatePaths(); // adding/removing units may change terrain type penalty based on creature native terrains + cl.updatePath(pack.dstArmy); // adding/removing units may change terrain type penalty based on creature native terrains } void ApplyClientNetPackVisitor::visitInsertNewStack(InsertNewStack & pack) { dispatchGarrisonChange(cl, pack.army, ObjectInstanceID()); - if(gs.getHero(pack.army)) - cl.invalidatePaths(); // adding/removing units may change terrain type penalty based on creature native terrains + cl.updatePath(pack.army); // adding/removing units may change terrain type penalty based on creature native terrains } void ApplyClientNetPackVisitor::visitRebalanceStacks(RebalanceStacks & pack) @@ -254,7 +258,10 @@ void ApplyClientNetPackVisitor::visitRebalanceStacks(RebalanceStacks & pack) dispatchGarrisonChange(cl, pack.srcArmy, pack.dstArmy); if(pack.srcArmy != pack.dstArmy) - cl.invalidatePaths(); // adding/removing units may change terrain type penalty based on creature native terrains + { + cl.updatePath(pack.srcArmy); // adding/removing units may change terrain type penalty based on creature native terrains + cl.updatePath(pack.dstArmy); + } } void ApplyClientNetPackVisitor::visitBulkRebalanceStacks(BulkRebalanceStacks & pack) @@ -267,7 +274,10 @@ void ApplyClientNetPackVisitor::visitBulkRebalanceStacks(BulkRebalanceStacks & p dispatchGarrisonChange(cl, pack.moves[0].srcArmy, destArmy); if(pack.moves[0].srcArmy != destArmy) - cl.invalidatePaths(); // adding/removing units may change terrain type penalty based on creature native terrains + { + cl.updatePath(destArmy); // adding/removing units may change terrain type penalty based on creature native terrains + cl.updatePath(pack.moves[0].srcArmy); + } } } @@ -291,50 +301,53 @@ void ApplyClientNetPackVisitor::visitPutArtifact(PutArtifact & pack) callInterfaceIfPresent(cl, cl.getOwner(pack.al.artHolder), &IGameEventsReceiver::askToAssembleArtifact, pack.al); } -void ApplyClientNetPackVisitor::visitEraseArtifact(EraseArtifact & pack) +void ApplyClientNetPackVisitor::visitEraseArtifact(BulkEraseArtifacts & pack) { - callInterfaceIfPresent(cl, cl.getOwner(pack.al.artHolder), &IGameEventsReceiver::artifactRemoved, pack.al); -} - -void ApplyClientNetPackVisitor::visitMoveArtifact(MoveArtifact & pack) -{ - auto moveArtifact = [this, &pack](PlayerColor player) -> void - { - callInterfaceIfPresent(cl, player, &IGameEventsReceiver::artifactMoved, pack.src, pack.dst); - if(pack.askAssemble) - callInterfaceIfPresent(cl, player, &IGameEventsReceiver::askToAssembleArtifact, pack.dst); - }; - - moveArtifact(pack.interfaceOwner); - if(pack.interfaceOwner != cl.getOwner(pack.dst.artHolder)) - moveArtifact(cl.getOwner(pack.dst.artHolder)); - - cl.invalidatePaths(); // hero might have equipped/unequipped Angel Wings + cl.updatePath(pack.artHolder); + for(const auto & slotErase : pack.posPack) + callInterfaceIfPresent(cl, cl.getOwner(pack.artHolder), &IGameEventsReceiver::artifactRemoved, ArtifactLocation(pack.artHolder, slotErase)); } void ApplyClientNetPackVisitor::visitBulkMoveArtifacts(BulkMoveArtifacts & pack) { - auto applyMove = [this, &pack](std::vector & artsPack) -> void + const auto dstOwner = cl.getOwner(pack.dstArtHolder); + const auto applyMove = [this, &pack, dstOwner](std::vector & artsPack) { - for(auto & slotToMove : artsPack) + for(const auto & slotToMove : artsPack) { - auto srcLoc = ArtifactLocation(pack.srcArtHolder, slotToMove.srcPos); - auto dstLoc = ArtifactLocation(pack.dstArtHolder, slotToMove.dstPos); - MoveArtifact ma(pack.interfaceOwner, srcLoc, dstLoc, pack.askAssemble); - visitMoveArtifact(ma); + const auto srcLoc = ArtifactLocation(pack.srcArtHolder, slotToMove.srcPos); + const auto dstLoc = ArtifactLocation(pack.dstArtHolder, slotToMove.dstPos); + + callInterfaceIfPresent(cl, pack.interfaceOwner, &IGameEventsReceiver::artifactMoved, srcLoc, dstLoc); + if(slotToMove.askAssemble) + callInterfaceIfPresent(cl, pack.interfaceOwner, &IGameEventsReceiver::askToAssembleArtifact, dstLoc); + if(pack.interfaceOwner != dstOwner) + callInterfaceIfPresent(cl, dstOwner, &IGameEventsReceiver::artifactMoved, srcLoc, dstLoc); + + cl.updatePath(pack.srcArtHolder); // hero might have equipped/unequipped Angel Wings + cl.updatePath(pack.dstArtHolder); } }; - auto srcOwner = cl.getOwner(pack.srcArtHolder); - auto dstOwner = cl.getOwner(pack.dstArtHolder); + size_t possibleAssemblyNumOfArts = 0; + const auto calcPossibleAssemblyNumOfArts = [&possibleAssemblyNumOfArts](const auto & slotToMove) + { + if(slotToMove.askAssemble) + possibleAssemblyNumOfArts++; + }; + std::for_each(pack.artsPack0.cbegin(), pack.artsPack0.cend(), calcPossibleAssemblyNumOfArts); + std::for_each(pack.artsPack1.cbegin(), pack.artsPack1.cend(), calcPossibleAssemblyNumOfArts); + // Begin a session of bulk movement of arts. It is not necessary but useful for the client optimization. - callInterfaceIfPresent(cl, srcOwner, &IGameEventsReceiver::bulkArtMovementStart, pack.artsPack0.size() + pack.artsPack1.size()); - if(srcOwner != dstOwner) - callInterfaceIfPresent(cl, dstOwner, &IGameEventsReceiver::bulkArtMovementStart, pack.artsPack0.size() + pack.artsPack1.size()); + callInterfaceIfPresent(cl, pack.interfaceOwner, &IGameEventsReceiver::bulkArtMovementStart, + pack.artsPack0.size() + pack.artsPack1.size(), possibleAssemblyNumOfArts); + if(pack.interfaceOwner != dstOwner) + callInterfaceIfPresent(cl, dstOwner, &IGameEventsReceiver::bulkArtMovementStart, + pack.artsPack0.size() + pack.artsPack1.size(), possibleAssemblyNumOfArts); applyMove(pack.artsPack0); - if(pack.swap) + if(!pack.artsPack1.empty()) applyMove(pack.artsPack1); } @@ -342,14 +355,14 @@ void ApplyClientNetPackVisitor::visitAssembledArtifact(AssembledArtifact & pack) { callInterfaceIfPresent(cl, cl.getOwner(pack.al.artHolder), &IGameEventsReceiver::artifactAssembled, pack.al); - cl.invalidatePaths(); // hero might have equipped/unequipped Angel Wings + cl.updatePath(pack.al.artHolder); // hero might have equipped/unequipped Angel Wings } void ApplyClientNetPackVisitor::visitDisassembledArtifact(DisassembledArtifact & pack) { callInterfaceIfPresent(cl, cl.getOwner(pack.al.artHolder), &IGameEventsReceiver::artifactDisassembled, pack.al); - cl.invalidatePaths(); // hero might have equipped/unequipped Angel Wings + cl.updatePath(pack.al.artHolder); // hero might have equipped/unequipped Angel Wings } void ApplyClientNetPackVisitor::visitHeroVisit(HeroVisit & pack) @@ -362,6 +375,14 @@ void ApplyClientNetPackVisitor::visitHeroVisit(HeroVisit & pack) void ApplyClientNetPackVisitor::visitNewTurn(NewTurn & pack) { cl.invalidatePaths(); + + if(pack.newWeekNotification) + { + const auto & newWeek = *pack.newWeekNotification; + + std::string str = newWeek.text.toString(); + callAllInterfaces(cl, &CGameInterface::showInfoDialog, newWeek.type, str, newWeek.components,(soundBase::soundID)newWeek.soundID); + } } void ApplyClientNetPackVisitor::visitGiveBonus(GiveBonus & pack) @@ -372,7 +393,7 @@ void ApplyClientNetPackVisitor::visitGiveBonus(GiveBonus & pack) case GiveBonus::ETarget::OBJECT: { const CGHeroInstance *h = gs.getHero(pack.id.as()); - if (h) + if(h) callInterfaceIfPresent(cl, h->tempOwner, &IGameEventsReceiver::heroBonusChanged, h, pack.bonus, true); } break; @@ -409,9 +430,10 @@ void ApplyClientNetPackVisitor::visitPlayerEndsGame(PlayerEndsGame & pack) { callAllInterfaces(cl, &IGameEventsReceiver::gameOver, pack.player, pack.victoryLossCheckResult); + bool localHumanWinsGame = vstd::contains(cl.playerint, pack.player) && cl.getPlayerState(pack.player)->human && pack.victoryLossCheckResult.victory(); bool lastHumanEndsGame = CSH->howManyPlayerInterfaces() == 1 && vstd::contains(cl.playerint, pack.player) && cl.getPlayerState(pack.player)->human && !settings["session"]["spectate"].Bool(); - if (lastHumanEndsGame) + if(lastHumanEndsGame || localHumanWinsGame) { assert(adventureInt); if(adventureInt) @@ -420,7 +442,7 @@ void ApplyClientNetPackVisitor::visitPlayerEndsGame(PlayerEndsGame & pack) adventureInt.reset(); } - CSH->showHighScoresAndEndGameplay(pack.player, pack.victoryLossCheckResult.victory()); + CSH->showHighScoresAndEndGameplay(pack.player, pack.victoryLossCheckResult.victory(), pack.statistic); } // In auto testing pack.mode we always close client if red pack.player won or lose @@ -438,9 +460,9 @@ void ApplyClientNetPackVisitor::visitPlayerReinitInterface(PlayerReinitInterface { cl.initPlayerInterfaces(); - for (PlayerColor player(0); player < PlayerColor::PLAYER_LIMIT; ++player) + for(PlayerColor player(0); player < PlayerColor::PLAYER_LIMIT; ++player) { - if (cl.gameState()->isPlayerMakingTurn(player)) + if(cl.gameState()->isPlayerMakingTurn(player)) { callAllInterfaces(cl, &IGameEventsReceiver::playerStartsTurn, player); callOnlyThatInterface(cl, player, &CGameInterface::yourTurn, QueryID::NONE); @@ -474,7 +496,7 @@ void ApplyClientNetPackVisitor::visitRemoveBonus(RemoveBonus & pack) case GiveBonus::ETarget::OBJECT: { const CGHeroInstance *h = gs.getHero(pack.whoID.as()); - if (h) + if(h) callInterfaceIfPresent(cl, h->tempOwner, &IGameEventsReceiver::heroBonusChanged, h, pack.bonus, false); } break; @@ -649,7 +671,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!"); } @@ -693,7 +715,7 @@ void ApplyFirstClientNetPackVisitor::visitSetObjectProperty(SetObjectProperty & } // invalidate section of map view with our object and force an update with new flag color - if (pack.what == ObjProperty::OWNER && CGI->mh) + if(pack.what == ObjProperty::OWNER && CGI->mh) { auto object = gs.getObjInstance(pack.id); CGI->mh->onObjectInstantRemove(object, object->getOwner()); @@ -710,7 +732,7 @@ void ApplyClientNetPackVisitor::visitSetObjectProperty(SetObjectProperty & pack) } // invalidate section of map view with our object and force an update with new flag color - if (pack.what == ObjProperty::OWNER && CGI->mh) + if(pack.what == ObjProperty::OWNER && CGI->mh) { auto object = gs.getObjInstance(pack.id); CGI->mh->onObjectInstantAdd(object, object->getOwner()); @@ -769,12 +791,12 @@ void ApplyClientNetPackVisitor::visitMapObjectSelectDialog(MapObjectSelectDialog void ApplyFirstClientNetPackVisitor::visitBattleStart(BattleStart & pack) { // Cannot use the usual code because curB is not set yet - callOnlyThatBattleInterface(cl, pack.info->sides[0].color, &IBattleEventsReceiver::battleStartBefore, pack.battleID, pack.info->sides[0].armyObject, pack.info->sides[1].armyObject, - pack.info->tile, pack.info->sides[0].hero, pack.info->sides[1].hero); - callOnlyThatBattleInterface(cl, pack.info->sides[1].color, &IBattleEventsReceiver::battleStartBefore, pack.battleID, pack.info->sides[0].armyObject, pack.info->sides[1].armyObject, - pack.info->tile, pack.info->sides[0].hero, pack.info->sides[1].hero); - callOnlyThatBattleInterface(cl, PlayerColor::SPECTATOR, &IBattleEventsReceiver::battleStartBefore, pack.battleID, pack.info->sides[0].armyObject, pack.info->sides[1].armyObject, - pack.info->tile, pack.info->sides[0].hero, pack.info->sides[1].hero); + callOnlyThatBattleInterface(cl, pack.info->getSide(BattleSide::ATTACKER).color, &IBattleEventsReceiver::battleStartBefore, pack.battleID, pack.info->getSide(BattleSide::ATTACKER).armyObject, pack.info->getSide(BattleSide::DEFENDER).armyObject, + pack.info->tile, pack.info->getSide(BattleSide::ATTACKER).hero, pack.info->getSide(BattleSide::DEFENDER).hero); + callOnlyThatBattleInterface(cl, pack.info->getSide(BattleSide::DEFENDER).color, &IBattleEventsReceiver::battleStartBefore, pack.battleID, pack.info->getSide(BattleSide::ATTACKER).armyObject, pack.info->getSide(BattleSide::DEFENDER).armyObject, + pack.info->tile, pack.info->getSide(BattleSide::ATTACKER).hero, pack.info->getSide(BattleSide::DEFENDER).hero); + callOnlyThatBattleInterface(cl, PlayerColor::SPECTATOR, &IBattleEventsReceiver::battleStartBefore, pack.battleID, pack.info->getSide(BattleSide::ATTACKER).armyObject, pack.info->getSide(BattleSide::DEFENDER).armyObject, + pack.info->tile, pack.info->getSide(BattleSide::ATTACKER).hero, pack.info->getSide(BattleSide::DEFENDER).hero); } void ApplyClientNetPackVisitor::visitBattleStart(BattleStart & pack) @@ -799,11 +821,11 @@ void ApplyClientNetPackVisitor::visitBattleSetActiveStack(BattleSetActiveStack & const CStack *activated = gs.getBattle(pack.battleID)->battleGetStackByID(pack.stack); PlayerColor playerToCall; //pack.player that will move activated stack - if (activated->hasBonusOfType(BonusType::HYPNOTIZED)) + if(activated->hasBonusOfType(BonusType::HYPNOTIZED)) { - playerToCall = (gs.getBattle(pack.battleID)->sides[0].color == activated->unitOwner() - ? gs.getBattle(pack.battleID)->sides[1].color - : gs.getBattle(pack.battleID)->sides[0].color); + playerToCall = gs.getBattle(pack.battleID)->getSide(BattleSide::ATTACKER).color == activated->unitOwner() + ? gs.getBattle(pack.battleID)->getSide(BattleSide::DEFENDER).color + : gs.getBattle(pack.battleID)->getSide(BattleSide::ATTACKER).color; } else { @@ -844,7 +866,7 @@ void ApplyFirstClientNetPackVisitor::visitBattleAttack(BattleAttack & pack) { callBattleInterfaceIfPresentForBothSides(cl, pack.battleID, &IBattleEventsReceiver::battleAttack, pack.battleID, &pack); - // battleStacksAttacked should be excuted before BattleAttack.applyGs() to play animation before damaging unit + // battleStacksAttacked should be executed before BattleAttack.applyGs() to play animation before damaging unit // so this has to be here instead of ApplyClientNetPackVisitor::visitBattleAttack() callBattleInterfaceIfPresentForBothSides(cl, pack.battleID, &IBattleEventsReceiver::battleStacksAttacked, pack.battleID, pack.bsa, pack.shot()); } @@ -999,7 +1021,7 @@ void ApplyClientNetPackVisitor::visitOpenWindow(OpenWindow & pack) case EOpenWindowMode::UNIVERSITY_WINDOW: { //displays University window (when hero enters University on adventure map) - const auto * market = dynamic_cast(cl.getObj(ObjectInstanceID(pack.object))); + const auto * market = cl.getMarket(ObjectInstanceID(pack.object)); const CGHeroInstance *hero = cl.getHero(ObjectInstanceID(pack.visitor)); callInterfaceIfPresent(cl, hero->tempOwner, &IGameEventsReceiver::showUniversityWindow, market, hero, pack.queryID); } @@ -1009,7 +1031,7 @@ void ApplyClientNetPackVisitor::visitOpenWindow(OpenWindow & pack) //displays Thieves' Guild window (when hero enters Den of Thieves) const CGObjectInstance *obj = cl.getObj(ObjectInstanceID(pack.object)); const CGHeroInstance *hero = cl.getHero(ObjectInstanceID(pack.visitor)); - const auto *market = dynamic_cast(obj); + const auto market = cl.getMarket(pack.object); callInterfaceIfPresent(cl, cl.getTile(obj->visitablePos())->visitableObjects.back()->tempOwner, &IGameEventsReceiver::showMarketWindow, market, hero, pack.queryID); } break; @@ -1048,7 +1070,7 @@ void ApplyClientNetPackVisitor::visitNewObject(NewObject & pack) { cl.invalidatePaths(); - const CGObjectInstance *obj = cl.getObj(pack.createdObjectID); + const CGObjectInstance *obj = pack.newObject; if(CGI->mh) CGI->mh->onObjectFadeIn(obj, pack.initiator); diff --git a/client/NetPacksLobbyClient.cpp b/client/NetPacksLobbyClient.cpp index d7dd519c9..49c86f11d 100644 --- a/client/NetPacksLobbyClient.cpp +++ b/client/NetPacksLobbyClient.cpp @@ -31,10 +31,14 @@ #include "gui/WindowHandler.h" #include "widgets/Buttons.h" #include "widgets/TextControls.h" +#include "media/CMusicHandler.h" +#include "media/IVideoPlayer.h" +#include "windows/GUIClasses.h" #include "../lib/CConfigHandler.h" -#include "../lib/CGeneralTextHandler.h" +#include "../lib/texts/CGeneralTextHandler.h" #include "../lib/serializer/Connection.h" +#include "../lib/campaign/CampaignState.h" void ApplyOnLobbyHandlerNetPackVisitor::visitLobbyClientConnected(LobbyClientConnected & pack) { @@ -202,8 +206,19 @@ void ApplyOnLobbyScreenNetPackVisitor::visitLobbyUpdateState(LobbyUpdateState & if(!lobby->bonusSel && handler.si->campState && handler.getState() == EClientState::LOBBY_CAMPAIGN) { - lobby->bonusSel = std::make_shared(); - GH.windows().pushWindow(lobby->bonusSel); + auto bonusSel = std::make_shared(); + lobby->bonusSel = bonusSel; + if(!handler.si->campState->conqueredScenarios().size() && !handler.si->campState->getIntroVideo().empty() && CCS->videoh->open(handler.si->campState->getIntroVideo(), 1)) + { + CCS->musich->stopMusic(); + GH.windows().createAndPushWindow(handler.si->campState->getIntroVideo(), handler.si->campState->getVideoRim().empty() ? ImagePath::builtin("INTRORIM") : handler.si->campState->getVideoRim(), false, 1, [bonusSel](bool skipped){ + if(!CSH->si->campState->getMusic().empty()) + CCS->musich->playMusic(CSH->si->campState->getMusic(), true, false); + GH.windows().pushWindow(bonusSel); + }); + } + else + GH.windows().pushWindow(bonusSel); } if(lobby->bonusSel) @@ -211,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 b67940bd8..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,27 +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; - - template - void serialize(Handler & h) - { - h & spellbookLastPageBattle; - h & spellbookLastPageAdvmap; - h & spellbookLastTabBattle; - h & spellbookLastTabAdvmap; - } - } spellbookSettings; explicit PlayerLocalState(CPlayerInterface & owner); @@ -62,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); @@ -90,24 +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); - - template - void serialize(Handler & h) - { - //WARNING: this code is broken and not used. See CClient::loadGame - std::map pathsMap; //hero -> dest - if(h.saving) - saveHeroPaths(pathsMap); - - h & pathsMap; - - if(!h.saving) - loadHeroPaths(pathsMap); - - h & ownedTowns; - h & wanderingHeroes; - h & sleepingHeroes; - } }; diff --git a/client/ServerRunner.cpp b/client/ServerRunner.cpp index 8ab4df84b..2979a757f 100644 --- a/client/ServerRunner.cpp +++ b/client/ServerRunner.cpp @@ -15,27 +15,48 @@ #include "../lib/CThreadHelper.h" #include "../server/CVCMIServer.h" -#ifndef VCMI_MOBILE +#ifdef ENABLE_SERVER_PROCESS + +#if BOOST_VERSION >= 108600 +// TODO: upgrade code to use v2 API instead of deprecated v1 +#include +#include +#else #include #include #endif +#endif + +#include + ServerThreadRunner::ServerThreadRunner() = default; ServerThreadRunner::~ServerThreadRunner() = default; -void ServerThreadRunner::start(uint16_t port, bool connectToLobby, std::shared_ptr startingInfo) +uint16_t ServerThreadRunner::start(uint16_t cfgport, bool connectToLobby, std::shared_ptr startingInfo) { - server = std::make_unique(port, connectToLobby, true); + // cfgport may be 0 -- the real port is returned after calling prepare() + server = std::make_unique(cfgport, true); if (startingInfo) { server->si = startingInfo; //Else use default } - threadRunLocalServer = boost::thread([this]{ + std::promise promise; + + threadRunLocalServer = boost::thread([this, connectToLobby, &promise]{ setThreadName("runServer"); + uint16_t port = server->prepare(connectToLobby); + promise.set_value(port); server->run(); }); + + logNetwork->trace("Waiting for server port..."); + auto srvport = promise.get_future().get(); + logNetwork->debug("Server port: %d", srvport); + + return srvport; } void ServerThreadRunner::shutdown() @@ -53,7 +74,7 @@ int ServerThreadRunner::exitCode() return 0; } -#ifndef VCMI_MOBILE +#ifdef ENABLE_SERVER_PROCESS ServerProcessRunner::ServerProcessRunner() = default; ServerProcessRunner::~ServerProcessRunner() = default; @@ -73,7 +94,7 @@ int ServerProcessRunner::exitCode() return child->exit_code(); } -void ServerProcessRunner::start(uint16_t port, bool connectToLobby, std::shared_ptr startingInfo) +uint16_t ServerProcessRunner::start(uint16_t port, bool connectToLobby, std::shared_ptr startingInfo) { boost::filesystem::path serverPath = VCMIDirs::get().serverPath(); boost::filesystem::path logPath = VCMIDirs::get().userLogsPath() / "server_log.txt"; @@ -88,6 +109,8 @@ void ServerProcessRunner::start(uint16_t port, bool connectToLobby, std::shared_ if (ec) throw std::runtime_error("Failed to start server! Reason: " + ec.message()); + + return port; } #endif diff --git a/client/ServerRunner.h b/client/ServerRunner.h index d045e0c75..313a61a03 100644 --- a/client/ServerRunner.h +++ b/client/ServerRunner.h @@ -20,7 +20,7 @@ class CVCMIServer; class IServerRunner { public: - virtual void start(uint16_t port, bool connectToLobby, std::shared_ptr startingInfo) = 0; + virtual uint16_t start(uint16_t port, bool connectToLobby, std::shared_ptr startingInfo) = 0; virtual void shutdown() = 0; virtual void wait() = 0; virtual int exitCode() = 0; @@ -34,7 +34,7 @@ class ServerThreadRunner : public IServerRunner, boost::noncopyable std::unique_ptr server; boost::thread threadRunLocalServer; public: - void start(uint16_t port, bool connectToLobby, std::shared_ptr startingInfo) override; + uint16_t start(uint16_t port, bool connectToLobby, std::shared_ptr startingInfo) override; void shutdown() override; void wait() override; int exitCode() override; @@ -44,10 +44,23 @@ public: }; #ifndef VCMI_MOBILE +// Enable support for running vcmiserver as separate process. Unavailable on mobile systems +#define ENABLE_SERVER_PROCESS +#endif +#ifdef ENABLE_SERVER_PROCESS + +#if BOOST_VERSION >= 108600 +namespace boost::process { +inline namespace v1 { +class child; +} +} +#else namespace boost::process { class child; } +#endif /// Class that runs server instance as a child process /// Available only on desktop systems where process management is allowed @@ -56,7 +69,7 @@ class ServerProcessRunner : public IServerRunner, boost::noncopyable std::unique_ptr child; public: - void start(uint16_t port, bool connectToLobby, std::shared_ptr startingInfo) override; + uint16_t start(uint16_t port, bool connectToLobby, std::shared_ptr startingInfo) override; void shutdown() override; void wait() override; int exitCode() override; diff --git a/client/VCMI_client.cbp b/client/VCMI_client.cbp deleted file mode 100644 index 081be52ae..000000000 --- a/client/VCMI_client.cbp +++ /dev/null @@ -1,267 +0,0 @@ - - - - - - diff --git a/client/VCMI_client.rc b/client/VCMI_client.rc deleted file mode 100644 index f901de682..000000000 --- a/client/VCMI_client.rc +++ /dev/null @@ -1 +0,0 @@ -IDI_ICON1 ICON "vcmi.ico" \ No newline at end of file diff --git a/client/VCMI_client.vcxproj b/client/VCMI_client.vcxproj deleted file mode 100644 index 997bdba6a..000000000 --- a/client/VCMI_client.vcxproj +++ /dev/null @@ -1,333 +0,0 @@ - - - - - Debug - Win32 - - - Debug - x64 - - - RD - Win32 - - - RD - x64 - - - - {8355EBA8-65C2-44A4-BC2D-78053E1BF2D6} - VCMI_client - 10.0 - - - - Application - Unicode - true - v142 - - - Application - Unicode - true - v140_xp - - - Application - Unicode - v140_xp - - - Application - Unicode - v140_xp - - - - - - - - - - - - - - - - - - - - - - - - - - - <_ProjectFileVersion>10.0.30128.1 - .. - $(VCMI_Out) - $(Configuration)\ - $(Configuration)\ - $(VCMI_Out) - $(VCMI_Out) - $(Configuration)\ - $(Configuration)\ - AllRules.ruleset - AllRules.ruleset - - - - - AllRules.ruleset - AllRules.ruleset - - - - - - - - - - - - - 4251;%(DisableSpecificWarnings) - NoListing - Use - StdInc.h - /MP4 /Zm150 - $(FFMPEGDIR);$(SDLDIR);$(BOOSTDIR);$(AdditionalIncludeDirectories) - - - avcodec.lib;avdevice.lib;avfilter.lib;avformat.lib;avutil.lib;postproc.lib;swresample.lib;swscale.lib;SDL.lib;zlib.lib;SDL_image.lib;SDL_ttf.lib;SDL_mixer.lib;VCMI_lib.lib;%(AdditionalDependencies) - NotSet - true - true - $(FFMPEGDIR)\lib;.;..\..\libs;.. - - - $(ProjectDir)DPIaware.manifest;%(AdditionalManifestFiles) - - - - - false - false - 4251;%(DisableSpecificWarnings) - NoListing - Use - StdInc.h - /MP4 /Zm150 - - - SDL.lib;zlib.lib;SDL_image.lib;SDL_ttf.lib;SDL_mixer.lib;VCMI_lib.lib;%(AdditionalDependencies) - LinkVerbose - false - true - - - $(ProjectDir)DPIaware.manifest;%(AdditionalManifestFiles) - - - - - Use - StdInc.h - - - $(FFMPEGDIR);$(SDLDIR);$(BOOSTDIR);$(AdditionalIncludeDirectories) - true - Disabled - - - avcodec.lib;avdevice.lib;avfilter.lib;avformat.lib;avutil.lib;postproc.lib;swresample.lib;swscale.lib;zlib.lib;SDL2.lib;SDL2main.lib;VCMI_lib.lib;SDL2_mixer.lib;SDL2_image.lib;SDL2_ttf.lib;%(AdditionalDependencies) - NotSet - - - NotSet - $(VCMI_Out) - false - true - MultiplyDefinedSymbolOnly - /LTCG /d2:-notypeopt %(AdditionalOptions) - true - - - - - - - - - /MP4 /Zm150 - Use - StdInc.h - - - SDL.lib;zlib.lib;SDL_image.lib;SDL_ttf.lib;SDL_mixer.lib;VCMI_lib.lib;%(AdditionalDependencies) - NotSet - - - NotSet - true - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Create - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - {b952ffc5-3039-4de1-9f08-90acda483d8f} - - - - \ No newline at end of file diff --git a/client/VCMI_client.vcxproj.filters b/client/VCMI_client.vcxproj.filters deleted file mode 100644 index 9b717e834..000000000 --- a/client/VCMI_client.vcxproj.filters +++ /dev/null @@ -1,302 +0,0 @@ - - - - - - - - - - - - - - - - - windows - - - windows - - - windows - - - windows - - - windows - - - windows - - - windows - - - windows - - - windows - - - windows - - - windows - - - windows - - - windows - - - widgets - - - widgets - - - widgets - - - widgets - - - widgets - - - widgets - - - widgets - - - widgets - - - widgets - - - battle - - - battle - - - battle - - - battle - - - gui - - - gui - - - gui - - - gui - - - gui - - - gui - - - gui - - - - - windows - - - windows - - - - - - - - - - - - - - - - - - - - - - - - - - - {c25dc848-6e1b-4a9b-b6f2-517626e9a23d} - - - {cec5376d-0f32-475e-bf51-3dbae35c6b98} - - - {2b5b57d6-28ba-4bcf-9691-0977171866d9} - - - {c9fcce39-f3af-4621-996c-0df7695134bd} - - - - - - - - - - - - - - - - - windows - - - windows - - - windows - - - windows - - - windows - - - windows - - - windows - - - windows - - - windows - - - windows - - - windows - - - windows - - - windows - - - widgets - - - widgets - - - widgets - - - widgets - - - widgets - - - widgets - - - widgets - - - widgets - - - widgets - - - battle - - - battle - - - battle - - - battle - - - gui - - - gui - - - gui - - - gui - - - gui - - - gui - - - gui - - - gui - - - gui - - - - - windows - - - windows - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/client/adventureMap/AdventureMapInterface.cpp b/client/adventureMap/AdventureMapInterface.cpp index 381e26c86..3b918de77 100644 --- a/client/adventureMap/AdventureMapInterface.cpp +++ b/client/adventureMap/AdventureMapInterface.cpp @@ -31,15 +31,18 @@ #include "../gui/Shortcut.h" #include "../gui/WindowHandler.h" #include "../render/Canvas.h" +#include "../render/IImage.h" #include "../render/IRenderHandler.h" +#include "../render/IScreenHandler.h" +#include "../render/AssetGenerator.h" #include "../CMT.h" #include "../PlayerLocalState.h" #include "../CPlayerInterface.h" #include "../../CCallback.h" -#include "../../lib/GameSettings.h" +#include "../../lib/IGameSettings.h" #include "../../lib/StartInfo.h" -#include "../../lib/CGeneralTextHandler.h" +#include "../../lib/texts/CGeneralTextHandler.h" #include "../../lib/spells/CSpellHandler.h" #include "../../lib/mapObjects/CGHeroInstance.h" #include "../../lib/mapObjects/CGTownInstance.h" @@ -57,11 +60,13 @@ AdventureMapInterface::AdventureMapInterface(): scrollingWasBlocked(false), backgroundDimLevel(settings["adventure"]["backgroundDimLevel"].Integer()) { - OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + OBJECT_CONSTRUCTION; pos.x = pos.y = 0; pos.w = GH.screenDimensions().x; pos.h = GH.screenDimensions().y; + AssetGenerator::createPaletteShiftedSprites(); + shortcuts = std::make_shared(*this); widget = std::make_shared(shortcuts); @@ -178,7 +183,7 @@ void AdventureMapInterface::dim(Canvas & to) { if(!std::dynamic_pointer_cast(window) && std::dynamic_pointer_cast(window) && isBigWindow(window)) { - to.fillTexture(GH.renderHandler().loadImage(ImagePath::builtin("DiBoxBck"))); + to.fillTexture(GH.renderHandler().loadImage(ImagePath::builtin("DiBoxBck"), EImageBlitMode::OPAQUE)); return; } } @@ -231,7 +236,7 @@ void AdventureMapInterface::handleMapScrollingUpdate(uint32_t timePassed) bool cursorInScrollArea = scrollDelta != Point(0,0); bool scrollingActive = cursorInScrollArea && shortcuts->optionMapScrollingActive() && !scrollingWasBlocked; - bool scrollingBlocked = GH.isKeyboardCtrlDown() || !settings["adventure"]["borderScroll"].Bool(); + bool scrollingBlocked = GH.isKeyboardCtrlDown() || !settings["adventure"]["borderScroll"].Bool() || !GH.screenHandler().hasFocus(); if (!scrollingWasActive && scrollingBlocked) { @@ -399,7 +404,7 @@ void AdventureMapInterface::onCurrentPlayerChanged(PlayerColor playerID) return; currentPlayerID = playerID; - widget->setPlayer(playerID); + widget->setPlayerColor(playerID); } void AdventureMapInterface::onPlayerTurnStarted(PlayerColor playerID) @@ -451,7 +456,7 @@ void AdventureMapInterface::onPlayerTurnStarted(PlayerColor playerID) widget->getInfoBar()->showDate(); onHeroChanged(nullptr); - Canvas canvas = Canvas::createFromSurface(screen); + Canvas canvas = Canvas::createFromSurface(screen, CanvasScalingPolicy::AUTO); showAll(canvas); mapAudio->onPlayerTurnStarted(); @@ -555,7 +560,8 @@ void AdventureMapInterface::onTileLeftClicked(const int3 &targetPosition) else //still here? we need to move hero if we clicked end of already selected path or calculate a new path otherwise { if(LOCPLINT->localState->hasPath(currentHero) && - LOCPLINT->localState->getPath(currentHero).endPos() == targetPosition)//we'll be moving + LOCPLINT->localState->getPath(currentHero).endPos() == targetPosition && + !GH.isKeyboardShiftDown())//we'll be moving { assert(!CGI->mh->hasOngoingAnimations()); if(!CGI->mh->hasOngoingAnimations() && LOCPLINT->localState->getPath(currentHero).nextNode().turns == 0) @@ -615,7 +621,7 @@ void AdventureMapInterface::onTileHovered(const int3 &targetPosition) case SpellID::DIMENSION_DOOR: if(isValidAdventureSpellTarget(targetPosition)) { - if(VLC->settings()->getBoolean(EGameSettings::DIMENSION_DOOR_TRIGGERS_GUARDS) && LOCPLINT->cb->isTileGuardedUnchecked(targetPosition)) + if(LOCPLINT->cb->getSettings().getBoolean(EGameSettings::DIMENSION_DOOR_TRIGGERS_GUARDS) && LOCPLINT->cb->isTileGuardedUnchecked(targetPosition)) CCS->curh->set(Cursor::Map::T1_ATTACK); else CCS->curh->set(Cursor::Map::TELEPORT); @@ -897,7 +903,7 @@ void AdventureMapInterface::hotkeyZoom(int delta, bool useDeadZone) void AdventureMapInterface::onScreenResize() { - OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + OBJECT_CONSTRUCTION; // remember our activation state and reactive after reconstruction // since othervice activate() calls for created elements will bypass virtual dispatch @@ -914,7 +920,7 @@ void AdventureMapInterface::onScreenResize() widget = std::make_shared(shortcuts); widget->getMapView()->onViewMapActivated(); - widget->setPlayer(currentPlayerID); + widget->setPlayerColor(currentPlayerID); widget->updateActiveState(); widget->getMinimap()->update(); widget->getInfoBar()->showSelection(); diff --git a/client/adventureMap/AdventureMapInterface.h b/client/adventureMap/AdventureMapInterface.h index b0509a77c..32ff1df89 100644 --- a/client/adventureMap/AdventureMapInterface.h +++ b/client/adventureMap/AdventureMapInterface.h @@ -31,7 +31,6 @@ class CAnimImage; class CGStatusBar; class AdventureMapWidget; class AdventureMapShortcuts; -class CAnimation; class MapView; class CResDataBar; class CHeroList; diff --git a/client/adventureMap/AdventureMapShortcuts.cpp b/client/adventureMap/AdventureMapShortcuts.cpp index ee196e0b8..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" @@ -31,16 +32,19 @@ #include "../../CCallback.h" #include "../../lib/CConfigHandler.h" -#include "../../lib/CGeneralTextHandler.h" +#include "../../lib/texts/CGeneralTextHandler.h" #include "../../lib/mapObjects/CGHeroInstance.h" #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) @@ -48,7 +52,7 @@ void AdventureMapShortcuts::setState(EAdventureState newState) state = newState; } -EAdventureState AdventureMapShortcuts::getState() +EAdventureState AdventureMapShortcuts::getState() const { return state; } @@ -71,6 +75,8 @@ std::vector AdventureMapShortcuts::getShortcuts() { EShortcut::ADVENTURE_QUEST_LOG, optionCanViewQuests(), [this]() { this->showQuestlog(); } }, { EShortcut::ADVENTURE_TOGGLE_SLEEP, optionHeroSelected(), [this]() { this->toggleSleepWake(); } }, { EShortcut::ADVENTURE_TOGGLE_GRID, optionInMapView(), [this]() { this->toggleGrid(); } }, + { EShortcut::ADVENTURE_TOGGLE_VISITABLE, optionInMapView(), [this]() { this->toggleVisitable(); } }, + { EShortcut::ADVENTURE_TOGGLE_BLOCKED, optionInMapView(), [this]() { this->toggleBlocked(); } }, { EShortcut::ADVENTURE_TRACK_HERO, optionInMapView(), [this]() { this->toggleTrackHero(); } }, { EShortcut::ADVENTURE_SET_HERO_ASLEEP, optionHeroAwake(), [this]() { this->setHeroSleeping(); } }, { EShortcut::ADVENTURE_SET_HERO_AWAKE, optionHeroSleeping(), [this]() { this->setHeroAwake(); } }, @@ -107,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; } @@ -168,6 +176,18 @@ void AdventureMapShortcuts::toggleGrid() s["showGrid"].Bool() = !settings["gameTweaks"]["showGrid"].Bool(); } +void AdventureMapShortcuts::toggleVisitable() +{ + Settings s = settings.write["session"]; + s["showVisitable"].Bool() = !settings["session"]["showVisitable"].Bool(); +} + +void AdventureMapShortcuts::toggleBlocked() +{ + Settings s = settings.write["session"]; + s["showBlocked"].Bool() = !settings["session"]["showBlocked"].Bool(); +} + void AdventureMapShortcuts::toggleSleepWake() { if (!optionHeroSelected()) @@ -311,7 +331,6 @@ void AdventureMapShortcuts::toMainMenu() []() { CSH->endGameplay(); - GH.defActionsDef = 63; CMM->menu->switchToTab("main"); }, 0 @@ -325,7 +344,6 @@ void AdventureMapShortcuts::newGame() []() { CSH->endGameplay(); - GH.defActionsDef = 63; CMM->menu->switchToTab("new"); }, nullptr @@ -445,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(); @@ -518,7 +592,6 @@ bool AdventureMapShortcuts::optionCanVisitObject() auto * hero = LOCPLINT->localState->getCurrentHero(); auto objects = LOCPLINT->cb->getVisitableObjs(hero->visitablePos()); - //assert(vstd::contains(objects,hero)); return objects.size() > 1; // there is object other than our hero } diff --git a/client/adventureMap/AdventureMapShortcuts.h b/client/adventureMap/AdventureMapShortcuts.h index 8682bf82d..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(); @@ -42,6 +45,8 @@ class AdventureMapShortcuts void showQuestlog(); void toggleTrackHero(); void toggleGrid(); + void toggleVisitable(); + void toggleBlocked(); void toggleSleepWake(); void setHeroSleeping(); void setHeroAwake(); @@ -69,6 +74,7 @@ class AdventureMapShortcuts void nextTown(); void nextObject(); void zoom( int distance); + void search(bool next); void moveHeroDirectional(const Point & direction); public: @@ -94,6 +100,6 @@ public: bool optionMapViewActive(); void setState(EAdventureState newState); - EAdventureState getState(); + EAdventureState getState() const; void onMapViewMoved(const Rect & visibleArea, int mapLevel); }; diff --git a/client/adventureMap/AdventureMapWidget.cpp b/client/adventureMap/AdventureMapWidget.cpp index 7333f1d37..7e22c21de 100644 --- a/client/adventureMap/AdventureMapWidget.cpp +++ b/client/adventureMap/AdventureMapWidget.cpp @@ -20,7 +20,6 @@ #include "../gui/CGuiHandler.h" #include "../gui/Shortcut.h" #include "../mapView/MapView.h" -#include "../render/CAnimation.h" #include "../render/IImage.h" #include "../render/IRenderHandler.h" #include "../widgets/Buttons.h" @@ -60,7 +59,7 @@ AdventureMapWidget::AdventureMapWidget( std::shared_ptr s const JsonNode config(JsonPath::builtin("config/widgets/adventureMap.json")); for(const auto & entry : config["options"]["imagesPlayerColored"].Vector()) - playerColorerImages.push_back(ImagePath::fromJson(entry)); + playerColoredImages.push_back(ImagePath::fromJson(entry)); build(config); addUsedEvents(KEYBOARD); @@ -125,26 +124,6 @@ Rect AdventureMapWidget::readArea(const JsonNode & source, const Rect & bounding return Rect(topLeft + boundingBox.topLeft(), dimensions); } -std::shared_ptr AdventureMapWidget::loadImage(const JsonNode & name) -{ - ImagePath resource = ImagePath::fromJson(name); - - if(images.count(resource) == 0) - images[resource] = GH.renderHandler().loadImage(resource); - - return images[resource]; -} - -std::shared_ptr AdventureMapWidget::loadAnimation(const JsonNode & name) -{ - AnimationPath resource = AnimationPath::fromJson(name); - - if(animations.count(resource) == 0) - animations[resource] = GH.renderHandler().loadAnimation(resource); - - return animations[resource]; -} - std::shared_ptr AdventureMapWidget::buildInfobox(const JsonNode & input) { Rect area = readTargetArea(input["area"]); @@ -156,8 +135,12 @@ std::shared_ptr AdventureMapWidget::buildMapImage(const JsonNode & i { Rect targetArea = readTargetArea(input["area"]); Rect sourceArea = readSourceArea(input["sourceArea"], input["area"]); + ImagePath path = ImagePath::fromJson(input["image"]); - return std::make_shared(loadImage(input["image"]), targetArea, sourceArea); + if (vstd::contains(playerColoredImages, path)) + return std::make_shared(path, targetArea, sourceArea); + else + return std::make_shared(path, targetArea, sourceArea); } std::shared_ptr AdventureMapWidget::buildMapButton(const JsonNode & input) @@ -257,7 +240,7 @@ std::shared_ptr AdventureMapWidget::buildMapIcon(const JsonNode & in size_t index = input["index"].Integer(); size_t perPlayer = input["perPlayer"].Integer(); - return std::make_shared(area.topLeft(), loadAnimation(input["image"]), index, perPlayer); + return std::make_shared(area.topLeft(), AnimationPath::fromJson(input["image"]), index, perPlayer); } std::shared_ptr AdventureMapWidget::buildMapTownList(const JsonNode & input) @@ -326,9 +309,8 @@ std::shared_ptr AdventureMapWidget::buildStatusBar(const JsonNode & std::shared_ptr AdventureMapWidget::buildTexturePlayerColored(const JsonNode & input) { logGlobal->debug("Building widget CFilledTexture"); - auto image = ImagePath::fromJson(input["image"]); Rect area = readTargetArea(input["area"]); - return std::make_shared(image, area); + return std::make_shared(area); } std::shared_ptr AdventureMapWidget::getHeroList() @@ -356,7 +338,7 @@ std::shared_ptr AdventureMapWidget::getInfoBar() return infoBar; } -void AdventureMapWidget::setPlayer(const PlayerColor & player) +void AdventureMapWidget::setPlayerColor(const PlayerColor & player) { setPlayerChildren(this, player); } @@ -369,43 +351,41 @@ void AdventureMapWidget::setPlayerChildren(CIntObject * widget, const PlayerColo auto icon = dynamic_cast(entry); auto button = dynamic_cast(entry); auto resDataBar = dynamic_cast(entry); - auto texture = dynamic_cast(entry); + auto textureColored = dynamic_cast(entry); + auto textureIndexed = dynamic_cast(entry); if(button) button->setPlayerColor(player); if(resDataBar) - resDataBar->colorize(player); + resDataBar->setPlayerColor(player); if(icon) - icon->setPlayer(player); + icon->setPlayerColor(player); if(container) setPlayerChildren(container, player); - if (texture) - texture->playerColored(player); - } + if (textureColored) + textureColored->setPlayerColor(player); - for(const auto & entry : playerColorerImages) - { - if(images.count(entry)) - images[entry]->playerColored(player); + if (textureIndexed) + textureIndexed->setPlayerColor(player); } redraw(); } -CAdventureMapIcon::CAdventureMapIcon(const Point & position, std::shared_ptr animation, size_t index, size_t iconsPerPlayer) +CAdventureMapIcon::CAdventureMapIcon(const Point & position, const AnimationPath & animation, size_t index, size_t iconsPerPlayer) : index(index) , iconsPerPlayer(iconsPerPlayer) { - OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + OBJECT_CONSTRUCTION; pos += position; image = std::make_shared(animation, index); } -void CAdventureMapIcon::setPlayer(const PlayerColor & player) +void CAdventureMapIcon::setPlayerColor(const PlayerColor & player) { image->setFrame(index + player.getNum() * iconsPerPlayer); } diff --git a/client/adventureMap/AdventureMapWidget.h b/client/adventureMap/AdventureMapWidget.h index 93a19eeb6..742e7d683 100644 --- a/client/adventureMap/AdventureMapWidget.h +++ b/client/adventureMap/AdventureMapWidget.h @@ -11,7 +11,6 @@ #include "../gui/InterfaceObjectConfigurable.h" -class CAnimation; class CHeroList; class CTownList; class CMinimap; @@ -29,11 +28,7 @@ class AdventureMapWidget : public InterfaceObjectConfigurable std::vector subwidgetSizes; /// list of images on which player-colored palette will be applied - std::vector playerColorerImages; - - /// list of named images shared between widgets - std::map> images; - std::map> animations; + std::vector playerColoredImages; /// Widgets that require access from adventure map std::shared_ptr heroList; @@ -48,9 +43,6 @@ class AdventureMapWidget : public InterfaceObjectConfigurable Rect readSourceArea(const JsonNode & source, const JsonNode & sourceCommon); Rect readArea(const JsonNode & source, const Rect & boundingBox); - std::shared_ptr loadImage(const JsonNode & name); - std::shared_ptr loadAnimation(const JsonNode & name); - std::shared_ptr buildInfobox(const JsonNode & input); std::shared_ptr buildMapImage(const JsonNode & input); std::shared_ptr buildMapButton(const JsonNode & input); @@ -64,7 +56,6 @@ class AdventureMapWidget : public InterfaceObjectConfigurable std::shared_ptr buildStatusBar(const JsonNode & input); std::shared_ptr buildTexturePlayerColored(const JsonNode &); - void setPlayerChildren(CIntObject * widget, const PlayerColor & player); void updateActiveStateChildden(CIntObject * widget); public: @@ -76,7 +67,7 @@ public: std::shared_ptr getMapView(); std::shared_ptr getInfoBar(); - void setPlayer(const PlayerColor & player); + void setPlayerColor(const PlayerColor & player); void onMapViewMoved(const Rect & visibleArea, int mapLevel); void updateActiveState(); @@ -104,7 +95,7 @@ class CAdventureMapIcon : public CIntObject size_t index; size_t iconsPerPlayer; public: - CAdventureMapIcon(const Point & position, std::shared_ptr image, size_t index, size_t iconsPerPlayer); + CAdventureMapIcon(const Point & position, const AnimationPath & image, size_t index, size_t iconsPerPlayer); - void setPlayer(const PlayerColor & player); + void setPlayerColor(const PlayerColor & player); }; diff --git a/client/adventureMap/AdventureOptions.cpp b/client/adventureMap/AdventureOptions.cpp index 138aa59d3..2de6371bc 100644 --- a/client/adventureMap/AdventureOptions.cpp +++ b/client/adventureMap/AdventureOptions.cpp @@ -23,12 +23,12 @@ #include "../../CCallback.h" #include "../../lib/StartInfo.h" -#include "../../lib/CGeneralTextHandler.h" +#include "../../lib/texts/CGeneralTextHandler.h" AdventureOptions::AdventureOptions() : CWindowObject(PLAYER_COLORED, ImagePath::builtin("ADVOPTS")) { - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + OBJECT_CONSTRUCTION; viewWorld = std::make_shared(Point(24, 23), AnimationPath::builtin("ADVVIEW.DEF"), CButton::tooltip(), [&](){ close(); }, EShortcut::ADVENTURE_VIEW_WORLD); viewWorld->addCallback([] { LOCPLINT->viewWorldMap(); }); diff --git a/client/adventureMap/CInGameConsole.cpp b/client/adventureMap/CInGameConsole.cpp index 07a8872b8..b28aa1ea0 100644 --- a/client/adventureMap/CInGameConsole.cpp +++ b/client/adventureMap/CInGameConsole.cpp @@ -12,7 +12,6 @@ #include "CInGameConsole.h" #include "../CGameInfo.h" -#include "../CMusicHandler.h" #include "../CPlayerInterface.h" #include "../CServerHandler.h" #include "../GameChatHandler.h" @@ -21,6 +20,7 @@ #include "../gui/WindowHandler.h" #include "../gui/Shortcut.h" #include "../gui/TextAlignment.h" +#include "../media/ISoundPlayer.h" #include "../render/Colors.h" #include "../render/Canvas.h" #include "../render/IScreenHandler.h" @@ -30,9 +30,8 @@ #include "../../CCallback.h" #include "../../lib/CConfigHandler.h" #include "../../lib/CThreadHelper.h" -#include "../../lib/TextOperations.h" #include "../../lib/mapObjects/CArmedInstance.h" -#include "../../lib/MetaString.h" +#include "../../lib/texts/TextOperations.h" CInGameConsole::CInGameConsole() : CIntObject(KEYBOARD | TIME | TEXTINPUT) @@ -221,7 +220,7 @@ void CInGameConsole::keyPressed (EShortcut key) } } -void CInGameConsole::textInputed(const std::string & inputtedText) +void CInGameConsole::textInputted(const std::string & inputtedText) { if (LOCPLINT->cingconsole != this) return; diff --git a/client/adventureMap/CInGameConsole.h b/client/adventureMap/CInGameConsole.h index 89bba9aa8..39030f9e3 100644 --- a/client/adventureMap/CInGameConsole.h +++ b/client/adventureMap/CInGameConsole.h @@ -50,7 +50,7 @@ public: void show(Canvas & to) override; void showAll(Canvas & to) override; void keyPressed(EShortcut key) override; - void textInputed(const std::string & enteredText) override; + void textInputted(const std::string & enteredText) override; void textEdited(const std::string & enteredText) override; bool captureThisKey(EShortcut key) override; diff --git a/client/adventureMap/CInfoBar.cpp b/client/adventureMap/CInfoBar.cpp index 68809752a..b319bf250 100644 --- a/client/adventureMap/CInfoBar.cpp +++ b/client/adventureMap/CInfoBar.cpp @@ -20,16 +20,16 @@ #include "../widgets/MiscWidgets.h" #include "../windows/InfoWindows.h" #include "../CGameInfo.h" -#include "../CMusicHandler.h" #include "../CPlayerInterface.h" #include "../PlayerLocalState.h" #include "../gui/CGuiHandler.h" #include "../gui/WindowHandler.h" +#include "../media/ISoundPlayer.h" #include "../render/IScreenHandler.h" #include "../../CCallback.h" #include "../../lib/CConfigHandler.h" -#include "../../lib/CGeneralTextHandler.h" +#include "../../lib/texts/CGeneralTextHandler.h" #include "../../lib/mapObjects/CGHeroInstance.h" #include "../../lib/mapObjects/CGTownInstance.h" @@ -51,7 +51,7 @@ CInfoBar::EmptyVisibleInfo::EmptyVisibleInfo() CInfoBar::VisibleHeroInfo::VisibleHeroInfo(const CGHeroInstance * hero) { - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + OBJECT_CONSTRUCTION; background = std::make_shared(ImagePath::builtin("ADSTATHR")); if(settings["gameTweaks"]["infoBarCreatureManagement"].Bool()) @@ -62,7 +62,7 @@ CInfoBar::VisibleHeroInfo::VisibleHeroInfo(const CGHeroInstance * hero) CInfoBar::VisibleTownInfo::VisibleTownInfo(const CGTownInstance * town) { - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + OBJECT_CONSTRUCTION; background = std::make_shared(ImagePath::builtin("ADSTATCS")); if(settings["gameTweaks"]["infoBarCreatureManagement"].Bool()) @@ -73,7 +73,7 @@ CInfoBar::VisibleTownInfo::VisibleTownInfo(const CGTownInstance * town) CInfoBar::VisibleDateInfo::VisibleDateInfo() { - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + OBJECT_CONSTRUCTION; animation = std::make_shared(1, 0, getNewDayName(), CShowableAnim::PLAY_ONCE, 180);// H3 uses around 175-180 ms per frame animation->setDuration(1500); @@ -114,7 +114,7 @@ AnimationPath CInfoBar::VisibleDateInfo::getNewDayName() CInfoBar::VisibleEnemyTurnInfo::VisibleEnemyTurnInfo(PlayerColor player) { - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + OBJECT_CONSTRUCTION; background = std::make_shared(ImagePath::builtin("ADSTATNX")); banner = std::make_shared(AnimationPath::builtin("CREST58"), player.getNum(), 0, 20, 51); sand = std::make_shared(99, 51, AnimationPath::builtin("HOURSAND"), 0, 100); // H3 uses around 100 ms per frame @@ -123,7 +123,7 @@ CInfoBar::VisibleEnemyTurnInfo::VisibleEnemyTurnInfo(PlayerColor player) CInfoBar::VisibleGameStatusInfo::VisibleGameStatusInfo() { - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + OBJECT_CONSTRUCTION; //get amount of halls of each level std::vector halls(4, 0); for(auto town : LOCPLINT->localState->getOwnedTowns()) @@ -180,7 +180,7 @@ CInfoBar::VisibleGameStatusInfo::VisibleGameStatusInfo() CInfoBar::VisibleComponentInfo::VisibleComponentInfo(const std::vector & compsToDisplay, std::string message, int textH, bool tiny) { - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + OBJECT_CONSTRUCTION; background = std::make_shared(ImagePath::builtin("ADSTATOT"), 1, 0); auto fullRect = Rect(CInfoBar::offset, CInfoBar::offset, data_width - 2 * CInfoBar::offset, data_height - 2 * CInfoBar::offset); @@ -250,14 +250,14 @@ void CInfoBar::playNewDaySound() void CInfoBar::reset() { - OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255-DISPOSE); + OBJECT_CONSTRUCTION; state = EMPTY; visibleInfo = std::make_shared(); } void CInfoBar::showSelection() { - OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255-DISPOSE); + OBJECT_CONSTRUCTION; if(LOCPLINT->localState->getCurrentHero()) { showHeroSelection(LOCPLINT->localState->getCurrentHero()); @@ -325,7 +325,7 @@ CInfoBar::CInfoBar(const Rect & position) state(EMPTY), listener(settings.listen["gameTweaks"]["infoBarCreatureManagement"]) { - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + OBJECT_CONSTRUCTION; pos.w = position.w; pos.h = position.h; listener(std::bind(&CInfoBar::OnInfoBarCreatureManagementChanged, this)); @@ -349,7 +349,7 @@ void CInfoBar::setTimer(uint32_t msToTrigger) void CInfoBar::showDate() { - OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255-DISPOSE); + OBJECT_CONSTRUCTION; playNewDaySound(); state = DATE; visibleInfo = std::make_shared(); @@ -475,7 +475,7 @@ void CInfoBar::popAll() void CInfoBar::popComponents(bool remove) { - OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255-DISPOSE); + OBJECT_CONSTRUCTION; if(remove && !componentsQueue.empty()) componentsQueue.pop(); if(!componentsQueue.empty()) @@ -492,7 +492,7 @@ void CInfoBar::popComponents(bool remove) void CInfoBar::pushComponents(const std::vector & comps, std::string message, int textH, bool tiny, int timer) { - OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255-DISPOSE); + OBJECT_CONSTRUCTION; componentsQueue.emplace(VisibleComponentInfo::Cache(comps, message, textH, tiny), timer); } @@ -503,7 +503,7 @@ bool CInfoBar::showingComponents() void CInfoBar::startEnemyTurn(PlayerColor color) { - OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255-DISPOSE); + OBJECT_CONSTRUCTION; state = AITURN; visibleInfo = std::make_shared(color); redraw(); @@ -511,7 +511,7 @@ void CInfoBar::startEnemyTurn(PlayerColor color) void CInfoBar::showHeroSelection(const CGHeroInstance * hero) { - OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255-DISPOSE); + OBJECT_CONSTRUCTION; if(!hero) { reset(); @@ -526,7 +526,7 @@ void CInfoBar::showHeroSelection(const CGHeroInstance * hero) void CInfoBar::showTownSelection(const CGTownInstance * town) { - OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255-DISPOSE); + OBJECT_CONSTRUCTION; if(!town) { reset(); @@ -541,7 +541,7 @@ void CInfoBar::showTownSelection(const CGTownInstance * town) void CInfoBar::showGameStatus() { - OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255-DISPOSE); + OBJECT_CONSTRUCTION; state = GAME; visibleInfo = std::make_shared(); setTimer(3000); diff --git a/client/adventureMap/CList.cpp b/client/adventureMap/CList.cpp index c535e80cf..d25a612a3 100644 --- a/client/adventureMap/CList.cpp +++ b/client/adventureMap/CList.cpp @@ -18,26 +18,28 @@ #include "../widgets/ObjectLists.h" #include "../widgets/RadialMenu.h" #include "../windows/InfoWindows.h" +#include "../windows/CCastleInterface.h" #include "../CGameInfo.h" #include "../CPlayerInterface.h" #include "../PlayerLocalState.h" #include "../gui/CGuiHandler.h" +#include "../gui/Shortcut.h" #include "../gui/WindowHandler.h" #include "../render/Canvas.h" #include "../render/Colors.h" -#include "../../lib/CGeneralTextHandler.h" -#include "../../lib/CHeroHandler.h" -#include "../../lib/GameSettings.h" +#include "../../lib/texts/CGeneralTextHandler.h" +#include "../../lib/IGameSettings.h" #include "../../lib/mapObjects/CGHeroInstance.h" #include "../../lib/mapObjects/CGTownInstance.h" +#include "../../CCallback.h" + CList::CListItem::CListItem(CList * Parent) : CIntObject(LCLICK | SHOW_POPUP | HOVER), parent(Parent), selection() { - defActions = 255-DISPOSE; } CList::CListItem::~CListItem() = default; @@ -71,7 +73,7 @@ void CList::CListItem::hover(bool on) void CList::CListItem::onSelect(bool on) { - OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255-DISPOSE); + OBJECT_CONSTRUCTION; selection.reset(); if(on) selection = genSelection(); @@ -96,7 +98,7 @@ void CList::showAll(Canvas & to) void CList::createList(Point firstItemPosition, Point itemPositionDelta, size_t listAmount) { - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + OBJECT_CONSTRUCTION; listBox = std::make_shared(std::bind(&CList::createItem, this, _1), firstItemPosition, itemPositionDelta, size, listAmount); } @@ -207,7 +209,7 @@ void CList::selectPrev() CHeroList::CEmptyHeroItem::CEmptyHeroItem() { - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + OBJECT_CONSTRUCTION; movement = std::make_shared(AnimationPath::builtin("IMOBIL"), 0, 0, 0, 1); portrait = std::make_shared(ImagePath::builtin("HPSXXX"), movement->pos.w + 1, 0); mana = std::make_shared(AnimationPath::builtin("IMANA"), 0, 0, movement->pos.w + portrait->pos.w + 2, 1 ); @@ -220,7 +222,7 @@ CHeroList::CHeroItem::CHeroItem(CHeroList *parent, const CGHeroInstance * Hero) : CListItem(parent), hero(Hero) { - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + OBJECT_CONSTRUCTION; movement = std::make_shared(AnimationPath::builtin("IMOBIL"), 0, 0, 0, 1); portrait = std::make_shared(AnimationPath::builtin("PortraitsSmall"), hero->getIconIndex(), 0, movement->pos.w + 1); mana = std::make_shared(AnimationPath::builtin("IMANA"), 0, 0, movement->pos.w + portrait->pos.w + 2, 1); @@ -230,7 +232,7 @@ CHeroList::CHeroItem::CHeroItem(CHeroList *parent, const CGHeroInstance * Hero) update(); - addUsedEvents(GESTURE); + addUsedEvents(GESTURE | KEYBOARD); } void CHeroList::CHeroItem::update() @@ -263,7 +265,7 @@ void CHeroList::CHeroItem::showTooltip() std::string CHeroList::CHeroItem::getHoverText() { - return boost::str(boost::format(CGI->generaltexth->allTexts[15]) % hero->getNameTranslated() % hero->getClassNameTranslated()); + return boost::str(boost::format(CGI->generaltexth->allTexts[15]) % hero->getNameTranslated() % hero->getClassNameTranslated()) + hero->getMovementPointsTextIfOwner(hero->getOwner()); } void CHeroList::CHeroItem::gesture(bool on, const Point & initialPosition, const Point & finalPosition) @@ -301,6 +303,55 @@ void CHeroList::CHeroItem::gesture(bool on, const Point & initialPosition, const GH.windows().createAndPushWindow(pos.center(), menuElements, true); } +void CHeroList::CHeroItem::keyPressed(EShortcut key) +{ + if(!hero) + return; + + if(parent->selected != this->shared_from_this()) + return; + + auto & heroes = LOCPLINT->localState->getWanderingHeroes(); + + if(key == EShortcut::LIST_HERO_DISMISS) + { + LOCPLINT->showYesNoDialog(CGI->generaltexth->allTexts[22], [=](){ LOCPLINT->cb->dismissHero(hero); }, nullptr); + return; + } + + if(heroes.size() < 2) + return; + + size_t heroPos = vstd::find_pos(heroes, hero); + const CGHeroInstance * heroUpper = (heroPos < 1) ? nullptr : heroes.at(heroPos - 1); + const CGHeroInstance * heroLower = (heroPos > heroes.size() - 2) ? nullptr : heroes.at(heroPos + 1); + + switch(key) + { + case EShortcut::LIST_HERO_UP: + if(heroUpper) + LOCPLINT->localState->swapWanderingHero(heroPos, heroPos - 1); + break; + + case EShortcut::LIST_HERO_DOWN: + if(heroLower) + LOCPLINT->localState->swapWanderingHero(heroPos, heroPos + 1); + break; + + case EShortcut::LIST_HERO_TOP: + if(heroUpper) + for (size_t i = heroPos; i > 0; i--) + LOCPLINT->localState->swapWanderingHero(i, i - 1); + break; + + case EShortcut::LIST_HERO_BOTTOM: + if(heroLower) + for (int i = heroPos; i < heroes.size() - 1; i++) + LOCPLINT->localState->swapWanderingHero(i, i + 1); + break; + } +} + std::shared_ptr CHeroList::createItem(size_t index) { if (LOCPLINT->localState->getWanderingHeroes().size() > index) @@ -365,12 +416,12 @@ CTownList::CTownItem::CTownItem(CTownList *parent, const CGTownInstance *Town): CListItem(parent), town(Town) { - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + OBJECT_CONSTRUCTION; picture = std::make_shared(AnimationPath::builtin("ITPA"), 0); pos = picture->pos; update(); - addUsedEvents(GESTURE); + addUsedEvents(GESTURE | KEYBOARD); } std::shared_ptr CTownList::CTownItem::genSelection() @@ -380,7 +431,7 @@ std::shared_ptr CTownList::CTownItem::genSelection() void CTownList::CTownItem::update() { - size_t iconIndex = town->town->clientInfo.icons[town->hasFort()][town->builded >= CGI->settings()->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(); @@ -399,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) @@ -419,24 +470,77 @@ void CTownList::CTownItem::gesture(bool on, const Point & initialPosition, const int townUpperPos = (townIndex < 1) ? -1 : townIndex - 1; int townLowerPos = (townIndex > towns.size() - 2) ? -1 : townIndex + 1; + auto updateList = [](){ + for (auto ki : GH.windows().findWindows()) + ki->townChange(); //update list + }; + std::vector menuElements = { - { RadialMenuConfig::ITEM_ALT_NN, townUpperPos > -1, "altUpTop", "vcmi.radialWheel.moveTop", [townIndex]() + { RadialMenuConfig::ITEM_ALT_NN, townUpperPos > -1, "altUpTop", "vcmi.radialWheel.moveTop", [updateList, townIndex]() { for (int i = townIndex; i > 0; i--) LOCPLINT->localState->swapOwnedTowns(i, i - 1); + updateList(); } }, - { RadialMenuConfig::ITEM_ALT_NW, townUpperPos > -1, "altUp", "vcmi.radialWheel.moveUp", [townIndex, townUpperPos](){LOCPLINT->localState->swapOwnedTowns(townIndex, townUpperPos); } }, - { RadialMenuConfig::ITEM_ALT_SW, townLowerPos > -1, "altDown", "vcmi.radialWheel.moveDown", [townIndex, townLowerPos](){ LOCPLINT->localState->swapOwnedTowns(townIndex, townLowerPos); } }, - { RadialMenuConfig::ITEM_ALT_SS, townLowerPos > -1, "altDownBottom", "vcmi.radialWheel.moveBottom", [townIndex, towns]() + { RadialMenuConfig::ITEM_ALT_NW, townUpperPos > -1, "altUp", "vcmi.radialWheel.moveUp", [updateList, townIndex, townUpperPos](){LOCPLINT->localState->swapOwnedTowns(townIndex, townUpperPos); updateList(); } }, + { RadialMenuConfig::ITEM_ALT_SW, townLowerPos > -1, "altDown", "vcmi.radialWheel.moveDown", [updateList, townIndex, townLowerPos](){ LOCPLINT->localState->swapOwnedTowns(townIndex, townLowerPos); updateList(); } }, + { RadialMenuConfig::ITEM_ALT_SS, townLowerPos > -1, "altDownBottom", "vcmi.radialWheel.moveBottom", [updateList, townIndex, towns]() { for (int i = townIndex; i < towns.size() - 1; i++) LOCPLINT->localState->swapOwnedTowns(i, i + 1); + updateList(); } }, }; GH.windows().createAndPushWindow(pos.center(), menuElements, true); } +void CTownList::CTownItem::keyPressed(EShortcut key) +{ + if(parent->selected != this->shared_from_this()) + return; + + const std::vector towns = LOCPLINT->localState->getOwnedTowns(); + size_t townIndex = vstd::find_pos(towns, town); + + if(townIndex + 1 > towns.size() || !towns.at(townIndex)) + return; + + if(towns.size() < 2) + return; + + int townUpperPos = (townIndex < 1) ? -1 : townIndex - 1; + int townLowerPos = (townIndex > towns.size() - 2) ? -1 : townIndex + 1; + + switch(key) + { + case EShortcut::LIST_TOWN_UP: + if(townUpperPos > -1) + LOCPLINT->localState->swapOwnedTowns(townIndex, townUpperPos); + break; + + case EShortcut::LIST_TOWN_DOWN: + if(townLowerPos > -1) + LOCPLINT->localState->swapOwnedTowns(townIndex, townLowerPos); + break; + + case EShortcut::LIST_TOWN_TOP: + if(townUpperPos > -1) + for (int i = townIndex; i > 0; i--) + LOCPLINT->localState->swapOwnedTowns(i, i - 1); + break; + + case EShortcut::LIST_TOWN_BOTTOM: + if(townLowerPos > -1) + for (int i = townIndex; i < towns.size() - 1; i++) + LOCPLINT->localState->swapOwnedTowns(i, i + 1); + break; + } + + for (auto ki : GH.windows().findWindows()) + ki->townChange(); //update list +} + std::string CTownList::CTownItem::getHoverText() { return town->getObjectName(); diff --git a/client/adventureMap/CList.h b/client/adventureMap/CList.h index 5b03f2b1e..bccd4fecf 100644 --- a/client/adventureMap/CList.h +++ b/client/adventureMap/CList.h @@ -29,9 +29,10 @@ class CList : public Scrollable protected: class CListItem : public CIntObject, public std::enable_shared_from_this { - CList * parent; std::shared_ptr selection; public: + CList * parent; + CListItem(CList * parent); ~CListItem(); @@ -55,9 +56,6 @@ protected: private: const size_t size; - - //for selection\deselection - std::shared_ptr selected; void select(std::shared_ptr which); friend class CListItem; @@ -81,6 +79,9 @@ protected: void update(); public: + //for selection\deselection + std::shared_ptr selected; + /// functions that will be called when selection changes CFunctionList onSelect; @@ -128,6 +129,7 @@ class CHeroList : public CList void open() override; void showTooltip() override; void gesture(bool on, const Point & initialPosition, const Point & finalPosition) override; + void keyPressed(EShortcut key) override; std::string getHoverText() override; }; @@ -162,6 +164,7 @@ class CTownList : public CList void open() override; void showTooltip() override; void gesture(bool on, const Point & initialPosition, const Point & finalPosition) override; + void keyPressed(EShortcut key) override; std::string getHoverText() override; }; diff --git a/client/adventureMap/CMinimap.cpp b/client/adventureMap/CMinimap.cpp index bdc142e01..fad393eae 100644 --- a/client/adventureMap/CMinimap.cpp +++ b/client/adventureMap/CMinimap.cpp @@ -26,7 +26,7 @@ #include "../windows/InfoWindows.h" #include "../../CCallback.h" -#include "../../lib/CGeneralTextHandler.h" +#include "../../lib/texts/CGeneralTextHandler.h" #include "../../lib/TerrainHandler.h" #include "../../lib/mapObjects/CGHeroInstance.h" #include "../../lib/mapping/CMapDefines.h" @@ -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) @@ -73,7 +73,7 @@ void CMinimapInstance::redrawMinimap() CMinimapInstance::CMinimapInstance(CMinimap *Parent, int Level): parent(Parent), - minimap(new Canvas(Point(LOCPLINT->cb->getMapSize().x, LOCPLINT->cb->getMapSize().y))), + minimap(new Canvas(Point(LOCPLINT->cb->getMapSize().x, LOCPLINT->cb->getMapSize().y), CanvasScalingPolicy::IGNORE)), level(Level) { pos.w = parent->pos.w; @@ -90,11 +90,11 @@ CMinimap::CMinimap(const Rect & position) : CIntObject(LCLICK | SHOW_POPUP | DRAG | MOVE | GESTURE, position.topLeft()), level(0) { - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + OBJECT_CONSTRUCTION; - double maxSideLenghtSrc = std::max(LOCPLINT->cb->getMapSize().x, LOCPLINT->cb->getMapSize().y); - double maxSideLenghtDst = std::max(position.w, position.h); - double resize = maxSideLenghtSrc / maxSideLenghtDst; + double maxSideLengthSrc = std::max(LOCPLINT->cb->getMapSize().x, LOCPLINT->cb->getMapSize().y); + double maxSideLengthDst = std::max(position.w, position.h); + double resize = maxSideLengthSrc / maxSideLengthDst; Point newMinimapSize = Point(LOCPLINT->cb->getMapSize().x/ resize, LOCPLINT->cb->getMapSize().y / resize); Point offset = Point((std::max(newMinimapSize.x, newMinimapSize.y) - newMinimapSize.x) / 2, (std::max(newMinimapSize.x, newMinimapSize.y) - newMinimapSize.y) / 2); @@ -202,7 +202,7 @@ void CMinimap::update() if(aiShield->recActions & UPDATE) //AI turn is going on. There is no need to update minimap return; - OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255-DISPOSE); + OBJECT_CONSTRUCTION; minimap = std::make_shared(this, level); redraw(); } diff --git a/client/adventureMap/CResDataBar.cpp b/client/adventureMap/CResDataBar.cpp index 7096b47bc..c26f41d65 100644 --- a/client/adventureMap/CResDataBar.cpp +++ b/client/adventureMap/CResDataBar.cpp @@ -21,7 +21,7 @@ #include "../../CCallback.h" #include "../../lib/CConfigHandler.h" -#include "../../lib/CGeneralTextHandler.h" +#include "../../lib/texts/CGeneralTextHandler.h" #include "../../lib/ResourceSet.h" CResDataBar::CResDataBar(const ImagePath & imageName, const Point & position) @@ -29,9 +29,9 @@ CResDataBar::CResDataBar(const ImagePath & imageName, const Point & position) pos.x += position.x; pos.y += position.y; - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + OBJECT_CONSTRUCTION; background = std::make_shared(imageName, 0, 0); - background->colorize(LOCPLINT->playerID); + background->setPlayerColor(LOCPLINT->playerID); pos.w = background->pos.w; pos.h = background->pos.h; @@ -84,7 +84,7 @@ void CResDataBar::showAll(Canvas & to) to.drawText(pos.topLeft() + *datePosition, FONT_SMALL, Colors::WHITE, ETextAlignment::TOPLEFT, buildDateString()); } -void CResDataBar::colorize(PlayerColor player) +void CResDataBar::setPlayerColor(PlayerColor player) { - background->colorize(player); + background->setPlayerColor(player); } diff --git a/client/adventureMap/CResDataBar.h b/client/adventureMap/CResDataBar.h index 3bf294ad4..3363802c4 100644 --- a/client/adventureMap/CResDataBar.h +++ b/client/adventureMap/CResDataBar.h @@ -34,7 +34,7 @@ public: void setDatePosition(const Point & position); void setResourcePosition(const GameResID & resource, const Point & position); - void colorize(PlayerColor player); + void setPlayerColor(PlayerColor player); void showAll(Canvas & to) override; }; diff --git a/client/adventureMap/MapAudioPlayer.cpp b/client/adventureMap/MapAudioPlayer.cpp index 81a7cf002..a6e360b3a 100644 --- a/client/adventureMap/MapAudioPlayer.cpp +++ b/client/adventureMap/MapAudioPlayer.cpp @@ -12,10 +12,12 @@ #include "../CCallback.h" #include "../CGameInfo.h" -#include "../CMusicHandler.h" #include "../CPlayerInterface.h" #include "../mapView/mapHandler.h" +#include "../media/IMusicPlayer.h" +#include "../media/ISoundPlayer.h" +#include "../../lib/CRandomGenerator.h" #include "../../lib/TerrainHandler.h" #include "../../lib/mapObjects/CArmedInstance.h" #include "../../lib/mapObjects/CGHeroInstance.h" @@ -79,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); } } @@ -106,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); @@ -135,8 +137,12 @@ std::vector MapAudioPlayer::getAmbientSounds(const int3 & tile) if (!object) logGlobal->warn("Already removed object %d found on tile! (%d %d %d)", objectID.getNum(), tile.x, tile.y, tile.z); - if(object && object->getAmbientSound()) - result.push_back(object->getAmbientSound().value()); + if(object) + { + auto ambientSound = object->getAmbientSound(CRandomGenerator::getDefault()); + if (ambientSound) + result.push_back(ambientSound.value()); + } } if(CGI->mh->getMap()->isCoastalTile(tile)) @@ -176,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/adventureMap/TurnTimerWidget.cpp b/client/adventureMap/TurnTimerWidget.cpp index c97d67f94..aea34c066 100644 --- a/client/adventureMap/TurnTimerWidget.cpp +++ b/client/adventureMap/TurnTimerWidget.cpp @@ -11,11 +11,11 @@ #include "TurnTimerWidget.h" #include "../CGameInfo.h" -#include "../CMusicHandler.h" #include "../CPlayerInterface.h" #include "../battle/BattleInterface.h" #include "../battle/BattleStacksController.h" #include "../gui/CGuiHandler.h" +#include "../media/ISoundPlayer.h" #include "../render/Graphics.h" #include "../widgets/Images.h" #include "../widgets/GraphicalPrimitiveCanvas.h" @@ -35,7 +35,7 @@ TurnTimerWidget::TurnTimerWidget(const Point & position, PlayerColor player) , lastSoundCheckSeconds(0) , isBattleMode(player.isValidPlayer()) { - OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + OBJECT_CONSTRUCTION; pos += position; pos.w = 0; diff --git a/client/battle/BattleActionsController.cpp b/client/battle/BattleActionsController.cpp index b66eea8f2..e5920ade9 100644 --- a/client/battle/BattleActionsController.cpp +++ b/client/battle/BattleActionsController.cpp @@ -28,7 +28,7 @@ #include "../../CCallback.h" #include "../../lib/CConfigHandler.h" -#include "../../lib/CGeneralTextHandler.h" +#include "../../lib/texts/CGeneralTextHandler.h" #include "../../lib/CRandomGenerator.h" #include "../../lib/CStack.h" #include "../../lib/battle/BattleAction.h" @@ -175,6 +175,18 @@ void BattleActionsController::enterCreatureCastingMode() if (!owner.stacksController->getActiveStack()) return; + if(owner.getBattle()->battleCanTargetEmptyHex(owner.stacksController->getActiveStack())) + { + auto actionFilterPredicate = [](const PossiblePlayerBattleAction x) + { + return x.get() != PossiblePlayerBattleAction::SHOOT; + }; + + vstd::erase_if(possibleActions, actionFilterPredicate); + GH.fakeMouseMove(); + return; + } + if (!isActiveStackSpellcaster()) return; @@ -263,6 +275,9 @@ void BattleActionsController::reorderPossibleActionsPriority(const CStack * stac return 2; break; case PossiblePlayerBattleAction::SHOOT: + if(targetStack == nullptr || targetStack->unitSide() == stack->unitSide() || !targetStack->alive()) + return 100; //bottom priority + return 4; break; case PossiblePlayerBattleAction::ATTACK_AND_RETURN: @@ -312,8 +327,8 @@ void BattleActionsController::castThisSpell(SpellID spellID) heroSpellToCast = std::make_shared(); heroSpellToCast->actionType = EActionType::HERO_SPELL; heroSpellToCast->spell = spellID; - heroSpellToCast->stackNumber = (owner.attackingHeroInstance->tempOwner == owner.curInt->playerID) ? -1 : -2; - heroSpellToCast->side = owner.defendingHeroInstance ? (owner.curInt->playerID == owner.defendingHeroInstance->tempOwner) : false; + heroSpellToCast->stackNumber = -1; + heroSpellToCast->side = owner.curInt->cb->getBattle(owner.getBattleID())->battleGetMySide(); //choosing possible targets const CGHeroInstance *castingHero = (owner.attackingHeroInstance->tempOwner == owner.curInt->playerID) ? owner.attackingHeroInstance : owner.defendingHeroInstance; @@ -356,6 +371,12 @@ const CSpell * BattleActionsController::getStackSpellToCast(BattleHex hoveredHex auto action = selectAction(hoveredHex); + if(owner.stacksController->getActiveStack()->hasBonusOfType(BonusType::SPELL_LIKE_ATTACK)) + { + auto bonus = owner.stacksController->getActiveStack()->getBonus(Selector::type()(BonusType::SPELL_LIKE_ATTACK)); + return bonus->subtype.as().toSpell(); + } + if (action.spell() == SpellID::NONE) return nullptr; @@ -514,6 +535,13 @@ std::string BattleActionsController::actionGetStatusMessage(PossiblePlayerBattle case PossiblePlayerBattleAction::SHOOT: { + if(targetStack == nullptr) //should be true only for spell-like attack + { + auto spellLikeAttackBonus = owner.stacksController->getActiveStack()->getBonus(Selector::type()(BonusType::SPELL_LIKE_ATTACK)); + assert(spellLikeAttackBonus != nullptr); + return boost::str(boost::format(CGI->generaltexth->allTexts[26]) % spellLikeAttackBonus->subtype.as().toSpell()->getNameTranslated()); + } + const auto * shooter = owner.stacksController->getActiveStack(); DamageEstimation retaliation; @@ -625,7 +653,20 @@ bool BattleActionsController::actionIsLegal(PossiblePlayerBattleAction action, B return false; case PossiblePlayerBattleAction::SHOOT: - return owner.getBattle()->battleCanShoot(owner.stacksController->getActiveStack(), targetHex); + { + auto currentStack = owner.stacksController->getActiveStack(); + if(!owner.getBattle()->battleCanShoot(currentStack, targetHex)) + return false; + + if(targetStack == nullptr && owner.getBattle()->battleCanTargetEmptyHex(currentStack)) + { + auto spellLikeAttackBonus = currentStack->getBonus(Selector::type()(BonusType::SPELL_LIKE_ATTACK)); + const CSpell * spellDataToCheck = spellLikeAttackBonus->subtype.as().toSpell(); + return isCastingPossibleHere(spellDataToCheck, nullptr, targetHex); + } + + return true; + } case PossiblePlayerBattleAction::NO_LOCATION: return false; @@ -771,7 +812,7 @@ void BattleActionsController::actionRealize(PossiblePlayerBattleAction action, B } } - if (!spellcastingModeActive()) + if (!heroSpellcastingModeActive()) { if (action.spell().hasValue()) { @@ -1018,14 +1059,9 @@ void BattleActionsController::activateStack() void BattleActionsController::onHexRightClicked(BattleHex clickedHex) { - auto spellcastActionPredicate = [](PossiblePlayerBattleAction & action) - { - return action.spellcast(); - }; + bool isCurrentStackInSpellcastMode = creatureSpellcastingModeActive(); - bool isCurrentStackInSpellcastMode = !possibleActions.empty() && std::all_of(possibleActions.begin(), possibleActions.end(), spellcastActionPredicate); - - if (spellcastingModeActive() || isCurrentStackInSpellcastMode) + if (heroSpellcastingModeActive() || isCurrentStackInSpellcastMode) { endCastingSpell(); CRClickPopup::createAndPush(CGI->generaltexth->translate("core.genrltxt.731")); // spell cancelled @@ -1044,11 +1080,21 @@ void BattleActionsController::onHexRightClicked(BattleHex clickedHex) owner.defendingHero->heroRightClicked(); } -bool BattleActionsController::spellcastingModeActive() const +bool BattleActionsController::heroSpellcastingModeActive() const { return heroSpellToCast != nullptr; } +bool BattleActionsController::creatureSpellcastingModeActive() const +{ + auto spellcastModePredicate = [](const PossiblePlayerBattleAction & action) + { + return action.spellcast() || action.get() == PossiblePlayerBattleAction::SHOOT; //for hotkey-eligible SPELL_LIKE_ATTACK creature should have only SHOOT action + }; + + return !possibleActions.empty() && std::all_of(possibleActions.begin(), possibleActions.end(), spellcastModePredicate); +} + bool BattleActionsController::currentActionSpellcasting(BattleHex hoveredHex) { if (heroSpellToCast) diff --git a/client/battle/BattleActionsController.h b/client/battle/BattleActionsController.h index a22f86251..74db750e1 100644 --- a/client/battle/BattleActionsController.h +++ b/client/battle/BattleActionsController.h @@ -82,8 +82,10 @@ public: /// initialize list of potential actions for new active stack void activateStack(); - /// returns true if UI is currently in target selection mode - bool spellcastingModeActive() const; + /// returns true if UI is currently in hero spell target selection mode + bool heroSpellcastingModeActive() const; + /// returns true if UI is currently in "F" hotkey creature spell target selection mode + bool creatureSpellcastingModeActive() const; /// returns true if one of the following is true: /// - we are casting spell by hero @@ -120,7 +122,7 @@ public: const std::vector & getPossibleActions() const; void removePossibleAction(PossiblePlayerBattleAction); - /// inserts possible action in the beggining in order to prioritize it + /// inserts possible action in the beginning in order to prioritize it void pushFrontPossibleAction(PossiblePlayerBattleAction); /// resets possible actions to original state diff --git a/client/battle/BattleAnimationClasses.cpp b/client/battle/BattleAnimationClasses.cpp index 2bff6e8ce..7d34e8978 100644 --- a/client/battle/BattleAnimationClasses.cpp +++ b/client/battle/BattleAnimationClasses.cpp @@ -20,10 +20,11 @@ #include "CreatureAnimation.h" #include "../CGameInfo.h" -#include "../CMusicHandler.h" #include "../CPlayerInterface.h" #include "../gui/CursorHandler.h" #include "../gui/CGuiHandler.h" +#include "../media/ISoundPlayer.h" +#include "../render/CAnimation.h" #include "../render/IRenderHandler.h" #include "../../CCallback.h" @@ -160,7 +161,7 @@ ECreatureAnimType AttackAnimation::findValidGroup( const std::vectorunitType()->getId() == CreatureID::ARROW_TOWERS) - return owner.siegeController->getTurretCreature(); + return owner.siegeController->getTurretCreature(attackingStack->initialPosition); else return attackingStack->unitType(); } @@ -355,9 +356,9 @@ bool MovementAnimation::init() myAnim->setType(ECreatureAnimType::MOVING); } - if (moveSoundHander == -1) + if (moveSoundHandler == -1) { - moveSoundHander = CCS->soundh->playSound(stack->unitType()->sounds.move, -1); + moveSoundHandler = CCS->soundh->playSound(stack->unitType()->sounds.move, -1); } Point begPosition = owner.stacksController->getStackPositionAtHex(prevHex, stack); @@ -398,12 +399,12 @@ void MovementAnimation::tick(uint32_t msPassed) myAnim->pos.moveTo(coords); // true if creature haven't reached the final destination hex - if ((curentMoveIndex + 1) < destTiles.size()) + if ((currentMoveIndex + 1) < destTiles.size()) { // update the next hex field which has to be reached by the stack - curentMoveIndex++; + currentMoveIndex++; prevHex = nextHex; - nextHex = destTiles[curentMoveIndex]; + nextHex = destTiles[currentMoveIndex]; // request re-initialization initialized = false; @@ -417,18 +418,18 @@ MovementAnimation::~MovementAnimation() { assert(stack); - if(moveSoundHander != -1) - CCS->soundh->stopSound(moveSoundHander); + if(moveSoundHandler != -1) + CCS->soundh->stopSound(moveSoundHandler); } MovementAnimation::MovementAnimation(BattleInterface & owner, const CStack *stack, std::vector _destTiles, int _distance) : StackMoveAnimation(owner, stack, stack->getPosition(), _destTiles.front()), destTiles(_destTiles), - curentMoveIndex(0), + currentMoveIndex(0), begX(0), begY(0), distanceX(0), distanceY(0), progressPerSecond(0.0), - moveSoundHander(-1), + moveSoundHandler(-1), progress(0.0) { logAnim->debug("Created MovementAnimation for %s", stack->getName()); @@ -649,7 +650,7 @@ void RangedAttackAnimation::setAnimationGroup() Point shooterPos = stackAnimation(attackingStack)->pos.topLeft(); Point shotTarget = owner.stacksController->getStackPositionAtHex(dest, defendingStack); - //maximal angle in radians between straight horizontal line and shooting line for which shot is considered to be straight (absoulte value) + //maximal angle in radians between straight horizontal line and shooting line for which shot is considered to be straight (absolute value) static const double straightAngle = 0.2; double projectileAngle = -atan2(shotTarget.y - shooterPos.y, std::abs(shotTarget.x - shooterPos.x)); @@ -672,18 +673,18 @@ void RangedAttackAnimation::initializeProjectile() if (getGroup() == getUpwardsGroup()) { - shotOrigin.x += ( -25 + shooterInfo->animation.upperRightMissleOffsetX ) * multiplier; - shotOrigin.y += shooterInfo->animation.upperRightMissleOffsetY; + shotOrigin.x += ( -25 + shooterInfo->animation.upperRightMissileOffsetX ) * multiplier; + shotOrigin.y += shooterInfo->animation.upperRightMissileOffsetY; } else if (getGroup() == getDownwardsGroup()) { - shotOrigin.x += ( -25 + shooterInfo->animation.lowerRightMissleOffsetX ) * multiplier; - shotOrigin.y += shooterInfo->animation.lowerRightMissleOffsetY; + shotOrigin.x += ( -25 + shooterInfo->animation.lowerRightMissileOffsetX ) * multiplier; + shotOrigin.y += shooterInfo->animation.lowerRightMissileOffsetY; } else if (getGroup() == getForwardGroup()) { - shotOrigin.x += ( -25 + shooterInfo->animation.rightMissleOffsetX ) * multiplier; - shotOrigin.y += shooterInfo->animation.rightMissleOffsetY; + shotOrigin.x += ( -25 + shooterInfo->animation.rightMissileOffsetX ) * multiplier; + shotOrigin.y += shooterInfo->animation.rightMissileOffsetY; } else { @@ -880,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)), + animation(GH.renderHandler().loadAnimation(animationName, EImageBlitMode::SIMPLE)), + transparencyFactor(transparencyFactor), effectFlags(effects), effectFinished(false), reversed(reversed) @@ -891,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); @@ -925,8 +927,6 @@ EffectAnimation::EffectAnimation(BattleInterface & owner, const AnimationPath & bool EffectAnimation::init() { - animation->preload(); - auto first = animation->getImage(0, 0, true); if(!first) { @@ -952,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 00b68bf7a..83233819b 100644 --- a/client/battle/BattleAnimationClasses.h +++ b/client/battle/BattleAnimationClasses.h @@ -141,10 +141,10 @@ protected: class MovementAnimation : public StackMoveAnimation { private: - int moveSoundHander; // sound handler used when moving a unit + int moveSoundHandler; // sound handler used when moving a unit std::vector destTiles; //full path, includes already passed hexes - ui32 curentMoveIndex; // index of nextHex in destTiles + ui32 currentMoveIndex; // index of nextHex in destTiles double begX, begY; // starting position double distanceX, distanceY; // full movement distance, may be negative if creture moves topleft @@ -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 f1db77c16..0d3caf2ea 100644 --- a/client/battle/BattleEffectsController.cpp +++ b/client/battle/BattleEffectsController.cpp @@ -18,9 +18,9 @@ #include "BattleStacksController.h" #include "BattleRenderer.h" -#include "../CMusicHandler.h" #include "../CGameInfo.h" #include "../CPlayerInterface.h" +#include "../media/ISoundPlayer.h" #include "../render/Canvas.h" #include "../render/CAnimation.h" #include "../render/Graphics.h" @@ -31,7 +31,7 @@ #include "../../lib/networkPacks/PacksForClientBattle.h" #include "../../lib/CStack.h" #include "../../lib/IGameEventsReceiver.h" -#include "../../lib/CGeneralTextHandler.h" +#include "../../lib/texts/CGeneralTextHandler.h" BattleEffectsController::BattleEffectsController(BattleInterface & owner): owner(owner) @@ -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 d55c22360..27c62535e 100644 --- a/client/battle/BattleFieldController.cpp +++ b/client/battle/BattleFieldController.cpp @@ -82,67 +82,47 @@ namespace HexMasks }; } -std::map hexEdgeMaskToFrameIndex; - -// Maps HexEdgesMask to "Frame" indexes for range highligt images -void initializeHexEdgeMaskToFrameIndex() +static const std::map hexEdgeMaskToFrameIndex = { - hexEdgeMaskToFrameIndex[HexMasks::empty] = 0; - - hexEdgeMaskToFrameIndex[HexMasks::topLeft] = 1; - hexEdgeMaskToFrameIndex[HexMasks::topRight] = 2; - hexEdgeMaskToFrameIndex[HexMasks::right] = 3; - hexEdgeMaskToFrameIndex[HexMasks::bottomRight] = 4; - hexEdgeMaskToFrameIndex[HexMasks::bottomLeft] = 5; - hexEdgeMaskToFrameIndex[HexMasks::left] = 6; - - hexEdgeMaskToFrameIndex[HexMasks::top] = 7; - hexEdgeMaskToFrameIndex[HexMasks::bottom] = 8; - - hexEdgeMaskToFrameIndex[HexMasks::topRightHalfCorner] = 9; - hexEdgeMaskToFrameIndex[HexMasks::bottomRightHalfCorner] = 10; - hexEdgeMaskToFrameIndex[HexMasks::bottomLeftHalfCorner] = 11; - hexEdgeMaskToFrameIndex[HexMasks::topLeftHalfCorner] = 12; - - hexEdgeMaskToFrameIndex[HexMasks::rightTopAndBottom] = 13; - hexEdgeMaskToFrameIndex[HexMasks::leftTopAndBottom] = 14; - - hexEdgeMaskToFrameIndex[HexMasks::rightHalf] = 13; - hexEdgeMaskToFrameIndex[HexMasks::leftHalf] = 14; - - hexEdgeMaskToFrameIndex[HexMasks::topRightCorner] = 15; - hexEdgeMaskToFrameIndex[HexMasks::bottomRightCorner] = 16; - hexEdgeMaskToFrameIndex[HexMasks::bottomLeftCorner] = 17; - hexEdgeMaskToFrameIndex[HexMasks::topLeftCorner] = 18; -} + { HexMasks::empty, 0 }, + { HexMasks::topLeft, 1 }, + { HexMasks::topRight, 2 }, + { HexMasks::right, 3 }, + { HexMasks::bottomRight, 4 }, + { HexMasks::bottomLeft, 5 }, + { HexMasks::left, 6 }, + { HexMasks::top, 7 }, + { HexMasks::bottom, 8 }, + { HexMasks::topRightHalfCorner, 9 }, + { HexMasks::bottomRightHalfCorner, 10 }, + { HexMasks::bottomLeftHalfCorner, 11 }, + { HexMasks::topLeftHalfCorner, 12 }, + { HexMasks::rightTopAndBottom, 13 }, + { HexMasks::leftTopAndBottom, 14 }, + { HexMasks::rightHalf, 13 }, + { HexMasks::leftHalf, 14 }, + { HexMasks::topRightCorner, 15 }, + { HexMasks::bottomRightCorner, 16 }, + { HexMasks::bottomLeftCorner, 17 }, + { HexMasks::topLeftCorner, 18 } +}; BattleFieldController::BattleFieldController(BattleInterface & owner): owner(owner) { - OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + OBJECT_CONSTRUCTION; //preparing cells and hexes cellBorder = GH.renderHandler().loadImage(ImagePath::builtin("CCELLGRD.BMP"), EImageBlitMode::COLORKEY); - cellShade = GH.renderHandler().loadImage(ImagePath::builtin("CCELLSHD.BMP")); + 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); - attackCursors = GH.renderHandler().loadAnimation(AnimationPath::builtin("CRCOMBAT")); - attackCursors->preload(); + attackCursors = GH.renderHandler().loadAnimation(AnimationPath::builtin("CRCOMBAT"), EImageBlitMode::COLORKEY); + spellCursors = GH.renderHandler().loadAnimation(AnimationPath::builtin("CRSPELL"), EImageBlitMode::COLORKEY); - spellCursors = GH.renderHandler().loadAnimation(AnimationPath::builtin("CRSPELL")); - spellCursors->preload(); - - initializeHexEdgeMaskToFrameIndex(); - - rangedFullDamageLimitImages = GH.renderHandler().loadAnimation(AnimationPath::builtin("battle/rangeHighlights/rangeHighlightsGreen.json")); - rangedFullDamageLimitImages->preload(); - - shootingRangeLimitImages = GH.renderHandler().loadAnimation(AnimationPath::builtin("battle/rangeHighlights/rangeHighlightsRed.json")); - shootingRangeLimitImages->preload(); - - flipRangeLimitImagesIntoPositions(rangedFullDamageLimitImages); - flipRangeLimitImagesIntoPositions(shootingRangeLimitImages); + rangedFullDamageLimitImages = GH.renderHandler().loadAnimation(AnimationPath::builtin("battle/rangeHighlights/rangeHighlightsGreen.json"), EImageBlitMode::COLORKEY); + shootingRangeLimitImages = GH.renderHandler().loadAnimation(AnimationPath::builtin("battle/rangeHighlights/rangeHighlightsRed.json"), EImageBlitMode::COLORKEY); if(!owner.siegeController) { @@ -162,7 +142,7 @@ BattleFieldController::BattleFieldController(BattleInterface & owner): pos.w = background->width(); pos.h = background->height(); - backgroundWithHexes = std::make_unique(Point(background->width(), background->height())); + backgroundWithHexes = std::make_unique(Point(background->width(), background->height()), CanvasScalingPolicy::AUTO); updateAccessibleHexes(); addUsedEvents(LCLICK | SHOW_POPUP | MOVE | TIME | GESTURE); @@ -176,7 +156,7 @@ void BattleFieldController::activate() void BattleFieldController::createHeroes() { - OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + OBJECT_CONSTRUCTION; // create heroes as part of our constructor for correct positioning inside battlefield if(owner.attackingHeroInstance) @@ -376,9 +356,6 @@ std::set BattleFieldController::getHighlightedHexesForSpellRange() std::set result; auto hoveredHex = getHoveredHex(); - if(!settings["battle"]["mouseShadow"].Bool()) - return result; - const spells::Caster *caster = nullptr; const CSpell *spell = nullptr; @@ -542,38 +519,18 @@ std::vector> BattleFieldController::calculateRangeLimitH mask.set(direction); uint8_t imageKey = static_cast(mask.to_ulong()); - output.push_back(limitImages->getImage(hexEdgeMaskToFrameIndex[imageKey])); + output.push_back(limitImages->getImage(hexEdgeMaskToFrameIndex.at(imageKey))); } return output; } -void BattleFieldController::calculateRangeLimitAndHighlightImages(uint8_t distance, std::shared_ptr rangeLimitImages, std::vector & rangeLimitHexes, std::vector> & rangeLimitHexesHighligts) +void BattleFieldController::calculateRangeLimitAndHighlightImages(uint8_t distance, std::shared_ptr rangeLimitImages, std::vector & rangeLimitHexes, std::vector> & rangeLimitHexesHighlights) { std::vector rangeHexes = getRangeHexes(hoveredHex, distance); rangeLimitHexes = getRangeLimitHexes(hoveredHex, rangeHexes, distance); std::vector> rangeLimitNeighbourDirections = getOutsideNeighbourDirectionsForLimitHexes(rangeHexes, rangeLimitHexes); - rangeLimitHexesHighligts = calculateRangeLimitHighlightImages(rangeLimitNeighbourDirections, rangeLimitImages); -} - -void BattleFieldController::flipRangeLimitImagesIntoPositions(std::shared_ptr images) -{ - images->getImage(hexEdgeMaskToFrameIndex[HexMasks::topRight])->verticalFlip(); - images->getImage(hexEdgeMaskToFrameIndex[HexMasks::right])->verticalFlip(); - images->getImage(hexEdgeMaskToFrameIndex[HexMasks::bottomRight])->doubleFlip(); - images->getImage(hexEdgeMaskToFrameIndex[HexMasks::bottomLeft])->horizontalFlip(); - - images->getImage(hexEdgeMaskToFrameIndex[HexMasks::bottom])->horizontalFlip(); - - images->getImage(hexEdgeMaskToFrameIndex[HexMasks::topRightHalfCorner])->verticalFlip(); - images->getImage(hexEdgeMaskToFrameIndex[HexMasks::bottomRightHalfCorner])->doubleFlip(); - images->getImage(hexEdgeMaskToFrameIndex[HexMasks::bottomLeftHalfCorner])->horizontalFlip(); - - images->getImage(hexEdgeMaskToFrameIndex[HexMasks::rightHalf])->verticalFlip(); - - images->getImage(hexEdgeMaskToFrameIndex[HexMasks::topRightCorner])->verticalFlip(); - images->getImage(hexEdgeMaskToFrameIndex[HexMasks::bottomRightCorner])->doubleFlip(); - images->getImage(hexEdgeMaskToFrameIndex[HexMasks::bottomLeftCorner])->horizontalFlip(); + rangeLimitHexesHighlights = calculateRangeLimitHighlightImages(rangeLimitNeighbourDirections, rangeLimitImages); } void BattleFieldController::showHighlightedHexes(Canvas & canvas) @@ -581,14 +538,16 @@ void BattleFieldController::showHighlightedHexes(Canvas & canvas) std::vector rangedFullDamageLimitHexes; std::vector shootingRangeLimitHexes; - std::vector> rangedFullDamageLimitHexesHighligts; - std::vector> shootingRangeLimitHexesHighligts; + std::vector> rangedFullDamageLimitHexesHighlights; + std::vector> shootingRangeLimitHexesHighlights; std::set hoveredStackMovementRangeHexes = getMovementRangeForHoveredStack(); std::set hoveredSpellHexes = getHighlightedHexesForSpellRange(); std::set hoveredMoveHexes = getHighlightedHexesForMovementTarget(); BattleHex hoveredHex = getHoveredHex(); + std::set hoveredMouseHex = hoveredHex.isValid() ? std::set({ hoveredHex }) : std::set(); + const CStack * hoveredStack = getHoveredStack(); if(!hoveredStack && hoveredHex == BattleHex::INVALID) return; @@ -598,14 +557,19 @@ void BattleFieldController::showHighlightedHexes(Canvas & canvas) { // calculate array with highlight images for ranged full damage limit auto rangedFullDamageDistance = hoveredStack->getRangedFullDamageDistance(); - calculateRangeLimitAndHighlightImages(rangedFullDamageDistance, rangedFullDamageLimitImages, rangedFullDamageLimitHexes, rangedFullDamageLimitHexesHighligts); + calculateRangeLimitAndHighlightImages(rangedFullDamageDistance, rangedFullDamageLimitImages, rangedFullDamageLimitHexes, rangedFullDamageLimitHexesHighlights); // calculate array with highlight images for shooting range limit auto shootingRangeDistance = hoveredStack->getShootingRangeDistance(); - calculateRangeLimitAndHighlightImages(shootingRangeDistance, shootingRangeLimitImages, shootingRangeLimitHexes, shootingRangeLimitHexesHighligts); + calculateRangeLimitAndHighlightImages(shootingRangeDistance, shootingRangeLimitImages, shootingRangeLimitHexes, shootingRangeLimitHexesHighlights); } - auto const & hoveredMouseHexes = hoveredHex != BattleHex::INVALID && owner.actionsController->currentActionSpellcasting(getHoveredHex()) ? hoveredSpellHexes : hoveredMoveHexes; + bool useSpellRangeForMouse = hoveredHex != BattleHex::INVALID + && (owner.actionsController->currentActionSpellcasting(getHoveredHex()) + || owner.actionsController->creatureSpellcastingModeActive()); //at least shooting with SPELL_LIKE_ATTACK can operate in spellcasting mode without being actual spellcast + bool useMoveRangeForMouse = !hoveredMoveHexes.empty() || !settings["battle"]["mouseShadow"].Bool(); + + const auto & hoveredMouseHexes = useSpellRangeForMouse ? hoveredSpellHexes : ( useMoveRangeForMouse ? hoveredMoveHexes : hoveredMouseHex); for(int hex = 0; hex < GameConstants::BFIELD_SIZE; ++hex) { @@ -635,11 +599,11 @@ void BattleFieldController::showHighlightedHexes(Canvas & canvas) } if(hexInRangedFullDamageLimit) { - showHighlightedHex(canvas, rangedFullDamageLimitHexesHighligts[hexIndexInRangedFullDamageLimit], hex, false); + showHighlightedHex(canvas, rangedFullDamageLimitHexesHighlights[hexIndexInRangedFullDamageLimit], hex, false); } if(hexInShootingRangeLimit) { - showHighlightedHex(canvas, shootingRangeLimitHexesHighligts[hexIndexInShootingRangeLimit], hex, false); + showHighlightedHex(canvas, shootingRangeLimitHexesHighlights[hexIndexInShootingRangeLimit], hex, false); } } } @@ -865,7 +829,7 @@ bool BattleFieldController::isTileAttackable(const BattleHex & number) const void BattleFieldController::updateAccessibleHexes() { - auto accessibility = owner.getBattle()->getAccesibility(); + auto accessibility = owner.getBattle()->getAccessibility(); for(int i = 0; i < accessibility.size(); i++) stackCountOutsideHexes[i] = (accessibility[i] == EAccessibility::ACCESSIBLE || (accessibility[i] == EAccessibility::SIDE_COLUMN)); diff --git a/client/battle/BattleFieldController.h b/client/battle/BattleFieldController.h index 4a713a5de..4500e95ab 100644 --- a/client/battle/BattleFieldController.h +++ b/client/battle/BattleFieldController.h @@ -73,7 +73,7 @@ class BattleFieldController : public CIntObject /// calculate if a hex is in range limit and return its index in range bool IsHexInRangeLimit(BattleHex hex, std::vector & rangeLimitHexes, int * hexIndexInRangeLimit); - /// get an array that has for each hex in range, an aray with all directions where an ouside neighbour hex exists + /// get an array that has for each hex in range, an array with all directions where an outside neighbour hex exists std::vector> getOutsideNeighbourDirectionsForLimitHexes(std::vector rangeHexes, std::vector rangeLimitHexes); /// calculates what image to use as range limit, depending on the direction of neighbors @@ -82,10 +82,7 @@ class BattleFieldController : public CIntObject std::vector> calculateRangeLimitHighlightImages(std::vector> hexesNeighbourDirections, std::shared_ptr limitImages); /// calculates all hexes for a range limit and what images to be shown as highlight for each of the hexes - void calculateRangeLimitAndHighlightImages(uint8_t distance, std::shared_ptr rangeLimitImages, std::vector & rangeLimitHexes, std::vector> & rangeLimitHexesHighligts); - - /// to reduce the number of source images used, some images will be used as flipped versions of preloaded ones - void flipRangeLimitImagesIntoPositions(std::shared_ptr images); + void calculateRangeLimitAndHighlightImages(uint8_t distance, std::shared_ptr rangeLimitImages, std::vector & rangeLimitHexes, std::vector> & rangeLimitHexesHighlights); void showBackground(Canvas & canvas); void showBackgroundImage(Canvas & canvas); diff --git a/client/battle/BattleInterface.cpp b/client/battle/BattleInterface.cpp index 290fb8c15..db02f9a2c 100644 --- a/client/battle/BattleInterface.cpp +++ b/client/battle/BattleInterface.cpp @@ -24,20 +24,21 @@ #include "BattleRenderer.h" #include "../CGameInfo.h" -#include "../CMusicHandler.h" #include "../CPlayerInterface.h" #include "../gui/CursorHandler.h" #include "../gui/CGuiHandler.h" #include "../gui/WindowHandler.h" +#include "../media/IMusicPlayer.h" +#include "../media/ISoundPlayer.h" #include "../windows/CTutorialWindow.h" #include "../render/Canvas.h" #include "../adventureMap/AdventureMapInterface.h" #include "../../CCallback.h" +#include "../../lib/BattleFieldHandler.h" #include "../../lib/CStack.h" #include "../../lib/CConfigHandler.h" -#include "../../lib/CGeneralTextHandler.h" -#include "../../lib/CHeroHandler.h" +#include "../../lib/texts/CGeneralTextHandler.h" #include "../../lib/gameState/InfoAboutArmy.h" #include "../../lib/mapObjects/CGTownInstance.h" #include "../../lib/networkPacks/PacksForClientBattle.h" @@ -83,7 +84,7 @@ BattleInterface::BattleInterface(const BattleID & battleID, const CCreatureSet * this->army2 = army2; const CGTownInstance *town = getBattle()->battleGetDefendedTown(); - if(town && town->hasFort()) + if(town && town->fortificationsLevel().wallsHealth > 0) siegeController.reset(new BattleSiegeController(*this, town)); windowObject = std::make_shared(*this); @@ -112,7 +113,23 @@ void BattleInterface::playIntroSoundAndUnlockInterface() onIntroSoundPlayed(); }; - int battleIntroSoundChannel = CCS->soundh->playSoundFromSet(CCS->soundh->battleIntroSounds); + auto bfieldType = getBattle()->battleGetBattlefieldType(); + const auto & battlefieldSound = bfieldType.getInfo()->musicFilename; + + std::vector battleIntroSounds = + { + soundBase::battle00, soundBase::battle01, + soundBase::battle02, soundBase::battle03, soundBase::battle04, + soundBase::battle05, soundBase::battle06, soundBase::battle07 + }; + + int battleIntroSoundChannel = -1; + + if (!battlefieldSound.empty()) + battleIntroSoundChannel = CCS->soundh->playSound(battlefieldSound); + else + battleIntroSoundChannel = CCS->soundh->playSoundFromSet(battleIntroSounds); + if (battleIntroSoundChannel != -1) { CCS->soundh->setCallback(battleIntroSoundChannel, onIntroPlayed); @@ -136,7 +153,13 @@ void BattleInterface::onIntroSoundPlayed() if (openingPlaying()) openingEnd(); - CCS->musich->playMusicFromSet("battle", true, true); + auto bfieldType = getBattle()->battleGetBattlefieldType(); + const auto & battlefieldMusic = bfieldType.getInfo()->musicFilename; + + if (!battlefieldMusic.empty()) + CCS->musich->playMusic(battlefieldMusic, true, true); + else + CCS->musich->playMusicFromSet("battle", true, true); } void BattleInterface::openingEnd() @@ -205,19 +228,19 @@ void BattleInterface::stacksAreAttacked(std::vector attackedI { stacksController->stacksAreAttacked(attackedInfos); - std::array killedBySide = {0, 0}; + BattleSideArray killedBySide; for(const StackAttackedInfo & attackedInfo : attackedInfos) { - ui8 side = attackedInfo.defender->unitSide(); + BattleSide side = attackedInfo.defender->unitSide(); killedBySide.at(side) += attackedInfo.amountKilled; } - for(ui8 side = 0; side < 2; side++) + for(BattleSide side : { BattleSide::ATTACKER, BattleSide::DEFENDER }) { - if(killedBySide.at(side) > killedBySide.at(1-side)) + if(killedBySide.at(side) > killedBySide.at(getBattle()->otherSide(side))) setHeroAnimation(side, EHeroAnimType::DEFEAT); - else if(killedBySide.at(side) < killedBySide.at(1-side)) + else if(killedBySide.at(side) < killedBySide.at(getBattle()->otherSide(side))) setHeroAnimation(side, EHeroAnimType::VICTORY); } } @@ -247,14 +270,14 @@ void BattleInterface::giveCommand(EActionType action, BattleHex tile, SpellID sp } auto side = getBattle()->playerToSide(curInt->playerID); - if(!side) + if(side == BattleSide::NONE) { logGlobal->error("Player %s is not in battle", curInt->playerID.toString()); return; } BattleAction ba; - ba.side = side.value(); + ba.side = side; ba.actionType = action; ba.aimToHex(tile); ba.spell = spell; @@ -385,7 +408,7 @@ void BattleInterface::spellCast(const BattleSpellCast * sc) } else { - auto hero = sc->side ? defendingHero : attackingHero; + auto hero = sc->side == BattleSide::DEFENDER ? defendingHero : attackingHero; assert(hero); addToAnimationStage(EAnimationEvents::BEFORE_HIT, [=]() @@ -442,11 +465,11 @@ void BattleInterface::spellCast(const BattleSpellCast * sc) { Point leftHero = Point(15, 30); Point rightHero = Point(755, 30); - bool side = sc->side; + BattleSide side = sc->side; addToAnimationStage(EAnimationEvents::AFTER_HIT, [=](){ - stacksController->addNewAnim(new EffectAnimation(*this, AnimationPath::builtin(side ? "SP07_A.DEF" : "SP07_B.DEF"), leftHero)); - stacksController->addNewAnim(new EffectAnimation(*this, AnimationPath::builtin(side ? "SP07_B.DEF" : "SP07_A.DEF"), rightHero)); + stacksController->addNewAnim(new EffectAnimation(*this, AnimationPath::builtin(side == BattleSide::DEFENDER ? "SP07_A.DEF" : "SP07_B.DEF"), leftHero)); + stacksController->addNewAnim(new EffectAnimation(*this, AnimationPath::builtin(side == BattleSide::DEFENDER ? "SP07_B.DEF" : "SP07_A.DEF"), rightHero)); }); } @@ -459,7 +482,7 @@ void BattleInterface::battleStacksEffectsSet(const SetStackEffect & sse) fieldController->redrawBackgroundWithHexes(); } -void BattleInterface::setHeroAnimation(ui8 side, EHeroAnimType phase) +void BattleInterface::setHeroAnimation(BattleSide side, EHeroAnimType phase) { if(side == BattleSide::ATTACKER) { @@ -512,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)); } } } @@ -632,7 +655,7 @@ void BattleInterface::tacticPhaseEnd() tacticsMode = false; auto side = tacticianInterface->cb->getBattle(battleID)->playerToSide(tacticianInterface->playerID); - auto action = BattleAction::makeEndOFTacticPhase(*side); + auto action = BattleAction::makeEndOFTacticPhase(side); tacticianInterface->cb->battleMakeTacticAction(battleID, action); } @@ -728,10 +751,10 @@ void BattleInterface::requestAutofightingAIToTakeAction() // FIXME: unsafe // Run task in separate thread to avoid UI lock while AI is making turn (which might take some time) // HOWEVER this thread won't atttempt to lock game state, potentially leading to races - boost::thread aiThread([battleID = this->battleID, curInt = this->curInt, activeStack]() + boost::thread aiThread([localBattleID = battleID, localCurInt = curInt, activeStack]() { setThreadName("autofightingAI"); - curInt->autofightingAI->activeStack(battleID, activeStack); + localCurInt->autofightingAI->activeStack(localBattleID, activeStack); }); aiThread.detach(); } @@ -836,3 +859,10 @@ void BattleInterface::setStickyHeroWindowsVisibility(bool visible) if(visible) windowObject->showStickyHeroWindows(); } + +void BattleInterface::setStickyQuickSpellWindowVisibility(bool visible) +{ + windowObject->hideStickyQuickSpellWindow(); + if(visible) + windowObject->showStickyQuickSpellWindow(); +} diff --git a/client/battle/BattleInterface.h b/client/battle/BattleInterface.h index 621cf1e84..a7c578ecf 100644 --- a/client/battle/BattleInterface.h +++ b/client/battle/BattleInterface.h @@ -39,7 +39,6 @@ class Canvas; class BattleResultWindow; class StackQueue; class CPlayerInterface; -class CAnimation; struct BattleEffect; class IImage; class StackQueue; @@ -171,7 +170,7 @@ public: void showInterface(Canvas & to); - void setHeroAnimation(ui8 side, EHeroAnimType phase); + void setHeroAnimation(BattleSide side, EHeroAnimType phase); void executeSpellCast(); //called when a hero casts a spell @@ -185,6 +184,7 @@ public: void setBattleQueueVisibility(bool visible); void setStickyHeroWindowsVisibility(bool visible); + void setStickyQuickSpellWindowVisibility(bool visible); void endNetwork(); void executeStagedAnimations(); @@ -206,7 +206,7 @@ public: void stacksAreAttacked(std::vector attackedInfos); //called when a certain amount of stacks has been attacked void stackAttacking(const StackAttackInfo & attackInfo); //called when stack with id ID is attacking something on hex dest void newRoundFirst(); - void newRound(); //caled when round is ended; + void newRound(); //called when round is ended; void stackIsCatapulting(const CatapultAttack & ca); //called when a stack is attacking walls void battleFinished(const BattleResult& br, QueryID queryID); //called when battle is finished - battleresult window should be printed void spellCast(const BattleSpellCast *sc); //called when a hero casts a spell diff --git a/client/battle/BattleInterfaceClasses.cpp b/client/battle/BattleInterfaceClasses.cpp index ab547e8e8..30f687919 100644 --- a/client/battle/BattleInterfaceClasses.cpp +++ b/client/battle/BattleInterfaceClasses.cpp @@ -19,42 +19,47 @@ #include "BattleWindow.h" #include "../CGameInfo.h" -#include "../CMusicHandler.h" #include "../CPlayerInterface.h" -#include "../CVideoHandler.h" #include "../gui/CursorHandler.h" #include "../gui/CGuiHandler.h" #include "../gui/Shortcut.h" #include "../gui/MouseButton.h" #include "../gui/WindowHandler.h" +#include "../media/IMusicPlayer.h" #include "../render/Canvas.h" #include "../render/IImage.h" #include "../render/IFont.h" #include "../render/Graphics.h" #include "../widgets/Buttons.h" +#include "../widgets/CComponent.h" #include "../widgets/Images.h" #include "../widgets/Slider.h" #include "../widgets/TextControls.h" +#include "../widgets/VideoWidget.h" #include "../widgets/GraphicalPrimitiveCanvas.h" #include "../windows/CMessage.h" #include "../windows/CCreatureWindow.h" #include "../windows/CSpellWindow.h" +#include "../windows/InfoWindows.h" #include "../render/CAnimation.h" #include "../render/IRenderHandler.h" #include "../adventureMap/CInGameConsole.h" +#include "../eventsSDL/InputHandler.h" #include "../../CCallback.h" #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/CGeneralTextHandler.h" -#include "../../lib/CTownHandler.h" -#include "../../lib/CHeroHandler.h" +#include "../../lib/texts/CGeneralTextHandler.h" +#include "../../lib/texts/TextOperations.h" #include "../../lib/StartInfo.h" #include "../../lib/mapObjects/CGTownInstance.h" #include "../../lib/networkPacks/PacksForClientBattle.h" -#include "../../lib/TextOperations.h" +#include "../../lib/json/JsonUtils.h" + void BattleConsole::showAll(Canvas & to) { @@ -72,7 +77,7 @@ void BattleConsole::showAll(Canvas & to) to.drawText(line2, FONT_SMALL, Colors::WHITE, ETextAlignment::CENTER, visibleText[1]); } -std::vector BattleConsole::getVisibleText() +std::vector BattleConsole::getVisibleText() const { // high priority texts that hide battle log entries for(const auto & text : {consoleText, hoverText}) @@ -108,9 +113,10 @@ std::vector BattleConsole::splitText(const std::string &text) boost::split(lines, text, boost::is_any_of("\n")); + const auto & font = GH.renderHandler().loadFont(FONT_SMALL); for(const auto & line : lines) { - if (graphics->fonts[FONT_SMALL]->getStringWidth(text) < pos.w) + if (font->getStringWidth(text) < pos.w) { output.push_back(line); } @@ -154,7 +160,7 @@ BattleConsole::BattleConsole(const BattleInterface & owner, std::shared_ptrspellcastingModeActive()) //we are casting a spell + if(owner.actionsController->heroSpellcastingModeActive()) //we are casting a spell return; if(!hero || !owner.makingTurn()) @@ -384,16 +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); - animation->preload(); + animation = GH.renderHandler().loadAnimation(animationPath, EImageBlitMode::WITH_SHADOW); pos.w = 64; pos.h = 136; @@ -404,11 +409,10 @@ BattleHero::BattleHero(const BattleInterface & owner, const CGHeroInstance * her animation->verticalFlip(); if(defender) - flagAnimation = GH.renderHandler().loadAnimation(AnimationPath::builtin("CMFLAGR")); + flagAnimation = GH.renderHandler().loadAnimation(AnimationPath::builtin("CMFLAGR"), EImageBlitMode::COLORKEY); else - flagAnimation = GH.renderHandler().loadAnimation(AnimationPath::builtin("CMFLAGL")); + flagAnimation = GH.renderHandler().loadAnimation(AnimationPath::builtin("CMFLAGL"), EImageBlitMode::COLORKEY); - flagAnimation->preload(); flagAnimation->playerColored(hero->tempOwner); switchToNextPhase(); @@ -417,18 +421,137 @@ BattleHero::BattleHero(const BattleInterface & owner, const CGHeroInstance * her addUsedEvents(TIME); } +QuickSpellPanel::QuickSpellPanel(BattleInterface & owner) + : CIntObject(0), owner(owner) +{ + OBJECT_CONSTRUCTION; + + addUsedEvents(LCLICK | SHOW_POPUP | MOVE | INPUT_MODE_CHANGE); + + pos = Rect(0, 0, 52, 600); + background = std::make_shared(ImagePath::builtin("DIBOXBCK"), pos); + rect = std::make_shared(Rect(0, 0, pos.w + 1, pos.h + 1), ColorRGBA(0, 0, 0, 0), ColorRGBA(241, 216, 120, 255)); + + create(); +} + +std::vector> QuickSpellPanel::getSpells() const +{ + std::vector spellIds; + std::vector spellIdsFromSetting; + for(int i = 0; i < QUICKSPELL_SLOTS; i++) + { + std::string spellIdentifier = persistentStorage["quickSpell"][std::to_string(i)].String(); + SpellID id; + try + { + id = SpellID::decode(spellIdentifier); + } + catch(const IdentifierResolutionException& e) + { + id = SpellID::NONE; + } + spellIds.push_back(id); + spellIdsFromSetting.push_back(id != SpellID::NONE); + } + + // autofill empty slots with spells if possible + auto hero = owner.getBattle()->battleGetMyHero(); + for(int i = 0; i < QUICKSPELL_SLOTS; i++) + { + if(spellIds[i] != SpellID::NONE) + continue; + + for(const auto & availableSpellID : CGI->spellh->getDefaultAllowed()) + { + const auto * availableSpell = availableSpellID.toSpell(); + if(!availableSpell->isAdventure() && !availableSpell->isCreatureAbility() && hero->canCastThisSpell(availableSpell) && !vstd::contains(spellIds, availableSpell->getId())) + { + spellIds[i] = availableSpell->getId(); + break; + } + } + } + + std::vector> ret; + for(int i = 0; i < QUICKSPELL_SLOTS; i++) + ret.push_back(std::make_tuple(spellIds[i], spellIdsFromSetting[i])); + return ret; +} + +void QuickSpellPanel::create() +{ + OBJECT_CONSTRUCTION; + + const JsonNode config = JsonUtils::assembleFromFiles("config/shortcutsConfig"); + + labels.clear(); + buttons.clear(); + buttonsDisabled.clear(); + + auto hero = owner.getBattle()->battleGetMyHero(); + if(!hero) + return; + + auto spells = getSpells(); + for(int i = 0; i < QUICKSPELL_SLOTS; i++) { + SpellID id; + bool fromSettings; + std::tie(id, fromSettings) = spells[i]; + + auto button = std::make_shared(Point(2, 7 + 50 * i), AnimationPath::builtin("spellint"), CButton::tooltip(), [this, id, hero](){ + if(id.hasValue() && id.toSpell()->canBeCast(owner.getBattle().get(), spells::Mode::HERO, hero)) + { + owner.castThisSpell(id); + } + }); + button->setOverlay(std::make_shared(AnimationPath::builtin("spellint"), id != SpellID::NONE ? id.num + 1 : 0)); + button->addPopupCallback([this, i, hero](){ + GH.input().hapticFeedback(); + GH.windows().createAndPushWindow(hero, owner.curInt.get(), true, [this, i](SpellID spell){ + Settings configID = persistentStorage.write["quickSpell"][std::to_string(i)]; + configID->String() = spell == SpellID::NONE ? "" : spell.toSpell()->identifier; + create(); + }); + }); + + if(fromSettings) + buttonsIsAutoGenerated.push_back(std::make_shared(Rect(45, 37 + 50 * i, 5, 5), Colors::ORANGE)); + + if(!id.hasValue() || !id.toSpell()->canBeCast(owner.getBattle().get(), spells::Mode::HERO, hero)) + { + buttonsDisabled.push_back(std::make_shared(Rect(2, 7 + 50 * i, 48, 36), ColorRGBA(0, 0, 0, 172))); + } + if(GH.input().getCurrentInputMode() == InputMode::KEYBOARD_AND_MOUSE) + labels.push_back(std::make_shared(7, 10 + 50 * i, EFonts::FONT_TINY, ETextAlignment::TOPLEFT, Colors::WHITE, config["keyboard"]["battleSpellShortcut" + std::to_string(i)].String())); + + buttons.push_back(button); + } +} + +void QuickSpellPanel::show(Canvas & to) +{ + showAll(to); + CIntObject::show(to); +} + +void QuickSpellPanel::inputModeChanged(InputMode modi) +{ + create(); + redraw(); +} + HeroInfoBasicPanel::HeroInfoBasicPanel(const InfoAboutHero & hero, Point * position, bool initializeBackground) : CIntObject(0) { - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + OBJECT_CONSTRUCTION; if (position != nullptr) moveTo(*position); if(initializeBackground) { background = std::make_shared(ImagePath::builtin("CHRPOP")); - background->getSurface()->setBlitMode(EImageBlitMode::OPAQUE); - background->colorize(hero.owner); + background->setPlayerColor(hero.owner); } initializeData(hero); @@ -436,7 +559,7 @@ HeroInfoBasicPanel::HeroInfoBasicPanel(const InfoAboutHero & hero, Point * posit void HeroInfoBasicPanel::initializeData(const InfoAboutHero & hero) { - OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + OBJECT_CONSTRUCTION; auto attack = hero.details->primskills[0]; auto defense = hero.details->primskills[1]; auto power = hero.details->primskills[2]; @@ -489,17 +612,15 @@ void HeroInfoBasicPanel::show(Canvas & to) StackInfoBasicPanel::StackInfoBasicPanel(const CStack * stack, bool initializeBackground) : CIntObject(0) { - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + OBJECT_CONSTRUCTION; if(initializeBackground) { background = std::make_shared(ImagePath::builtin("CCRPOP")); background->pos.y += 37; - background->getSurface()->setBlitMode(EImageBlitMode::OPAQUE); - background->colorize(stack->getOwner()); + background->setPlayerColor(stack->getOwner()); background2 = std::make_shared(ImagePath::builtin("CHRPOP")); - background2->getSurface()->setBlitMode(EImageBlitMode::OPAQUE); - background2->colorize(stack->getOwner()); + background2->setPlayerColor(stack->getOwner()); } initializeData(stack); @@ -507,7 +628,7 @@ StackInfoBasicPanel::StackInfoBasicPanel(const CStack * stack, bool initializeBa void StackInfoBasicPanel::initializeData(const CStack * stack) { - OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + OBJECT_CONSTRUCTION; icons.push_back(std::make_shared(AnimationPath::builtin("TWCRPORT"), stack->creatureId() + 2, 0, 10, 6)); labels.push_back(std::make_shared(10 + 58, 6 + 64, FONT_MEDIUM, ETextAlignment::BOTTOMRIGHT, Colors::WHITE, TextOperations::formatMetric(stack->getCount(), 4))); @@ -599,22 +720,22 @@ void StackInfoBasicPanel::show(Canvas & to) HeroInfoWindow::HeroInfoWindow(const InfoAboutHero & hero, Point * position) : CWindowObject(RCLICK_POPUP | SHADOW_DISABLED, ImagePath::builtin("CHRPOP")) { - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + OBJECT_CONSTRUCTION; if (position != nullptr) moveTo(*position); - background->colorize(hero.owner); //maybe add this functionality to base class? + background->setPlayerColor(hero.owner); //maybe add this functionality to base class? content = std::make_shared(hero, nullptr, false); } BattleResultWindow::BattleResultWindow(const BattleResult & br, CPlayerInterface & _owner, bool allowReplay) - : owner(_owner), currentVideo(BattleResultVideo::NONE) + : owner(_owner) { - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + OBJECT_CONSTRUCTION; background = std::make_shared(ImagePath::builtin("CPRESULT")); - background->colorize(owner.playerID); + background->setPlayerColor(owner.playerID); pos = center(background->pos); exit = std::make_shared(Point(384, 505), AnimationPath::builtin("iok6432.def"), std::make_pair("", ""), [&](){ bExitf();}, EShortcut::GLOBAL_ACCEPT); @@ -627,7 +748,7 @@ BattleResultWindow::BattleResultWindow(const BattleResult & br, CPlayerInterface labels.push_back(std::make_shared(232, 520, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->translate("vcmi.battleResultsWindow.applyResultsLabel"))); } - if(br.winner == 0) //attacker won + if(br.winner == BattleSide::ATTACKER) { labels.push_back(std::make_shared(59, 124, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->allTexts[410])); } @@ -635,8 +756,8 @@ BattleResultWindow::BattleResultWindow(const BattleResult & br, CPlayerInterface { labels.push_back(std::make_shared(59, 124, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->allTexts[411])); } - - if(br.winner == 1) + + if(br.winner == BattleSide::DEFENDER) { labels.push_back(std::make_shared(412, 124, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->allTexts[410])); } @@ -651,15 +772,15 @@ BattleResultWindow::BattleResultWindow(const BattleResult & br, CPlayerInterface std::string sideNames[2] = {"N/A", "N/A"}; - for(int i = 0; i < 2; i++) + for(auto i : {BattleSide::ATTACKER, BattleSide::DEFENDER}) { auto heroInfo = owner.cb->getBattle(br.battleID)->battleGetHeroInfo(i); const int xs[] = {21, 392}; if(heroInfo.portraitSource.isValid()) //attacking hero { - icons.push_back(std::make_shared(AnimationPath::builtin("PortraitsLarge"), heroInfo.getIconIndex(), 0, xs[i], 38)); - sideNames[i] = heroInfo.name; + icons.push_back(std::make_shared(AnimationPath::builtin("PortraitsLarge"), heroInfo.getIconIndex(), 0, xs[static_cast(i)], 38)); + sideNames[static_cast(i)] = heroInfo.name; } else { @@ -676,8 +797,8 @@ BattleResultWindow::BattleResultWindow(const BattleResult & br, CPlayerInterface if(best != stacks.end()) //should be always but to be safe... { - icons.push_back(std::make_shared(AnimationPath::builtin("TWCRPORT"), (*best)->unitType()->getIconIndex(), 0, xs[i], 38)); - sideNames[i] = (*best)->unitType()->getNamePluralTranslated(); + icons.push_back(std::make_shared(AnimationPath::builtin("TWCRPORT"), (*best)->unitType()->getIconIndex(), 0, xs[static_cast(i)], 38)); + sideNames[static_cast(i)] = (*best)->unitType()->getNamePluralTranslated(); } } } @@ -687,16 +808,16 @@ BattleResultWindow::BattleResultWindow(const BattleResult & br, CPlayerInterface labels.push_back(std::make_shared(381, 53, FONT_SMALL, ETextAlignment::BOTTOMRIGHT, Colors::WHITE, sideNames[1])); //printing casualties - for(int step = 0; step < 2; ++step) + for(auto step : {BattleSide::ATTACKER, BattleSide::DEFENDER}) { if(br.casualties[step].size()==0) { - labels.push_back(std::make_shared(235, 360 + 97 * step, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->allTexts[523])); + labels.push_back(std::make_shared(235, 360 + 97 * static_cast(step), FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->allTexts[523])); } else { int xPos = 235 - ((int)br.casualties[step].size()*32 + ((int)br.casualties[step].size() - 1)*10)/2; //increment by 42 with each picture - int yPos = 344 + step * 97; + int yPos = 344 + static_cast(step) * 97; for(auto & elem : br.casualties[step]) { auto creature = CGI->creatures()->getByIndex(elem.first); @@ -711,68 +832,98 @@ BattleResultWindow::BattleResultWindow(const BattleResult & br, CPlayerInterface } } } + + auto resources = getResources(br); + + description = std::make_shared(resources.resultText.toString(), Rect(69, 203, 330, 68), 0, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE); + videoPlayer = std::make_shared(Point(107, 70), resources.prologueVideo, resources.loopedVideo, false); + + CCS->musich->playMusic(resources.musicName, false, true); +} + +BattleResultResources BattleResultWindow::getResources(const BattleResult & br) +{ //printing result description - bool weAreAttacker = !(owner.cb->getBattle(br.battleID)->battleGetMySide()); - if((br.winner == 0 && weAreAttacker) || (br.winner == 1 && !weAreAttacker)) //we've won + bool weAreAttacker = owner.cb->getBattle(br.battleID)->battleGetMySide() == BattleSide::ATTACKER; + bool weAreDefender = !weAreAttacker; + bool weWon = (br.winner == BattleSide::ATTACKER && weAreAttacker) || (br.winner == BattleSide::DEFENDER && !weAreAttacker); + bool isSiege = owner.cb->getBattle(br.battleID)->battleGetDefendedTown() != nullptr; + + BattleResultResources resources; + + if(weWon) { - int text = 304; - currentVideo = BattleResultVideo::WIN; + if(isSiege && weAreDefender) + { + resources.musicName = AudioPath::builtin("Music/Defend Castle"); + resources.prologueVideo = VideoPath::builtin("DEFENDALL.BIK"); + resources.loopedVideo = VideoPath::builtin("defendloop.bik"); + } + else + { + resources.musicName = AudioPath::builtin("Music/Win Battle"); + resources.prologueVideo = VideoPath::builtin("WIN3.BIK"); + resources.loopedVideo = VideoPath::builtin("WIN3.BIK"); + } + switch(br.result) { case EBattleResult::NORMAL: - if(owner.cb->getBattle(br.battleID)->battleGetDefendedTown() && !weAreAttacker) - currentVideo = BattleResultVideo::WIN_SIEGE; + resources.resultText.appendTextID("core.genrltxt.304"); break; case EBattleResult::ESCAPE: - text = 303; + resources.resultText.appendTextID("core.genrltxt.303"); break; case EBattleResult::SURRENDER: - text = 302; + resources.resultText.appendTextID("core.genrltxt.302"); break; default: - logGlobal->error("Invalid battle result code %d. Assumed normal.", static_cast(br.result)); - break; + throw std::runtime_error("Invalid battle result!"); } - playVideo(); - - std::string str = CGI->generaltexth->allTexts[text]; const CGHeroInstance * ourHero = owner.cb->getBattle(br.battleID)->battleGetMyHero(); if (ourHero) { - str += CGI->generaltexth->allTexts[305]; - boost::algorithm::replace_first(str, "%s", ourHero->getNameTranslated()); - boost::algorithm::replace_first(str, "%d", std::to_string(br.exp[weAreAttacker ? 0 : 1])); + resources.resultText.appendTextID("core.genrltxt.305"); + resources.resultText.replaceTextID(ourHero->getNameTranslated()); + resources.resultText.replaceNumber(br.exp[weAreAttacker ? BattleSide::ATTACKER : BattleSide::DEFENDER]); } - - description = std::make_shared(str, Rect(69, 203, 330, 68), 0, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE); } else // we lose { - int text = 311; - currentVideo = BattleResultVideo::DEFEAT; switch(br.result) { case EBattleResult::NORMAL: - if(owner.cb->getBattle(br.battleID)->battleGetDefendedTown() && !weAreAttacker) - currentVideo = BattleResultVideo::DEFEAT_SIEGE; + resources.resultText.appendTextID("core.genrltxt.311"); + resources.musicName = AudioPath::builtin("Music/LoseCombat"); + resources.prologueVideo = VideoPath::builtin("LBSTART.BIK"); + resources.loopedVideo = VideoPath::builtin("LBLOOP.BIK"); break; case EBattleResult::ESCAPE: - currentVideo = BattleResultVideo::RETREAT; - text = 310; + resources.resultText.appendTextID("core.genrltxt.310"); + resources.musicName = AudioPath::builtin("Music/Retreat Battle"); + resources.prologueVideo = VideoPath::builtin("RTSTART.BIK"); + resources.loopedVideo = VideoPath::builtin("RTLOOP.BIK"); break; case EBattleResult::SURRENDER: - currentVideo = BattleResultVideo::SURRENDER; - text = 309; + resources.resultText.appendTextID("core.genrltxt.309"); + resources.musicName = AudioPath::builtin("Music/Surrender Battle"); + resources.prologueVideo = VideoPath(); + resources.loopedVideo = VideoPath::builtin("SURRENDER.BIK"); break; default: - logGlobal->error("Invalid battle result code %d. Assumed normal.", static_cast(br.result)); - break; + throw std::runtime_error("Invalid battle result!"); } - playVideo(); - labels.push_back(std::make_shared(235, 235, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->allTexts[text])); + if(isSiege && weAreDefender) + { + resources.musicName = AudioPath::builtin("Music/LoseCastle"); + resources.prologueVideo = VideoPath::builtin("LOSECSTL.BIK"); + resources.loopedVideo = VideoPath::builtin("LOSECSLP.BIK"); + } } + + return resources; } void BattleResultWindow::activate() @@ -781,81 +932,6 @@ void BattleResultWindow::activate() CIntObject::activate(); } -void BattleResultWindow::show(Canvas & to) -{ - CIntObject::show(to); - CCS->videoh->update(pos.x + 107, pos.y + 70, to.getInternalSurface(), true, false, - [&]() - { - playVideo(true); - }); -} - -void BattleResultWindow::playVideo(bool startLoop) -{ - AudioPath musicName = AudioPath(); - VideoPath videoName = VideoPath(); - - if(!startLoop) - { - switch(currentVideo) - { - case BattleResultVideo::WIN: - musicName = AudioPath::builtin("Music/Win Battle"); - videoName = VideoPath::builtin("WIN3.BIK"); - break; - case BattleResultVideo::SURRENDER: - musicName = AudioPath::builtin("Music/Surrender Battle"); - videoName = VideoPath::builtin("SURRENDER.BIK"); - break; - case BattleResultVideo::RETREAT: - musicName = AudioPath::builtin("Music/Retreat Battle"); - videoName = VideoPath::builtin("RTSTART.BIK"); - break; - case BattleResultVideo::DEFEAT: - musicName = AudioPath::builtin("Music/LoseCombat"); - videoName = VideoPath::builtin("LBSTART.BIK"); - break; - case BattleResultVideo::DEFEAT_SIEGE: - musicName = AudioPath::builtin("Music/LoseCastle"); - videoName = VideoPath::builtin("LOSECSTL.BIK"); - break; - case BattleResultVideo::WIN_SIEGE: - musicName = AudioPath::builtin("Music/Defend Castle"); - videoName = VideoPath::builtin("DEFENDALL.BIK"); - break; - } - } - else - { - switch(currentVideo) - { - case BattleResultVideo::RETREAT: - currentVideo = BattleResultVideo::RETREAT_LOOP; - videoName = VideoPath::builtin("RTLOOP.BIK"); - break; - case BattleResultVideo::DEFEAT: - currentVideo = BattleResultVideo::DEFEAT_LOOP; - videoName = VideoPath::builtin("LBLOOP.BIK"); - break; - case BattleResultVideo::DEFEAT_SIEGE: - currentVideo = BattleResultVideo::DEFEAT_SIEGE_LOOP; - videoName = VideoPath::builtin("LOSECSLP.BIK"); - break; - case BattleResultVideo::WIN_SIEGE: - currentVideo = BattleResultVideo::WIN_SIEGE_LOOP; - videoName = VideoPath::builtin("DEFENDLOOP.BIK"); - break; - } - } - - if(musicName != AudioPath()) - CCS->musich->playMusic(musicName, false, true); - - if(videoName != VideoPath()) - CCS->videoh->open(videoName); -} - void BattleResultWindow::buttonPressed(int button) { if (resultCallback) @@ -871,7 +947,6 @@ void BattleResultWindow::buttonPressed(int button) //Result window and battle interface are gone. We requested all dialogs to be closed before opening the battle, //so we can be sure that there is no dialogs left on GUI stack. intTmp.showingDialog->setFree(); - CCS->videoh->close(); } void BattleResultWindow::bExitf() @@ -888,7 +963,7 @@ StackQueue::StackQueue(bool Embedded, BattleInterface & owner) : embedded(Embedded), owner(owner) { - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + OBJECT_CONSTRUCTION; uint32_t queueSize = QUEUE_SIZE_BIG; @@ -902,9 +977,6 @@ StackQueue::StackQueue(bool Embedded, BattleInterface & owner) pos.h = 49; pos.x += parent->pos.w/2 - pos.w/2; pos.y += queueSmallOutside ? -queueSmallOutsideYOffset : 10; - - icons = GH.renderHandler().loadAnimation(AnimationPath::builtin("CPRSMALL")); - stateIcons = GH.renderHandler().loadAnimation(AnimationPath::builtin("VCMI/BATTLEQUEUE/STATESSMALL")); } else { @@ -914,13 +986,7 @@ StackQueue::StackQueue(bool Embedded, BattleInterface & owner) pos.y -= pos.h; background = std::make_shared(ImagePath::builtin("DIBOXBCK"), Rect(0, 0, pos.w, pos.h)); - - icons = GH.renderHandler().loadAnimation(AnimationPath::builtin("TWCRPORT")); - stateIcons = GH.renderHandler().loadAnimation(AnimationPath::builtin("VCMI/BATTLEQUEUE/STATESSMALL")); - //TODO: where use big icons? - //stateIcons = GH.renderHandler().loadAnimation("VCMI/BATTLEQUEUE/STATESBIG"); } - stateIcons->preload(); stackBoxes.resize(queueSize); for (int i = 0; i < stackBoxes.size(); i++) @@ -962,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 @@ -981,7 +1047,7 @@ std::optional StackQueue::getHoveredUnitIdIfAny() const StackQueue::StackBox::StackBox(StackQueue * owner): CIntObject(SHOW_POPUP | HOVER), owner(owner) { - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + OBJECT_CONSTRUCTION; background = std::make_shared(ImagePath::builtin(owner->embedded ? "StackQueueSmall" : "StackQueueLarge")); pos.w = background->pos.w; @@ -989,22 +1055,24 @@ StackQueue::StackBox::StackBox(StackQueue * owner): if(owner->embedded) { - icon = std::make_shared(owner->icons, 0, 0, 5, 2); + icon = std::make_shared(AnimationPath::builtin("CPRSMALL"), 0, 0, 5, 2); amount = std::make_shared(pos.w/2, pos.h - 7, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE); roundRect = std::make_shared(Rect(0, 0, 2, 48), ColorRGBA(0, 0, 0, 255), ColorRGBA(0, 255, 0, 255)); } else { - icon = std::make_shared(owner->icons, 0, 0, 9, 1); + icon = std::make_shared(AnimationPath::builtin("TWCRPORT"), 0, 0, 9, 1); amount = std::make_shared(pos.w/2, pos.h - 8, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE); 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); + round = std::make_shared(6, 9, FONT_SMALL, ETextAlignment::CENTER, 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(owner->stateIcons, 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(); } @@ -1014,7 +1082,7 @@ void StackQueue::StackBox::setUnit(const battle::Unit * unit, size_t turn, std:: if(unit) { boundUnitID = unit->unitId(); - background->colorize(unit->unitOwner()); + background->setPlayerColor(unit->unitOwner()); icon->visible = true; // temporary code for mod compatibility: @@ -1034,39 +1102,34 @@ void StackQueue::StackBox::setUnit(const battle::Unit * unit, size_t turn, std:: if(currentTurn && !owner->embedded) { std::string tmp = std::to_string(*currentTurn); - int len = graphics->fonts[FONT_SMALL]->getStringWidth(tmp); + const auto & font = GH.renderHandler().loadFont(FONT_SMALL); + int len = font->getStringWidth(tmp); roundRect->pos.w = len + 6; + round->pos = Rect(roundRect->pos.center().x, roundRect->pos.center().y, 0, 0); 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 { boundUnitID = std::nullopt; - background->colorize(PlayerColor::NEUTRAL); + background->setPlayerColor(PlayerColor::NEUTRAL); 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 d50a9e41e..9465502bc 100644 --- a/client/battle/BattleInterfaceClasses.h +++ b/client/battle/BattleInterfaceClasses.h @@ -13,6 +13,7 @@ #include "../gui/CIntObject.h" #include "../../lib/FunctionList.h" #include "../../lib/battle/BattleHex.h" +#include "../../lib/texts/MetaString.h" #include "../windows/CWindowObject.h" VCMI_LIB_NAMESPACE_BEGIN @@ -21,6 +22,7 @@ class CGHeroInstance; struct BattleResult; struct InfoAboutHero; class CStack; +class CPlayerBattleCallback; namespace battle { @@ -42,6 +44,8 @@ class CAnimImage; class TransparentFilledRectangle; class CPlayerInterface; class BattleRenderer; +class VideoWidget; +class QuickSpellPanel; /// Class which shows the console at the bottom of the battle screen and manages the text of the console class BattleConsole : public CIntObject, public IStatusBar @@ -70,7 +74,7 @@ private: std::vector splitText(const std::string &text); /// select line(s) that will be visible in UI - std::vector getVisibleText(); + std::vector getVisibleText() const; public: BattleConsole(const BattleInterface & owner, std::shared_ptr backgroundSource, const Point & objectPos, const Point & imagePos, const Point &size); @@ -145,6 +149,32 @@ public: BattleHero(const BattleInterface & owner, const CGHeroInstance * hero, bool defender); }; +class QuickSpellPanel : public CIntObject +{ +private: + std::shared_ptr background; + std::shared_ptr rect; + std::vector> buttons; + std::vector> buttonsIsAutoGenerated; + std::vector> buttonsDisabled; + std::vector> labels; + + BattleInterface & owner; +public: + int QUICKSPELL_SLOTS = 12; + + bool isEnabled; // isActive() is not working on multiple conditions, because of this we need a seperate flag + + QuickSpellPanel(BattleInterface & owner); + + void create(); + + std::vector> getSpells() const; + + void show(Canvas & to) override; + void inputModeChanged(InputMode modi) override; +}; + class HeroInfoBasicPanel : public CIntObject //extracted from InfoWindow to fit better as non-popup embed element { private: @@ -185,6 +215,14 @@ public: HeroInfoWindow(const InfoAboutHero & hero, Point * position); }; +struct BattleResultResources +{ + VideoPath prologueVideo; + VideoPath loopedVideo; + AudioPath musicName; + MetaString resultText; +}; + /// Class which is responsible for showing the battle result window class BattleResultWindow : public WindowBase { @@ -195,25 +233,10 @@ private: std::shared_ptr repeat; std::vector> icons; std::shared_ptr description; + std::shared_ptr videoPlayer; CPlayerInterface & owner; - enum BattleResultVideo - { - NONE, - WIN, - SURRENDER, - RETREAT, - RETREAT_LOOP, - DEFEAT, - DEFEAT_LOOP, - DEFEAT_SIEGE, - DEFEAT_SIEGE_LOOP, - WIN_SIEGE, - WIN_SIEGE_LOOP, - }; - BattleResultVideo currentVideo; - - void playVideo(bool startLoop = false); + BattleResultResources getResources(const BattleResult & br); void buttonPressed(int button); //internal function for button callbacks public: @@ -224,7 +247,6 @@ public: std::function resultCallback; //callback receiving which button was pressed void activate() override; - void show(Canvas & to) override; }; /// Shows the stack queue @@ -238,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; @@ -258,9 +281,6 @@ class StackQueue : public CIntObject std::vector> stackBoxes; BattleInterface & owner; - std::shared_ptr icons; - std::shared_ptr stateIcons; - int32_t getSiegeShooterIconID(); public: const bool embedded; diff --git a/client/battle/BattleObstacleController.cpp b/client/battle/BattleObstacleController.cpp index 0c6e7e809..c5c8391b3 100644 --- a/client/battle/BattleObstacleController.cpp +++ b/client/battle/BattleObstacleController.cpp @@ -17,10 +17,11 @@ #include "BattleRenderer.h" #include "CreatureAnimation.h" -#include "../CMusicHandler.h" #include "../CGameInfo.h" #include "../CPlayerInterface.h" #include "../gui/CGuiHandler.h" +#include "../media/ISoundPlayer.h" +#include "../render/CAnimation.h" #include "../render/Canvas.h" #include "../render/IRenderHandler.h" @@ -46,24 +47,16 @@ void BattleObstacleController::loadObstacleImage(const CObstacleInstance & oi) { AnimationPath animationName = oi.getAnimation(); - if (animationsCache.count(animationName) == 0) + if (oi.obstacleType == CObstacleInstance::ABSOLUTE_OBSTACLE) { - if (oi.obstacleType == CObstacleInstance::ABSOLUTE_OBSTACLE) - { - // obstacle uses single bitmap image for animations - auto animation = GH.renderHandler().createAnimation(); - animation->setCustom(animationName.getName(), 0, 0); - animationsCache[animationName] = animation; - animation->preload(); - } - else - { - auto animation = GH.renderHandler().loadAnimation(animationName); - animationsCache[animationName] = animation; - animation->preload(); - } + // obstacle uses single bitmap image for animations + obstacleImages[oi.uniqueID] = GH.renderHandler().loadImage(animationName.toType(), EImageBlitMode::SIMPLE); + } + else + { + obstacleAnimations[oi.uniqueID] = GH.renderHandler().loadAnimation(animationName, EImageBlitMode::SIMPLE); + obstacleImages[oi.uniqueID] = obstacleAnimations[oi.uniqueID]->getImage(0); } - obstacleAnimations[oi.uniqueID] = animationsCache[animationName]; } void BattleObstacleController::obstacleRemoved(const std::vector & obstacles) @@ -85,9 +78,7 @@ void BattleObstacleController::obstacleRemoved(const std::vectorpreload(); - + auto animation = GH.renderHandler().loadAnimation(animationPath, EImageBlitMode::SIMPLE); auto first = animation->getImage(0, 0); if(!first) continue; @@ -99,6 +90,7 @@ void BattleObstacleController::obstacleRemoved(const std::vectoraddNewAnim(new EffectAnimation(owner, animationPath, whereTo, obstacle["position"].Integer(), 0, true)); obstacleAnimations.erase(oi.id); + obstacleImages.erase(oi.id); //so when multiple obstacles are removed, they show up one after another owner.waitForAnimations(); } @@ -110,12 +102,10 @@ void BattleObstacleController::obstaclePlaced(const std::vectorplayerToSide(owner.curInt->playerID); - if(!oi->visibleForSide(side.value(), owner.getBattle()->battleHasNativeStack(side.value()))) + if(!oi->visibleForSide(side, owner.getBattle()->battleHasNativeStack(side))) continue; - auto animation = GH.renderHandler().loadAnimation(oi->getAppearAnimation()); - animation->preload(); - + auto animation = GH.renderHandler().loadAnimation(oi->getAppearAnimation(), EImageBlitMode::SIMPLE); auto first = animation->getImage(0, 0); if(!first) continue; @@ -187,26 +177,22 @@ void BattleObstacleController::collectRenderableObjects(BattleRenderer & rendere void BattleObstacleController::tick(uint32_t msPassed) { timePassed += msPassed / 1000.f; + int framesCount = timePassed * AnimationControls::getObstaclesSpeed(); + + for(auto & animation : obstacleAnimations) + { + int frameIndex = framesCount % animation.second->size(0); + obstacleImages[animation.first] = animation.second->getImage(frameIndex, 0); + } } std::shared_ptr BattleObstacleController::getObstacleImage(const CObstacleInstance & oi) { - int framesCount = timePassed * AnimationControls::getObstaclesSpeed(); - std::shared_ptr animation; - // obstacle is not loaded yet, don't show anything - if (obstacleAnimations.count(oi.uniqueID) == 0) + if (obstacleImages.count(oi.uniqueID) == 0) return nullptr; - animation = obstacleAnimations[oi.uniqueID]; - assert(animation); - - if(animation) - { - int frameIndex = framesCount % animation->size(0); - return animation->getImage(frameIndex, 0); - } - return nullptr; + return obstacleImages[oi.uniqueID]; } Point BattleObstacleController::getObstaclePosition(std::shared_ptr image, const CObstacleInstance & obstacle) diff --git a/client/battle/BattleObstacleController.h b/client/battle/BattleObstacleController.h index c4a7467a4..39119cf32 100644 --- a/client/battle/BattleObstacleController.h +++ b/client/battle/BattleObstacleController.h @@ -36,12 +36,12 @@ class BattleObstacleController /// total time, in seconds, since start of battle. Used for animating obstacles float timePassed; - /// cached animations of all obstacles in current battle - std::map> animationsCache; - /// list of all obstacles that are currently being rendered std::map> obstacleAnimations; + /// Current images for all present obstacles + std::map> obstacleImages; + void loadObstacleImage(const CObstacleInstance & oi); std::shared_ptr getObstacleImage(const CObstacleInstance & oi); diff --git a/client/battle/BattleOverlayLogVisualizer.cpp b/client/battle/BattleOverlayLogVisualizer.cpp new file mode 100644 index 000000000..fd7af4720 --- /dev/null +++ b/client/battle/BattleOverlayLogVisualizer.cpp @@ -0,0 +1,41 @@ +/* + * BattleOverlayLogVisualizer.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 "BattleOverlayLogVisualizer.h" +#include "BattleInterface.h" +#include "BattleFieldController.h" + +#include "../render/Canvas.h" +#include "../render/Colors.h" +#include "../render/EFont.h" +#include "../render/IFont.h" +#include "../render/IRenderHandler.h" +#include "../gui/TextAlignment.h" +#include "../gui/CGuiHandler.h" +#include "../render/Graphics.h" + +BattleOverlayLogVisualizer::BattleOverlayLogVisualizer( + BattleRenderer::RendererRef & target, + BattleInterface & owner) + : target(target), owner(owner) +{ +} + +void BattleOverlayLogVisualizer::drawText(BattleHex hex, int lineNumber, const std::string & text) +{ + Point offset = owner.fieldController->hexPositionLocal(hex).topLeft() + Point(20, 20); + const auto & font = GH.renderHandler().loadFont(FONT_TINY); + int h = font->getLineHeight(); + + offset.y += h * lineNumber; + + target.drawText(offset, EFonts::FONT_TINY, Colors::YELLOW, ETextAlignment::TOPCENTER, text); +} diff --git a/client/battle/BattleOverlayLogVisualizer.h b/client/battle/BattleOverlayLogVisualizer.h new file mode 100644 index 000000000..e8dd1bf7d --- /dev/null +++ b/client/battle/BattleOverlayLogVisualizer.h @@ -0,0 +1,28 @@ +/* + * BattleOverlayLogVisualizer.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 "../../lib/logging/VisualLogger.h" +#include "BattleRenderer.h" + +class Canvas; +class BattleInterface; + +class BattleOverlayLogVisualizer : public IBattleOverlayLogVisualizer +{ +private: + BattleRenderer::RendererRef & target; + BattleInterface & owner; + +public: + BattleOverlayLogVisualizer(BattleRenderer::RendererRef & target, BattleInterface & owner); + + void drawText(BattleHex hex, int lineNumber, const std::string & text) override; +}; diff --git a/client/battle/BattleProjectileController.cpp b/client/battle/BattleProjectileController.cpp index 404540c82..59ae5c1b2 100644 --- a/client/battle/BattleProjectileController.cpp +++ b/client/battle/BattleProjectileController.cpp @@ -15,6 +15,7 @@ #include "BattleStacksController.h" #include "CreatureAnimation.h" +#include "../render/CAnimation.h" #include "../render/Canvas.h" #include "../render/IRenderHandler.h" #include "../gui/CGuiHandler.h" @@ -159,12 +160,12 @@ const CCreature & BattleProjectileController::getShooter(const CStack * stack) c const CCreature * creature = stack->unitType(); if(creature->getId() == CreatureID::ARROW_TOWERS) - creature = owner.siegeController->getTurretCreature(); + creature = owner.siegeController->getTurretCreature(stack->initialPosition); - if(creature->animation.missleFrameAngles.empty()) + if(creature->animation.missileFrameAngles.empty()) { logAnim->error("Mod error: Creature '%s' on the Archer's tower is not a shooter. Mod should be fixed. Trying to use archer's data instead...", creature->getNameSingularTranslated()); - creature = CGI->creh->objects[CreatureID::ARCHER]; + creature = CreatureID(CreatureID::ARCHER).toCreature(); } return *creature; @@ -191,8 +192,7 @@ void BattleProjectileController::initStackProjectile(const CStack * stack) std::shared_ptr BattleProjectileController::createProjectileImage(const AnimationPath & path ) { - std::shared_ptr projectile = GH.renderHandler().loadAnimation(path); - projectile->preload(); + std::shared_ptr projectile = GH.renderHandler().loadAnimation(path, EImageBlitMode::COLORKEY); if(projectile->size(1) != 0) logAnim->error("Expected empty group 1 in stack projectile"); @@ -277,7 +277,7 @@ int BattleProjectileController::computeProjectileFrameID( Point from, Point dest { const CCreature & creature = getShooter(stack); - auto & angles = creature.animation.missleFrameAngles; + auto & angles = creature.animation.missileFrameAngles; auto animation = getProjectileImage(stack); // only frames below maxFrame are usable: anything higher is either no present or we don't know when it should be used diff --git a/client/battle/BattleRenderer.cpp b/client/battle/BattleRenderer.cpp index fbbe3e943..197aa2120 100644 --- a/client/battle/BattleRenderer.cpp +++ b/client/battle/BattleRenderer.cpp @@ -17,6 +17,7 @@ #include "BattleSiegeController.h" #include "BattleStacksController.h" #include "BattleObstacleController.h" +#include "BattleOverlayLogVisualizer.h" void BattleRenderer::collectObjects() { @@ -73,4 +74,7 @@ void BattleRenderer::execute(BattleRenderer::RendererRef targetCanvas) collectObjects(); sortObjects(); renderObjects(targetCanvas); + + BattleOverlayLogVisualizer r(targetCanvas, owner); + logVisual->visualize(r); } diff --git a/client/battle/BattleSiegeController.cpp b/client/battle/BattleSiegeController.cpp index 660440f1e..2f3c4df5e 100644 --- a/client/battle/BattleSiegeController.cpp +++ b/client/battle/BattleSiegeController.cpp @@ -17,16 +17,17 @@ #include "BattleFieldController.h" #include "BattleRenderer.h" -#include "../CMusicHandler.h" #include "../CGameInfo.h" #include "../CPlayerInterface.h" #include "../gui/CGuiHandler.h" +#include "../media/ISoundPlayer.h" #include "../render/Canvas.h" #include "../render/IImage.h" #include "../render/IRenderHandler.h" #include "../../CCallback.h" #include "../../lib/CStack.h" +#include "../../lib/entities/building/TownFortifications.h" #include "../../lib/mapObjects/CGTownInstance.h" #include "../../lib/networkPacks/PacksForClientBattle.h" @@ -34,40 +35,37 @@ ImagePath BattleSiegeController::getWallPieceImageName(EWallVisual::EWallVisual { auto getImageIndex = [&]() -> int { - bool isTower = (what == EWallVisual::KEEP || what == EWallVisual::BOTTOM_TOWER || what == EWallVisual::UPPER_TOWER); + int health = static_cast(state); - switch (state) + switch (what) { - case EWallState::REINFORCED : - return 1; - case EWallState::INTACT : - if (town->hasBuilt(BuildingID::CASTLE)) - return 2; // reinforced walls were damaged - else - return 1; - case EWallState::DAMAGED : - // towers don't have separate image here - INTACT and DAMAGED is 1, DESTROYED is 2 - if (isTower) - return 1; - else - return 2; - case EWallState::DESTROYED : - if (isTower) - return 2; - else + case EWallVisual::KEEP: + case EWallVisual::BOTTOM_TOWER: + case EWallVisual::UPPER_TOWER: + if (health > 0) + return 1; + else + return 2; + default: + { + int healthTotal = town->fortificationsLevel().wallsHealth; + if (healthTotal == health) + return 1; + if (health > 0) + return 2; return 3; - } - return 1; + } + }; }; - 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"); @@ -113,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()) @@ -122,22 +120,21 @@ 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"); } -bool BattleSiegeController::getWallPieceExistance(EWallVisual::EWallVisual what) const +bool BattleSiegeController::getWallPieceExistence(EWallVisual::EWallVisual what) const { - //FIXME: use this instead of buildings test? - //ui8 siegeLevel = owner.curInt->cb->battleGetSiegeLevel(); + const auto & fortifications = town->fortificationsLevel(); switch (what) { - case EWallVisual::MOAT: return town->hasBuilt(BuildingID::CITADEL) && town->town->clientInfo.siegePositions.at(EWallVisual::MOAT).isValid(); - case EWallVisual::MOAT_BANK: return town->hasBuilt(BuildingID::CITADEL) && town->town->clientInfo.siegePositions.at(EWallVisual::MOAT_BANK).isValid(); - case EWallVisual::KEEP_BATTLEMENT: return town->hasBuilt(BuildingID::CITADEL) && owner.getBattle()->battleGetWallState(EWallPart::KEEP) != EWallState::DESTROYED; - case EWallVisual::UPPER_BATTLEMENT: return town->hasBuilt(BuildingID::CASTLE) && owner.getBattle()->battleGetWallState(EWallPart::UPPER_TOWER) != EWallState::DESTROYED; - case EWallVisual::BOTTOM_BATTLEMENT: return town->hasBuilt(BuildingID::CASTLE) && owner.getBattle()->battleGetWallState(EWallPart::BOTTOM_TOWER) != EWallState::DESTROYED; + 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; default: return true; } } @@ -179,16 +176,26 @@ BattleSiegeController::BattleSiegeController(BattleInterface & owner, const CGTo if ( g == EWallVisual::GATE ) // gate is initially closed and has no image to display in this state continue; - if ( !getWallPieceExistance(EWallVisual::EWallVisual(g)) ) + if ( !getWallPieceExistence(EWallVisual::EWallVisual(g)) ) continue; - wallPieceImages[g] = GH.renderHandler().loadImage(getWallPieceImageName(EWallVisual::EWallVisual(g), EWallState::REINFORCED)); + wallPieceImages[g] = GH.renderHandler().loadImage(getWallPieceImageName(EWallVisual::EWallVisual(g), EWallState::REINFORCED), EImageBlitMode::COLORKEY); } } -const CCreature *BattleSiegeController::getTurretCreature() const +const CCreature *BattleSiegeController::getTurretCreature(BattleHex position) const { - return CGI->creh->objects[town->town->clientInfo.siegeShooter]; + switch (position) + { + case BattleHex::CASTLE_CENTRAL_TOWER: + return town->fortificationsLevel().citadelShooter.toCreature(); + case BattleHex::CASTLE_UPPER_TOWER: + return town->fortificationsLevel().upperTowerShooter.toCreature(); + case BattleHex::CASTLE_BOTTOM_TOWER: + return town->fortificationsLevel().lowerTowerShooter.toCreature(); + } + + throw std::runtime_error("Unable to select shooter for tower at " + std::to_string(position.hex)); } Point BattleSiegeController::getTurretCreaturePosition( BattleHex position ) const @@ -211,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 }; } @@ -248,7 +255,7 @@ void BattleSiegeController::gateStateChanged(const EGateState state) wallPieceImages[EWallVisual::GATE] = nullptr; if (stateId != EWallState::NONE) - wallPieceImages[EWallVisual::GATE] = GH.renderHandler().loadImage(getWallPieceImageName(EWallVisual::GATE, stateId)); + wallPieceImages[EWallVisual::GATE] = GH.renderHandler().loadImage(getWallPieceImageName(EWallVisual::GATE, stateId), EImageBlitMode::COLORKEY); if (playSound) CCS->soundh->playSound(soundBase::DRAWBRG); @@ -256,10 +263,10 @@ void BattleSiegeController::gateStateChanged(const EGateState state) void BattleSiegeController::showAbsoluteObstacles(Canvas & canvas) { - if (getWallPieceExistance(EWallVisual::MOAT)) + if (getWallPieceExistence(EWallVisual::MOAT)) showWallPiece(canvas, EWallVisual::MOAT); - if (getWallPieceExistance(EWallVisual::MOAT_BANK)) + if (getWallPieceExistence(EWallVisual::MOAT_BANK)) showWallPiece(canvas, EWallVisual::MOAT_BANK); } @@ -292,7 +299,7 @@ void BattleSiegeController::collectRenderableObjects(BattleRenderer & renderer) { auto wallPiece = EWallVisual::EWallVisual(i); - if ( !getWallPieceExistance(wallPiece)) + if ( !getWallPieceExistence(wallPiece)) continue; if ( getWallPiecePosition(wallPiece) == BattleHex::INVALID) @@ -357,7 +364,7 @@ void BattleSiegeController::stackIsCatapulting(const CatapultAttack & ca) auto wallState = EWallState(owner.getBattle()->battleGetWallState(attackInfo.attackedPart)); - wallPieceImages[wallId] = GH.renderHandler().loadImage(getWallPieceImageName(EWallVisual::EWallVisual(wallId), wallState)); + wallPieceImages[wallId] = GH.renderHandler().loadImage(getWallPieceImageName(EWallVisual::EWallVisual(wallId), wallState), EImageBlitMode::COLORKEY); } } diff --git a/client/battle/BattleSiegeController.h b/client/battle/BattleSiegeController.h index 196967296..060c10540 100644 --- a/client/battle/BattleSiegeController.h +++ b/client/battle/BattleSiegeController.h @@ -83,7 +83,7 @@ class BattleSiegeController BattleHex getWallPiecePosition(EWallVisual::EWallVisual what) const; /// returns true if chosen wall piece should be present in current battle - bool getWallPieceExistance(EWallVisual::EWallVisual what) const; + bool getWallPieceExistence(EWallVisual::EWallVisual what) const; void showWallPiece(Canvas & canvas, EWallVisual::EWallVisual what); @@ -104,7 +104,7 @@ public: /// queries from other battle controllers bool isAttackableByCatapult(BattleHex hex) const; ImagePath getBattleBackgroundName() const; - const CCreature *getTurretCreature() const; + const CCreature *getTurretCreature(BattleHex turretPosition) const; Point getTurretCreaturePosition( BattleHex position ) const; const CGTownInstance *getSiegedTown() const; diff --git a/client/battle/BattleStacksController.cpp b/client/battle/BattleStacksController.cpp index 230914047..173925244 100644 --- a/client/battle/BattleStacksController.cpp +++ b/client/battle/BattleStacksController.cpp @@ -23,10 +23,11 @@ #include "CreatureAnimation.h" #include "../CPlayerInterface.h" -#include "../CMusicHandler.h" #include "../CGameInfo.h" #include "../gui/CGuiHandler.h" #include "../gui/WindowHandler.h" +#include "../media/ISoundPlayer.h" +#include "../render/AssetGenerator.h" #include "../render/Colors.h" #include "../render/Canvas.h" #include "../render/IRenderHandler.h" @@ -37,9 +38,9 @@ #include "../../lib/spells/ISpellMechanics.h" #include "../../lib/battle/BattleAction.h" #include "../../lib/battle/BattleHex.h" +#include "../../lib/texts/TextOperations.h" #include "../../lib/CRandomGenerator.h" #include "../../lib/CStack.h" -#include "../../lib/TextOperations.h" static void onAnimationFinished(const CStack *stack, std::weak_ptr anim) { @@ -79,24 +80,12 @@ BattleStacksController::BattleStacksController(BattleInterface & owner): stackToActivate(nullptr), animIDhelper(0) { + AssetGenerator::createCombatUnitNumberWindow(); //preparing graphics for displaying amounts of creatures - amountNormal = GH.renderHandler().loadImage(ImagePath::builtin("CMNUMWIN.BMP"), EImageBlitMode::COLORKEY); - amountPositive = GH.renderHandler().loadImage(ImagePath::builtin("CMNUMWIN.BMP"), EImageBlitMode::COLORKEY); - amountNegative = GH.renderHandler().loadImage(ImagePath::builtin("CMNUMWIN.BMP"), EImageBlitMode::COLORKEY); - amountEffNeutral = GH.renderHandler().loadImage(ImagePath::builtin("CMNUMWIN.BMP"), EImageBlitMode::COLORKEY); - - static const auto shifterNormal = ColorFilter::genRangeShifter( 0.f, 0.f, 0.f, 0.6f, 0.2f, 1.0f ); - static const auto shifterPositive = ColorFilter::genRangeShifter( 0.f, 0.f, 0.f, 0.2f, 1.0f, 0.2f ); - static const auto shifterNegative = ColorFilter::genRangeShifter( 0.f, 0.f, 0.f, 1.0f, 0.2f, 0.2f ); - static const auto shifterNeutral = ColorFilter::genRangeShifter( 0.f, 0.f, 0.f, 1.0f, 1.0f, 0.2f ); - - // do not change border color - static const int32_t ignoredMask = 1 << 26; - - amountNormal->adjustPalette(shifterNormal, ignoredMask); - amountPositive->adjustPalette(shifterPositive, ignoredMask); - amountNegative->adjustPalette(shifterNegative, ignoredMask); - amountEffNeutral->adjustPalette(shifterNeutral, ignoredMask); + amountNormal = GH.renderHandler().loadImage(ImagePath::builtin("combatUnitNumberWindowDefault"), EImageBlitMode::COLORKEY); + amountPositive = GH.renderHandler().loadImage(ImagePath::builtin("combatUnitNumberWindowPositive"), EImageBlitMode::COLORKEY); + amountNegative = GH.renderHandler().loadImage(ImagePath::builtin("combatUnitNumberWindowNegative"), EImageBlitMode::COLORKEY); + amountEffNeutral = GH.renderHandler().loadImage(ImagePath::builtin("combatUnitNumberWindowNeutral"), EImageBlitMode::COLORKEY); std::vector stacks = owner.getBattle()->battleGetAllStacks(true); for(const CStack * s : stacks) @@ -191,7 +180,7 @@ void BattleStacksController::stackAdded(const CStack * stack, bool instant) { assert(owner.siegeController); - const CCreature *turretCreature = owner.siegeController->getTurretCreature(); + const CCreature *turretCreature = owner.siegeController->getTurretCreature(stack->initialPosition); stackAnimation[stack->unitId()] = AnimationControls::getAnimation(turretCreature); stackAnimation[stack->unitId()]->pos.h = turretCreatureAnimationHeight; @@ -329,10 +318,10 @@ void BattleStacksController::showStackAmountBox(Canvas & canvas, const CStack * boxPosition = owner.fieldController->hexPositionLocal(frontPos).center() + Point(-8, -14); } - Point textPosition = Point(amountBG->dimensions().x/2 + boxPosition.x, boxPosition.y + graphics->fonts[EFonts::FONT_TINY]->getLineHeight() - 6); + Point textPosition = Point(amountBG->dimensions().x/2 + boxPosition.x, boxPosition.y + amountBG->dimensions().y/2); canvas.draw(amountBG, boxPosition); - canvas.drawText(textPosition, EFonts::FONT_TINY, Colors::WHITE, ETextAlignment::TOPCENTER, TextOperations::formatMetric(stack->getCount(), 4)); + canvas.drawText(textPosition, EFonts::FONT_TINY, Colors::WHITE, ETextAlignment::CENTER, TextOperations::formatMetric(stack->getCount(), 4)); } void BattleStacksController::showStack(Canvas & canvas, const CStack * stack) @@ -438,7 +427,7 @@ void BattleStacksController::stacksAreAttacked(std::vector at // defender need to face in direction opposited to out attacker bool needsReverse = shouldAttackFacingRight(attackedInfo.attacker, attackedInfo.defender) == facingRight(attackedInfo.defender); - // FIXME: this check is better, however not usable since stacksAreAttacked is called after net pack is applyed - petrification is already removed + // FIXME: this check is better, however not usable since stacksAreAttacked is called after net pack is applied - petrification is already removed // if (needsReverse && !attackedInfo.defender->isFrozen()) if (needsReverse && stackAnimation[attackedInfo.defender->unitId()]->getType() != ECreatureAnimType::FROZEN) { @@ -647,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); }); } @@ -873,7 +862,8 @@ std::vector BattleStacksController::selectHoveredStacks() spell = owner.actionsController->getCurrentSpell(hoveredHex); caster = owner.actionsController->getCurrentSpellcaster(); - if(caster && spell && owner.actionsController->currentActionSpellcasting(hoveredHex) ) //when casting spell + //casting spell or in explicit spellcasting mode that also handles SPELL_LIKE_ATTACK + if(caster && spell && (owner.actionsController->currentActionSpellcasting(hoveredHex) || owner.actionsController->creatureSpellcastingModeActive())) { spells::Target target; target.emplace_back(hoveredHex); diff --git a/client/battle/BattleWindow.cpp b/client/battle/BattleWindow.cpp index 5b228d82f..adcd10e14 100644 --- a/client/battle/BattleWindow.cpp +++ b/client/battle/BattleWindow.cpp @@ -18,7 +18,6 @@ #include "../CGameInfo.h" #include "../CPlayerInterface.h" -#include "../CMusicHandler.h" #include "../gui/CursorHandler.h" #include "../gui/CGuiHandler.h" #include "../gui/Shortcut.h" @@ -35,7 +34,7 @@ #include "../adventureMap/TurnTimerWidget.h" #include "../../CCallback.h" -#include "../../lib/CGeneralTextHandler.h" +#include "../../lib/texts/CGeneralTextHandler.h" #include "../../lib/gameState/InfoAboutArmy.h" #include "../../lib/mapObjects/CGHeroInstance.h" #include "../../lib/CStack.h" @@ -46,11 +45,11 @@ #include "../../lib/CPlayerState.h" #include "../windows/settings/SettingsMainWindow.h" -BattleWindow::BattleWindow(BattleInterface & owner): - owner(owner), +BattleWindow::BattleWindow(BattleInterface & Owner): + owner(Owner), lastAlternativeAction(PossiblePlayerBattleAction::INVALID) { - OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + OBJECT_CONSTRUCTION; pos.w = 800; pos.h = 600; pos = center(); @@ -65,6 +64,20 @@ BattleWindow::BattleWindow(BattleInterface & owner): const JsonNode config(JsonPath::builtin("config/widgets/BattleWindow2.json")); + addShortcut(EShortcut::BATTLE_TOGGLE_QUICKSPELL, [this](){ this->toggleStickyQuickSpellVisibility();}); + addShortcut(EShortcut::BATTLE_SPELL_SHORTCUT_0, [this](){ useSpellIfPossible(0); }); + addShortcut(EShortcut::BATTLE_SPELL_SHORTCUT_1, [this](){ useSpellIfPossible(1); }); + addShortcut(EShortcut::BATTLE_SPELL_SHORTCUT_2, [this](){ useSpellIfPossible(2); }); + addShortcut(EShortcut::BATTLE_SPELL_SHORTCUT_3, [this](){ useSpellIfPossible(3); }); + addShortcut(EShortcut::BATTLE_SPELL_SHORTCUT_4, [this](){ useSpellIfPossible(4); }); + addShortcut(EShortcut::BATTLE_SPELL_SHORTCUT_5, [this](){ useSpellIfPossible(5); }); + addShortcut(EShortcut::BATTLE_SPELL_SHORTCUT_6, [this](){ useSpellIfPossible(6); }); + addShortcut(EShortcut::BATTLE_SPELL_SHORTCUT_7, [this](){ useSpellIfPossible(7); }); + addShortcut(EShortcut::BATTLE_SPELL_SHORTCUT_8, [this](){ useSpellIfPossible(8); }); + addShortcut(EShortcut::BATTLE_SPELL_SHORTCUT_9, [this](){ useSpellIfPossible(9); }); + addShortcut(EShortcut::BATTLE_SPELL_SHORTCUT_10, [this](){ useSpellIfPossible(10); }); + addShortcut(EShortcut::BATTLE_SPELL_SHORTCUT_11, [this](){ useSpellIfPossible(11); }); + addShortcut(EShortcut::GLOBAL_OPTIONS, std::bind(&BattleWindow::bOptionsf, this)); addShortcut(EShortcut::BATTLE_SURRENDER, std::bind(&BattleWindow::bSurrenderf, this)); addShortcut(EShortcut::BATTLE_RETREAT, std::bind(&BattleWindow::bFleef, this)); @@ -96,6 +109,7 @@ BattleWindow::BattleWindow(BattleInterface & owner): owner.fieldController->createHeroes(); createQueue(); + createQuickSpellWindow(); createStickyHeroInfoWindows(); createTimerInfoWindows(); @@ -109,7 +123,7 @@ BattleWindow::BattleWindow(BattleInterface & owner): void BattleWindow::createQueue() { - OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + OBJECT_CONSTRUCTION; //create stack queue and adjust our own position bool embedQueue; @@ -136,7 +150,7 @@ void BattleWindow::createQueue() void BattleWindow::createStickyHeroInfoWindows() { - OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + OBJECT_CONSTRUCTION; if(owner.defendingHeroInstance) { @@ -165,9 +179,68 @@ void BattleWindow::createStickyHeroInfoWindows() setPositionInfoWindow(); } +void BattleWindow::createQuickSpellWindow() +{ + OBJECT_CONSTRUCTION; + + quickSpellWindow = std::make_shared(owner); + quickSpellWindow->moveTo(Point(pos.x - 67, pos.y)); + + if(settings["battle"]["enableQuickSpellPanel"].Bool()) + showStickyQuickSpellWindow(); + else + hideStickyQuickSpellWindow(); +} + +void BattleWindow::toggleStickyQuickSpellVisibility() +{ + if(settings["battle"]["enableQuickSpellPanel"].Bool()) + hideStickyQuickSpellWindow(); + else + showStickyQuickSpellWindow(); +} + +void BattleWindow::hideStickyQuickSpellWindow() +{ + Settings showStickyQuickSpellWindow = settings.write["battle"]["enableQuickSpellPanel"]; + showStickyQuickSpellWindow->Bool() = false; + + quickSpellWindow->disable(); + quickSpellWindow->isEnabled = false; + + setPositionInfoWindow(); + createTimerInfoWindows(); + GH.windows().totalRedraw(); +} + +void BattleWindow::showStickyQuickSpellWindow() +{ + Settings showStickyQuickSpellWindow = settings.write["battle"]["enableQuickSpellPanel"]; + showStickyQuickSpellWindow->Bool() = true; + + auto hero = owner.getBattle()->battleGetMyHero(); + + if(GH.screenDimensions().x >= 1050 && hero != nullptr && hero->hasSpellbook()) + { + quickSpellWindow->enable(); + quickSpellWindow->isEnabled = true; + } + else + { + quickSpellWindow->disable(); + quickSpellWindow->isEnabled = false; + } + + setPositionInfoWindow(); + createTimerInfoWindows(); + GH.windows().totalRedraw(); +} + void BattleWindow::createTimerInfoWindows() { - OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + OBJECT_CONSTRUCTION; + + int xOffsetAttacker = quickSpellWindow->isEnabled ? -53 : 0; if(LOCPLINT->cb->getStartInfo()->turnTimerInfo.battleTimer != 0 || LOCPLINT->cb->getStartInfo()->turnTimerInfo.unitTimer != 0) { @@ -177,7 +250,7 @@ void BattleWindow::createTimerInfoWindows() if (attacker.isValidPlayer()) { if (GH.screenDimensions().x >= 1000) - attackerTimerWidget = std::make_shared(Point(-92, 1), attacker); + attackerTimerWidget = std::make_shared(Point(-92 + xOffsetAttacker, 1), attacker); else attackerTimerWidget = std::make_shared(Point(1, 135), attacker); } @@ -200,6 +273,21 @@ std::shared_ptr BattleWindow::buildBattleConsole(const JsonNode & return std::make_shared(owner, background, rect.topLeft(), offset, rect.dimensions() ); } +void BattleWindow::useSpellIfPossible(int slot) +{ + SpellID id; + bool fromSettings; + std::tie(id, fromSettings) = quickSpellWindow->getSpells()[slot]; + + if(id == SpellID::NONE) + return; + + if(id.hasValue() && owner.getBattle()->battleGetMyHero() && id.toSpell()->canBeCast(owner.getBattle().get(), spells::Mode::HERO, owner.getBattle()->battleGetMyHero())) + { + owner.castThisSpell(id); + } +}; + void BattleWindow::toggleQueueVisibility() { if(settings["battle"]["showQueue"].Bool()) @@ -284,10 +372,12 @@ void BattleWindow::showStickyHeroWindows() void BattleWindow::updateQueue() { queue->update(); + createQuickSpellWindow(); } void BattleWindow::setPositionInfoWindow() { + int xOffsetAttacker = quickSpellWindow->isEnabled ? -53 : 0; if(defenderHeroWindow) { Point position = (GH.screenDimensions().x >= 1000) @@ -298,7 +388,7 @@ void BattleWindow::setPositionInfoWindow() if(attackerHeroWindow) { Point position = (GH.screenDimensions().x >= 1000) - ? Point(pos.x - 93, pos.y + 60) + ? Point(pos.x - 93 + xOffsetAttacker, pos.y + 60) : Point(pos.x + 1, pos.y + 195); attackerHeroWindow->moveTo(position); } @@ -312,7 +402,7 @@ void BattleWindow::setPositionInfoWindow() if(attackerStackWindow) { Point position = (GH.screenDimensions().x >= 1000) - ? Point(pos.x - 93, attackerHeroWindow ? attackerHeroWindow->pos.y + 210 : pos.y + 60) + ? Point(pos.x - 93 + xOffsetAttacker, attackerHeroWindow ? attackerHeroWindow->pos.y + 210 : pos.y + 60) : Point(pos.x + 1, attackerHeroWindow ? attackerHeroWindow->pos.y : pos.y + 195); attackerStackWindow->moveTo(position); } @@ -326,7 +416,7 @@ void BattleWindow::updateHeroInfoWindow(uint8_t side, const InfoAboutHero & hero void BattleWindow::updateStackInfoWindow(const CStack * stack) { - OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + OBJECT_CONSTRUCTION; bool showInfoWindows = settings["battle"]["stickyHeroInfoWindows"].Bool(); @@ -347,6 +437,7 @@ void BattleWindow::updateStackInfoWindow(const CStack * stack) attackerStackWindow = nullptr; setPositionInfoWindow(); + createTimerInfoWindows(); } void BattleWindow::heroManaPointsChanged(const CGHeroInstance * hero) @@ -447,7 +538,7 @@ void BattleWindow::tacticPhaseEnded() void BattleWindow::bOptionsf() { - if (owner.actionsController->spellcastingModeActive()) + if (owner.actionsController->heroSpellcastingModeActive()) return; CCS->curh->set(Cursor::Map::POINTER); @@ -457,7 +548,7 @@ void BattleWindow::bOptionsf() void BattleWindow::bSurrenderf() { - if (owner.actionsController->spellcastingModeActive()) + if (owner.actionsController->heroSpellcastingModeActive()) return; int cost = owner.getBattle()->battleGetSurrenderCost(); @@ -477,7 +568,7 @@ void BattleWindow::bSurrenderf() void BattleWindow::bFleef() { - if (owner.actionsController->spellcastingModeActive()) + if (owner.actionsController->heroSpellcastingModeActive()) return; if ( owner.getBattle()->battleCanFlee() ) @@ -584,7 +675,7 @@ void BattleWindow::setAlternativeActions(const std::listspellcastingModeActive()) + if (owner.actionsController->heroSpellcastingModeActive()) return; if(settings["battle"]["endWithAutocombat"].Bool() && onlyOnePlayerHuman) @@ -621,7 +712,7 @@ void BattleWindow::bAutofightf() void BattleWindow::bSpellf() { - if (owner.actionsController->spellcastingModeActive()) + if (owner.actionsController->heroSpellcastingModeActive()) return; if (!owner.makingTurn()) @@ -652,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]) @@ -694,7 +785,7 @@ void BattleWindow::bSwitchActionf() void BattleWindow::bWaitf() { - if (owner.actionsController->spellcastingModeActive()) + if (owner.actionsController->heroSpellcastingModeActive()) return; if (owner.stacksController->getActiveStack() != nullptr) @@ -703,7 +794,7 @@ void BattleWindow::bWaitf() void BattleWindow::bDefencef() { - if (owner.actionsController->spellcastingModeActive()) + if (owner.actionsController->heroSpellcastingModeActive()) return; if (owner.stacksController->getActiveStack() != nullptr) @@ -712,7 +803,7 @@ void BattleWindow::bDefencef() void BattleWindow::bConsoleUpf() { - if (owner.actionsController->spellcastingModeActive()) + if (owner.actionsController->heroSpellcastingModeActive()) return; console->scrollUp(); @@ -720,7 +811,7 @@ void BattleWindow::bConsoleUpf() void BattleWindow::bConsoleDownf() { - if (owner.actionsController->spellcastingModeActive()) + if (owner.actionsController->heroSpellcastingModeActive()) return; console->scrollDown(); @@ -760,12 +851,14 @@ void BattleWindow::blockUI(bool on) setShortcutBlocked(EShortcut::BATTLE_WAIT, on || owner.tacticsMode || !canWait); setShortcutBlocked(EShortcut::BATTLE_DEFEND, on || owner.tacticsMode); setShortcutBlocked(EShortcut::BATTLE_SELECT_ACTION, on || owner.tacticsMode); - setShortcutBlocked(EShortcut::BATTLE_AUTOCOMBAT, (settings["battle"]["endWithAutocombat"].Bool() && onlyOnePlayerHuman) ? on || owner.tacticsMode || owner.actionsController->spellcastingModeActive() : owner.actionsController->spellcastingModeActive()); - setShortcutBlocked(EShortcut::BATTLE_END_WITH_AUTOCOMBAT, on || owner.tacticsMode || !onlyOnePlayerHuman || owner.actionsController->spellcastingModeActive()); + setShortcutBlocked(EShortcut::BATTLE_AUTOCOMBAT, (settings["battle"]["endWithAutocombat"].Bool() && onlyOnePlayerHuman) ? on || owner.tacticsMode || owner.actionsController->heroSpellcastingModeActive() : owner.actionsController->heroSpellcastingModeActive()); + setShortcutBlocked(EShortcut::BATTLE_END_WITH_AUTOCOMBAT, on || owner.tacticsMode || !onlyOnePlayerHuman || owner.actionsController->heroSpellcastingModeActive()); setShortcutBlocked(EShortcut::BATTLE_TACTICS_END, on || !owner.tacticsMode); setShortcutBlocked(EShortcut::BATTLE_TACTICS_NEXT, on || !owner.tacticsMode); setShortcutBlocked(EShortcut::BATTLE_CONSOLE_DOWN, on && !owner.tacticsMode); setShortcutBlocked(EShortcut::BATTLE_CONSOLE_UP, on && !owner.tacticsMode); + + quickSpellWindow->setInputEnabled(!on); } void BattleWindow::bOpenActiveUnit() @@ -773,7 +866,7 @@ void BattleWindow::bOpenActiveUnit() const auto * unit = owner.stacksController->getActiveStack(); if (unit) - GH.windows().createAndPushWindow(unit, false);; + GH.windows().createAndPushWindow(unit, false); } void BattleWindow::bOpenHoveredUnit() diff --git a/client/battle/BattleWindow.h b/client/battle/BattleWindow.h index 8b6b4f30d..e9246179b 100644 --- a/client/battle/BattleWindow.h +++ b/client/battle/BattleWindow.h @@ -27,6 +27,7 @@ class StackQueue; class TurnTimerWidget; class HeroInfoBasicPanel; class StackInfoBasicPanel; +class QuickSpellPanel; /// GUI object that handles functionality of panel at the bottom of combat screen class BattleWindow : public InterfaceObjectConfigurable @@ -40,6 +41,8 @@ class BattleWindow : public InterfaceObjectConfigurable std::shared_ptr attackerStackWindow; std::shared_ptr defenderStackWindow; + std::shared_ptr quickSpellWindow; + std::shared_ptr attackerTimerWidget; std::shared_ptr defenderTimerWidget; @@ -68,12 +71,16 @@ class BattleWindow : public InterfaceObjectConfigurable PossiblePlayerBattleAction lastAlternativeAction; void showAlternativeActionIcon(PossiblePlayerBattleAction); + void useSpellIfPossible(int slot); + /// flip battle queue visibility to opposite void toggleQueueVisibility(); void createQueue(); void toggleStickyHeroWindowsVisibility(); + void toggleStickyQuickSpellVisibility(); void createStickyHeroInfoWindows(); + void createQuickSpellWindow(); void createTimerInfoWindows(); std::shared_ptr buildBattleConsole(const JsonNode &) const; @@ -94,6 +101,10 @@ public: void hideStickyHeroWindows(); void showStickyHeroWindows(); + /// Toggle permanent quickspell windows visibility + void hideStickyQuickSpellWindow(); + void showStickyQuickSpellWindow(); + /// Event handler for netpack changing hero mana points void heroManaPointsChanged(const CGHeroInstance * hero); diff --git a/client/battle/CreatureAnimation.cpp b/client/battle/CreatureAnimation.cpp index 2addb4c3d..31ab870cf 100644 --- a/client/battle/CreatureAnimation.cpp +++ b/client/battle/CreatureAnimation.cpp @@ -14,8 +14,10 @@ #include "../../lib/CCreatureHandler.h" #include "../gui/CGuiHandler.h" +#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 }; @@ -198,12 +200,8 @@ CreatureAnimation::CreatureAnimation(const AnimationPath & name_, TSpeedControll speedController(controller), once(false) { - forward = GH.renderHandler().loadAnimation(name_); - reverse = GH.renderHandler().loadAnimation(name_); - - //todo: optimize - forward->preload(); - reverse->preload(); + 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) @@ -326,33 +324,6 @@ static ColorRGBA genBorderColor(ui8 alpha, const ColorRGBA & base) return ColorRGBA(base.r, base.g, base.b, ui8(base.a * alpha / 256)); } -static ui8 mixChannels(ui8 c1, ui8 c2, ui8 a1, ui8 a2) -{ - return c1*a1 / 256 + c2*a2*(255 - a1) / 256 / 256; -} - -static ColorRGBA addColors(const ColorRGBA & base, const ColorRGBA & over) -{ - return ColorRGBA( - mixChannels(over.r, base.r, over.a, base.a), - mixChannels(over.g, base.g, over.a, base.a), - mixChannels(over.b, base.b, over.a, base.a), - ui8(over.a + base.a * (255 - over.a) / 256) - ); -} - -void CreatureAnimation::genSpecialPalette(IImage::SpecialPalette & target) -{ - target.resize(8); - target[0] = genShadow(0); - target[1] = genShadow(shadowAlpha / 2); - // colors 2 & 3 are not used in creatures - target[4] = genShadow(shadowAlpha); - target[5] = genBorderColor(getBorderStrength(elapsedTime), border); - target[6] = addColors(genShadow(shadowAlpha), genBorderColor(getBorderStrength(elapsedTime), border)); - target[7] = addColors(genShadow(shadowAlpha / 2), genBorderColor(getBorderStrength(elapsedTime), border)); -} - void CreatureAnimation::nextFrame(Canvas & canvas, const ColorFilter & shifter, bool facingRight) { ColorRGBA shadowTest = shifter.shiftColor(genShadow(128)); @@ -369,14 +340,14 @@ void CreatureAnimation::nextFrame(Canvas & canvas, const ColorFilter & shifter, if(image) { - IImage::SpecialPalette SpecialPalette; - genSpecialPalette(SpecialPalette); + if (isIdle()) + image->setOverlayColor(genBorderColor(getBorderStrength(elapsedTime), border)); + else + image->setOverlayColor(Colors::TRANSPARENCY); - image->setSpecialPallete(SpecialPalette, IImage::SPECIAL_PALETTE_MASK_CREATURES); - image->adjustPalette(shifter, IImage::SPECIAL_PALETTE_MASK_CREATURES); + image->adjustPalette(shifter, 0); canvas.draw(image, pos.topLeft(), Rect(0, 0, pos.w, pos.h)); - } } diff --git a/client/battle/CreatureAnimation.h b/client/battle/CreatureAnimation.h index 686e941b5..09e3fd259 100644 --- a/client/battle/CreatureAnimation.h +++ b/client/battle/CreatureAnimation.h @@ -12,7 +12,6 @@ #include "../../lib/FunctionList.h" #include "../../lib/Color.h" #include "../widgets/Images.h" -#include "../render/CAnimation.h" #include "../render/IImage.h" class CIntObject; @@ -108,9 +107,7 @@ private: void endAnimation(); - void genSpecialPalette(IImage::SpecialPalette & target); public: - /// function(s) that will be called when animation ends, after reset to 1st frame /// NOTE that these functions will be fired only once CFunctionList onAnimationReset; diff --git a/client/eventsSDL/InputHandler.cpp b/client/eventsSDL/InputHandler.cpp index 194928c29..00a60c402 100644 --- a/client/eventsSDL/InputHandler.cpp +++ b/client/eventsSDL/InputHandler.cpp @@ -22,25 +22,28 @@ #include "../gui/CursorHandler.h" #include "../gui/EventDispatcher.h" #include "../gui/MouseButton.h" +#include "../media/IMusicPlayer.h" +#include "../media/ISoundPlayer.h" #include "../CMT.h" #include "../CPlayerInterface.h" #include "../CGameInfo.h" -#include "../CMusicHandler.h" #include "../../lib/CConfigHandler.h" #include #include +#include InputHandler::InputHandler() - : mouseHandler(std::make_unique()) + : enableMouse(settings["input"]["enableMouse"].Bool()) + , enableTouch(settings["input"]["enableTouch"].Bool()) + , enableController(settings["input"]["enableController"].Bool()) + , currentInputMode(InputMode::KEYBOARD_AND_MOUSE) + , mouseHandler(std::make_unique()) , keyboardHandler(std::make_unique()) , fingerHandler(std::make_unique()) , textHandler(std::make_unique()) , gameControllerHandler(std::make_unique()) - , enableMouse(settings["input"]["enableMouse"].Bool()) - , enableTouch(settings["input"]["enableTouch"].Bool()) - , enableController(settings["input"]["enableController"].Bool()) { } @@ -51,6 +54,7 @@ void InputHandler::handleCurrentEvent(const SDL_Event & current) switch (current.type) { case SDL_KEYDOWN: + setCurrentInputMode(InputMode::KEYBOARD_AND_MOUSE); keyboardHandler->handleEventKeyDown(current.key); return; case SDL_KEYUP: @@ -59,11 +63,17 @@ void InputHandler::handleCurrentEvent(const SDL_Event & current) #ifndef VCMI_EMULATE_TOUCHSCREEN_WITH_MOUSE case SDL_MOUSEMOTION: if (enableMouse) + { + setCurrentInputMode(InputMode::KEYBOARD_AND_MOUSE); mouseHandler->handleEventMouseMotion(current.motion); + } return; case SDL_MOUSEBUTTONDOWN: if (enableMouse) + { + setCurrentInputMode(InputMode::KEYBOARD_AND_MOUSE); mouseHandler->handleEventMouseButtonDown(current.button); + } return; case SDL_MOUSEBUTTONUP: if (enableMouse) @@ -82,11 +92,17 @@ void InputHandler::handleCurrentEvent(const SDL_Event & current) return; case SDL_FINGERMOTION: if (enableTouch) + { + setCurrentInputMode(InputMode::TOUCH); fingerHandler->handleEventFingerMotion(current.tfinger); + } return; case SDL_FINGERDOWN: if (enableTouch) + { + setCurrentInputMode(InputMode::TOUCH); fingerHandler->handleEventFingerDown(current.tfinger); + } return; case SDL_FINGERUP: if (enableTouch) @@ -94,11 +110,17 @@ void InputHandler::handleCurrentEvent(const SDL_Event & current) return; case SDL_CONTROLLERAXISMOTION: if (enableController) + { + setCurrentInputMode(InputMode::CONTROLLER); gameControllerHandler->handleEventAxisMotion(current.caxis); + } return; case SDL_CONTROLLERBUTTONDOWN: if (enableController) + { + setCurrentInputMode(InputMode::CONTROLLER); gameControllerHandler->handleEventButtonDown(current.cbutton); + } return; case SDL_CONTROLLERBUTTONUP: if (enableController) @@ -107,6 +129,25 @@ void InputHandler::handleCurrentEvent(const SDL_Event & current) } } +void InputHandler::setCurrentInputMode(InputMode modi) +{ + if(currentInputMode != modi) + { + currentInputMode = modi; + GH.events().dispatchInputModeChanged(modi); + } +} + +InputMode InputHandler::getCurrentInputMode() +{ + return currentInputMode; +} + +void InputHandler::copyToClipBoard(const std::string & text) +{ + SDL_SetClipboardText(text.c_str()); +} + std::vector InputHandler::acquireEvents() { boost::unique_lock lock(eventsMutex); @@ -334,7 +375,8 @@ void InputHandler::stopTextInput() void InputHandler::hapticFeedback() { - fingerHandler->hapticFeedback(); + if(currentInputMode == InputMode::TOUCH) + fingerHandler->hapticFeedback(); } uint32_t InputHandler::getTicks() diff --git a/client/eventsSDL/InputHandler.h b/client/eventsSDL/InputHandler.h index d887b7734..71d58c0d4 100644 --- a/client/eventsSDL/InputHandler.h +++ b/client/eventsSDL/InputHandler.h @@ -23,6 +23,13 @@ class InputSourceTouch; class InputSourceText; class InputSourceGameController; +enum class InputMode +{ + KEYBOARD_AND_MOUSE, + TOUCH, + CONTROLLER +}; + class InputHandler { std::vector eventsQueue; @@ -34,6 +41,9 @@ class InputHandler const bool enableTouch; const bool enableController; + InputMode currentInputMode; + void setCurrentInputMode(InputMode modi); + std::vector acquireEvents(); void preprocessEvent(const SDL_Event & event); @@ -91,4 +101,8 @@ public: bool isKeyboardCmdDown() const; bool isKeyboardCtrlDown() const; bool isKeyboardShiftDown() const; + + InputMode getCurrentInputMode(); + + void copyToClipBoard(const std::string & text); }; diff --git a/client/eventsSDL/InputSourceGameController.cpp b/client/eventsSDL/InputSourceGameController.cpp index 055bceb42..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" @@ -28,11 +29,6 @@ void InputSourceGameController::gameControllerDeleter(SDL_GameController * gameC } InputSourceGameController::InputSourceGameController(): - configTriggerTreshold(settings["input"]["controllerTriggerTreshold"].Float()), - configAxisDeadZone(settings["input"]["controllerAxisDeadZone"].Float()), - configAxisFullZone(settings["input"]["controllerAxisFullZone"].Float()), - configAxisSpeed(settings["input"]["controllerAxisSpeed"].Float()), - configAxisScale(settings["input"]["controllerAxisScale"].Float()), cursorAxisValueX(0), cursorAxisValueY(0), cursorPlanDisX(0.0), @@ -43,7 +39,12 @@ InputSourceGameController::InputSourceGameController(): scrollAxisValueX(0), scrollAxisValueY(0), scrollPlanDisX(0.0), - scrollPlanDisY(0.0) + scrollPlanDisY(0.0), + configTriggerThreshold(settings["input"]["controllerTriggerThreshold"].Float()), + configAxisDeadZone(settings["input"]["controllerAxisDeadZone"].Float()), + configAxisFullZone(settings["input"]["controllerAxisFullZone"].Float()), + configAxisSpeed(settings["input"]["controllerAxisSpeed"].Float()), + configAxisScale(settings["input"]["controllerAxisScale"].Float()) { tryOpenAllGameControllers(); } @@ -127,7 +128,7 @@ void InputSourceGameController::handleEventDeviceRemapped(const SDL_ControllerDe openGameController(device.which); } -double InputSourceGameController::getRealAxisValue(int value) +double InputSourceGameController::getRealAxisValue(int value) const { double ratio = static_cast(value) / SDL_JOYSTICK_AXIS_MAX; double greenZone = configAxisFullZone - configAxisDeadZone; @@ -142,7 +143,7 @@ double InputSourceGameController::getRealAxisValue(int value) void InputSourceGameController::dispatchAxisShortcuts(const std::vector & shortcutsVector, SDL_GameControllerAxis axisID, int axisValue) { - if(getRealAxisValue(axisValue) > configTriggerTreshold) + if(getRealAxisValue(axisValue) > configTriggerThreshold) { if(!pressedAxes.count(axisID)) { @@ -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) @@ -321,7 +324,7 @@ void InputSourceGameController::handleScrollUpdate(int32_t deltaTimeMs) } } -bool InputSourceGameController::isScrollAxisReleased() +bool InputSourceGameController::isScrollAxisReleased() const { - return scrollAxisValueX == 0 && scrollAxisValueY == 0; + return vstd::isAlmostZero(scrollAxisValueX) && vstd::isAlmostZero(scrollAxisValueY); } diff --git a/client/eventsSDL/InputSourceGameController.h b/client/eventsSDL/InputSourceGameController.h index b3efd9e4d..e61a92fe9 100644 --- a/client/eventsSDL/InputSourceGameController.h +++ b/client/eventsSDL/InputSourceGameController.h @@ -39,7 +39,7 @@ class InputSourceGameController double scrollPlanDisX; double scrollPlanDisY; - const double configTriggerTreshold; + const double configTriggerThreshold; const double configAxisDeadZone; const double configAxisFullZone; const double configAxisSpeed; @@ -47,14 +47,14 @@ class InputSourceGameController void openGameController(int index); int getJoystickIndex(SDL_GameController * controller); - double getRealAxisValue(int value); + double getRealAxisValue(int value) const; void dispatchAxisShortcuts(const std::vector & shortcutsVector, SDL_GameControllerAxis axisID, int axisValue); void tryToConvertCursor(); void doCursorMove(int deltaX, int deltaY); int getMoveDis(float planDis); void handleCursorUpdate(int32_t deltaTimeMs); void handleScrollUpdate(int32_t deltaTimeMs); - bool isScrollAxisReleased(); + bool isScrollAxisReleased() const; public: InputSourceGameController(); diff --git a/client/eventsSDL/InputSourceKeyboard.cpp b/client/eventsSDL/InputSourceKeyboard.cpp index ee43e5c53..65c21088c 100644 --- a/client/eventsSDL/InputSourceKeyboard.cpp +++ b/client/eventsSDL/InputSourceKeyboard.cpp @@ -32,15 +32,22 @@ InputSourceKeyboard::InputSourceKeyboard() #endif } -std::string InputSourceKeyboard::getKeyNameWithModifiers(const std::string & keyName) const +std::string InputSourceKeyboard::getKeyNameWithModifiers(const std::string & keyName, bool keyUp) { std::string result; - if (isKeyboardCtrlDown()) + if(!keyUp) + { + wasKeyboardCtrlDown = isKeyboardCtrlDown(); + wasKeyboardAltDown = isKeyboardAltDown(); + wasKeyboardShiftDown = isKeyboardShiftDown(); + } + + if (wasKeyboardCtrlDown) result += "Ctrl+"; - if (isKeyboardAltDown()) + if (wasKeyboardAltDown) result += "Alt+"; - if (isKeyboardShiftDown()) + if (wasKeyboardShiftDown) result += "Shift+"; result += keyName; @@ -49,7 +56,7 @@ std::string InputSourceKeyboard::getKeyNameWithModifiers(const std::string & key void InputSourceKeyboard::handleEventKeyDown(const SDL_KeyboardEvent & key) { - std::string keyName = getKeyNameWithModifiers(SDL_GetKeyName(key.keysym.sym)); + std::string keyName = getKeyNameWithModifiers(SDL_GetKeyName(key.keysym.sym), false); logGlobal->trace("keyboard: key '%s' pressed", keyName); assert(key.state == SDL_PRESSED); @@ -111,7 +118,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), true); logGlobal->trace("keyboard: key '%s' released", keyName); if (SDL_IsTextInputActive() == SDL_TRUE) diff --git a/client/eventsSDL/InputSourceKeyboard.h b/client/eventsSDL/InputSourceKeyboard.h index 41755b6c7..d27756c85 100644 --- a/client/eventsSDL/InputSourceKeyboard.h +++ b/client/eventsSDL/InputSourceKeyboard.h @@ -15,7 +15,11 @@ struct SDL_KeyboardEvent; /// Class that handles keyboard input from SDL events class InputSourceKeyboard { - std::string getKeyNameWithModifiers(const std::string & keyName) const; + bool wasKeyboardCtrlDown; + bool wasKeyboardAltDown; + bool wasKeyboardShiftDown; + + std::string getKeyNameWithModifiers(const std::string & keyName, bool keyUp); public: InputSourceKeyboard(); diff --git a/client/eventsSDL/InputSourceMouse.cpp b/client/eventsSDL/InputSourceMouse.cpp index b8eb6e0b4..d3d9e1866 100644 --- a/client/eventsSDL/InputSourceMouse.cpp +++ b/client/eventsSDL/InputSourceMouse.cpp @@ -16,11 +16,14 @@ #include "../gui/EventDispatcher.h" #include "../gui/MouseButton.h" +#include "../render/IScreenHandler.h" + #include "../../lib/Point.h" #include "../../lib/CConfigHandler.h" #include #include +#include InputSourceMouse::InputSourceMouse() :mouseToleranceDistance(settings["input"]["mouseToleranceDistance"].Integer()) @@ -30,8 +33,8 @@ InputSourceMouse::InputSourceMouse() void InputSourceMouse::handleEventMouseMotion(const SDL_MouseMotionEvent & motion) { - Point newPosition(motion.x, motion.y); - Point distance(-motion.xrel, -motion.yrel); + Point newPosition = Point(motion.x, motion.y) / GH.screenHandler().getScalingFactor(); + Point distance = Point(-motion.xrel, -motion.yrel) / GH.screenHandler().getScalingFactor(); mouseButtonsMask = motion.state; @@ -39,13 +42,15 @@ void InputSourceMouse::handleEventMouseMotion(const SDL_MouseMotionEvent & motio GH.events().dispatchGesturePanning(middleClickPosition, newPosition, distance); else if (mouseButtonsMask & SDL_BUTTON(SDL_BUTTON_LEFT)) GH.events().dispatchMouseDragged(newPosition, distance); + else if (mouseButtonsMask & SDL_BUTTON(SDL_BUTTON_RIGHT)) + GH.events().dispatchMouseDraggedPopup(newPosition, distance); else GH.input().setCursorPosition(newPosition); } void InputSourceMouse::handleEventMouseButtonDown(const SDL_MouseButtonEvent & button) { - Point position(button.x, button.y); + Point position = Point(button.x, button.y) / GH.screenHandler().getScalingFactor(); switch(button.button) { @@ -67,12 +72,18 @@ void InputSourceMouse::handleEventMouseButtonDown(const SDL_MouseButtonEvent & b void InputSourceMouse::handleEventMouseWheel(const SDL_MouseWheelEvent & wheel) { + //NOTE: while mouseX / mouseY properties are available since 2.26.0, they are not converted into logical coordinates so don't account for resolution scaling + // This SDL bug was fixed in 2.30.1: https://github.com/libsdl-org/SDL/issues/9097 +#if SDL_VERSION_ATLEAST(2,30,1) + GH.events().dispatchMouseScrolled(Point(wheel.x, wheel.y), Point(wheel.mouseX, wheel.mouseY) / GH.screenHandler().getScalingFactor()); +#else GH.events().dispatchMouseScrolled(Point(wheel.x, wheel.y), GH.getCursorPosition()); +#endif } void InputSourceMouse::handleEventMouseButtonUp(const SDL_MouseButtonEvent & button) { - Point position(button.x, button.y); + Point position = Point(button.x, button.y) / GH.screenHandler().getScalingFactor(); switch(button.button) { diff --git a/client/eventsSDL/InputSourceText.cpp b/client/eventsSDL/InputSourceText.cpp index b1fdfa77d..d33a4e035 100644 --- a/client/eventsSDL/InputSourceText.cpp +++ b/client/eventsSDL/InputSourceText.cpp @@ -11,7 +11,6 @@ #include "StdInc.h" #include "InputSourceText.h" -#include "../CMT.h" #include "../gui/CGuiHandler.h" #include "../gui/EventDispatcher.h" #include "../render/IScreenHandler.h" diff --git a/client/eventsSDL/InputSourceTouch.cpp b/client/eventsSDL/InputSourceTouch.cpp index 992daeb2c..1135a0b52 100644 --- a/client/eventsSDL/InputSourceTouch.cpp +++ b/client/eventsSDL/InputSourceTouch.cpp @@ -14,7 +14,6 @@ #include "InputHandler.h" #include "../../lib/CConfigHandler.h" -#include "../CMT.h" #include "../CGameInfo.h" #include "../gui/CursorHandler.h" #include "../gui/CGuiHandler.h" @@ -84,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; @@ -104,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; @@ -158,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; @@ -206,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/GlobalLobbyClient.cpp b/client/globalLobby/GlobalLobbyClient.cpp index 6b7be1513..9a29ddae3 100644 --- a/client/globalLobby/GlobalLobbyClient.cpp +++ b/client/globalLobby/GlobalLobbyClient.cpp @@ -17,18 +17,18 @@ #include "GlobalLobbyWindow.h" #include "../CGameInfo.h" -#include "../CMusicHandler.h" #include "../CServerHandler.h" #include "../gui/CGuiHandler.h" #include "../gui/WindowHandler.h" #include "../mainmenu/CMainMenu.h" +#include "../media/ISoundPlayer.h" #include "../windows/InfoWindows.h" #include "../../lib/CConfigHandler.h" -#include "../../lib/MetaString.h" #include "../../lib/json/JsonUtils.h" -#include "../../lib/TextOperations.h" -#include "../../lib/CGeneralTextHandler.h" +#include "../../lib/texts/CGeneralTextHandler.h" +#include "../../lib/texts/MetaString.h" +#include "../../lib/texts/TextOperations.h" GlobalLobbyClient::GlobalLobbyClient() { @@ -43,7 +43,7 @@ void GlobalLobbyClient::onPacketReceived(const std::shared_ptr"); if(json["type"].String() == "accountCreated") return receiveAccountCreated(json); diff --git a/client/globalLobby/GlobalLobbyInviteWindow.cpp b/client/globalLobby/GlobalLobbyInviteWindow.cpp index 64a4e0026..43373a5d0 100644 --- a/client/globalLobby/GlobalLobbyInviteWindow.cpp +++ b/client/globalLobby/GlobalLobbyInviteWindow.cpp @@ -22,8 +22,8 @@ #include "../widgets/ObjectLists.h" #include "../widgets/TextControls.h" -#include "../../lib/MetaString.h" #include "../../lib/json/JsonNode.h" +#include "../../lib/texts/MetaString.h" GlobalLobbyInviteAccountCard::GlobalLobbyInviteAccountCard(const GlobalLobbyAccount & accountDescription) : accountID(accountDescription.accountID) @@ -47,7 +47,7 @@ GlobalLobbyInviteAccountCard::GlobalLobbyInviteAccountCard(const GlobalLobbyAcco } } - OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + OBJECT_CONSTRUCTION; if (thisAccountInvited) backgroundOverlay = std::make_shared(Rect(0, 0, pos.w, pos.h), ColorRGBA(0, 0, 0, 128), Colors::WHITE, 1); else @@ -73,13 +73,13 @@ void GlobalLobbyInviteAccountCard::clickPressed(const Point & cursorPosition) GlobalLobbyInviteWindow::GlobalLobbyInviteWindow() : CWindowObject(BORDERED) { - OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + OBJECT_CONSTRUCTION; pos.w = 236; pos.h = 420; - filledBackground = std::make_shared(ImagePath::builtin("DiBoxBck"), Rect(0, 0, pos.w, pos.h)); - filledBackground->playerColored(PlayerColor(1)); + filledBackground = std::make_shared(Rect(0, 0, pos.w, pos.h)); + filledBackground->setPlayerColor(PlayerColor(1)); labelTitle = std::make_shared( pos.w / 2, 20, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW, MetaString::createFromTextID("vcmi.lobby.invite.header").toString() ); diff --git a/client/globalLobby/GlobalLobbyLoginWindow.cpp b/client/globalLobby/GlobalLobbyLoginWindow.cpp index 7fadce981..d76f80d17 100644 --- a/client/globalLobby/GlobalLobbyLoginWindow.cpp +++ b/client/globalLobby/GlobalLobbyLoginWindow.cpp @@ -25,13 +25,13 @@ #include "../widgets/TextControls.h" #include "../../lib/CConfigHandler.h" -#include "../../lib/CGeneralTextHandler.h" -#include "../../lib/MetaString.h" +#include "../../lib/texts/CGeneralTextHandler.h" +#include "../../lib/texts/MetaString.h" GlobalLobbyLoginWindow::GlobalLobbyLoginWindow() : CWindowObject(BORDERED) { - OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + OBJECT_CONSTRUCTION; pos.w = 284; pos.h = 220; @@ -40,7 +40,7 @@ GlobalLobbyLoginWindow::GlobalLobbyLoginWindow() loginAs.appendTextID("vcmi.lobby.login.as"); loginAs.replaceRawString(CSH->getGlobalLobby().getAccountDisplayName()); - filledBackground = std::make_shared(ImagePath::builtin("DiBoxBck"), Rect(0, 0, pos.w, pos.h)); + filledBackground = std::make_shared(Rect(0, 0, pos.w, pos.h)); labelTitle = std::make_shared( pos.w / 2, 20, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->translate("vcmi.lobby.login.title")); labelUsernameTitle = std::make_shared( 10, 65, FONT_MEDIUM, ETextAlignment::TOPLEFT, Colors::WHITE, CGI->generaltexth->translate("vcmi.lobby.login.username")); labelUsername = std::make_shared( 10, 65, FONT_MEDIUM, ETextAlignment::TOPLEFT, Colors::WHITE, loginAs.toString(), 265); @@ -65,7 +65,7 @@ GlobalLobbyLoginWindow::GlobalLobbyLoginWindow() { buttonLogin->block(true); toggleMode->setSelected(0); - onLoginModeChanged(0); // call it manually to disable widgets - toggleMode will not emit this call if this is currenly selected option + onLoginModeChanged(0); // call it manually to disable widgets - toggleMode will not emit this call if this is currently selected option } else { @@ -73,7 +73,7 @@ GlobalLobbyLoginWindow::GlobalLobbyLoginWindow() onLoginModeChanged(1); } - filledBackground->playerColored(PlayerColor(1)); + filledBackground->setPlayerColor(PlayerColor(1)); inputUsername->setCallback([this](const std::string & text) { this->buttonLogin->block(text.empty()); diff --git a/client/globalLobby/GlobalLobbyRoomWindow.cpp b/client/globalLobby/GlobalLobbyRoomWindow.cpp index 00b4f77d6..943737b72 100644 --- a/client/globalLobby/GlobalLobbyRoomWindow.cpp +++ b/client/globalLobby/GlobalLobbyRoomWindow.cpp @@ -26,14 +26,14 @@ #include "../widgets/GraphicalPrimitiveCanvas.h" #include "../widgets/ObjectLists.h" -#include "../../lib/CGeneralTextHandler.h" -#include "../../lib/MetaString.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" GlobalLobbyRoomAccountCard::GlobalLobbyRoomAccountCard(const GlobalLobbyAccount & accountDescription) { - OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + OBJECT_CONSTRUCTION; pos.w = 130; pos.h = 40; backgroundOverlay = std::make_shared(Rect(0, 0, pos.w, pos.h), ColorRGBA(0, 0, 0, 128), ColorRGBA(64, 64, 64, 64), 1); @@ -51,7 +51,7 @@ GlobalLobbyRoomModCard::GlobalLobbyRoomModCard(const GlobalLobbyRoomModInfo & mo { ModVerificationStatus::FULL_MATCH, "compatible" } }; - OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + OBJECT_CONSTRUCTION; pos.w = 200; pos.h = 40; backgroundOverlay = std::make_shared(Rect(0, 0, pos.w, pos.h), ColorRGBA(0, 0, 0, 128), ColorRGBA(64, 64, 64, 64), 1); @@ -66,7 +66,7 @@ GlobalLobbyRoomModCard::GlobalLobbyRoomModCard(const GlobalLobbyRoomModInfo & mo labelStatus = std::make_shared(5, 30, FONT_SMALL, ETextAlignment::CENTERLEFT, statusColor, CGI->generaltexth->translate("vcmi.lobby.mod.state." + statusToString.at(modInfo.status))); } -static const std::string getJoinRoomErrorMessage(const GlobalLobbyRoom & roomDescription, const std::vector & modVerificationList) +static std::string getJoinRoomErrorMessage(const GlobalLobbyRoom & roomDescription, const std::vector & modVerificationList) { bool publicRoom = roomDescription.statusID == "public"; bool privateRoom = roomDescription.statusID == "private"; @@ -114,10 +114,10 @@ static const std::string getJoinRoomErrorMessage(const GlobalLobbyRoom & roomDes GlobalLobbyRoomWindow::GlobalLobbyRoomWindow(GlobalLobbyWindow * window, const std::string & roomUUID) : CWindowObject(BORDERED) - , roomUUID(roomUUID) , window(window) + , roomUUID(roomUUID) { - OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + OBJECT_CONSTRUCTION; pos.w = 400; pos.h = 400; @@ -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); } @@ -152,7 +152,7 @@ GlobalLobbyRoomWindow::GlobalLobbyRoomWindow(GlobalLobbyWindow * window, const s subtitleText.replaceRawString(roomDescription.description); subtitleText.replaceRawString(roomDescription.hostAccountDisplayName); - filledBackground = std::make_shared(ImagePath::builtin("DiBoxBck"), Rect(0, 0, pos.w, pos.h)); + filledBackground = std::make_shared(Rect(0, 0, pos.w, pos.h)); labelTitle = std::make_shared( pos.w / 2, 20, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW, MetaString::createFromTextID("vcmi.lobby.preview.title").toString()); labelSubtitle = std::make_shared( pos.w / 2, 40, FONT_MEDIUM, ETextAlignment::CENTER, Colors::YELLOW, subtitleText.toString(), 400); @@ -200,7 +200,7 @@ GlobalLobbyRoomWindow::GlobalLobbyRoomWindow(GlobalLobbyWindow * window, const s modListTitle = std::make_shared( 182, 59, FONT_SMALL, ETextAlignment::CENTERLEFT, Colors::YELLOW, MetaString::createFromTextID("vcmi.lobby.preview.mods").toString()); buttonJoin->block(!errorMessage.empty()); - filledBackground->playerColored(PlayerColor(1)); + filledBackground->setPlayerColor(PlayerColor(1)); center(); } diff --git a/client/globalLobby/GlobalLobbyServerSetup.cpp b/client/globalLobby/GlobalLobbyServerSetup.cpp index b23ff2a9a..baf647af2 100644 --- a/client/globalLobby/GlobalLobbyServerSetup.cpp +++ b/client/globalLobby/GlobalLobbyServerSetup.cpp @@ -23,18 +23,18 @@ #include "../widgets/TextControls.h" #include "../../lib/CConfigHandler.h" -#include "../../lib/CGeneralTextHandler.h" -#include "../../lib/MetaString.h" +#include "../../lib/texts/CGeneralTextHandler.h" +#include "../../lib/texts/MetaString.h" GlobalLobbyServerSetup::GlobalLobbyServerSetup() : CWindowObject(BORDERED) { - OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + OBJECT_CONSTRUCTION; pos.w = 284; pos.h = 340; - filledBackground = std::make_shared(ImagePath::builtin("DiBoxBck"), Rect(0, 0, pos.w, pos.h)); + filledBackground = std::make_shared(Rect(0, 0, pos.w, pos.h)); labelTitle = std::make_shared( pos.w / 2, 20, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->translate("vcmi.lobby.room.create")); labelPlayerLimit = std::make_shared( pos.w / 2, 48, FONT_MEDIUM, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->translate("vcmi.lobby.room.players.limit")); labelRoomType = std::make_shared( pos.w / 2, 108, FONT_MEDIUM, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->translate("vcmi.lobby.room.type")); @@ -78,7 +78,7 @@ GlobalLobbyServerSetup::GlobalLobbyServerSetup() buttonCreate = std::make_shared(Point(10, 300), AnimationPath::builtin("MuBchck"), CButton::tooltip(), [this](){ onCreate(); }, EShortcut::GLOBAL_ACCEPT); buttonClose = std::make_shared(Point(210, 300), AnimationPath::builtin("MuBcanc"), CButton::tooltip(), [this](){ onClose(); }, EShortcut::GLOBAL_CANCEL); - filledBackground->playerColored(PlayerColor(1)); + filledBackground->setPlayerColor(PlayerColor(1)); updateDescription(); center(); diff --git a/client/globalLobby/GlobalLobbyWidget.cpp b/client/globalLobby/GlobalLobbyWidget.cpp index d7272140b..74d4583d6 100644 --- a/client/globalLobby/GlobalLobbyWidget.cpp +++ b/client/globalLobby/GlobalLobbyWidget.cpp @@ -16,10 +16,10 @@ #include "GlobalLobbyRoomWindow.h" #include "../CGameInfo.h" -#include "../CMusicHandler.h" #include "../CServerHandler.h" #include "../gui/CGuiHandler.h" #include "../gui/WindowHandler.h" +#include "../media/ISoundPlayer.h" #include "../render/Colors.h" #include "../widgets/Buttons.h" #include "../widgets/CTextInput.h" @@ -30,8 +30,8 @@ #include "../widgets/TextControls.h" #include "../../lib/CConfigHandler.h" -#include "../../lib/Languages.h" -#include "../../lib/MetaString.h" +#include "../../lib/texts/Languages.h" +#include "../../lib/texts/MetaString.h" GlobalLobbyWidget::GlobalLobbyWidget(GlobalLobbyWindow * window) : window(window) @@ -187,7 +187,7 @@ GlobalLobbyChannelCardBase::GlobalLobbyChannelCardBase(GlobalLobbyWindow * windo pos.h = dimensions.y; addUsedEvents(LCLICK); - OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + OBJECT_CONSTRUCTION; if (window->isChannelOpen(channelType, channelName)) backgroundOverlay = std::make_shared(Rect(0, 0, pos.w, pos.h), ColorRGBA(0, 0, 0, 128), Colors::YELLOW, 2); @@ -206,16 +206,16 @@ void GlobalLobbyChannelCardBase::clickPressed(const Point & cursorPosition) GlobalLobbyAccountCard::GlobalLobbyAccountCard(GlobalLobbyWindow * window, const GlobalLobbyAccount & accountDescription) : GlobalLobbyChannelCardBase(window, Point(130, 40), "player", accountDescription.accountID, accountDescription.displayName) { - OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + OBJECT_CONSTRUCTION; labelName = std::make_shared(5, 10, FONT_SMALL, ETextAlignment::CENTERLEFT, Colors::WHITE, accountDescription.displayName, 120); labelStatus = std::make_shared(5, 30, FONT_SMALL, ETextAlignment::CENTERLEFT, Colors::YELLOW, accountDescription.status); } GlobalLobbyRoomCard::GlobalLobbyRoomCard(GlobalLobbyWindow * window, const GlobalLobbyRoom & roomDescription) - : roomUUID(roomDescription.gameRoomID) - , window(window) + : window(window) + , roomUUID(roomDescription.gameRoomID) { - OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + OBJECT_CONSTRUCTION; addUsedEvents(LCLICK); bool hasInvite = CSH->getGlobalLobby().isInvitedToRoom(roomDescription.gameRoomID); @@ -253,14 +253,14 @@ void GlobalLobbyRoomCard::clickPressed(const Point & cursorPosition) GlobalLobbyChannelCard::GlobalLobbyChannelCard(GlobalLobbyWindow * window, const std::string & channelName) : GlobalLobbyChannelCardBase(window, Point(146, 40), "global", channelName, Languages::getLanguageOptions(channelName).nameNative) { - OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + OBJECT_CONSTRUCTION; labelName = std::make_shared(5, 20, FONT_SMALL, ETextAlignment::CENTERLEFT, Colors::WHITE, Languages::getLanguageOptions(channelName).nameNative); } GlobalLobbyMatchCard::GlobalLobbyMatchCard(GlobalLobbyWindow * window, const GlobalLobbyRoom & matchDescription) : GlobalLobbyChannelCardBase(window, Point(130, 40), "match", matchDescription.gameRoomID, matchDescription.startDateFormatted) { - OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + OBJECT_CONSTRUCTION; labelMatchDate = std::make_shared(5, 10, FONT_SMALL, ETextAlignment::CENTERLEFT, Colors::WHITE, matchDescription.startDateFormatted); MetaString opponentDescription; diff --git a/client/globalLobby/GlobalLobbyWindow.cpp b/client/globalLobby/GlobalLobbyWindow.cpp index 70a195101..03d9798de 100644 --- a/client/globalLobby/GlobalLobbyWindow.cpp +++ b/client/globalLobby/GlobalLobbyWindow.cpp @@ -23,14 +23,14 @@ #include "../widgets/ObjectLists.h" #include "../widgets/TextControls.h" -#include "../../lib/Languages.h" -#include "../../lib/MetaString.h" -#include "../../lib/TextOperations.h" +#include "../../lib/texts/Languages.h" +#include "../../lib/texts/MetaString.h" +#include "../../lib/texts/TextOperations.h" GlobalLobbyWindow::GlobalLobbyWindow() : CWindowObject(BORDERED) { - OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + OBJECT_CONSTRUCTION; widget = std::make_shared(this); pos = widget->pos; center(); diff --git a/client/gui/CGuiHandler.cpp b/client/gui/CGuiHandler.cpp index f5dd29eab..6608ca44d 100644 --- a/client/gui/CGuiHandler.cpp +++ b/client/gui/CGuiHandler.cpp @@ -39,33 +39,17 @@ CGuiHandler GH; static thread_local bool inGuiThread = false; -SObjectConstruction::SObjectConstruction(CIntObject *obj) -:myObj(obj) +ObjectConstruction::ObjectConstruction(CIntObject *obj) { GH.createdObj.push_front(obj); GH.captureChildren = true; } -SObjectConstruction::~SObjectConstruction() +ObjectConstruction::~ObjectConstruction() { - assert(GH.createdObj.size()); - assert(GH.createdObj.front() == myObj); + assert(!GH.createdObj.empty()); GH.createdObj.pop_front(); - GH.captureChildren = GH.createdObj.size(); -} - -SSetCaptureState::SSetCaptureState(bool allow, ui8 actions) -{ - previousCapture = GH.captureChildren; - GH.captureChildren = false; - prevActions = GH.defActionsDef; - GH.defActionsDef = actions; -} - -SSetCaptureState::~SSetCaptureState() -{ - GH.captureChildren = previousCapture; - GH.defActionsDef = prevActions; + GH.captureChildren = !GH.createdObj.empty(); } void CGuiHandler::init() @@ -139,8 +123,7 @@ void CGuiHandler::renderFrame() } CGuiHandler::CGuiHandler() - : defActionsDef(0) - , captureChildren(false) + : captureChildren(false) , curInt(nullptr) , fakeStatusBar(std::make_shared()) { @@ -193,22 +176,24 @@ const Point & CGuiHandler::getCursorPosition() const Point CGuiHandler::screenDimensions() const { - return Point(screen->w, screen->h); + return screenHandlerInstance->getLogicalResolution(); } void CGuiHandler::drawFPSCounter() { - int x = 7; - int y = screen->h-20; - int width3digitFPSIncludingPadding = 48; - int heightFPSTextIncludingPadding = 11; + int scaling = screenHandlerInstance->getScalingFactor(); + int x = 7 * scaling; + int y = screen->h-20 * scaling; + int width3digitFPSIncludingPadding = 48 * scaling; + int heightFPSTextIncludingPadding = 11 * scaling; SDL_Rect overlay = { x, y, width3digitFPSIncludingPadding, heightFPSTextIncludingPadding}; uint32_t black = SDL_MapRGB(screen->format, 10, 10, 10); SDL_FillRect(screen, &overlay, black); std::string fps = std::to_string(framerate().getFramerate())+" FPS"; - graphics->fonts[FONT_SMALL]->renderTextLeft(screen, fps, Colors::WHITE, Point(8, screen->h-22)); + const auto & font = GH.renderHandler().loadFont(FONT_SMALL); + font->renderTextLeft(screen, fps, Colors::WHITE, Point(8 * scaling, screen->h-22 * scaling)); } bool CGuiHandler::amIGuiThread() @@ -265,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/CGuiHandler.h b/client/gui/CGuiHandler.h index c010336e4..a91361d04 100644 --- a/client/gui/CGuiHandler.h +++ b/client/gui/CGuiHandler.h @@ -89,7 +89,6 @@ public: IUpdateable *curInt; - ui8 defActionsDef; //default auto actions bool captureChildren; //all newly created objects will get their parents from stack and will be added to parents children list std::list createdObj; //stack of objs being created @@ -113,25 +112,3 @@ public: }; extern CGuiHandler GH; //global gui handler - -struct SObjectConstruction -{ - CIntObject *myObj; - SObjectConstruction(CIntObject *obj); - ~SObjectConstruction(); -}; - -struct SSetCaptureState -{ - bool previousCapture; - ui8 prevActions; - SSetCaptureState(bool allow, ui8 actions); - ~SSetCaptureState(); -}; - -#define OBJ_CONSTRUCTION SObjectConstruction obj__i(this) -#define OBJ_CONSTRUCTION_TARGETED(obj) SObjectConstruction obj__i(obj) -#define OBJECT_CONSTRUCTION_CAPTURING(actions) defActions = actions; SSetCaptureState obj__i1(true, actions); SObjectConstruction obj__i(this) -#define OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(actions) SSetCaptureState obj__i1(true, actions); SObjectConstruction obj__i(this) - -#define OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE defActions = 255 - DISPOSE; SSetCaptureState obj__i1(true, 255 - DISPOSE); SObjectConstruction obj__i(this) diff --git a/client/gui/CIntObject.cpp b/client/gui/CIntObject.cpp index 3f136151e..3535985f1 100644 --- a/client/gui/CIntObject.cpp +++ b/client/gui/CIntObject.cpp @@ -24,8 +24,7 @@ CIntObject::CIntObject(int used_, Point pos_): redrawParent(false), inputEnabled(true), used(used_), - recActions(GH.defActionsDef), - defActions(GH.defActionsDef), + recActions(ALL_ACTIONS), pos(pos_, Point()) { if(GH.captureChildren) @@ -38,12 +37,7 @@ CIntObject::~CIntObject() deactivate(); while(!children.empty()) - { - if((defActions & DISPOSE) && (children.front()->recActions & DISPOSE)) - delete children.front(); - else - removeChild(children.front()); - } + removeChild(children.front()); if(parent_m) parent_m->removeChild(this); @@ -51,20 +45,16 @@ CIntObject::~CIntObject() void CIntObject::show(Canvas & to) { - if(defActions & UPDATE) - for(auto & elem : children) - if(elem->recActions & UPDATE) - elem->show(to); + for(auto & elem : children) + if(elem->recActions & UPDATE) + elem->show(to); } void CIntObject::showAll(Canvas & to) { - if(defActions & SHOWALL) - { - for(auto & elem : children) - if(elem->recActions & SHOWALL) - elem->showAll(to); - } + for(auto & elem : children) + if(elem->recActions & SHOWALL) + elem->showAll(to); } void CIntObject::activate() @@ -79,10 +69,9 @@ void CIntObject::activate() assert(isActive()); - if(defActions & ACTIVATE) - for(auto & elem : children) - if(elem->recActions & ACTIVATE) - elem->activate(); + for(auto & elem : children) + if(elem->recActions & ACTIVATE) + elem->activate(); } void CIntObject::deactivate() @@ -94,10 +83,9 @@ void CIntObject::deactivate() assert(!isActive()); - if(defActions & DEACTIVATE) - for(auto & elem : children) - if(elem->recActions & DEACTIVATE) - elem->deactivate(); + for(auto & elem : children) + if(elem->recActions & DEACTIVATE) + elem->deactivate(); } void CIntObject::addUsedEvents(ui16 newActions) @@ -119,7 +107,7 @@ void CIntObject::disable() if(isActive()) deactivate(); - recActions = DISPOSE; + recActions = NO_ACTIONS; } void CIntObject::enable() @@ -130,7 +118,7 @@ void CIntObject::enable() redraw(); } - recActions = 255; + recActions = ALL_ACTIONS; } void CIntObject::setEnabled(bool on) @@ -168,12 +156,17 @@ void CIntObject::setRedrawParent(bool on) } void CIntObject::fitToScreen(int borderWidth, bool propagate) +{ + fitToRect(Rect(Point(0, 0), GH.screenDimensions()), borderWidth, propagate); +} + +void CIntObject::fitToRect(Rect rect, int borderWidth, bool propagate) { Point newPos = pos.topLeft(); - vstd::amax(newPos.x, borderWidth); - vstd::amax(newPos.y, borderWidth); - vstd::amin(newPos.x, GH.screenDimensions().x - borderWidth - pos.w); - vstd::amin(newPos.y, GH.screenDimensions().y - borderWidth - pos.h); + vstd::amax(newPos.x, rect.x + borderWidth); + vstd::amax(newPos.y, rect.y + borderWidth); + vstd::amin(newPos.x, rect.x + rect.w - borderWidth - pos.w); + vstd::amin(newPos.y, rect.y + rect.h - borderWidth - pos.h); if (newPos != pos.topLeft()) moveTo(newPos, propagate); } @@ -245,12 +238,12 @@ void CIntObject::redraw() } else { - Canvas buffer = Canvas::createFromSurface(screenBuf); + Canvas buffer = Canvas::createFromSurface(screenBuf, CanvasScalingPolicy::AUTO); showAll(buffer); if(screenBuf != screen) { - Canvas screenBuffer = Canvas::createFromSurface(screen); + Canvas screenBuffer = Canvas::createFromSurface(screen, CanvasScalingPolicy::AUTO); showAll(screenBuffer); } @@ -258,6 +251,15 @@ void CIntObject::redraw() } } +void CIntObject::moveChildForeground(const CIntObject * childToMove) +{ + for(auto child = children.begin(); child != children.end(); child++) + if(*child == childToMove && child != children.end()) + { + std::rotate(child, child + 1, children.end()); + } +} + bool CIntObject::receiveEvent(const Point & position, int eventType) const { return pos.isInside(position); @@ -341,6 +343,6 @@ WindowBase::WindowBase(int used_, Point pos_) void WindowBase::close() { if(!GH.windows().isTopWindow(this)) - logGlobal->error("Only top interface must be closed"); + throw std::runtime_error("Only top interface can be closed"); GH.windows().popWindows(1); } diff --git a/client/gui/CIntObject.h b/client/gui/CIntObject.h index bde3c1f81..700b7d990 100644 --- a/client/gui/CIntObject.h +++ b/client/gui/CIntObject.h @@ -73,13 +73,12 @@ public: void addUsedEvents(ui16 newActions); void removeUsedEvents(ui16 newActions); - enum {ACTIVATE=1, DEACTIVATE=2, UPDATE=4, SHOWALL=8, DISPOSE=16, SHARE_POS=32}; - ui8 defActions; //which calls will be tried to be redirected to children + enum {NO_ACTIONS = 0, ACTIVATE=1, DEACTIVATE=2, UPDATE=4, SHOWALL=8, SHARE_POS=16, ALL_ACTIONS=31}; ui8 recActions; //which calls we allow to receive from parent /// deactivates if needed, blocks all automatic activity, allows only disposal void disable(); - /// activates if needed, all activity enabled (Warning: may not be symetric with disable if recActions was limited!) + /// activates if needed, all activity enabled (Warning: may not be symmetric with disable if recActions was limited!) void enable(); /// deactivates or activates UI element based on flag void setEnabled(bool on); @@ -102,6 +101,8 @@ public: void showAll(Canvas & to) override; //request complete redraw of this object void redraw() override; + // Move child object to foreground + void moveChildForeground(const CIntObject * childToMove); /// returns true if this element is a popup window /// called only for windows @@ -121,6 +122,7 @@ public: const Rect & center(const Point &p, bool propagate = true); //moves object so that point p will be in its center const Rect & center(bool propagate = true); //centers when pos.w and pos.h are set, returns new position void fitToScreen(int borderWidth, bool propagate = true); //moves window to fit into screen + void fitToRect(Rect rect, int borderWidth, bool propagate = true); //moves window to fit into rect void moveBy(const Point &p, bool propagate = true); void moveTo(const Point &p, bool propagate = true);//move this to new position, coordinates are absolute (0,0 is topleft screen corner) @@ -146,7 +148,6 @@ class WindowBase : public CIntObject { public: WindowBase(int used_ = 0, Point pos_ = Point()); -protected: virtual void close(); }; @@ -165,6 +166,15 @@ public: virtual void updateGarrisons() = 0; }; +class IMarketHolder +{ +public: + virtual void updateResources() {}; + virtual void updateExperience() {}; + virtual void updateSecondarySkills() {}; + virtual void updateArtifacts() {}; +}; + class ITownHolder { public: @@ -201,3 +211,16 @@ class EmptyStatusBar : public IStatusBar virtual void setEnteringMode(bool on){}; virtual void setEnteredText(const std::string & text){}; }; + +class ObjectConstruction : boost::noncopyable +{ +public: + ObjectConstruction(CIntObject *obj); + ~ObjectConstruction(); +}; + +/// If used, all UI widgets created inside this scope will be added to children of 'this' +#define OBJECT_CONSTRUCTION ObjectConstruction obj__i(this) + +/// If used, all UI widgets created inside this scope will be added to children of provided object +#define OBJECT_CONSTRUCTION_TARGETED(obj) ObjectConstruction obj__i(obj) diff --git a/client/gui/CursorHandler.cpp b/client/gui/CursorHandler.cpp index 2052ea2c9..fbc5922f8 100644 --- a/client/gui/CursorHandler.cpp +++ b/client/gui/CursorHandler.cpp @@ -17,6 +17,7 @@ #include "../renderSDL/CursorHardware.h" #include "../render/CAnimation.h" #include "../render/IImage.h" +#include "../render/IScreenHandler.h" #include "../render/IRenderHandler.h" #include "../../lib/CConfigHandler.h" @@ -47,15 +48,12 @@ CursorHandler::CursorHandler() cursors = { - GH.renderHandler().loadAnimation(AnimationPath::builtin("CRADVNTR")), - GH.renderHandler().loadAnimation(AnimationPath::builtin("CRCOMBAT")), - GH.renderHandler().loadAnimation(AnimationPath::builtin("CRDEFLT")), - GH.renderHandler().loadAnimation(AnimationPath::builtin("CRSPELL")) + GH.renderHandler().loadAnimation(AnimationPath::builtin("CRADVNTR"), EImageBlitMode::COLORKEY), + GH.renderHandler().loadAnimation(AnimationPath::builtin("CRCOMBAT"), EImageBlitMode::COLORKEY), + GH.renderHandler().loadAnimation(AnimationPath::builtin("CRDEFLT"), EImageBlitMode::COLORKEY), + GH.renderHandler().loadAnimation(AnimationPath::builtin("CRSPELL"), EImageBlitMode::COLORKEY) }; - for (auto & cursor : cursors) - cursor->preload(); - set(Cursor::Map::POINTER); showType = dynamic_cast(cursor.get()) ? Cursor::ShowType::SOFTWARE : Cursor::ShowType::HARDWARE; } @@ -104,8 +102,7 @@ void CursorHandler::dragAndDropCursor(std::shared_ptr image) void CursorHandler::dragAndDropCursor (const AnimationPath & path, size_t index) { - auto anim = GH.renderHandler().loadAnimation(path); - anim->load(index); + auto anim = GH.renderHandler().loadAnimation(path, EImageBlitMode::COLORKEY); dragAndDropCursor(anim->getImage(index)); } @@ -179,7 +176,7 @@ Point CursorHandler::getPivotOffsetMap(size_t index) assert(offsets.size() == size_t(Cursor::Map::COUNT)); //Invalid number of pivot offsets for cursor assert(index < offsets.size()); - return offsets[index]; + return offsets[index] * GH.screenHandler().getScalingFactor(); } Point CursorHandler::getPivotOffsetCombat(size_t index) @@ -209,12 +206,12 @@ Point CursorHandler::getPivotOffsetCombat(size_t index) assert(offsets.size() == size_t(Cursor::Combat::COUNT)); //Invalid number of pivot offsets for cursor assert(index < offsets.size()); - return offsets[index]; + return offsets[index] * GH.screenHandler().getScalingFactor(); } Point CursorHandler::getPivotOffsetSpellcast() { - return { 18, 28}; + return Point(18, 28) * GH.screenHandler().getScalingFactor(); } Point CursorHandler::getPivotOffset() @@ -315,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/EventDispatcher.cpp b/client/gui/EventDispatcher.cpp index df4a7edd4..94bbfcbe0 100644 --- a/client/gui/EventDispatcher.cpp +++ b/client/gui/EventDispatcher.cpp @@ -19,6 +19,7 @@ #include "../../lib/CConfigHandler.h" #include "../../lib/Rect.h" +#include "../eventsSDL/InputHandler.h" template void EventDispatcher::processLists(ui16 activityFlag, const Functor & cb) @@ -34,12 +35,14 @@ void EventDispatcher::processLists(ui16 activityFlag, const Functor & cb) processList(AEventsReceiver::HOVER, hoverable); processList(AEventsReceiver::MOVE, motioninterested); processList(AEventsReceiver::DRAG, draginterested); + processList(AEventsReceiver::DRAG_POPUP, dragPopupInterested); processList(AEventsReceiver::KEYBOARD, keyinterested); processList(AEventsReceiver::TIME, timeinterested); processList(AEventsReceiver::WHEEL, wheelInterested); processList(AEventsReceiver::DOUBLECLICK, doubleClickInterested); processList(AEventsReceiver::TEXTINPUT, textInterested); processList(AEventsReceiver::GESTURE, panningInterested); + processList(AEventsReceiver::INPUT_MODE_CHANGE, inputModeChangeInterested); } void EventDispatcher::activateElement(AEventsReceiver * elem, ui16 activityFlag) @@ -251,6 +254,10 @@ void EventDispatcher::handleLeftButtonClick(const Point & position, int toleranc i->mouseClickedState = isPressed; i->clickCancel(position); } + else if(isPressed) + { + i->notFocusedClick(); + } } } } @@ -304,7 +311,7 @@ void EventDispatcher::dispatchTextInput(const std::string & text) { for(auto it : textInterested) { - it->textInputed(text); + it->textInputted(text); } } @@ -316,6 +323,14 @@ void EventDispatcher::dispatchTextEditing(const std::string & text) } } +void EventDispatcher::dispatchInputModeChanged(const InputMode & modi) +{ + for(auto it : inputModeChangeInterested) + { + it->inputModeChanged(modi); + } +} + void EventDispatcher::dispatchGesturePanningStarted(const Point & initialPosition) { auto copied = panningInterested; @@ -423,3 +438,10 @@ void EventDispatcher::dispatchMouseDragged(const Point & currentPosition, const elem->mouseDragged(currentPosition, lastUpdateDistance); } } + +void EventDispatcher::dispatchMouseDraggedPopup(const Point & currentPosition, const Point & lastUpdateDistance) +{ + EventReceiversList diCopy = dragPopupInterested; + for(auto & elem : diCopy) + elem->mouseDraggedPopup(currentPosition, lastUpdateDistance); +} diff --git a/client/gui/EventDispatcher.h b/client/gui/EventDispatcher.h index 0ab25ed5c..4c212779e 100644 --- a/client/gui/EventDispatcher.h +++ b/client/gui/EventDispatcher.h @@ -16,6 +16,7 @@ VCMI_LIB_NAMESPACE_END class AEventsReceiver; enum class MouseButton; enum class EShortcut; +enum class InputMode; /// Class that receives events from event producers and dispatches it to UI elements that are interested in this event class EventDispatcher @@ -29,11 +30,13 @@ class EventDispatcher EventReceiversList keyinterested; EventReceiversList motioninterested; EventReceiversList draginterested; + EventReceiversList dragPopupInterested; EventReceiversList timeinterested; EventReceiversList wheelInterested; EventReceiversList doubleClickInterested; EventReceiversList textInterested; EventReceiversList panningInterested; + EventReceiversList inputModeChangeInterested; void handleLeftButtonClick(const Point & position, int tolerance, bool isPressed); void handleDoubleButtonClick(const Point & position, int tolerance); @@ -64,6 +67,7 @@ public: void dispatchMouseMoved(const Point & distance, const Point & position); void dispatchMouseDragged(const Point & currentPosition, const Point & lastUpdateDistance); + void dispatchMouseDraggedPopup(const Point & currentPosition, const Point & lastUpdateDistance); void dispatchShowPopup(const Point & position, int tolerance); void dispatchClosePopup(const Point & position); @@ -76,4 +80,6 @@ public: /// Text input events void dispatchTextInput(const std::string & text); void dispatchTextEditing(const std::string & text); + + void dispatchInputModeChanged(const InputMode & modi); }; diff --git a/client/gui/EventsReceiver.h b/client/gui/EventsReceiver.h index 2d5fedede..5e09d800c 100644 --- a/client/gui/EventsReceiver.h +++ b/client/gui/EventsReceiver.h @@ -16,6 +16,7 @@ VCMI_LIB_NAMESPACE_END class EventDispatcher; enum class EShortcut; +enum class InputMode; /// Class that is capable of subscribing and receiving input events /// Acts as base class for all UI elements @@ -50,6 +51,7 @@ public: virtual void clickCancel(const Point & cursorPosition) {} virtual void showPopupWindow(const Point & cursorPosition) {} virtual void clickDouble(const Point & cursorPosition) {} + virtual void notFocusedClick() {}; /// Called when user pans screen by specified distance virtual void gesturePanning(const Point & initialPosition, const Point & currentPosition, const Point & lastUpdateDistance) {} @@ -60,6 +62,7 @@ public: virtual void wheelScrolled(int distance) {} virtual void mouseMoved(const Point & cursorPosition, const Point & lastUpdateDistance) {} virtual void mouseDragged(const Point & cursorPosition, const Point & lastUpdateDistance) {} + virtual void mouseDraggedPopup(const Point & cursorPosition, const Point & lastUpdateDistance) {} /// Called when UI element hover status changes virtual void hover(bool on) {} @@ -67,7 +70,7 @@ public: /// Called when UI element gesture status changes virtual void gesture(bool on, const Point & initialPosition, const Point & finalPosition) {} - virtual void textInputed(const std::string & enteredText) {} + virtual void textInputted(const std::string & enteredText) {} virtual void textEdited(const std::string & enteredText) {} virtual void keyPressed(EShortcut key) {} @@ -75,6 +78,8 @@ public: virtual void tick(uint32_t msPassed) {} + virtual void inputModeChanged(InputMode modi) {} + public: AEventsReceiver(); virtual ~AEventsReceiver() = default; @@ -94,6 +99,8 @@ public: TEXTINPUT = 512, GESTURE = 1024, DRAG = 2048, + INPUT_MODE_CHANGE = 4096, + DRAG_POPUP = 8192 }; /// Returns true if element is currently hovered by mouse diff --git a/client/gui/FramerateManager.h b/client/gui/FramerateManager.h index d653bd667..24080280f 100644 --- a/client/gui/FramerateManager.h +++ b/client/gui/FramerateManager.h @@ -22,7 +22,7 @@ class FramerateManager Duration targetFrameTime; TimePoint lastTimePoint; - /// index of last measured frome in lastFrameTimes array + /// index of last measured from in lastFrameTimes array ui32 lastFrameIndex; bool vsyncEnabled; diff --git a/client/gui/InterfaceObjectConfigurable.cpp b/client/gui/InterfaceObjectConfigurable.cpp index 11a2e4ade..8771898cc 100644 --- a/client/gui/InterfaceObjectConfigurable.cpp +++ b/client/gui/InterfaceObjectConfigurable.cpp @@ -19,6 +19,7 @@ #include "../gui/Shortcut.h" #include "../render/Graphics.h" #include "../render/IFont.h" +#include "../render/IRenderHandler.h" #include "../widgets/CComponent.h" #include "../widgets/ComboBox.h" #include "../widgets/Buttons.h" @@ -32,7 +33,7 @@ #include "../../lib/constants/StringConstants.h" #include "../../lib/json/JsonUtils.h" -#include "../../lib/CGeneralTextHandler.h" +#include "../../lib/texts/CGeneralTextHandler.h" #include "../../lib/filesystem/ResourcePath.h" InterfaceObjectConfigurable::InterfaceObjectConfigurable(const JsonNode & config, int used, Point offset): @@ -106,7 +107,7 @@ void InterfaceObjectConfigurable::loadCustomBuilders(const JsonNode & config) void InterfaceObjectConfigurable::build(const JsonNode &config) { - OBJ_CONSTRUCTION; + OBJECT_CONSTRUCTION; logGlobal->debug("Building configurable interface object"); auto * items = &config; @@ -284,7 +285,7 @@ EFonts InterfaceObjectConfigurable::readFont(const JsonNode & config) const return EFonts::FONT_CALLI; } logGlobal->debug("Unknown font attribute"); - return EFonts::FONT_TIMES; + return EFonts::FONT_MEDIUM; } std::pair InterfaceObjectConfigurable::readHintText(const JsonNode & config) const @@ -333,7 +334,7 @@ std::shared_ptr InterfaceObjectConfigurable::buildPicture(const JsonNo auto pic = std::make_shared(image, position.x, position.y); if ( config["playerColored"].Bool() && LOCPLINT) - pic->colorize(LOCPLINT->playerID); + pic->setPlayerColor(LOCPLINT->playerID); return pic; } @@ -357,8 +358,9 @@ std::shared_ptr InterfaceObjectConfigurable::buildMultiLineLabe auto color = readColor(config["color"]); auto text = readText(config["text"]); Rect rect = readRect(config["rect"]); + const auto & fontPtr = GH.renderHandler().loadFont(font); if(!config["adoptHeight"].isNull() && config["adoptHeight"].Bool()) - rect.h = graphics->fonts[font]->getLineHeight() * 2; + rect.h = fontPtr->getLineHeight() * 2; return std::make_shared(rect, font, alignment, color, text); } @@ -371,7 +373,7 @@ std::shared_ptr InterfaceObjectConfigurable::buildToggleGroup(cons group->pos += position; if(!config["items"].isNull()) { - OBJ_CONSTRUCTION_TARGETED(group.get()); + OBJECT_CONSTRUCTION_TARGETED(group.get()); int itemIdx = -1; for(const auto & item : config["items"].Vector()) { @@ -566,16 +568,19 @@ std::shared_ptr InterfaceObjectConfigurable::buildImage(const JsonNo std::shared_ptr InterfaceObjectConfigurable::buildTexture(const JsonNode & config) const { logGlobal->debug("Building widget CFilledTexture"); - auto image = ImagePath::fromJson(config["image"]); auto rect = readRect(config["rect"]); auto playerColor = readPlayerColor(config["color"]); if(playerColor.isValidPlayer()) { - auto result = std::make_shared(image, rect); - result->playerColored(playerColor); + auto result = std::make_shared(rect); + result->setPlayerColor(playerColor); return result; } - return std::make_shared(image, rect); + else + { + auto image = ImagePath::fromJson(config["image"]); + return std::make_shared(image, rect); + } } std::shared_ptr InterfaceObjectConfigurable::buildComboBox(const JsonNode & config) diff --git a/client/gui/InterfaceObjectConfigurable.h b/client/gui/InterfaceObjectConfigurable.h index c4fd3aad0..56b397ce9 100644 --- a/client/gui/InterfaceObjectConfigurable.h +++ b/client/gui/InterfaceObjectConfigurable.h @@ -41,7 +41,7 @@ public: InterfaceObjectConfigurable(const JsonNode & config, int used=0, Point offset=Point()); protected: - /// Set blocked status for all buttons assotiated with provided shortcut + /// Set blocked status for all buttons associated with provided shortcut void setShortcutBlocked(EShortcut shortcut, bool isBlocked); /// Registers provided callback to be called whenever specified shortcut is triggered diff --git a/client/gui/Shortcut.h b/client/gui/Shortcut.h index ebe89e4e2..bd8c57a26 100644 --- a/client/gui/Shortcut.h +++ b/client/gui/Shortcut.h @@ -74,6 +74,7 @@ enum class EShortcut HIGH_SCORES_CAMPAIGNS, HIGH_SCORES_SCENARIOS, HIGH_SCORES_RESET, + HIGH_SCORES_STATISTICS, // Game lobby / scenario selection LOBBY_BEGIN_STANDARD_GAME, // b @@ -91,6 +92,7 @@ enum class EShortcut LOBBY_FLIP_COIN, LOBBY_RANDOM_TOWN, LOBBY_RANDOM_TOWN_VS, + LOBBY_HANDICAP, MAPS_SIZE_S, MAPS_SIZE_M, @@ -120,6 +122,8 @@ enum class EShortcut // Adventure map screen ADVENTURE_GAME_OPTIONS, // 'o', Open CAdventureOptions window ADVENTURE_TOGGLE_GRID, // F6, Toggles map grid + ADVENTURE_TOGGLE_VISITABLE, // Toggles visitable tiles overlay + ADVENTURE_TOGGLE_BLOCKED, // Toggles blocked tiles overlay ADVENTURE_TOGGLE_SLEEP, // Toggles hero sleep status ADVENTURE_SET_HERO_ASLEEP, // Moves hero to sleep state ADVENTURE_SET_HERO_AWAKE, // Move hero to awake state @@ -157,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, @@ -186,6 +192,19 @@ enum class EShortcut BATTLE_TOGGLE_HEROES_STATS, BATTLE_OPEN_ACTIVE_UNIT, BATTLE_OPEN_HOVERED_UNIT, + BATTLE_TOGGLE_QUICKSPELL, + BATTLE_SPELL_SHORTCUT_0, + BATTLE_SPELL_SHORTCUT_1, + BATTLE_SPELL_SHORTCUT_2, + BATTLE_SPELL_SHORTCUT_3, + BATTLE_SPELL_SHORTCUT_4, + BATTLE_SPELL_SHORTCUT_5, + BATTLE_SPELL_SHORTCUT_6, + BATTLE_SPELL_SHORTCUT_7, + BATTLE_SPELL_SHORTCUT_8, + BATTLE_SPELL_SHORTCUT_9, + BATTLE_SPELL_SHORTCUT_10, + BATTLE_SPELL_SHORTCUT_11, MARKET_DEAL, MARKET_MAX_AMOUNT, @@ -277,5 +296,15 @@ enum class EShortcut SPELLBOOK_TAB_ADVENTURE, SPELLBOOK_TAB_COMBAT, + LIST_HERO_UP, + LIST_HERO_DOWN, + LIST_HERO_TOP, + LIST_HERO_BOTTOM, + LIST_HERO_DISMISS, + LIST_TOWN_UP, + LIST_TOWN_DOWN, + LIST_TOWN_TOP, + LIST_TOWN_BOTTOM, + AFTER_LAST }; diff --git a/client/gui/ShortcutHandler.cpp b/client/gui/ShortcutHandler.cpp index 3a5033882..19c3cc728 100644 --- a/client/gui/ShortcutHandler.cpp +++ b/client/gui/ShortcutHandler.cpp @@ -171,6 +171,8 @@ EShortcut ShortcutHandler::findShortcut(const std::string & identifier ) const {"gameActivateConsole", EShortcut::GAME_ACTIVATE_CONSOLE }, {"adventureGameOptions", EShortcut::ADVENTURE_GAME_OPTIONS }, {"adventureToggleGrid", EShortcut::ADVENTURE_TOGGLE_GRID }, + {"adventureToggleVisitable", EShortcut::ADVENTURE_TOGGLE_VISITABLE}, + {"adventureToggleBlocked", EShortcut::ADVENTURE_TOGGLE_BLOCKED }, {"adventureToggleSleep", EShortcut::ADVENTURE_TOGGLE_SLEEP }, {"adventureSetHeroAsleep", EShortcut::ADVENTURE_SET_HERO_ASLEEP }, {"adventureSetHeroAwake", EShortcut::ADVENTURE_SET_HERO_AWAKE }, @@ -207,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 }, @@ -222,6 +226,19 @@ EShortcut ShortcutHandler::findShortcut(const std::string & identifier ) const {"battleTacticsNext", EShortcut::BATTLE_TACTICS_NEXT }, {"battleTacticsEnd", EShortcut::BATTLE_TACTICS_END }, {"battleSelectAction", EShortcut::BATTLE_SELECT_ACTION }, + {"battleToggleQuickSpell", EShortcut::BATTLE_TOGGLE_QUICKSPELL }, + {"battleSpellShortcut0", EShortcut::BATTLE_SPELL_SHORTCUT_0 }, + {"battleSpellShortcut1", EShortcut::BATTLE_SPELL_SHORTCUT_1 }, + {"battleSpellShortcut2", EShortcut::BATTLE_SPELL_SHORTCUT_2 }, + {"battleSpellShortcut3", EShortcut::BATTLE_SPELL_SHORTCUT_3 }, + {"battleSpellShortcut4", EShortcut::BATTLE_SPELL_SHORTCUT_4 }, + {"battleSpellShortcut5", EShortcut::BATTLE_SPELL_SHORTCUT_5 }, + {"battleSpellShortcut6", EShortcut::BATTLE_SPELL_SHORTCUT_6 }, + {"battleSpellShortcut7", EShortcut::BATTLE_SPELL_SHORTCUT_7 }, + {"battleSpellShortcut8", EShortcut::BATTLE_SPELL_SHORTCUT_8 }, + {"battleSpellShortcut9", EShortcut::BATTLE_SPELL_SHORTCUT_9 }, + {"battleSpellShortcut10", EShortcut::BATTLE_SPELL_SHORTCUT_10 }, + {"battleSpellShortcut11", EShortcut::BATTLE_SPELL_SHORTCUT_11 }, {"spectateTrackHero", EShortcut::SPECTATE_TRACK_HERO }, {"spectateSkipBattle", EShortcut::SPECTATE_SKIP_BATTLE }, {"spectateSkipBattleResult", EShortcut::SPECTATE_SKIP_BATTLE_RESULT }, @@ -260,12 +277,22 @@ EShortcut ShortcutHandler::findShortcut(const std::string & identifier ) const {"heroCostumeLoad9", EShortcut::HERO_COSTUME_LOAD_9 }, {"spellbookTabAdventure", EShortcut::SPELLBOOK_TAB_ADVENTURE }, {"spellbookTabCombat", EShortcut::SPELLBOOK_TAB_COMBAT }, + {"listHeroUp", EShortcut::LIST_HERO_UP }, + {"listHeroDown", EShortcut::LIST_HERO_DOWN }, + {"listHeroTop", EShortcut::LIST_HERO_TOP }, + {"listHeroBottom", EShortcut::LIST_HERO_BOTTOM }, + {"listHeroDismiss", EShortcut::LIST_HERO_DISMISS }, + {"listTownUp", EShortcut::LIST_TOWN_UP }, + {"listTownDown", EShortcut::LIST_TOWN_DOWN }, + {"listTownTop", EShortcut::LIST_TOWN_TOP }, + {"listTownBottom", EShortcut::LIST_TOWN_BOTTOM }, {"mainMenuHotseat", EShortcut::MAIN_MENU_HOTSEAT }, {"mainMenuHostGame", EShortcut::MAIN_MENU_HOST_GAME }, {"mainMenuJoinGame", EShortcut::MAIN_MENU_JOIN_GAME }, {"highScoresCampaigns", EShortcut::HIGH_SCORES_CAMPAIGNS }, {"highScoresScenarios", EShortcut::HIGH_SCORES_SCENARIOS }, {"highScoresReset", EShortcut::HIGH_SCORES_RESET }, + {"highScoresStatistics", EShortcut::HIGH_SCORES_STATISTICS }, {"lobbyReplayVideo", EShortcut::LOBBY_REPLAY_VIDEO }, {"lobbyExtraOptions", EShortcut::LOBBY_EXTRA_OPTIONS }, {"lobbyTurnOptions", EShortcut::LOBBY_TURN_OPTIONS }, @@ -273,6 +300,7 @@ EShortcut ShortcutHandler::findShortcut(const std::string & identifier ) const {"lobbyFlipCoin", EShortcut::LOBBY_FLIP_COIN }, {"lobbyRandomTown", EShortcut::LOBBY_RANDOM_TOWN }, {"lobbyRandomTownVs", EShortcut::LOBBY_RANDOM_TOWN_VS }, + {"lobbyHandicap", EShortcut::LOBBY_HANDICAP }, {"mapsSizeS", EShortcut::MAPS_SIZE_S }, {"mapsSizeM", EShortcut::MAPS_SIZE_M }, {"mapsSizeL", EShortcut::MAPS_SIZE_L }, diff --git a/client/gui/WindowHandler.cpp b/client/gui/WindowHandler.cpp index ba8066089..6c5940c30 100644 --- a/client/gui/WindowHandler.cpp +++ b/client/gui/WindowHandler.cpp @@ -111,7 +111,7 @@ void WindowHandler::totalRedrawImpl() { logGlobal->debug("totalRedraw requested!"); - Canvas target = Canvas::createFromSurface(screen2); + Canvas target = Canvas::createFromSurface(screen2, CanvasScalingPolicy::AUTO); for(auto & elem : windowsStack) elem->showAll(target); @@ -134,7 +134,7 @@ void WindowHandler::simpleRedrawImpl() if(windowsStack.size() > 1) CSDL_Ext::blitAt(screen2, 0, 0, screen); //blit background - Canvas target = Canvas::createFromSurface(screen); + Canvas target = Canvas::createFromSurface(screen, CanvasScalingPolicy::AUTO); if(!windowsStack.empty()) windowsStack.back()->show(target); //blit active interface/window diff --git a/client/lobby/CBonusSelection.cpp b/client/lobby/CBonusSelection.cpp index e49e7e021..ced7705fd 100644 --- a/client/lobby/CBonusSelection.cpp +++ b/client/lobby/CBonusSelection.cpp @@ -18,17 +18,17 @@ #include "ExtraOptionsTab.h" #include "../CGameInfo.h" -#include "../CMusicHandler.h" -#include "../CVideoHandler.h" #include "../CPlayerInterface.h" #include "../CServerHandler.h" #include "../mainmenu/CMainMenu.h" #include "../mainmenu/CPrologEpilogVideo.h" +#include "../media/IMusicPlayer.h" #include "../widgets/CComponent.h" #include "../widgets/Buttons.h" #include "../widgets/MiscWidgets.h" #include "../widgets/ObjectLists.h" #include "../widgets/TextControls.h" +#include "../widgets/VideoWidget.h" #include "../windows/GUIClasses.h" #include "../windows/InfoWindows.h" #include "../render/IImage.h" @@ -38,25 +38,28 @@ #include "../gui/CGuiHandler.h" #include "../gui/Shortcut.h" #include "../gui/WindowHandler.h" +#include "../adventureMap/AdventureMapInterface.h" -#include "../../lib/filesystem/Filesystem.h" -#include "../../lib/CGeneralTextHandler.h" - -#include "../../lib/CBuildingHandler.h" - -#include "../../lib/CSkillHandler.h" -#include "../../lib/CTownHandler.h" -#include "../../lib/CHeroHandler.h" +#include "../../lib/CConfigHandler.h" #include "../../lib/CCreatureHandler.h" +#include "../../lib/CSkillHandler.h" #include "../../lib/StartInfo.h" +#include "../../lib/entities/building/CBuilding.h" +#include "../../lib/entities/building/CBuildingHandler.h" +#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" #include "../../lib/campaign/CampaignState.h" #include "../../lib/mapping/CMapService.h" #include "../../lib/mapping/CMapInfo.h" +#include "../../lib/mapping/CMapHeader.h" #include "../../lib/mapObjects/CGHeroInstance.h" - std::shared_ptr CBonusSelection::getCampaign() { return CSH->si->campState; @@ -65,7 +68,7 @@ std::shared_ptr CBonusSelection::getCampaign() CBonusSelection::CBonusSelection() : CWindowObject(BORDERED) { - OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + OBJECT_CONSTRUCTION; setBackground(getCampaign()->getRegions().getBackgroundName()); @@ -85,16 +88,18 @@ CBonusSelection::CBonusSelection() buttonVideo = std::make_shared(Point(705, 214), AnimationPath::builtin("CBVIDEB.DEF"), CButton::tooltip(), playVideo, EShortcut::LOBBY_REPLAY_VIDEO); buttonBack = std::make_shared(Point(624, 536), AnimationPath::builtin("CBCANCB.DEF"), CButton::tooltip(), std::bind(&CBonusSelection::goBack, this), EShortcut::GLOBAL_CANCEL); - campaignName = std::make_shared(481, 28, FONT_BIG, ETextAlignment::TOPLEFT, Colors::YELLOW, CSH->si->getCampaignName()); + campaignName = std::make_shared(481, 28, FONT_BIG, ETextAlignment::TOPLEFT, Colors::YELLOW, CSH->si->getCampaignName(), 250); iconsMapSizes = std::make_shared(AnimationPath::builtin("SCNRMPSZ"), 4, 0, 735, 26); labelCampaignDescription = std::make_shared(481, 63, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::YELLOW, CGI->generaltexth->allTexts[38]); campaignDescription = std::make_shared(getCampaign()->getDescriptionTranslated(), Rect(480, 86, 286, 117), 1); - mapName = std::make_shared(481, 219, FONT_BIG, ETextAlignment::TOPLEFT, Colors::YELLOW, CSH->mi->getNameTranslated()); + bool videoButtonActive = CSH->getState() == EClientState::GAMEPLAY; + 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)); @@ -123,9 +128,11 @@ CBonusSelection::CBonusSelection() for(auto scenarioID : getCampaign()->allScenarios()) { if(getCampaign()->isAvailable(scenarioID)) - regions.push_back(std::make_shared(scenarioID, true, true, getCampaign()->getRegions())); + regions.push_back(std::make_shared(scenarioID, true, true, false, getCampaign()->getRegions())); else if(getCampaign()->isConquered(scenarioID)) //display as striped - regions.push_back(std::make_shared(scenarioID, false, false, getCampaign()->getRegions())); + regions.push_back(std::make_shared(scenarioID, false, false, false, getCampaign()->getRegions())); + else + regions.push_back(std::make_shared(scenarioID, false, false, true, getCampaign()->getRegions())); } if (!getCampaign()->getMusic().empty()) @@ -144,7 +151,7 @@ CBonusSelection::CBonusSelection() void CBonusSelection::createBonusesIcons() { - OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + OBJECT_CONSTRUCTION; const CampaignScenario & scenario = getCampaign()->scenario(CSH->campaignMap); const std::vector & bonDescs = scenario.travelOptions.bonusesToChoose; groupBonuses = std::make_shared(std::bind(&IServerAPI::setCampaignBonus, CSH, _1)); @@ -311,7 +318,10 @@ void CBonusSelection::createBonusesIcons() break; } - std::shared_ptr bonusButton = std::make_shared(Point(475 + i * 68, 455), AnimationPath::builtin("campaignBonusSelection"), CButton::tooltip(desc.toString(), desc.toString())); + std::shared_ptr bonusButton = std::make_shared(Point(475 + i * 68, 455), AnimationPath::builtin("campaignBonusSelection"), CButton::tooltip(desc.toString(), desc.toString()), nullptr, EShortcut::NONE, false, [this](){ + if(buttonStart->isActive() && !buttonStart->isBlocked()) + CBonusSelection::startMap(); + }); if(picNumber != -1) bonusButton->setOverlay(std::make_shared(AnimationPath::builtin(picName), picNumber)); @@ -380,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 @@ -470,10 +483,10 @@ void CBonusSelection::decreaseDifficulty() CSH->setDifficulty(CSH->si->difficulty - 1); } -CBonusSelection::CRegion::CRegion(CampaignScenarioID id, bool accessible, bool selectable, const CampaignRegions & campDsc) - : CIntObject(LCLICK | SHOW_POPUP), idOfMapAndRegion(id), accessible(accessible), selectable(selectable) +CBonusSelection::CRegion::CRegion(CampaignScenarioID id, bool accessible, bool selectable, bool labelOnly, const CampaignRegions & campDsc) + : CIntObject(LCLICK | SHOW_POPUP), idOfMapAndRegion(id), accessible(accessible), selectable(selectable), labelOnly(labelOnly) { - OBJ_CONSTRUCTION; + OBJECT_CONSTRUCTION; pos += campDsc.getPosition(id); @@ -487,10 +500,20 @@ CBonusSelection::CRegion::CRegion(CampaignScenarioID id, bool accessible, bool s graphicsStriped->disable(); pos.w = graphicsNotSelected->pos.w; pos.h = graphicsNotSelected->pos.h; + + auto labelPos = campDsc.getLabelPosition(id); + if(labelPos) + { + auto mapHeader = CSH->si->campState->getMapHeader(idOfMapAndRegion); + label = std::make_shared((*labelPos).x, (*labelPos).y, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, mapHeader->name.toString()); + } } void CBonusSelection::CRegion::updateState() { + if(labelOnly) + return; + if(!accessible) { graphicsNotSelected->disable(); @@ -513,7 +536,7 @@ void CBonusSelection::CRegion::updateState() void CBonusSelection::CRegion::clickReleased(const Point & cursorPosition) { - if(selectable && !graphicsNotSelected->getSurface()->isTransparent(cursorPosition - pos.topLeft())) + if(!labelOnly && selectable && !graphicsNotSelected->getSurface()->isTransparent(cursorPosition - pos.topLeft())) { CSH->setCampaignMap(idOfMapAndRegion); } @@ -523,7 +546,7 @@ void CBonusSelection::CRegion::showPopupWindow(const Point & cursorPosition) { // FIXME: For some reason "down" is only ever contain indeterminate_value auto & text = CSH->si->campState->scenario(idOfMapAndRegion).regionText; - if(!graphicsNotSelected->getSurface()->isTransparent(cursorPosition - pos.topLeft()) && !text.empty()) + if(!labelOnly && !graphicsNotSelected->getSurface()->isTransparent(cursorPosition - pos.topLeft()) && !text.empty()) { CRClickPopup::createAndPush(text.toString()); } diff --git a/client/lobby/CBonusSelection.h b/client/lobby/CBonusSelection.h index 989e459b3..7b26bcc88 100644 --- a/client/lobby/CBonusSelection.h +++ b/client/lobby/CBonusSelection.h @@ -12,6 +12,7 @@ #include "../windows/CWindowObject.h" #include "../lib/campaign/CampaignConstants.h" +#include "../lib/filesystem/ResourcePath.h" VCMI_LIB_NAMESPACE_BEGIN @@ -28,6 +29,9 @@ class CLabel; class CFlagBox; class ISelectionScreenInfo; class ExtraOptionsTab; +class VideoWidgetOnce; +class CBonusSelection; + /// Campaign screen where you can choose one out of three starting bonuses class CBonusSelection : public CWindowObject @@ -45,8 +49,10 @@ public: CampaignScenarioID idOfMapAndRegion; bool accessible; // false if region should be striped bool selectable; // true if region should be selectable + bool labelOnly; + std::shared_ptr label; public: - CRegion(CampaignScenarioID id, bool accessible, bool selectable, const CampaignRegions & campDsc); + CRegion(CampaignScenarioID id, bool accessible, bool selectable, bool labelOnly, const CampaignRegions & campDsc); void updateState(); void clickReleased(const Point & cursorPosition) override; void showPopupWindow(const Point & cursorPosition) override; diff --git a/client/lobby/CCampaignInfoScreen.cpp b/client/lobby/CCampaignInfoScreen.cpp index 56183b31c..1d0bcad6e 100644 --- a/client/lobby/CCampaignInfoScreen.cpp +++ b/client/lobby/CCampaignInfoScreen.cpp @@ -12,7 +12,7 @@ #include "CCampaignInfoScreen.h" #include "../../CCallback.h" -#include "../../lib/CGeneralTextHandler.h" +#include "../../lib/texts/CGeneralTextHandler.h" #include "../../lib/StartInfo.h" #include "../../lib/mapping/CMapInfo.h" #include "../../lib/mapping/CMapHeader.h" @@ -22,7 +22,7 @@ CCampaignInfoScreen::CCampaignInfoScreen() { - OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + OBJECT_CONSTRUCTION; localSi = new StartInfo(*LOCPLINT->cb->getStartInfo()); localMi = new CMapInfo(); localMi->mapHeader = std::unique_ptr(new CMapHeader(*LOCPLINT->cb->getMapHeader())); diff --git a/client/lobby/CLobbyScreen.cpp b/client/lobby/CLobbyScreen.cpp index fac1bd173..ead6f3ef3 100644 --- a/client/lobby/CLobbyScreen.cpp +++ b/client/lobby/CLobbyScreen.cpp @@ -28,7 +28,7 @@ #include "../../CCallback.h" #include "../../lib/CConfigHandler.h" -#include "../../lib/CGeneralTextHandler.h" +#include "../../lib/texts/CGeneralTextHandler.h" #include "../../lib/campaign/CampaignHandler.h" #include "../../lib/mapping/CMapInfo.h" #include "../../lib/networkPacks/PacksForLobby.h" @@ -38,7 +38,7 @@ CLobbyScreen::CLobbyScreen(ESelectionScreen screenType) : CSelectionBase(screenType), bonusSel(nullptr) { - OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + OBJECT_CONSTRUCTION; tabSel = std::make_shared(screenType); curTab = tabSel; @@ -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/CSavingScreen.cpp b/client/lobby/CSavingScreen.cpp index 1ec536aa7..c8629fe20 100644 --- a/client/lobby/CSavingScreen.cpp +++ b/client/lobby/CSavingScreen.cpp @@ -21,7 +21,7 @@ #include "../../CCallback.h" #include "../../lib/CConfigHandler.h" -#include "../../lib/CGeneralTextHandler.h" +#include "../../lib/texts/CGeneralTextHandler.h" #include "../../lib/StartInfo.h" #include "../../lib/filesystem/Filesystem.h" #include "../../lib/mapping/CMapInfo.h" @@ -30,7 +30,7 @@ CSavingScreen::CSavingScreen() : CSelectionBase(ESelectionScreen::saveGame) { - OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + OBJECT_CONSTRUCTION; center(pos); localMi = std::make_shared(); localMi->mapHeader = std::unique_ptr(new CMapHeader(*LOCPLINT->cb->getMapHeader())); diff --git a/client/lobby/CScenarioInfoScreen.cpp b/client/lobby/CScenarioInfoScreen.cpp index 51a6f242a..8e8dfcaad 100644 --- a/client/lobby/CScenarioInfoScreen.cpp +++ b/client/lobby/CScenarioInfoScreen.cpp @@ -20,14 +20,14 @@ #include "../../CCallback.h" -#include "../../lib/CGeneralTextHandler.h" +#include "../../lib/texts/CGeneralTextHandler.h" #include "../../lib/StartInfo.h" #include "../../lib/mapping/CMapInfo.h" #include "../../lib/mapping/CMapHeader.h" CScenarioInfoScreen::CScenarioInfoScreen() { - OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + OBJECT_CONSTRUCTION; pos.w = 800; pos.h = 600; pos = center(); diff --git a/client/lobby/CSelectionBase.cpp b/client/lobby/CSelectionBase.cpp index 765c7fb54..7b49da41c 100644 --- a/client/lobby/CSelectionBase.cpp +++ b/client/lobby/CSelectionBase.cpp @@ -20,14 +20,13 @@ #include "../CGameInfo.h" #include "../CPlayerInterface.h" -#include "../CMusicHandler.h" -#include "../CVideoHandler.h" #include "../CServerHandler.h" #include "../gui/CGuiHandler.h" #include "../gui/Shortcut.h" #include "../gui/WindowHandler.h" #include "../globalLobby/GlobalLobbyClient.h" #include "../mainmenu/CMainMenu.h" +#include "../media/ISoundPlayer.h" #include "../widgets/Buttons.h" #include "../widgets/CComponent.h" #include "../widgets/CTextInput.h" @@ -44,16 +43,16 @@ #include "../render/IFont.h" #include "../render/IRenderHandler.h" -#include "../../lib/CGeneralTextHandler.h" -#include "../../lib/CHeroHandler.h" -#include "../../lib/CTownHandler.h" #include "../../lib/CRandomGenerator.h" #include "../../lib/CThreadHelper.h" -#include "../../lib/MetaString.h" #include "../../lib/filesystem/Filesystem.h" #include "../../lib/mapping/CMapHeader.h" #include "../../lib/mapping/CMapInfo.h" #include "../../lib/networkPacks/PacksForLobby.h" +#include "../../lib/texts/CGeneralTextHandler.h" +#include "../../lib/entities/faction/CFaction.h" +#include "../../lib/entities/faction/CTown.h" +#include "../../lib/entities/faction/CTownHandler.h" ISelectionScreenInfo::ISelectionScreenInfo(ESelectionScreen ScreenType) : screenType(ScreenType) @@ -81,7 +80,7 @@ PlayerInfo ISelectionScreenInfo::getPlayerInfo(PlayerColor color) CSelectionBase::CSelectionBase(ESelectionScreen type) : CWindowObject(BORDERED | SHADOW_DISABLED), ISelectionScreenInfo(type) { - OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + OBJECT_CONSTRUCTION; pos.w = 762; pos.h = 584; if(screenType == ESelectionScreen::campaignList) @@ -102,14 +101,12 @@ void CSelectionBase::toggleTab(std::shared_ptr tab) { if(curTab && curTab->isActive()) { - curTab->deactivate(); - curTab->recActions = 0; + curTab->disable(); } if(curTab != tab) { - tab->recActions = 255 - DISPOSE; - tab->activate(); + tab->enable(); curTab = tab; } else @@ -130,14 +127,14 @@ void CSelectionBase::toggleTab(std::shared_ptr tab) InfoCard::InfoCard() : showChat(true) { - OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + OBJECT_CONSTRUCTION; setRedrawParent(true); pos.x += 393; pos.y += 6; labelSaveDate = std::make_shared(310, 38, FONT_SMALL, ETextAlignment::BOTTOMRIGHT, Colors::WHITE); labelMapSize = std::make_shared(333, 56, FONT_TINY, ETextAlignment::CENTER, Colors::WHITE); - mapName = std::make_shared(26, 39, FONT_BIG, ETextAlignment::TOPLEFT, Colors::YELLOW, "", 285); + mapName = std::make_shared(26, 39, FONT_BIG, ETextAlignment::TOPLEFT, Colors::YELLOW, "", SEL->screenType == ESelectionScreen::campaignList ? 325 : 285); Rect descriptionRect(26, 149, 320, 115); mapDescription = std::make_shared("", descriptionRect, 1); playerListBg = std::make_shared(ImagePath::builtin("CHATPLUG.bmp"), 16, 276); @@ -190,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); @@ -252,21 +249,23 @@ void InfoCard::changeSelection() const std::array difficultyPercent = {"80%", "100%", "130%", "160%", "200%"}; labelDifficultyPercent->setText(difficultyPercent[SEL->getCurrentDifficulty()]); - OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + OBJECT_CONSTRUCTION; // FIXME: We recreate them each time because CLabelGroup don't use smart pointers labelGroupPlayers = std::make_shared(FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE); if(!showChat) labelGroupPlayers->disable(); + const auto & font = GH.renderHandler().loadFont(FONT_SMALL); + for(const auto & p : CSH->playerNames) { int slotsUsed = labelGroupPlayers->currentSize(); Point labelPosition; if(slotsUsed < 4) - labelPosition = Point(24, 285 + slotsUsed * graphics->fonts[FONT_SMALL]->getLineHeight()); // left column + labelPosition = Point(24, 285 + slotsUsed * font->getLineHeight()); // left column else - labelPosition = Point(193, 285 + (slotsUsed - 4) * graphics->fonts[FONT_SMALL]->getLineHeight()); // right column + labelPosition = Point(193, 285 + (slotsUsed - 4) * font->getLineHeight()); // right column labelGroupPlayers->add(labelPosition.x, labelPosition.y, p.second.name); } @@ -356,11 +355,12 @@ void InfoCard::setChat(bool activateChat) CChatBox::CChatBox(const Rect & rect) : CIntObject(KEYBOARD | TEXTINPUT) { - OBJ_CONSTRUCTION; + OBJECT_CONSTRUCTION; pos += rect.topLeft(); setRedrawParent(true); - const int height = static_cast(graphics->fonts[FONT_SMALL]->getLineHeight()); + const auto & font = GH.renderHandler().loadFont(FONT_SMALL); + const int height = font->getLineHeight(); Rect textInputArea(1, rect.h - height, rect.w - 1, height); Rect chatHistoryArea(3, 1, rect.w - 3, rect.h - height - 1); inputBackground = std::make_shared(textInputArea, ColorRGBA(0,0,0,192)); @@ -397,19 +397,19 @@ void CChatBox::addNewMessage(const std::string & text) PvPBox::PvPBox(const Rect & rect) { - OBJ_CONSTRUCTION; + OBJECT_CONSTRUCTION; pos += rect.topLeft(); setRedrawParent(true); - backgroundTexture = std::make_shared(ImagePath::builtin("DiBoxBck"), Rect(0, 0, rect.w, rect.h)); - backgroundTexture->playerColored(PlayerColor(1)); + backgroundTexture = std::make_shared(Rect(0, 0, rect.w, rect.h)); + backgroundTexture->setPlayerColor(PlayerColor(1)); backgroundBorder = std::make_shared(Rect(0, 0, rect.w, rect.h), ColorRGBA(0, 0, 0, 64), ColorRGBA(96, 96, 96, 255), 1); townSelector = std::make_shared(Point(5, 3)); auto getBannedTowns = [this](){ std::vector bannedTowns; - for(auto & town : townSelector->townsEnabled) + for(const auto & town : townSelector->townsEnabled) if(!town.second) bannedTowns.push_back(town.first); return bannedTowns; @@ -437,20 +437,27 @@ PvPBox::PvPBox(const Rect & rect) CSH->sendLobbyPack(lpa); }, EShortcut::LOBBY_RANDOM_TOWN_VS); buttonRandomTownVs->setTextOverlay(CGI->generaltexth->translate("vcmi.lobby.pvp.randomTownVs.hover"), EFonts::FONT_SMALL, Colors::WHITE); + + buttonHandicap = std::make_shared(Point(190, 81), AnimationPath::builtin("GSPBUT2.DEF"), CButton::tooltip("", CGI->generaltexth->translate("vcmi.lobby.handicap")), [](){ + if(!CSH->isHost()) + return; + GH.windows().createAndPushWindow(); + }, EShortcut::LOBBY_HANDICAP); + buttonHandicap->setTextOverlay(CGI->generaltexth->translate("vcmi.lobby.handicap"), EFonts::FONT_SMALL, Colors::WHITE); } TownSelector::TownSelector(const Point & loc) { - OBJ_CONSTRUCTION; + OBJECT_CONSTRUCTION; pos += loc; setRedrawParent(true); int count = 0; - for(auto const & factionID : VLC->townh->getDefaultAllowed()) + for(auto const & factionID : CGI->townh->getDefaultAllowed()) { townsEnabled[factionID] = true; count++; - }; + } auto divisionRoundUp = [](int x, int y){ return (x + (y - 1)) / y; }; @@ -466,7 +473,7 @@ TownSelector::TownSelector(const Point & loc) void TownSelector::updateListItems() { - OBJ_CONSTRUCTION; + OBJECT_CONSTRUCTION; int line = slider ? slider->getValue() : 0; int x_offset = slider ? 0 : 8; @@ -475,20 +482,17 @@ void TownSelector::updateListItems() int x = 0; int y = 0; - CGI->factions()->forEach([this, &x, &y, line, x_offset](const Faction *entity, bool &stop){ - if(!entity->hasTown()) - return; - + for (auto const & factionID : CGI->townh->getDefaultAllowed()) + { if(y >= line && (y - line) < 3) { - FactionID factionID = entity->getFaction(); - auto getImageIndex = [](FactionID factionID, bool enabled){ return (*CGI->townh)[factionID]->town->clientInfo.icons[true][!enabled] + 2; }; + auto getImageIndex = [](FactionID targetFactionID, bool enabled){ return targetFactionID.toFaction()->town->clientInfo.icons[true][!enabled] + 2; }; towns[factionID] = std::make_shared(AnimationPath::builtin("ITPA"), getImageIndex(factionID, townsEnabled[factionID]), 0, x_offset + 48 * x, 32 * (y - line)); townsArea[factionID] = std::make_shared(Rect(x_offset + 48 * x, 32 * (y - line), 48, 32), [this, getImageIndex, factionID](){ townsEnabled[factionID] = !townsEnabled[factionID]; towns[factionID]->setFrame(getImageIndex(factionID, townsEnabled[factionID])); redraw(); - }, [factionID](){ CRClickPopup::createAndPush((*CGI->townh)[factionID]->town->faction->getNameTranslated()); }); + }, [factionID](){ CRClickPopup::createAndPush(factionID.toFaction()->town->faction->getNameTranslated()); }); } if (x < 2) @@ -498,7 +502,7 @@ void TownSelector::updateListItems() x = 0; y++; } - }); + } } void TownSelector::sliderMove(int slidPos) @@ -515,25 +519,22 @@ CFlagBox::CFlagBox(const Rect & rect) pos += rect.topLeft(); pos.w = rect.w; pos.h = rect.h; - OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + OBJECT_CONSTRUCTION; labelAllies = std::make_shared(0, 0, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE, CGI->generaltexth->allTexts[390] + ":"); labelEnemies = std::make_shared(133, 0, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE, CGI->generaltexth->allTexts[391] + ":"); - - iconsTeamFlags = GH.renderHandler().loadAnimation(AnimationPath::builtin("ITGFLAGS.DEF")); - iconsTeamFlags->preload(); } void CFlagBox::recreate() { flagsAllies.clear(); flagsEnemies.clear(); - OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + OBJECT_CONSTRUCTION; const int alliesX = 5 + (int)labelAllies->getWidth(); const int enemiesX = 5 + 133 + (int)labelEnemies->getWidth(); for(auto i = CSH->si->playerInfos.cbegin(); i != CSH->si->playerInfos.cend(); i++) { - auto flag = std::make_shared(iconsTeamFlags, i->first.getNum(), 0); + auto flag = std::make_shared(AnimationPath::builtin("ITGFLAGS.DEF"), i->first.getNum(), 0); if(i->first == CSH->myFirstColor() || CSH->getPlayerTeamId(i->first) == CSH->getPlayerTeamId(CSH->myFirstColor())) { flag->moveTo(Point(pos.x + alliesX + (int)flagsAllies.size()*flag->pos.w, pos.y)); @@ -550,13 +551,13 @@ void CFlagBox::recreate() void CFlagBox::showPopupWindow(const Point & cursorPosition) { if(SEL->getMapInfo()) - GH.windows().createAndPushWindow(iconsTeamFlags); + GH.windows().createAndPushWindow(); } -CFlagBox::CFlagBoxTooltipBox::CFlagBoxTooltipBox(std::shared_ptr icons) +CFlagBox::CFlagBoxTooltipBox::CFlagBoxTooltipBox() : CWindowObject(BORDERED | RCLICK_POPUP | SHADOW_DISABLED, ImagePath::builtin("DIBOXBCK")) { - OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + OBJECT_CONSTRUCTION; labelTeamAlignment = std::make_shared(128, 30, FONT_MEDIUM, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->allTexts[657]); labelGroupTeams = std::make_shared(FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE); @@ -581,7 +582,7 @@ CFlagBox::CFlagBoxTooltipBox::CFlagBoxTooltipBox(std::shared_ptr ico int curx = 128 - 9 * team.size(); for(const auto & player : team) { - iconsFlags.push_back(std::make_shared(icons, player, 0, curx, 75 + 50 * curIdx)); + iconsFlags.push_back(std::make_shared(AnimationPath::builtin("ITGFLAGS.DEF"), player, 0, curx, 75 + 50 * curIdx)); curx += 18; } ++curIdx; diff --git a/client/lobby/CSelectionBase.h b/client/lobby/CSelectionBase.h index 3f545892f..52320e6d6 100644 --- a/client/lobby/CSelectionBase.h +++ b/client/lobby/CSelectionBase.h @@ -153,6 +153,7 @@ class PvPBox : public CIntObject std::shared_ptr buttonFlipCoin; std::shared_ptr buttonRandomTown; std::shared_ptr buttonRandomTownVs; + std::shared_ptr buttonHandicap; public: PvPBox(const Rect & rect); }; @@ -174,7 +175,6 @@ public: class CFlagBox : public CIntObject { - std::shared_ptr iconsTeamFlags; std::shared_ptr labelAllies; std::shared_ptr labelEnemies; std::vector> flagsAllies; @@ -192,7 +192,7 @@ public: std::shared_ptr labelGroupTeams; std::vector> iconsFlags; public: - CFlagBoxTooltipBox(std::shared_ptr icons); + CFlagBoxTooltipBox(); }; }; diff --git a/client/lobby/OptionsTab.cpp b/client/lobby/OptionsTab.cpp index 0ee53b900..4d5aa771c 100644 --- a/client/lobby/OptionsTab.cpp +++ b/client/lobby/OptionsTab.cpp @@ -14,12 +14,13 @@ #include "../CGameInfo.h" #include "../CServerHandler.h" -#include "../CMusicHandler.h" #include "../gui/CGuiHandler.h" #include "../gui/Shortcut.h" #include "../gui/WindowHandler.h" #include "../render/Graphics.h" #include "../render/IFont.h" +#include "../render/IRenderHandler.h" +#include "../media/ISoundPlayer.h" #include "../widgets/CComponent.h" #include "../widgets/ComboBox.h" #include "../widgets/CTextInput.h" @@ -29,17 +30,22 @@ #include "../widgets/ObjectLists.h" #include "../widgets/Slider.h" #include "../widgets/TextControls.h" +#include "../widgets/GraphicalPrimitiveCanvas.h" #include "../windows/GUIClasses.h" #include "../windows/InfoWindows.h" #include "../windows/CHeroOverview.h" #include "../eventsSDL/InputHandler.h" +#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/CGeneralTextHandler.h" +#include "../../lib/texts/CGeneralTextHandler.h" #include "../../lib/CArtHandler.h" -#include "../../lib/CTownHandler.h" -#include "../../lib/CHeroHandler.h" +#include "../../lib/CConfigHandler.h" #include "../../lib/mapping/CMapInfo.h" #include "../../lib/mapping/CMapHeader.h" @@ -67,7 +73,7 @@ void OptionsTab::recreate() selectionWindow->reopen(); } - OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + OBJECT_CONSTRUCTION; for(auto & pInfo : SEL->getStartInfo()->playerInfos) { if(pInfo.second.isControlledByHuman()) @@ -329,7 +335,7 @@ std::string OptionsTab::CPlayerSettingsHelper::getDescription() OptionsTab::CPlayerOptionTooltipBox::CPlayerOptionTooltipBox(CPlayerSettingsHelper & helper) : CWindowObject(BORDERED | RCLICK_POPUP), CPlayerSettingsHelper(helper) { - OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + OBJECT_CONSTRUCTION; switch(selectionType) { @@ -401,7 +407,7 @@ void OptionsTab::CPlayerOptionTooltipBox::genBonusWindow() textBonusDescription = std::make_shared(getDescription(), Rect(10, 100, pos.w - 20, 70), 0, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE); } -OptionsTab::SelectionWindow::SelectionWindow(const PlayerColor & color, SelType _type) +OptionsTab::SelectionWindow::SelectionWindow(const PlayerColor & color, SelType _type, int sliderPos) : CWindowObject(BORDERED), color(color) { addUsedEvents(LCLICK | SHOW_POPUP); @@ -433,25 +439,31 @@ OptionsTab::SelectionWindow::SelectionWindow(const PlayerColor & color, SelType if(initialFaction.isValid()) allowedBonus.push_back(PlayerStartingBonus::RESOURCE); - recreate(); + recreate(sliderPos); } -int OptionsTab::SelectionWindow::calcLines(FactionID faction) +std::tuple OptionsTab::SelectionWindow::calcLines(FactionID faction) { - double additionalItems = 1; // random + int additionalItems = 1; // random if(!faction.isValid()) - return std::ceil(((double)allowedFactions.size() + additionalItems) / elementsPerLine); + return std::make_tuple( + std::ceil(((double)allowedFactions.size() + additionalItems) / MAX_ELEM_PER_LINES), + (allowedFactions.size() + additionalItems) % MAX_ELEM_PER_LINES + ); int count = 0; for(auto & elemh : allowedHeroes) { - CHero * type = VLC->heroh->objects[elemh]; + const CHero * type = elemh.toHeroType(); if(type->heroClass->faction == faction) count++; } - return std::ceil(std::max((double)count + additionalItems, (double)allowedFactions.size() + additionalItems) / (double)elementsPerLine); + return std::make_tuple( + std::ceil(((double)count + additionalItems) / MAX_ELEM_PER_LINES), + (count + additionalItems) % MAX_ELEM_PER_LINES + ); } void OptionsTab::SelectionWindow::apply() @@ -481,48 +493,38 @@ void OptionsTab::SelectionWindow::setSelection() void OptionsTab::SelectionWindow::reopen() { - auto window = std::shared_ptr(new SelectionWindow(color, type)); - close(); - if(CSH->isMyColor(color) || CSH->isHost()) - GH.windows().pushWindow(window); + if(type == SelType::HERO && SEL->getStartInfo()->playerInfos.find(color)->second.castle == FactionID::RANDOM) + close(); + else{ + auto window = std::make_shared(color, type, slider ? slider->getValue() : 0); + close(); + if(CSH->isMyColor(color) || CSH->isHost()) + GH.windows().pushWindow(window); + } } -void OptionsTab::SelectionWindow::recreate() +void OptionsTab::SelectionWindow::recreate(int sliderPos) { - OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + OBJECT_CONSTRUCTION; int amountLines = 1; if(type == SelType::BONUS) elementsPerLine = allowedBonus.size(); else { - // try to make squarish - if(type == SelType::TOWN) - elementsPerLine = floor(sqrt(allowedFactions.size())); - if(type == SelType::HERO) - { - int count = 0; - for(auto & elem : allowedHeroes) - { - const CHero * type = elem.toHeroType(); - if(type->heroClass->faction == selectedFaction) - { - count++; - } - } - elementsPerLine = floor(sqrt(count)); - } - - amountLines = calcLines((type > SelType::TOWN) ? selectedFaction : FactionID::RANDOM); + std::tie(amountLines, elementsPerLine) = calcLines((type > SelType::TOWN) ? selectedFaction : FactionID::RANDOM); + if(amountLines > 1 || elementsPerLine == 0) + elementsPerLine = MAX_ELEM_PER_LINES; } int x = (elementsPerLine) * (ICON_BIG_WIDTH-1); - int y = (amountLines) * (ICON_BIG_HEIGHT-1); + int y = (std::min(amountLines, MAX_LINES)) * (ICON_BIG_HEIGHT-1); - pos = Rect(0, 0, x, y); + int sliderWidth = ((amountLines > MAX_LINES) ? 16 : 0); - backgroundTexture = std::make_shared(ImagePath::builtin("DiBoxBck"), pos); - backgroundTexture->playerColored(PlayerColor(1)); + pos = Rect(pos.x, pos.y, x + sliderWidth, y); + backgroundTexture = std::make_shared(Rect(0, 0, pos.w - sliderWidth, pos.h)); + backgroundTexture->setPlayerColor(PlayerColor(1)); updateShadow(); if(type == SelType::TOWN) @@ -531,7 +533,15 @@ void OptionsTab::SelectionWindow::recreate() genContentHeroes(); if(type == SelType::BONUS) genContentBonus(); - genContentGrid(amountLines); + genContentGrid(std::min(amountLines, MAX_LINES)); + + if(!slider && amountLines > MAX_LINES) + { + slider = std::make_shared(Point(x, 0), y, std::bind(&OptionsTab::SelectionWindow::sliderMove, this, _1), MAX_LINES, amountLines, 0, Orientation::VERTICAL, CSlider::BLUE); + slider->setPanningStep(ICON_BIG_HEIGHT); + slider->setScrollBounds(Rect(-pos.w + slider->pos.w, 0, x + slider->pos.w, y)); + slider->scrollTo(sliderPos); + } center(); } @@ -569,22 +579,26 @@ void OptionsTab::SelectionWindow::genContentFactions() if(selectedFaction == FactionID::RANDOM) components.push_back(std::make_shared(ImagePath::builtin("lobby/townBorderSmallActivated"), 6, (ICON_SMALL_HEIGHT/2))); + factions.clear(); for(auto & elem : allowedFactions) { int x = i % elementsPerLine; - int y = i / elementsPerLine; + int y = (i / elementsPerLine) - (slider ? slider->getValue() : 0); PlayerSettings set = PlayerSettings(); set.castle = elem; CPlayerSettingsHelper helper = CPlayerSettingsHelper(set, SelType::TOWN); + factions.push_back(elem); + i++; + + if(y < 0 || y > MAX_LINES - 1) + continue; + components.push_back(std::make_shared(helper.getImageName(true), helper.getImageIndex(true), 0, x * (ICON_BIG_WIDTH-1), y * (ICON_BIG_HEIGHT-1))); components.push_back(std::make_shared(ImagePath::builtin(selectedFaction == elem ? "lobby/townBorderBigActivated" : "lobby/townBorderBig"), x * (ICON_BIG_WIDTH-1), y * (ICON_BIG_HEIGHT-1))); drawOutlinedText(x * (ICON_BIG_WIDTH-1) + TEXT_POS_X, y * (ICON_BIG_HEIGHT-1) + TEXT_POS_Y, (selectedFaction == elem) ? Colors::YELLOW : Colors::WHITE, helper.getName()); - factions.push_back(elem); - - i++; } } @@ -601,33 +615,36 @@ void OptionsTab::SelectionWindow::genContentHeroes() if(selectedHero == HeroTypeID::RANDOM) components.push_back(std::make_shared(ImagePath::builtin("lobby/townBorderSmallActivated"), 6, (ICON_SMALL_HEIGHT/2))); + heroes.clear(); for(auto & elem : allowedHeroes) { - CHero * type = VLC->heroh->objects[elem]; + const CHero * type = elem.toHeroType(); - if(type->heroClass->faction == selectedFaction) - { + if(type->heroClass->faction != selectedFaction) + continue; - int x = i % elementsPerLine; - int y = i / elementsPerLine; + int x = i % elementsPerLine; + int y = (i / elementsPerLine) - (slider ? slider->getValue() : 0); - PlayerSettings set = PlayerSettings(); - set.hero = elem; + PlayerSettings set = PlayerSettings(); + set.hero = elem; - CPlayerSettingsHelper helper = CPlayerSettingsHelper(set, SelType::HERO); + CPlayerSettingsHelper helper = CPlayerSettingsHelper(set, SelType::HERO); - components.push_back(std::make_shared(helper.getImageName(true), helper.getImageIndex(true), 0, x * (ICON_BIG_WIDTH-1), y * (ICON_BIG_HEIGHT-1))); - drawOutlinedText(x * (ICON_BIG_WIDTH-1) + TEXT_POS_X, y * (ICON_BIG_HEIGHT-1) + TEXT_POS_Y, (selectedHero == elem) ? Colors::YELLOW : Colors::WHITE, helper.getName()); - ImagePath image = ImagePath::builtin("lobby/townBorderBig"); - if(selectedHero == elem) - image = ImagePath::builtin("lobby/townBorderBigActivated"); - if(unusableHeroes.count(elem)) - image = ImagePath::builtin("lobby/townBorderBigGrayedOut"); - components.push_back(std::make_shared(image, x * (ICON_BIG_WIDTH-1), y * (ICON_BIG_HEIGHT-1))); - heroes.push_back(elem); + heroes.push_back(elem); + i++; - i++; - } + if(y < 0 || y > MAX_LINES - 1) + continue; + + components.push_back(std::make_shared(helper.getImageName(true), helper.getImageIndex(true), 0, x * (ICON_BIG_WIDTH-1), y * (ICON_BIG_HEIGHT-1))); + drawOutlinedText(x * (ICON_BIG_WIDTH-1) + TEXT_POS_X, y * (ICON_BIG_HEIGHT-1) + TEXT_POS_Y, (selectedHero == elem) ? Colors::YELLOW : Colors::WHITE, helper.getName()); + ImagePath image = ImagePath::builtin("lobby/townBorderBig"); + if(selectedHero == elem) + image = ImagePath::builtin("lobby/townBorderBigActivated"); + if(unusableHeroes.count(elem)) + image = ImagePath::builtin("lobby/townBorderBigGrayedOut"); + components.push_back(std::make_shared(image, x * (ICON_BIG_WIDTH-1), y * (ICON_BIG_HEIGHT-1))); } } @@ -658,7 +675,7 @@ void OptionsTab::SelectionWindow::genContentBonus() int OptionsTab::SelectionWindow::getElement(const Point & cursorPosition) { int x = (cursorPosition.x - pos.x) / (ICON_BIG_WIDTH-1); - int y = (cursorPosition.y - pos.y) / (ICON_BIG_HEIGHT-1); + int y = (cursorPosition.y - pos.y) / (ICON_BIG_HEIGHT-1) + (slider ? slider->getValue() : 0); return x + y * elementsPerLine; } @@ -740,18 +757,23 @@ void OptionsTab::SelectionWindow::setElement(int elem, bool doApply) apply(); } -bool OptionsTab::SelectionWindow::receiveEvent(const Point & position, int eventType) const +void OptionsTab::SelectionWindow::sliderMove(int slidPos) { - return true; // capture click also outside of window + if(!slider) + return; // ignore spurious call when slider is being created + recreate(); + redraw(); +} + +void OptionsTab::SelectionWindow::notFocusedClick() +{ + close(); } void OptionsTab::SelectionWindow::clickReleased(const Point & cursorPosition) { - if(!pos.isInside(cursorPosition)) - { - close(); + if(slider && slider->pos.isInside(cursorPosition)) return; - } int elem = getElement(cursorPosition); @@ -760,7 +782,7 @@ void OptionsTab::SelectionWindow::clickReleased(const Point & cursorPosition) void OptionsTab::SelectionWindow::showPopupWindow(const Point & cursorPosition) { - if(!pos.isInside(cursorPosition)) + if(!pos.isInside(cursorPosition) || (slider && slider->pos.isInside(cursorPosition))) return; int elem = getElement(cursorPosition); @@ -768,11 +790,118 @@ void OptionsTab::SelectionWindow::showPopupWindow(const Point & cursorPosition) setElement(elem, false); } +OptionsTab::HandicapWindow::HandicapWindow() + : CWindowObject(BORDERED) +{ + OBJECT_CONSTRUCTION; + + addUsedEvents(LCLICK); + + pos = Rect(0, 0, 660, 100 + SEL->getStartInfo()->playerInfos.size() * 30); + + backgroundTexture = std::make_shared(pos); + backgroundTexture->setPlayerColor(PlayerColor(1)); + + labels.push_back(std::make_shared(pos.w / 2 + 8, 15, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->translate("vcmi.lobby.handicap"))); + + enum Columns : int32_t + { + INCOME = 1000, + GROWTH = 2000, + }; + auto columns = std::vector{EGameResID::GOLD, EGameResID::WOOD, EGameResID::MERCURY, EGameResID::ORE, EGameResID::SULFUR, EGameResID::CRYSTAL, EGameResID::GEMS, Columns::INCOME, Columns::GROWTH}; + + int i = 0; + for(auto & pInfo : SEL->getStartInfo()->playerInfos) + { + PlayerColor player = pInfo.first; + anim.push_back(std::make_shared(AnimationPath::builtin("ITGFLAGS"), player.getNum(), 0, 7, 57 + i * 30)); + for(int j = 0; j < columns.size(); j++) + { + bool isIncome = int(columns[j]) == Columns::INCOME; + bool isGrowth = int(columns[j]) == Columns::GROWTH; + EGameResID resource = columns[j]; + + const PlayerSettings &ps = SEL->getStartInfo()->getIthPlayersSettings(player); + + int xPos = 30 + j * 70; + xPos += j > 0 ? 10 : 0; // Gold field is larger + + if(i == 0) + { + if(isIncome) + 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, 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)); + } + + auto area = Rect(xPos, 60 + i * 30, j == 0 ? 60 : 50, 16); + textinputbackgrounds.push_back(std::make_shared(area.resize(3), ColorRGBA(0,0,0,128), ColorRGBA(64,64,64,64))); + textinputs[player][resource] = std::make_shared(area, FONT_SMALL, ETextAlignment::CENTERLEFT, true); + textinputs[player][resource]->setText(std::to_string(isIncome ? ps.handicap.percentIncome : (isGrowth ? ps.handicap.percentGrowth : ps.handicap.startBonus[resource]))); + textinputs[player][resource]->setCallback([this, player, resource, isIncome, isGrowth](const std::string & s){ + // text input processing: add/remove sign when pressing "-"; remove non digits; cut length; fill empty field with 0 + std::string tmp = s; + bool negative = std::count_if( s.begin(), s.end(), []( char c ){ return c == '-'; }) == 1 && !isIncome && !isGrowth; + tmp.erase(std::remove_if(tmp.begin(), tmp.end(), [](char c) { return !isdigit(c); }), tmp.end()); + int maxLength = isIncome || isGrowth ? 3 : (resource == EGameResID::GOLD ? 6 : 5); + tmp = tmp.substr(0, maxLength); + textinputs[player][resource]->setText(tmp.length() == 0 ? "0" : (negative ? "-" : "") + std::to_string(stoi(tmp))); + }); + textinputs[player][resource]->setPopupCallback([isIncome, isGrowth](){ + // Help for the textinputs + if(isIncome) + CRClickPopup::createAndPush(CGI->generaltexth->translate("vcmi.lobby.handicap.income")); + else if(isGrowth) + CRClickPopup::createAndPush(CGI->generaltexth->translate("vcmi.lobby.handicap.growth")); + else + CRClickPopup::createAndPush(CGI->generaltexth->translate("vcmi.lobby.handicap.resource")); + }); + if(isIncome || isGrowth) + labels.push_back(std::make_shared(area.topRight().x, area.center().y, FONT_SMALL, ETextAlignment::CENTERRIGHT, Colors::WHITE, "%")); + } + i++; + } + + buttons.push_back(std::make_shared(Point(pos.w / 2 - 32, 60 + SEL->getStartInfo()->playerInfos.size() * 30), AnimationPath::builtin("MuBchck"), CButton::tooltip(), [this](){ + for (const auto& player : textinputs) + { + TResources resources = TResources(); + int income = 100; + int growth = 100; + for (const auto& resource : player.second) + { + bool isIncome = int(resource.first) == Columns::INCOME; + bool isGrowth = int(resource.first) == Columns::GROWTH; + if(isIncome) + income = std::stoi(resource.second->getText()); + else if(isGrowth) + growth = std::stoi(resource.second->getText()); + else + resources[resource.first] = std::stoi(resource.second->getText()); + } + CSH->setPlayerHandicap(player.first, Handicap{resources, income, growth}); + } + + close(); + }, EShortcut::GLOBAL_RETURN)); + + updateShadow(); + center(); +} + +void OptionsTab::HandicapWindow::notFocusedClick() +{ + close(); +} + OptionsTab::SelectedBox::SelectedBox(Point position, PlayerSettings & playerSettings, SelType type) : Scrollable(LCLICK | SHOW_POPUP, position, Orientation::HORIZONTAL) , CPlayerSettingsHelper(playerSettings, type) { - OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + OBJECT_CONSTRUCTION; image = std::make_shared(getImageName(), getImageIndex()); subtitle = std::make_shared(24, 39, FONT_TINY, ETextAlignment::CENTER, Colors::WHITE, getName(), 71); @@ -853,8 +982,7 @@ OptionsTab::PlayerOptionsEntry::PlayerOptionsEntry(const PlayerSettings & S, con , parentTab(parent) , name(S.name) { - OBJ_CONSTRUCTION; - defActions |= SHARE_POS; + OBJECT_CONSTRUCTION; int serial = 0; for(PlayerColor g = PlayerColor(0); g < s->color; ++g) @@ -896,7 +1024,49 @@ 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); } - labelWhoCanPlay = std::make_shared(Rect(6, 23, 45, (int)graphics->fonts[EFonts::FONT_TINY]->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(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; + + GH.windows().createAndPushWindow(); + }, [this, hasHandicap](){ + if(hasHandicap()) + CRClickPopup::createAndPush(MetaString::createFromTextID("core.help.124.help").toString()); + else + { + auto str = MetaString::createFromTextID("vcmi.lobby.handicap"); + str.appendRawString(":\n"); + for(auto & res : EGameResID::ALL_RESOURCES()) + if(s->handicap.startBonus[res] != 0) + { + str.appendRawString("\n"); + str.appendName(res); + str.appendRawString(": "); + str.appendRawString(std::to_string(s->handicap.startBonus[res])); + } + if(s->handicap.percentIncome != 100) + { + str.appendRawString("\n"); + str.appendTextID("core.jktext.32"); + str.appendRawString(": "); + str.appendRawString(std::to_string(s->handicap.percentIncome) + "%"); + } + if(s->handicap.percentGrowth != 100) + { + str.appendRawString("\n"); + str.appendTextID("core.genrltxt.194"); + str.appendRawString(": "); + str.appendRawString(std::to_string(s->handicap.percentGrowth) + "%"); + } + CRClickPopup::createAndPush(str.toString()); + } + }); if(SEL->screenType == ESelectionScreen::newGame) { diff --git a/client/lobby/OptionsTab.h b/client/lobby/OptionsTab.h index d25737fe8..e74666e6d 100644 --- a/client/lobby/OptionsTab.h +++ b/client/lobby/OptionsTab.h @@ -26,8 +26,11 @@ class CAnimImage; class CComponentBox; class CTextBox; class CButton; +class CSlider; +class LRClickableArea; class FilledTexturePlayerColored; +class TransparentFilledRectangle; /// The options tab which is shown at the map selection phase. class OptionsTab : public OptionsTabBase @@ -50,6 +53,21 @@ public: BONUS }; + class HandicapWindow : public CWindowObject + { + std::shared_ptr backgroundTexture; + + std::vector> labels; + std::vector> anim; + std::vector> textinputbackgrounds; + std::map>> textinputs; + std::vector> buttons; + + void notFocusedClick() override; + public: + HandicapWindow(); + }; + private: struct CPlayerSettingsHelper @@ -105,8 +123,13 @@ private: const int TEXT_POS_X = 29; const int TEXT_POS_Y = 56; + const int MAX_LINES = 5; + const int MAX_ELEM_PER_LINES = 5; + int elementsPerLine; + std::shared_ptr slider; + PlayerColor color; SelType type; @@ -134,21 +157,23 @@ private: void genContentBonus(); void drawOutlinedText(int x, int y, ColorRGBA color, std::string text); - int calcLines(FactionID faction); + std::tuple calcLines(FactionID faction); void apply(); - void recreate(); + void recreate(int sliderPos = 0); void setSelection(); int getElement(const Point & cursorPosition); void setElement(int element, bool doApply); - bool receiveEvent(const Point & position, int eventType) const override; + void sliderMove(int slidPos); + + void notFocusedClick() override; void clickReleased(const Point & cursorPosition) override; void showPopupWindow(const Point & cursorPosition) override; public: void reopen(); - SelectionWindow(const PlayerColor & color, SelType _type); + SelectionWindow(const PlayerColor & color, SelType _type, int sliderPos = 0); }; /// Image with current town/hero/bonus @@ -184,6 +209,8 @@ private: std::shared_ptr town; std::shared_ptr hero; std::shared_ptr bonus; + std::shared_ptr handicap; + std::shared_ptr labelHandicap; enum {HUMAN_OR_CPU, HUMAN, CPU} whoCanPlay; PlayerOptionsEntry(const PlayerSettings & S, const OptionsTab & parentTab); diff --git a/client/lobby/OptionsTabBase.cpp b/client/lobby/OptionsTabBase.cpp index 17936a6d0..8335d9f9d 100644 --- a/client/lobby/OptionsTabBase.cpp +++ b/client/lobby/OptionsTabBase.cpp @@ -18,11 +18,12 @@ #include "../widgets/TextControls.h" #include "../CServerHandler.h" #include "../CGameInfo.h" +#include "../render/AssetGenerator.h" #include "../../lib/StartInfo.h" -#include "../../lib/Languages.h" -#include "../../lib/MetaString.h" -#include "../../lib/CGeneralTextHandler.h" +#include "../../lib/texts/CGeneralTextHandler.h" +#include "../../lib/texts/Languages.h" +#include "../../lib/texts/MetaString.h" #include "../../lib/CConfigHandler.h" static std::string timeToString(int time) @@ -68,6 +69,8 @@ std::vector OptionsTabBase::getSimturnsPresets() const OptionsTabBase::OptionsTabBase(const JsonPath & configPath) { + AssetGenerator::createAdventureOptionsCleanBackground(); + recActions = 0; auto setTimerPresetCallback = [this](int index){ diff --git a/client/lobby/RandomMapTab.cpp b/client/lobby/RandomMapTab.cpp index 310f9dae2..9b13ece93 100644 --- a/client/lobby/RandomMapTab.cpp +++ b/client/lobby/RandomMapTab.cpp @@ -29,7 +29,7 @@ #include "../windows/GUIClasses.h" #include "../windows/InfoWindows.h" -#include "../../lib/CGeneralTextHandler.h" +#include "../../lib/texts/CGeneralTextHandler.h" #include "../../lib/mapping/CMapInfo.h" #include "../../lib/mapping/CMapHeader.h" #include "../../lib/mapping/MapFormat.h" @@ -114,12 +114,12 @@ RandomMapTab::RandomMapTab(): GH.windows().createAndPushWindow(*this); }); - for(auto road : VLC->roadTypeHandler->objects) + for(const auto & road : VLC->roadTypeHandler->objects) { std::string cbRoadType = "selectRoad_" + road->getJsonKey(); - addCallback(cbRoadType, [&, road](bool on) + addCallback(cbRoadType, [&, roadID = road->getId()](bool on) { - mapGenOptions->setRoadEnabled(road->getId(), on); + mapGenOptions->setRoadEnabled(roadID, on); updateMapInfoByHost(); }); } @@ -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(); @@ -372,7 +372,7 @@ void RandomMapTab::setMapGenOptions(std::shared_ptr opts) else w->setTextOverlay(readText(variables["randomTemplate"]), EFonts::FONT_SMALL, Colors::WHITE); } - for(auto r : VLC->roadTypeHandler->objects) + for(const auto & r : VLC->roadTypeHandler->objects) { // Workaround for vcmi-extras bug std::string jsonKey = r->getJsonKey(); @@ -447,7 +447,7 @@ void TeamAlignmentsWidget::checkTeamCount() TeamAlignments::TeamAlignments(RandomMapTab & randomMapTab) : CWindowObject(BORDERED) { - OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + OBJECT_CONSTRUCTION; widget = std::make_shared(randomMapTab); pos = widget->pos; @@ -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; @@ -501,7 +503,7 @@ TeamAlignmentsWidget::TeamAlignmentsWidget(RandomMapTab & randomMapTab): center(pos); - OBJ_CONSTRUCTION; + OBJECT_CONSTRUCTION; // Window should have X * X columns, where X is max players allowed for current settings // For random player count, X is 8 @@ -529,7 +531,7 @@ TeamAlignmentsWidget::TeamAlignmentsWidget(RandomMapTab & randomMapTab): players.push_back(std::make_shared([&, totalPlayers, plId](int sel) { variables["player_id"].Integer() = plId; - OBJ_CONSTRUCTION_TARGETED(players[plId].get()); + OBJECT_CONSTRUCTION_TARGETED(players[plId].get()); for(int teamId = 0; teamId < totalPlayers; ++teamId) { auto button = std::dynamic_pointer_cast(players[plId]->buttons[teamId]); @@ -549,17 +551,17 @@ TeamAlignmentsWidget::TeamAlignmentsWidget(RandomMapTab & randomMapTab): } })); - OBJ_CONSTRUCTION_TARGETED(players.back().get()); + OBJECT_CONSTRUCTION_TARGETED(players.back().get()); 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)); } - // plId is not neccessarily player color, just an index + // plId is not necessarily player color, just an index auto team = settingsVec.at(plId).getTeam(); if(team == TeamID::NO_TEAM) { @@ -601,8 +603,14 @@ void RandomMapTab::loadOptions() { w->setItem(mapGenOptions->getMapTemplate()); } + } else + { + // Default settings + mapGenOptions->setRoadEnabled(RoadId(Road::DIRT_ROAD), true); + mapGenOptions->setRoadEnabled(RoadId(Road::GRAVEL_ROAD), true); + mapGenOptions->setRoadEnabled(RoadId(Road::COBBLESTONE_ROAD), true); } updateMapInfoByHost(); // TODO: Save & load difficulty? -} \ No newline at end of file +} diff --git a/client/lobby/SelectionTab.cpp b/client/lobby/SelectionTab.cpp index 6711419f0..6be7e9d40 100644 --- a/client/lobby/SelectionTab.cpp +++ b/client/lobby/SelectionTab.cpp @@ -35,15 +35,18 @@ #include "../../CCallback.h" -#include "../../lib/CGeneralTextHandler.h" #include "../../lib/CConfigHandler.h" -#include "../../lib/GameSettings.h" +#include "../../lib/IGameSettings.h" #include "../../lib/filesystem/Filesystem.h" #include "../../lib/campaign/CampaignState.h" #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) { @@ -151,9 +154,9 @@ 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) { - OBJ_CONSTRUCTION; + OBJECT_CONSTRUCTION; generalSortingBy = getSortBySelectionScreen(tabType); sortingBy = _format; @@ -191,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; @@ -224,15 +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); + } } - iconsMapFormats = GH.renderHandler().loadAnimation(AnimationPath::builtin("SCSELC.DEF")); - iconsVictoryCondition = GH.renderHandler().loadAnimation(AnimationPath::builtin("SCNRVICT.DEF")); - iconsLossCondition = GH.renderHandler().loadAnimation(AnimationPath::builtin("SCNRLOSS.DEF")); for(int i = 0; i < positionsToShow; i++) - listItems.push_back(std::make_shared(Point(30, 129 + i * 25), iconsMapFormats, iconsVictoryCondition, iconsLossCondition)); + 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); @@ -244,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); } @@ -265,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)); @@ -309,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 @@ -391,7 +439,33 @@ void SelectionTab::showPopupWindow(const Point & cursorPosition) return; if(!curItems[py]->isFolder) - GH.windows().createAndPushWindow(curItems[py]->getNameTranslated(), curItems[py]->fullFileURI, curItems[py]->date, ResourcePath(curItems[py]->fileURI), tabType); + { + std::string creationDateTime; + std::string author; + std::string mapVersion; + if(tabType != ESelectionScreen::campaignList) + { + author = curItems[py]->mapHeader->author.toString() + (!curItems[py]->mapHeader->authorContact.toString().empty() ? (" <" + curItems[py]->mapHeader->authorContact.toString() + ">") : ""); + mapVersion = curItems[py]->mapHeader->mapVersion.toString(); + creationDateTime = tabType == ESelectionScreen::newGame && curItems[py]->mapHeader->creationDateTime ? TextOperations::getFormattedDateTimeLocal(curItems[py]->mapHeader->creationDateTime) : curItems[py]->date; + } + else + { + author = curItems[py]->campaign->getAuthor() + (!curItems[py]->campaign->getAuthorContact().empty() ? (" <" + curItems[py]->campaign->getAuthorContact() + ">") : ""); + mapVersion = curItems[py]->campaign->getCampaignVersion(); + creationDateTime = curItems[py]->campaign->getCreationDateTime() ? TextOperations::getFormattedDateTimeLocal(curItems[py]->campaign->getCreationDateTime()) : curItems[py]->date; + } + + GH.windows().createAndPushWindow( + curItems[py]->getNameTranslated(), + curItems[py]->fullFileURI, + creationDateTime, + author, + mapVersion, + ResourcePath(curItems[py]->fileURI), + tabType + ); + } else CRClickPopup::createAndPush(curItems[py]->folderName); } @@ -451,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) @@ -477,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); @@ -537,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(); @@ -669,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 + "/" : ""; @@ -680,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); @@ -691,14 +773,14 @@ 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() { time_t newestTime = 0; std::string newestFile = ""; - for(int i = (int)allItems.size() - 1; i >= 0; i--) + for(int i = static_cast(allItems.size()) - 1; i >= 0; i--) if(allItems[i]->lastWrite > newestTime) { newestTime = allItems[i]->lastWrite; @@ -758,17 +840,19 @@ bool SelectionTab::isMapSupported(const CMapInfo & info) switch (info.mapHeader->version) { case EMapFormat::ROE: - return CGI->settings()->getValue(EGameSettings::MAP_FORMAT_RESTORATION_OF_ERATHIA)["supported"].Bool(); + return CGI->engineSettings()->getValue(EGameSettings::MAP_FORMAT_RESTORATION_OF_ERATHIA)["supported"].Bool(); case EMapFormat::AB: - return CGI->settings()->getValue(EGameSettings::MAP_FORMAT_ARMAGEDDONS_BLADE)["supported"].Bool(); + return CGI->engineSettings()->getValue(EGameSettings::MAP_FORMAT_ARMAGEDDONS_BLADE)["supported"].Bool(); case EMapFormat::SOD: - return CGI->settings()->getValue(EGameSettings::MAP_FORMAT_SHADOW_OF_DEATH)["supported"].Bool(); + return CGI->engineSettings()->getValue(EGameSettings::MAP_FORMAT_SHADOW_OF_DEATH)["supported"].Bool(); + case EMapFormat::CHR: + return CGI->engineSettings()->getValue(EGameSettings::MAP_FORMAT_CHRONICLES)["supported"].Bool(); case EMapFormat::WOG: - return CGI->settings()->getValue(EGameSettings::MAP_FORMAT_IN_THE_WAKE_OF_GODS)["supported"].Bool(); + return CGI->engineSettings()->getValue(EGameSettings::MAP_FORMAT_IN_THE_WAKE_OF_GODS)["supported"].Bool(); case EMapFormat::HOTA: - return CGI->settings()->getValue(EGameSettings::MAP_FORMAT_HORN_OF_THE_ABYSS)["supported"].Bool(); + return CGI->engineSettings()->getValue(EGameSettings::MAP_FORMAT_HORN_OF_THE_ABYSS)["supported"].Bool(); case EMapFormat::VCMI: - return CGI->settings()->getValue(EGameSettings::MAP_FORMAT_JSON_VCMI)["supported"].Bool(); + return CGI->engineSettings()->getValue(EGameSettings::MAP_FORMAT_JSON_VCMI)["supported"].Bool(); } return false; } @@ -782,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); @@ -794,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; @@ -833,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); + } } } @@ -870,12 +1006,12 @@ std::unordered_set SelectionTab::getFiles(std::string dirURI, ERes return ret; } -SelectionTab::ListItem::ListItem(Point position, std::shared_ptr iconsFormats, std::shared_ptr iconsVictory, std::shared_ptr iconsLoss) +SelectionTab::ListItem::ListItem(Point position) : CIntObject(LCLICK, position) { - OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; - pictureEmptyLine = std::make_shared(GH.renderHandler().loadImage(ImagePath::builtin("camcust")), Rect(25, 121, 349, 26), -8, -14); - labelName = std::make_shared(184, 0, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, "", 185); + OBJECT_CONSTRUCTION; + pictureEmptyLine = std::make_shared(ImagePath::builtin("camcust"), Rect(25, 121, 349, 26), -8, -14); + 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); @@ -885,9 +1021,9 @@ SelectionTab::ListItem::ListItem(Point position, std::shared_ptr ico labelMapSizeLetter->setAutoRedraw(false); // FIXME: This -12 should not be needed, but for some reason CAnimImage displaced otherwise iconFolder = std::make_shared(ImagePath::builtin("lobby/iconFolder.png"), -8, -12); - iconFormat = std::make_shared(iconsFormats, 0, 0, 59, -12); - iconVictoryCondition = std::make_shared(iconsVictory, 0, 0, 277, -12); - iconLossCondition = std::make_shared(iconsLoss, 0, 0, 310, -12); + iconFormat = std::make_shared(AnimationPath::builtin("SCSELC.DEF"), 0, 0, 59, -12); + iconVictoryCondition = std::make_shared(AnimationPath::builtin("SCNRVICT.DEF"), 0, 0, 277, -12); + iconLossCondition = std::make_shared(AnimationPath::builtin("SCNRLOSS.DEF"), 0, 0, 310, -12); } void SelectionTab::ListItem::updateItem(std::shared_ptr info, bool selected) @@ -919,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; @@ -940,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 { @@ -961,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 e19fa055f..2cef080ed 100644 --- a/client/lobby/SelectionTab.h +++ b/client/lobby/SelectionTab.h @@ -20,6 +20,7 @@ class CSlider; class CLabel; class CPicture; class IImage; +class CAnimation; enum ESortBy { @@ -32,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 @@ -58,7 +61,9 @@ class SelectionTab : public CIntObject std::shared_ptr pictureEmptyLine; std::shared_ptr labelName; - ListItem(Point position, std::shared_ptr iconsFormats, std::shared_ptr iconsVictory, std::shared_ptr iconsLoss); + const int LABEL_POS_X = 184; + + ListItem(Point position); void updateItem(std::shared_ptr info = {}, bool selected = false); }; std::vector> listItems; @@ -67,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; @@ -115,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 9db6cf79c..68d4cbdc4 100644 --- a/client/mainmenu/CCampaignScreen.cpp +++ b/client/mainmenu/CCampaignScreen.cpp @@ -14,32 +14,29 @@ #include "CMainMenu.h" #include "../CGameInfo.h" -#include "../CMusicHandler.h" -#include "../CVideoHandler.h" #include "../CPlayerInterface.h" #include "../CServerHandler.h" #include "../gui/CGuiHandler.h" #include "../gui/Shortcut.h" +#include "../media/IMusicPlayer.h" #include "../render/Canvas.h" #include "../widgets/CComponent.h" #include "../widgets/Buttons.h" #include "../widgets/MiscWidgets.h" #include "../widgets/ObjectLists.h" #include "../widgets/TextControls.h" +#include "../widgets/VideoWidget.h" #include "../windows/GUIClasses.h" #include "../windows/InfoWindows.h" #include "../windows/CWindowObject.h" #include "../../lib/filesystem/Filesystem.h" -#include "../../lib/CGeneralTextHandler.h" +#include "../../lib/texts/CGeneralTextHandler.h" #include "../../lib/CArtHandler.h" -#include "../../lib/CBuildingHandler.h" #include "../../lib/spells/CSpellHandler.h" - +#include "../../lib/CConfigHandler.h" #include "../../lib/CSkillHandler.h" -#include "../../lib/CTownHandler.h" -#include "../../lib/CHeroHandler.h" #include "../../lib/CCreatureHandler.h" #include "../../lib/campaign/CampaignHandler.h" @@ -50,7 +47,7 @@ CCampaignScreen::CCampaignScreen(const JsonNode & config, std::string name) : CWindowObject(BORDERED), campaignSet(name) { - OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + OBJECT_CONSTRUCTION; for(const JsonNode & node : config[name]["images"].Vector()) images.push_back(CMainMenu::createPicture(node)); @@ -70,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() @@ -92,7 +90,7 @@ std::shared_ptr CCampaignScreen::createExitButton(const JsonNode & butt CCampaignScreen::CCampaignButton::CCampaignButton(const JsonNode & config, const JsonNode & parentConfig, std::string campaignSet) : campaignSet(campaignSet) { - OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + OBJECT_CONSTRUCTION; pos.x += static_cast(config["x"].Float()); pos.y += static_cast(config["y"].Float()); @@ -100,7 +98,7 @@ CCampaignScreen::CCampaignButton::CCampaignButton(const JsonNode & config, const pos.h = 116; campFile = config["file"].String(); - video = VideoPath::fromJson(config["video"]); + videoPath = VideoPath::fromJson(config["video"]); status = CCampaignScreen::ENABLED; @@ -127,7 +125,6 @@ CCampaignScreen::CCampaignButton::CCampaignButton(const JsonNode & config, const { addUsedEvents(LCLICK | HOVER); graphicsImage = std::make_shared(ImagePath::fromJson(config["image"])); - hoverLabel = std::make_shared(pos.w / 2, pos.h + 20, FONT_MEDIUM, ETextAlignment::CENTER, Colors::YELLOW, ""); parent->addChild(hoverLabel.get()); } @@ -136,30 +133,19 @@ CCampaignScreen::CCampaignButton::CCampaignButton(const JsonNode & config, const graphicsCompleted = std::make_shared(ImagePath::builtin("CAMPCHK")); } -void CCampaignScreen::CCampaignButton::show(Canvas & to) -{ - if(status == CCampaignScreen::DISABLED) - return; - - CIntObject::show(to); - - // Play the campaign button video when the mouse cursor is placed over the button - if(isHovered()) - CCS->videoh->update(pos.x, pos.y, to.getInternalSurface(), true, false); // plays sequentially frame by frame, starts at the beginning when the video is over -} - void CCampaignScreen::CCampaignButton::clickReleased(const Point & cursorPosition) { - CCS->videoh->close(); CMainMenu::openCampaignLobby(campFile, campaignSet); } void CCampaignScreen::CCampaignButton::hover(bool on) { - if (on) - CCS->videoh->open(video); + OBJECT_CONSTRUCTION; + + if (on && !videoPath.empty()) + videoPlayer = std::make_shared(Point(), videoPath, false); else - CCS->videoh->close(); + videoPlayer.reset(); if(hoverLabel) { diff --git a/client/mainmenu/CCampaignScreen.h b/client/mainmenu/CCampaignScreen.h index e19395974..87b69bc5e 100644 --- a/client/mainmenu/CCampaignScreen.h +++ b/client/mainmenu/CCampaignScreen.h @@ -20,6 +20,7 @@ VCMI_LIB_NAMESPACE_END class CLabel; class CPicture; class CButton; +class VideoWidget; class CCampaignScreen : public CWindowObject { @@ -34,10 +35,11 @@ private: std::shared_ptr hoverLabel; std::shared_ptr graphicsImage; std::shared_ptr graphicsCompleted; + std::shared_ptr videoPlayer; CampaignStatus status; + VideoPath videoPath; std::string campFile; // the filename/resourcename of the campaign - VideoPath video; // the resource name of the video std::string hoverText; std::string campaignSet; @@ -47,7 +49,6 @@ private: public: CCampaignButton(const JsonNode & config, const JsonNode & parentConfig, std::string campaignSet); - void show(Canvas & to) override; }; std::string campaignSet; diff --git a/client/mainmenu/CHighScoreScreen.cpp b/client/mainmenu/CHighScoreScreen.cpp index 656a85d8a..ab985497a 100644 --- a/client/mainmenu/CHighScoreScreen.cpp +++ b/client/mainmenu/CHighScoreScreen.cpp @@ -11,102 +11,38 @@ #include "StdInc.h" #include "CHighScoreScreen.h" +#include "CStatisticScreen.h" +#include "CMainMenu.h" #include "../gui/CGuiHandler.h" #include "../gui/WindowHandler.h" #include "../gui/Shortcut.h" +#include "../media/IMusicPlayer.h" +#include "../media/ISoundPlayer.h" #include "../widgets/Buttons.h" #include "../widgets/CTextInput.h" #include "../widgets/Images.h" #include "../widgets/GraphicalPrimitiveCanvas.h" +#include "../widgets/VideoWidget.h" #include "../windows/InfoWindows.h" #include "../widgets/TextControls.h" #include "../render/Canvas.h" #include "../render/IRenderHandler.h" #include "../CGameInfo.h" -#include "../CVideoHandler.h" -#include "../CMusicHandler.h" -#include "../../lib/CGeneralTextHandler.h" +#include "../../lib/texts/CGeneralTextHandler.h" +#include "../../lib/texts/TextOperations.h" #include "../../lib/CConfigHandler.h" #include "../../lib/CCreatureHandler.h" #include "../../lib/constants/EntityIdentifiers.h" -#include "../../lib/TextOperations.h" -#include "../../lib/Languages.h" - -auto HighScoreCalculation::calculate() -{ - struct Result - { - int basic = 0; - int total = 0; - int sumDays = 0; - bool cheater = false; - }; - - Result firstResult; - Result summary; - const std::array difficultyMultipliers{0.8, 1.0, 1.3, 1.6, 2.0}; - for(auto & param : parameters) - { - double tmp = 200 - (param.day + 10) / (param.townAmount + 5) + (param.allDefeated ? 25 : 0) + (param.hasGrail ? 25 : 0); - firstResult = Result{static_cast(tmp), static_cast(tmp * difficultyMultipliers.at(param.difficulty)), param.day, param.usedCheat}; - summary.basic += firstResult.basic * 5.0 / parameters.size(); - summary.total += firstResult.total * 5.0 / parameters.size(); - summary.sumDays += firstResult.sumDays; - summary.cheater |= firstResult.cheater; - } - - if(parameters.size() == 1) - return firstResult; - - return summary; -} - -struct HighScoreCreature -{ - CreatureID creature; - int min; - int max; -}; - -static std::vector getHighscoreCreaturesList() -{ - JsonNode configCreatures(JsonPath::builtin("CONFIG/highscoreCreatures.json")); - - std::vector ret; - - for(auto & json : configCreatures["creatures"].Vector()) - { - HighScoreCreature entry; - entry.creature = CreatureID::decode(json["creature"].String()); - entry.max = json["max"].isNull() ? std::numeric_limits::max() : json["max"].Integer(); - entry.min = json["min"].isNull() ? std::numeric_limits::min() : json["min"].Integer(); - - ret.push_back(entry); - } - - return ret; -} - -CreatureID HighScoreCalculation::getCreatureForPoints(int points, bool campaign) -{ - static const std::vector creatures = getHighscoreCreaturesList(); - - int divide = campaign ? 5 : 1; - - for(auto & creature : creatures) - if(points / divide <= creature.max && points / divide >= creature.min) - return creature.creature; - - throw std::runtime_error("Unable to find creature for score " + std::to_string(points)); -} +#include "../../lib/gameState/HighScore.h" +#include "../../lib/gameState/GameStatistics.h" CHighScoreScreen::CHighScoreScreen(HighScorePage highscorepage, int highlighted) : CWindowObject(BORDERED), highscorepage(highscorepage), highlighted(highlighted) { addUsedEvents(SHOW_POPUP); - OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + OBJECT_CONSTRUCTION; pos = center(Rect(0, 0, 800, 600)); backgroundAroundMenu = std::make_shared(ImagePath::builtin("DIBOXBCK"), Rect(-pos.x, -pos.y, GH.screenDimensions().x, GH.screenDimensions().y)); @@ -133,19 +69,19 @@ void CHighScoreScreen::showPopupWindow(const Point & cursorPosition) void CHighScoreScreen::addButtons() { - OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + OBJECT_CONSTRUCTION; buttons.clear(); - buttons.push_back(std::make_shared(Point(31, 113), AnimationPath::builtin("HISCCAM.DEF"), CButton::tooltip(), [&](){ buttonCampaignClick(); }, EShortcut::HIGH_SCORES_CAMPAIGNS)); - buttons.push_back(std::make_shared(Point(31, 345), AnimationPath::builtin("HISCSTA.DEF"), CButton::tooltip(), [&](){ buttonScenarioClick(); }, EShortcut::HIGH_SCORES_SCENARIOS)); - buttons.push_back(std::make_shared(Point(726, 113), AnimationPath::builtin("HISCRES.DEF"), CButton::tooltip(), [&](){ buttonResetClick(); }, EShortcut::HIGH_SCORES_RESET)); - buttons.push_back(std::make_shared(Point(726, 345), AnimationPath::builtin("HISCEXT.DEF"), CButton::tooltip(), [&](){ buttonExitClick(); }, EShortcut::GLOBAL_RETURN)); + buttons.push_back(std::make_shared(Point(31, 113), AnimationPath::builtin("HISCCAM.DEF"), CButton::tooltip(), [this](){ buttonCampaignClick(); }, EShortcut::HIGH_SCORES_CAMPAIGNS)); + buttons.push_back(std::make_shared(Point(31, 345), AnimationPath::builtin("HISCSTA.DEF"), CButton::tooltip(), [this](){ buttonScenarioClick(); }, EShortcut::HIGH_SCORES_SCENARIOS)); + buttons.push_back(std::make_shared(Point(726, 113), AnimationPath::builtin("HISCRES.DEF"), CButton::tooltip(), [this](){ buttonResetClick(); }, EShortcut::HIGH_SCORES_RESET)); + buttons.push_back(std::make_shared(Point(726, 345), AnimationPath::builtin("HISCEXT.DEF"), CButton::tooltip(), [this](){ buttonExitClick(); }, EShortcut::GLOBAL_RETURN)); } void CHighScoreScreen::addHighScores() { - OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + OBJECT_CONSTRUCTION; background = std::make_shared(ImagePath::builtin(highscorepage == HighScorePage::SCENARIO ? "HISCORE" : "HISCORE2")); @@ -208,7 +144,7 @@ void CHighScoreScreen::buttonCampaignClick() void CHighScoreScreen::buttonScenarioClick() { - OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + OBJECT_CONSTRUCTION; highscorepage = HighScorePage::SCENARIO; addHighScores(); addButtons(); @@ -235,14 +171,15 @@ void CHighScoreScreen::buttonResetClick() void CHighScoreScreen::buttonExitClick() { close(); + CMM->playMusic(); } -CHighScoreInputScreen::CHighScoreInputScreen(bool won, HighScoreCalculation calc) - : CWindowObject(BORDERED), won(won), calc(calc), videoSoundHandle(-1) +CHighScoreInputScreen::CHighScoreInputScreen(bool won, HighScoreCalculation calc, const StatisticDataSet & statistic) + : CWindowObject(BORDERED), won(won), calc(calc), stat(statistic) { addUsedEvents(LCLICK | KEYBOARD); - OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + OBJECT_CONSTRUCTION; pos = center(Rect(0, 0, 800, 600)); backgroundAroundMenu = std::make_shared(ImagePath::builtin("DIBOXBCK"), Rect(-pos.x, -pos.y, GH.screenDimensions().x, GH.screenDimensions().y)); @@ -250,6 +187,9 @@ CHighScoreInputScreen::CHighScoreInputScreen(bool won, HighScoreCalculation calc if(won) { + + videoPlayer = std::make_shared(Point(0, 0), VideoPath::builtin("HSANIM.SMK"), VideoPath::builtin("HSLOOP.SMK"), true); + int border = 100; int textareaW = ((pos.w - 2 * border) / 4); std::vector t = { "438", "439", "440", "441", "676" }; // time, score, difficulty, final score, rank @@ -264,9 +204,21 @@ CHighScoreInputScreen::CHighScoreInputScreen(bool won, HighScoreCalculation calc CCS->musich->playMusic(AudioPath::builtin("music/Win Scenario"), true, true); } else + { + videoPlayer = std::make_shared(Point(0, 0), VideoPath::builtin("LOSEGAME.SMK"), true, this); CCS->musich->playMusic(AudioPath::builtin("music/UltimateLose"), false, true); + } - video = won ? "HSANIM.SMK" : "LOSEGAME.SMK"; + if (settings["general"]["enableUiEnhancements"].Bool()) + { + statisticButton = std::make_shared(Point(726, 10), AnimationPath::builtin("TPTAV02.DEF"), CButton::tooltip(CGI->generaltexth->translate("vcmi.statisticWindow.statistics")), [this](){ GH.windows().createAndPushWindow(stat); }, EShortcut::HIGH_SCORES_STATISTICS); + texts.push_back(std::make_shared(716, 25, EFonts::FONT_HIGH_SCORE, ETextAlignment::CENTERRIGHT, Colors::WHITE, CGI->generaltexth->translate("vcmi.statisticWindow.statistics") + ":")); + } +} + +void CHighScoreInputScreen::onVideoPlaybackFinished() +{ + close(); } int CHighScoreInputScreen::addEntry(std::string text) { @@ -311,56 +263,15 @@ int CHighScoreInputScreen::addEntry(std::string text) { void CHighScoreInputScreen::show(Canvas & to) { - if(background) - background->show(to); - - CCS->videoh->update(pos.x, pos.y, to.getInternalSurface(), true, false, - [&]() - { - if(won) - { - CCS->videoh->close(); - video = "HSLOOP.SMK"; - auto audioData = CCS->videoh->getAudio(VideoPath::builtin(video)); - videoSoundHandle = CCS->soundh->playSound(audioData); - CCS->videoh->open(VideoPath::builtin(video)); - } - else - close(); - }); - - if(input) - input->showAll(to); - for(auto & text : texts) - text->showAll(to); - - CIntObject::show(to); -} - -void CHighScoreInputScreen::activate() -{ - auto audioData = CCS->videoh->getAudio(VideoPath::builtin(video)); - videoSoundHandle = CCS->soundh->playSound(audioData); - if(!CCS->videoh->open(VideoPath::builtin(video))) - { - if(!won) - close(); - } - else - background = nullptr; - CIntObject::activate(); -} - -void CHighScoreInputScreen::deactivate() -{ - CCS->videoh->close(); - CCS->soundh->stopSound(videoSoundHandle); - CIntObject::deactivate(); + CWindowObject::showAll(to); } void CHighScoreInputScreen::clickPressed(const Point & cursorPosition) { - OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + if(statisticButton->pos.isInside(cursorPosition)) + return; + + OBJECT_CONSTRUCTION; if(!won) { @@ -387,13 +298,15 @@ void CHighScoreInputScreen::clickPressed(const Point & cursorPosition) void CHighScoreInputScreen::keyPressed(EShortcut key) { + if(key == EShortcut::HIGH_SCORES_STATISTICS) // ignore shortcut for skipping video with key + return; clickPressed(Point()); } CHighScoreInput::CHighScoreInput(std::string playerName, std::function readyCB) : CWindowObject(NEEDS_ANIMATED_BACKGROUND, ImagePath::builtin("HIGHNAME")), ready(readyCB) { - OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + OBJECT_CONSTRUCTION; pos = center(Rect(0, 0, 232, 212)); updateShadow(); diff --git a/client/mainmenu/CHighScoreScreen.h b/client/mainmenu/CHighScoreScreen.h index 50e8b290f..e0c3b5c69 100644 --- a/client/mainmenu/CHighScoreScreen.h +++ b/client/mainmenu/CHighScoreScreen.h @@ -8,41 +8,22 @@ * */ #pragma once + +#include "../widgets/IVideoHolder.h" #include "../windows/CWindowObject.h" +#include "../../lib/gameState/HighScore.h" +#include "../../lib/gameState/GameStatistics.h" class CButton; class CLabel; class CMultiLineLabel; class CAnimImage; class CTextInput; +class VideoWidgetBase; class CFilledTexture; class TransparentFilledRectangle; -class HighScoreParameter -{ -public: - int difficulty; - int day; - int townAmount; - bool usedCheat; - bool hasGrail; - bool allDefeated; - std::string campaignName; - std::string scenarioName; - std::string playerName; -}; - -class HighScoreCalculation -{ -public: - std::vector parameters = std::vector(); - bool isCampaign = false; - - auto calculate(); - static CreatureID getCreatureForPoints(int points, bool campaign); -}; - class CHighScoreScreen : public CWindowObject { public: @@ -90,25 +71,27 @@ public: CHighScoreInput(std::string playerName, std::function readyCB); }; -class CHighScoreInputScreen : public CWindowObject +class CHighScoreInputScreen : public CWindowObject, public IVideoHolder { - std::vector> texts; + std::vector> texts; std::shared_ptr input; std::shared_ptr background; + std::shared_ptr videoPlayer; std::shared_ptr backgroundAroundMenu; - std::string video; - int videoSoundHandle; + std::shared_ptr statisticButton; + bool won; HighScoreCalculation calc; + StatisticDataSet stat; + + void onVideoPlaybackFinished() override; public: - CHighScoreInputScreen(bool won, HighScoreCalculation calc); + CHighScoreInputScreen(bool won, HighScoreCalculation calc, const StatisticDataSet & statistic); int addEntry(std::string text); - void show(Canvas & to) override; - void activate() override; - void deactivate() override; void clickPressed(const Point & cursorPosition) override; void keyPressed(EShortcut key) override; -}; \ No newline at end of file + void show(Canvas & to) override; +}; diff --git a/client/mainmenu/CMainMenu.cpp b/client/mainmenu/CMainMenu.cpp index 98a894ceb..f3702101c 100644 --- a/client/mainmenu/CMainMenu.cpp +++ b/client/mainmenu/CMainMenu.cpp @@ -17,6 +17,8 @@ #include "../lobby/CBonusSelection.h" #include "../lobby/CSelectionBase.h" #include "../lobby/CLobbyScreen.h" +#include "../media/IMusicPlayer.h" +#include "../media/IVideoPlayer.h" #include "../gui/CursorHandler.h" #include "../windows/GUIClasses.h" #include "../gui/CGuiHandler.h" @@ -33,21 +35,20 @@ #include "../widgets/MiscWidgets.h" #include "../widgets/ObjectLists.h" #include "../widgets/TextControls.h" +#include "../widgets/VideoWidget.h" #include "../windows/InfoWindows.h" #include "../CServerHandler.h" +#include "../render/AssetGenerator.h" #include "../CGameInfo.h" -#include "../CMusicHandler.h" -#include "../CVideoHandler.h" #include "../CPlayerInterface.h" #include "../Client.h" #include "../CMT.h" #include "../../CCallback.h" -#include "../../lib/CGeneralTextHandler.h" +#include "../../lib/texts/CGeneralTextHandler.h" #include "../../lib/campaign/CampaignHandler.h" -#include "../../lib/serializer/CTypeList.h" #include "../../lib/filesystem/Filesystem.h" #include "../../lib/filesystem/CCompressedStream.h" #include "../../lib/mapping/CMapInfo.h" @@ -73,7 +74,7 @@ static void do_quit() CMenuScreen::CMenuScreen(const JsonNode & configNode) : CWindowObject(BORDERED), config(configNode) { - OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + OBJECT_CONSTRUCTION; background = std::make_shared(ImagePath::fromJson(config["background"])); if(config["scalable"].Bool()) @@ -81,6 +82,12 @@ CMenuScreen::CMenuScreen(const JsonNode & configNode) pos = background->center(); + if(!config["video"].isNull()) + { + Point videoPosition(config["video"]["x"].Integer(), config["video"]["y"].Integer()); + videoPlayer = std::make_shared(videoPosition, VideoPath::fromJson(config["video"]["name"]), false); + } + for(const JsonNode & node : config["items"].Vector()) menuNameToEntry.push_back(node["name"].String()); @@ -93,6 +100,7 @@ CMenuScreen::CMenuScreen(const JsonNode & configNode) tabs = std::make_shared(std::bind(&CMenuScreen::createTab, this, _1)); if(config["video"].isNull()) tabs->setRedrawParent(true); + } std::shared_ptr CMenuScreen::createTab(size_t index) @@ -105,32 +113,15 @@ std::shared_ptr CMenuScreen::createTab(size_t index) void CMenuScreen::show(Canvas & to) { - if(!config["video"].isNull()) - { - // redraw order: background -> video -> buttons and pictures - background->showAll(to); - CCS->videoh->update((int)config["video"]["x"].Float() + pos.x, (int)config["video"]["y"].Float() + pos.y, to.getInternalSurface(), true, false); - tabs->showAll(to); - } - CIntObject::show(to); + // TODO: avoid excessive redraws + CIntObject::showAll(to); } void CMenuScreen::activate() { - CCS->musich->playMusic(AudioPath::builtin("Music/MainMenu"), true, true); - if(!config["video"].isNull()) - CCS->videoh->open(VideoPath::fromJson(config["video"]["name"])); CIntObject::activate(); } -void CMenuScreen::deactivate() -{ - if(!config["video"].isNull()) - CCS->videoh->close(); - - CIntObject::deactivate(); -} - void CMenuScreen::switchToTab(size_t index) { tabs->setActive(index); @@ -146,7 +137,7 @@ size_t CMenuScreen::getActiveTab() const return tabs->getActive(); } -//funciton for std::string -> std::function conversion for main menu +//function for std::string -> std::function conversion for main menu static std::function genCommand(CMenuScreen * menu, std::vector menuType, const std::string & string) { static const std::vector commandType = {"to", "campaigns", "start", "load", "exit", "highscores"}; @@ -181,13 +172,13 @@ static std::function genCommand(CMenuScreen * menu, std::vector(ESelectionScreen::newGame); }; case 2: - return []() { CMainMenu::openLobby(ESelectionScreen::campaignList, true, {}, ELoadMode::NONE);}; + return []() { CMainMenu::openLobby(ESelectionScreen::campaignList, true, {}, ELoadMode::NONE); }; case 3: - return std::bind(CMainMenu::startTutorial); + return []() { CMainMenu::startTutorial(); }; } break; } @@ -196,25 +187,25 @@ static std::function genCommand(CMenuScreen * menu, std::vector(ESelectionScreen::loadGame); }; case 2: - return []() { CMainMenu::openLobby(ESelectionScreen::loadGame, true, {}, ELoadMode::CAMPAIGN);}; + return []() { CMainMenu::openLobby(ESelectionScreen::loadGame, true, {}, ELoadMode::CAMPAIGN); }; case 3: - return []() { CMainMenu::openLobby(ESelectionScreen::loadGame, true, {}, ELoadMode::TUTORIAL);}; + return []() { CMainMenu::openLobby(ESelectionScreen::loadGame, true, {}, ELoadMode::TUTORIAL); }; } } break; case 4: //exit { - return std::bind(CInfoWindow::showYesNoDialog, CGI->generaltexth->allTexts[69], std::vector>(), do_quit, 0, PlayerColor(1)); + return []() { CInfoWindow::showYesNoDialog(CGI->generaltexth->allTexts[69], std::vector>(), do_quit, 0, PlayerColor(1)); }; } break; case 5: //highscores { - return std::bind(CMainMenu::openHighScoreScreen); + return []() { CMainMenu::openHighScoreScreen(); }; } } } @@ -255,7 +246,7 @@ std::shared_ptr CMenuEntry::createButton(CMenuScreen * parent, const Js CMenuEntry::CMenuEntry(CMenuScreen * parent, const JsonNode & config) { - OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + OBJECT_CONSTRUCTION; setRedrawParent(true); pos = parent->pos; @@ -299,9 +290,8 @@ CMainMenu::CMainMenu() pos.w = GH.screenDimensions().x; pos.h = GH.screenDimensions().y; - GH.defActionsDef = 63; menu = std::make_shared(CMainMenuConfig::get().getConfig()["window"]); - OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + OBJECT_CONSTRUCTION; backgroundAroundMenu = std::make_shared(ImagePath::builtin("DIBOXBCK"), pos); } @@ -311,6 +301,35 @@ CMainMenu::~CMainMenu() GH.curInt = nullptr; } +void CMainMenu::playIntroVideos() +{ + auto playVideo = [](std::string video, bool rim, float scaleFactor, std::function cb){ + if(CCS->videoh->open(VideoPath::builtin(video), scaleFactor)) + GH.windows().createAndPushWindow(VideoPath::builtin(video), rim ? ImagePath::builtin("INTRORIM") : ImagePath::builtin(""), true, scaleFactor, [cb](bool skipped){ cb(skipped); }); + else + cb(true); + }; + + playVideo("3DOLOGO.SMK", false, 1.25, [playVideo, this](bool skipped){ + if(!skipped) + playVideo("NWCLOGO.SMK", false, 2, [playVideo, this](bool skipped){ + if(!skipped) + playVideo("H3INTRO.SMK", true, 1, [this](bool skipped){ + playMusic(); + }); + else + playMusic(); + }); + else + playMusic(); + }); +} + +void CMainMenu::playMusic() +{ + CCS->musich->playMusic(AudioPath::builtin("Music/MainMenu"), true, true); +} + void CMainMenu::activate() { // check if screen was resized while main menu was inactive - e.g. in gameplay mode @@ -343,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(); @@ -385,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); @@ -395,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; } @@ -445,7 +458,7 @@ std::shared_ptr CMainMenu::createPicture(const JsonNode & config) CMultiMode::CMultiMode(ESelectionScreen ScreenType) : screenType(ScreenType) { - OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + OBJECT_CONSTRUCTION; background = std::make_shared(ImagePath::builtin("MUPOPUP.bmp")); pos = background->center(); //center, window has size of bg graphic @@ -454,7 +467,7 @@ CMultiMode::CMultiMode(ESelectionScreen ScreenType) statusBar = CGStatusBar::create(std::make_shared(background->getSurface(), Rect(7, 465, 440, 18), 7, 465)); playerName = std::make_shared(Rect(19, 436, 334, 16), background->getSurface()); - playerName->setText(getPlayerName()); + playerName->setText(getPlayersNames()[0]); playerName->setCallback(std::bind(&CMultiMode::onNameChange, this, _1)); buttonHotseat = std::make_shared(Point(373, 78 + 57 * 0), AnimationPath::builtin("MUBHOT.DEF"), CGI->generaltexth->zelp[266], std::bind(&CMultiMode::hostTCP, this), EShortcut::MAIN_MENU_HOTSEAT); @@ -476,22 +489,35 @@ void CMultiMode::hostTCP() { auto savedScreenType = screenType; close(); - GH.windows().createAndPushWindow(getPlayerName(), savedScreenType, true, ELoadMode::MULTI); + GH.windows().createAndPushWindow(getPlayersNames(), savedScreenType, true, ELoadMode::MULTI); } void CMultiMode::joinTCP() { auto savedScreenType = screenType; close(); - GH.windows().createAndPushWindow(getPlayerName(), savedScreenType, false, ELoadMode::MULTI); + GH.windows().createAndPushWindow(getPlayersNames(), savedScreenType, false, ELoadMode::MULTI); } -std::string CMultiMode::getPlayerName() +std::vector CMultiMode::getPlayersNames() { - std::string name = settings["general"]["playerName"].String(); - if(name == "Player") - name = CGI->generaltexth->translate("core.genrltxt.434"); - return name; + std::vector playerNames; + + std::string playerNameStr = settings["general"]["playerName"].String(); + if (playerNameStr == "Player") + playerNameStr = CGI->generaltexth->translate("core.genrltxt.434"); + playerNames.push_back(playerNameStr); + + for (const auto & playerName : settings["general"]["multiPlayerNames"].Vector()) + { + const std::string &nameStr = playerName.String(); + if (!nameStr.empty()) + { + playerNames.push_back(nameStr); + } + } + + return playerNames; } void CMultiMode::onNameChange(std::string newText) @@ -500,16 +526,16 @@ void CMultiMode::onNameChange(std::string newText) name->String() = newText; } -CMultiPlayers::CMultiPlayers(const std::string & firstPlayer, ESelectionScreen ScreenType, bool Host, ELoadMode LoadMode) +CMultiPlayers::CMultiPlayers(const std::vector & playerNames, ESelectionScreen ScreenType, bool Host, ELoadMode LoadMode) : loadMode(LoadMode), screenType(ScreenType), host(Host) { - OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + OBJECT_CONSTRUCTION; background = std::make_shared(ImagePath::builtin("MUHOTSEA.bmp")); pos = background->center(); //center, window has size of bg graphic 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++) { @@ -521,7 +547,10 @@ CMultiPlayers::CMultiPlayers(const std::string & firstPlayer, ESelectionScreen S buttonCancel = std::make_shared(Point(205, 338), AnimationPath::builtin("MUBCANC.DEF"), CGI->generaltexth->zelp[561], [=](){ close();}, EShortcut::GLOBAL_CANCEL); statusBar = CGStatusBar::create(std::make_shared(background->getSurface(), Rect(7, 381, 348, 18), 7, 381)); - inputNames[0]->setText(firstPlayer); + for(int i = 0; i < playerNames.size(); i++) + { + inputNames[i]->setText(playerNames[i]); + } #ifndef VCMI_MOBILE inputNames[0]->giveFocus(); #endif @@ -533,22 +562,39 @@ void CMultiPlayers::onChange(std::string newText) void CMultiPlayers::enterSelectionScreen() { - std::vector names; - for(auto name : inputNames) + std::vector playerNames; + for(auto playerName : inputNames) { - if(name->getText().length()) - names.push_back(name->getText()); + if (playerName->getText().length()) + playerNames.push_back(playerName->getText()); } - Settings name = settings.write["general"]["playerName"]; - name->String() = names[0]; + Settings playerName = settings.write["general"]["playerName"]; + Settings multiPlayerNames = settings.write["general"]["multiPlayerNames"]; + multiPlayerNames->Vector().clear(); + if (!playerNames.empty()) + { + playerName->String() = playerNames.front(); + for (auto playerNameIt = playerNames.begin()+1; playerNameIt != playerNames.end(); playerNameIt++) + { + multiPlayerNames->Vector().push_back(JsonNode(*playerNameIt)); + } + } + else + { + // Without the check the saving crashes directly. + // When empty reset the player's name. This would translate to it being + // the default for the next run. But enables deleting players, by just + // deleting the names, otherwise some UI element should have been added. + playerName->clear(); + } - CMainMenu::openLobby(screenType, host, names, loadMode); + CMainMenu::openLobby(screenType, host, playerNames, loadMode); } CSimpleJoinScreen::CSimpleJoinScreen(bool host) { - OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + OBJECT_CONSTRUCTION; background = std::make_shared(ImagePath::builtin("MUDIALOG.bmp")); // address background pos = background->center(); //center, window has size of bg graphic (x,y = 396,278 w=232 h=212) @@ -607,9 +653,14 @@ void CSimpleJoinScreen::startConnection(const std::string & addr, ui16 port) } CLoadingScreen::CLoadingScreen() - : CWindowObject(BORDERED, getBackground()) + : CLoadingScreen(getBackground()) { - OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; +} + +CLoadingScreen::CLoadingScreen(ImagePath background) + : CWindowObject(BORDERED, background) +{ + OBJECT_CONSTRUCTION; addUsedEvents(TIME); diff --git a/client/mainmenu/CMainMenu.h b/client/mainmenu/CMainMenu.h index e4d62aef1..82b684a43 100644 --- a/client/mainmenu/CMainMenu.h +++ b/client/mainmenu/CMainMenu.h @@ -24,11 +24,10 @@ class CGStatusBar; class CTextBox; class CTabbedInt; class CAnimImage; -class CAnimation; class CButton; class CFilledTexture; class CLabel; - +class VideoWidget; // TODO: Find new location for these enums enum class ESelectionScreen : ui8 { @@ -48,6 +47,7 @@ class CMenuScreen : public CWindowObject std::shared_ptr tabs; std::shared_ptr background; + std::shared_ptr videoPlayer; std::vector> images; std::shared_ptr createTab(size_t index); @@ -57,9 +57,8 @@ public: CMenuScreen(const JsonNode & configNode); - void show(Canvas & to) override; void activate() override; - void deactivate() override; + void show(Canvas & to) override; void switchToTab(size_t index); void switchToTab(std::string name); @@ -96,7 +95,9 @@ public: void openLobby(); void hostTCP(); void joinTCP(); - std::string getPlayerName(); + + /// Get all configured player names. The first name would always be present and initialized to its default value. + std::vector getPlayersNames(); void onNameChange(std::string newText); }; @@ -118,7 +119,7 @@ class CMultiPlayers : public WindowBase void enterSelectionScreen(); public: - CMultiPlayers(const std::string & firstPlayer, ESelectionScreen ScreenType, bool Host, ELoadMode LoadMode); + CMultiPlayers(const std::vector & playerNames, ESelectionScreen ScreenType, bool Host, ELoadMode LoadMode); }; /// Manages the configuration of pregame GUI elements like campaign screen, main menu, loading screen,... @@ -141,6 +142,8 @@ class CMainMenu : public CIntObject, public IUpdateable, public std::enable_shar { std::shared_ptr backgroundAroundMenu; + std::vector videoPlayList; + CMainMenu(); //Use CMainMenu::create public: @@ -161,6 +164,8 @@ public: static std::shared_ptr createPicture(const JsonNode & config); + void playIntroVideos(); + void playMusic(); }; /// Simple window to enter the server's address. @@ -191,6 +196,7 @@ class CLoadingScreen : virtual public CWindowObject, virtual public Load::Progre public: CLoadingScreen(); + CLoadingScreen(ImagePath background); ~CLoadingScreen(); void tick(uint32_t msPassed) override; diff --git a/client/mainmenu/CPrologEpilogVideo.cpp b/client/mainmenu/CPrologEpilogVideo.cpp index b1a7e1dad..fbd7a9c22 100644 --- a/client/mainmenu/CPrologEpilogVideo.cpp +++ b/client/mainmenu/CPrologEpilogVideo.cpp @@ -12,28 +12,43 @@ #include "CPrologEpilogVideo.h" #include "../CGameInfo.h" -#include "../CMusicHandler.h" -#include "../CVideoHandler.h" -#include "../gui/WindowHandler.h" +#include "../media/IMusicPlayer.h" +#include "../media/ISoundPlayer.h" +//#include "../gui/WindowHandler.h" #include "../gui/CGuiHandler.h" -#include "../gui/FramerateManager.h" +//#include "../gui/FramerateManager.h" #include "../widgets/TextControls.h" +#include "../widgets/VideoWidget.h" #include "../widgets/Images.h" #include "../render/Canvas.h" - CPrologEpilogVideo::CPrologEpilogVideo(CampaignScenarioPrologEpilog _spe, std::function callback) : CWindowObject(BORDERED), spe(_spe), positionCounter(0), voiceSoundHandle(-1), videoSoundHandle(-1), exitCb(callback), elapsedTimeMilliseconds(0) { - OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + OBJECT_CONSTRUCTION; addUsedEvents(LCLICK | TIME); pos = center(Rect(0, 0, 800, 600)); backgroundAroundMenu = std::make_shared(ImagePath::builtin("DIBOXBCK"), Rect(-pos.x, -pos.y, GH.screenDimensions().x, GH.screenDimensions().y)); - auto audioData = CCS->videoh->getAudio(spe.prologVideo); - videoSoundHandle = CCS->soundh->playSound(audioData, -1); - CCS->videoh->open(spe.prologVideo); + //TODO: remove hardcoded paths. Some of campaigns video actually consist from 2 parts + // however, currently our campaigns format expects only a single video file + static const std::map pairedVideoFiles = { + { VideoPath::builtin("EVIL2AP1"), VideoPath::builtin("EVIL2AP2") }, + { VideoPath::builtin("H3ABdb4"), VideoPath::builtin("H3ABdb4b") }, + { VideoPath::builtin("H3x2_RNe1"), VideoPath::builtin("H3x2_RNe2") }, + }; + + if (pairedVideoFiles.count(spe.prologVideo)) + videoPlayer = std::make_shared(Point(0, 0), spe.prologVideo, pairedVideoFiles.at(spe.prologVideo), true); + else + videoPlayer = std::make_shared(Point(0, 0), spe.prologVideo, true); + + //some videos are 800x600 in size while some are 800x400 + 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); @@ -45,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) @@ -67,14 +85,14 @@ void CPrologEpilogVideo::tick(uint32_t msPassed) void CPrologEpilogVideo::show(Canvas & to) { to.drawColor(pos, Colors::BLACK); - //some videos are 800x600 in size while some are 800x400 - CCS->videoh->update(pos.x, pos.y + (CCS->videoh->size().y == 400 ? 100 : 0), to.getInternalSurface(), true, false); + videoPlayer->show(to); text->showAll(to); // blit text over video, if needed } 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/mainmenu/CPrologEpilogVideo.h b/client/mainmenu/CPrologEpilogVideo.h index 69dad8597..40c5bcfb8 100644 --- a/client/mainmenu/CPrologEpilogVideo.h +++ b/client/mainmenu/CPrologEpilogVideo.h @@ -13,6 +13,7 @@ #include "../../lib/campaign/CampaignScenarioPrologEpilog.h" class CMultiLineLabel; +class VideoWidget; class CFilledTexture; class CPrologEpilogVideo : public CWindowObject @@ -26,6 +27,7 @@ class CPrologEpilogVideo : public CWindowObject std::function exitCb; std::shared_ptr text; + std::shared_ptr videoPlayer; std::shared_ptr backgroundAroundMenu; bool voiceStopped = false; diff --git a/client/mainmenu/CStatisticScreen.cpp b/client/mainmenu/CStatisticScreen.cpp new file mode 100644 index 000000000..cb3ce3be7 --- /dev/null +++ b/client/mainmenu/CStatisticScreen.cpp @@ -0,0 +1,563 @@ +/* + * CStatisticScreen.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 "CStatisticScreen.h" +#include "../CGameInfo.h" + +#include "../gui/CGuiHandler.h" +#include "../gui/WindowHandler.h" +#include "../eventsSDL/InputHandler.h" +#include "../gui/Shortcut.h" + +#include "../render/Graphics.h" +#include "../render/IImage.h" +#include "../render/IRenderHandler.h" + +#include "../widgets/ComboBox.h" +#include "../widgets/Images.h" +#include "../widgets/GraphicalPrimitiveCanvas.h" +#include "../widgets/TextControls.h" +#include "../widgets/Buttons.h" +#include "../windows/InfoWindows.h" +#include "../widgets/Slider.h" + +#include "../../lib/gameState/GameStatistics.h" +#include "../../lib/gameState/CGameState.h" +#include "../../lib/texts/CGeneralTextHandler.h" +#include "../../lib/texts/TextOperations.h" + +#include + +std::string CStatisticScreen::getDay(int d) +{ + return std::to_string(CGameState::getDate(d, Date::MONTH)) + "/" + std::to_string(CGameState::getDate(d, Date::WEEK)) + "/" + std::to_string(CGameState::getDate(d, Date::DAY_OF_WEEK)); +} + +CStatisticScreen::CStatisticScreen(const StatisticDataSet & stat) + : CWindowObject(BORDERED), statistic(stat) +{ + OBJECT_CONSTRUCTION; + pos = center(Rect(0, 0, 800, 600)); + filledBackground = std::make_shared(Rect(0, 0, pos.w, pos.h)); + filledBackground->setPlayerColor(PlayerColor(1)); + + contentArea = Rect(10, 40, 780, 510); + layout.emplace_back(std::make_shared(400, 20, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->translate("vcmi.statisticWindow.statistics"))); + layout.emplace_back(std::make_shared(contentArea, ColorRGBA(0, 0, 0, 128), ColorRGBA(64, 80, 128, 255), 1)); + layout.emplace_back(std::make_shared(Point(725, 558), AnimationPath::builtin("MUBCHCK"), CButton::tooltip(), [this](){ close(); }, EShortcut::GLOBAL_ACCEPT)); + + buttonSelect = std::make_shared(Point(10, 564), AnimationPath::builtin("GSPBUT2"), CButton::tooltip(), [this](bool on){ onSelectButton(); }); + buttonSelect->setTextOverlay(CGI->generaltexth->translate("vcmi.statisticWindow.selectView"), EFonts::FONT_SMALL, Colors::YELLOW); + + buttonCsvSave = std::make_shared(Point(150, 564), AnimationPath::builtin("GSPBUT2"), CButton::tooltip(), [this](bool on){ GH.input().copyToClipBoard(statistic.toCsv("\t")); }); + buttonCsvSave->setTextOverlay(CGI->generaltexth->translate("vcmi.statisticWindow.tsvCopy"), EFonts::FONT_SMALL, Colors::YELLOW); + + mainContent = getContent(OVERVIEW, EGameResID::NONE); +} + +void CStatisticScreen::onSelectButton() +{ + std::vector texts; + for(auto & val : contentInfo) + texts.emplace_back(CGI->generaltexth->translate(std::get<0>(val.second))); + GH.windows().createAndPushWindow(texts, [this](int selectedIndex) + { + OBJECT_CONSTRUCTION; + if(!std::get<1>(contentInfo[static_cast(selectedIndex)])) + mainContent = getContent(static_cast(selectedIndex), EGameResID::NONE); + else + { + auto content = static_cast(selectedIndex); + auto possibleRes = std::vector{EGameResID::GOLD, EGameResID::WOOD, EGameResID::MERCURY, EGameResID::ORE, EGameResID::SULFUR, EGameResID::CRYSTAL, EGameResID::GEMS}; + std::vector resourceText; + for(const auto & res : possibleRes) + resourceText.emplace_back(CGI->generaltexth->translate(TextIdentifier("core.restypes", res.getNum()).get())); + + GH.windows().createAndPushWindow(resourceText, [this, content, possibleRes](int index) + { + OBJECT_CONSTRUCTION; + mainContent = getContent(content, possibleRes[index]); + }); + } + }); +} + +TData CStatisticScreen::extractData(const StatisticDataSet & stat, const ExtractFunctor & selector) const +{ + auto tmpData = stat.data; + std::sort(tmpData.begin(), tmpData.end(), [](const StatisticDataSetEntry & v1, const StatisticDataSetEntry & v2){ return v1.player == v2.player ? v1.day < v2.day : v1.player < v2.player; }); + + PlayerColor tmpColor = PlayerColor::NEUTRAL; + std::vector tmpColorSet; + TData plotData; + EPlayerStatus statusLastRound = EPlayerStatus::INGAME; + for(const auto & val : tmpData) + { + if(tmpColor != val.player) + { + if(tmpColorSet.size()) + { + plotData.push_back({graphics->playerColors[tmpColor.getNum()], std::vector(tmpColorSet)}); + tmpColorSet.clear(); + } + + tmpColor = val.player; + } + if(val.status == EPlayerStatus::INGAME || (statusLastRound == EPlayerStatus::INGAME && val.status == EPlayerStatus::LOSER)) + tmpColorSet.emplace_back(selector(val)); + statusLastRound = val.status; //to keep at least one dataset after loose + } + if(tmpColorSet.size()) + plotData.push_back({graphics->playerColors[tmpColor.getNum()], std::vector(tmpColorSet)}); + + return plotData; +} + +TIcons CStatisticScreen::extractIcons() const +{ + TIcons icons; + + auto tmpData = statistic.data; + std::sort(tmpData.begin(), tmpData.end(), [](const StatisticDataSetEntry & v1, const StatisticDataSetEntry & v2){ return v1.player == v2.player ? v1.day < v2.day : v1.player < v2.player; }); + + auto imageTown = GH.renderHandler().loadImage(AnimationPath::builtin("cradvntr"), 3, 0, EImageBlitMode::COLORKEY); + auto imageBattle = GH.renderHandler().loadImage(AnimationPath::builtin("cradvntr"), 5, 0, EImageBlitMode::COLORKEY); + auto imageDefeated = GH.renderHandler().loadImage(AnimationPath::builtin("crcombat"), 0, 0, EImageBlitMode::COLORKEY); + auto imageGrail = GH.renderHandler().loadImage(AnimationPath::builtin("vwsymbol"), 2, 0, EImageBlitMode::COLORKEY); + + std::map foundDefeated; + std::map foundGrail; + + for(const auto & val : tmpData) + { + if(val.eventCapturedTown) + icons.push_back({ graphics->playerColors[val.player], val.day, imageTown, CGI->generaltexth->translate("vcmi.statisticWindow.icon.townCaptured") }); + if(val.eventDefeatedStrongestHero) + icons.push_back({ graphics->playerColors[val.player], val.day, imageBattle, CGI->generaltexth->translate("vcmi.statisticWindow.icon.strongestHeroDefeated") }); + if(val.status == EPlayerStatus::LOSER && !foundDefeated[val.player]) + { + foundDefeated[val.player] = true; + icons.push_back({ graphics->playerColors[val.player], val.day, imageDefeated, CGI->generaltexth->translate("vcmi.statisticWindow.icon.defeated") }); + } + if(val.hasGrail && !foundGrail[val.player]) + { + foundGrail[val.player] = true; + icons.push_back({ graphics->playerColors[val.player], val.day, imageGrail, CGI->generaltexth->translate("vcmi.statisticWindow.icon.grailFound") }); + } + } + + return icons; +} + +std::shared_ptr CStatisticScreen::getContent(Content c, EGameResID res) +{ + TData plotData; + TIcons icons = extractIcons(); + + switch (c) + { + case OVERVIEW: + return std::make_shared(contentArea.resize(-15), CGI->generaltexth->translate(std::get<0>(contentInfo[c])), statistic); + + case CHART_RESOURCES: + plotData = extractData(statistic, [res](const StatisticDataSetEntry & val) -> float { return val.resources[res]; }); + return std::make_shared(contentArea.resize(-5), CGI->generaltexth->translate(std::get<0>(contentInfo[c])) + " - " + CGI->generaltexth->translate(TextIdentifier("core.restypes", res.getNum()).get()), plotData, icons, 0); + + case CHART_INCOME: + plotData = extractData(statistic, [](const StatisticDataSetEntry & val) -> float { return val.income; }); + return std::make_shared(contentArea.resize(-5), CGI->generaltexth->translate(std::get<0>(contentInfo[c])), plotData, icons, 0); + + case CHART_NUMBER_OF_HEROES: + plotData = extractData(statistic, [](const StatisticDataSetEntry & val) -> float { return val.numberHeroes; }); + return std::make_shared(contentArea.resize(-5), CGI->generaltexth->translate(std::get<0>(contentInfo[c])), plotData, icons, 0); + + case CHART_NUMBER_OF_TOWNS: + plotData = extractData(statistic, [](const StatisticDataSetEntry & val) -> float { return val.numberTowns; }); + return std::make_shared(contentArea.resize(-5), CGI->generaltexth->translate(std::get<0>(contentInfo[c])), plotData, icons, 0); + + case CHART_NUMBER_OF_ARTIFACTS: + plotData = extractData(statistic, [](const StatisticDataSetEntry & val) -> float { return val.numberArtifacts; }); + return std::make_shared(contentArea.resize(-5), CGI->generaltexth->translate(std::get<0>(contentInfo[c])), plotData, icons, 0); + + case CHART_NUMBER_OF_DWELLINGS: + plotData = extractData(statistic, [](const StatisticDataSetEntry & val) -> float { return val.numberDwellings; }); + return std::make_shared(contentArea.resize(-5), CGI->generaltexth->translate(std::get<0>(contentInfo[c])), plotData, icons, 0); + + case CHART_NUMBER_OF_MINES: + plotData = extractData(statistic, [res](StatisticDataSetEntry val) -> float { return val.numMines[res]; }); + return std::make_shared(contentArea.resize(-5), CGI->generaltexth->translate(std::get<0>(contentInfo[c])) + " - " + CGI->generaltexth->translate(TextIdentifier("core.restypes", res.getNum()).get()), plotData, icons, 0); + + case CHART_ARMY_STRENGTH: + plotData = extractData(statistic, [](const StatisticDataSetEntry & val) -> float { return val.armyStrength; }); + return std::make_shared(contentArea.resize(-5), CGI->generaltexth->translate(std::get<0>(contentInfo[c])), plotData, icons, 0); + + case CHART_EXPERIENCE: + plotData = extractData(statistic, [](const StatisticDataSetEntry & val) -> float { return val.totalExperience; }); + return std::make_shared(contentArea.resize(-5), CGI->generaltexth->translate(std::get<0>(contentInfo[c])), plotData, icons, 0); + + case CHART_RESOURCES_SPENT_ARMY: + plotData = extractData(statistic, [res](const StatisticDataSetEntry & val) -> float { return val.spentResourcesForArmy[res]; }); + return std::make_shared(contentArea.resize(-5), CGI->generaltexth->translate(std::get<0>(contentInfo[c])) + " - " + CGI->generaltexth->translate(TextIdentifier("core.restypes", res.getNum()).get()), plotData, icons, 0); + + case CHART_RESOURCES_SPENT_BUILDINGS: + plotData = extractData(statistic, [res](const StatisticDataSetEntry & val) -> float { return val.spentResourcesForBuildings[res]; }); + return std::make_shared(contentArea.resize(-5), CGI->generaltexth->translate(std::get<0>(contentInfo[c])) + " - " + CGI->generaltexth->translate(TextIdentifier("core.restypes", res.getNum()).get()), plotData, icons, 0); + + case CHART_MAP_EXPLORED: + plotData = extractData(statistic, [](const StatisticDataSetEntry & val) -> float { return val.mapExploredRatio; }); + return std::make_shared(contentArea.resize(-5), CGI->generaltexth->translate(std::get<0>(contentInfo[c])), plotData, icons, 1); + } + + return nullptr; +} + +StatisticSelector::StatisticSelector(const std::vector & texts, const std::function & cb) + : CWindowObject(BORDERED | NEEDS_ANIMATED_BACKGROUND), texts(texts), cb(cb) +{ + OBJECT_CONSTRUCTION; + pos = center(Rect(0, 0, 128 + 16, std::min(static_cast(texts.size()), LINES) * 40)); + filledBackground = std::make_shared(Rect(0, 0, pos.w, pos.h)); + filledBackground->setPlayerColor(PlayerColor(1)); + + slider = std::make_shared(Point(pos.w - 16, 0), pos.h, [this](int to){ update(to); redraw(); }, LINES, texts.size(), 0, Orientation::VERTICAL, CSlider::BLUE); + slider->setPanningStep(40); + slider->setScrollBounds(Rect(-pos.w + slider->pos.w, 0, pos.w, pos.h)); + + update(0); +} + +void StatisticSelector::update(int to) +{ + OBJECT_CONSTRUCTION; + buttons.clear(); + for(int i = to; i < LINES + to; i++) + { + if(i>=texts.size()) + continue; + + auto button = std::make_shared(Point(0, 10 + (i - to) * 40), AnimationPath::builtin("GSPBUT2"), CButton::tooltip(), [this, i](bool on){ close(); cb(i); }); + button->setTextOverlay(texts[i], EFonts::FONT_SMALL, Colors::WHITE); + buttons.emplace_back(button); + } +} + +OverviewPanel::OverviewPanel(Rect position, std::string title, const StatisticDataSet & stat) + : CIntObject(), data(stat) +{ + OBJECT_CONSTRUCTION; + + pos = position + pos.topLeft(); + + layout.emplace_back(std::make_shared(pos.w / 2, 10, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, title)); + + canvas = std::make_shared(Rect(0, Y_OFFS, pos.w - 16, pos.h - Y_OFFS)); + + dataExtract = { + { + CGI->generaltexth->translate("vcmi.statisticWindow.param.playerName"), [this](PlayerColor color){ + return playerDataFilter(color).front().playerName; + } + }, + { + CGI->generaltexth->translate("vcmi.statisticWindow.param.daysSurvived"), [this](PlayerColor color){ + auto playerData = playerDataFilter(color); + for(int i = 0; i < playerData.size(); i++) + if(playerData[i].status == EPlayerStatus::LOSER) + return CStatisticScreen::getDay(i + 1); + return CStatisticScreen::getDay(playerData.size()); + } + }, + { + CGI->generaltexth->translate("vcmi.statisticWindow.param.maxHeroLevel"), [this](PlayerColor color){ + int maxLevel = 0; + for(const auto & val : playerDataFilter(color)) + if(maxLevel < val.maxHeroLevel) + maxLevel = val.maxHeroLevel; + return std::to_string(maxLevel); + } + }, + { + CGI->generaltexth->translate("vcmi.statisticWindow.param.battleWinRatioHero"), [this](PlayerColor color){ + auto val = playerDataFilter(color).back(); + if(!val.numBattlesPlayer) + return std::string(""); + float tmp = (static_cast(val.numWinBattlesPlayer) / static_cast(val.numBattlesPlayer)) * 100; + return std::to_string(static_cast(tmp)) + " %"; + } + }, + { + CGI->generaltexth->translate("vcmi.statisticWindow.param.battleWinRatioNeutral"), [this](PlayerColor color){ + auto val = playerDataFilter(color).back(); + if(!val.numWinBattlesNeutral) + return std::string(""); + float tmp = (static_cast(val.numWinBattlesNeutral) / static_cast(val.numBattlesNeutral)) * 100; + return std::to_string(static_cast(tmp)) + " %"; + } + }, + { + CGI->generaltexth->translate("vcmi.statisticWindow.param.battlesHero"), [this](PlayerColor color){ + auto val = playerDataFilter(color).back(); + return std::to_string(val.numBattlesPlayer); + } + }, + { + CGI->generaltexth->translate("vcmi.statisticWindow.param.battlesNeutral"), [this](PlayerColor color){ + auto val = playerDataFilter(color).back(); + return std::to_string(val.numBattlesNeutral); + } + }, + { + CGI->generaltexth->translate("vcmi.statisticWindow.param.obeliskVisited"), [this](PlayerColor color){ + auto val = playerDataFilter(color).back(); + return std::to_string(static_cast(val.obeliskVisitedRatio * 100)) + " %"; + } + }, + { + CGI->generaltexth->translate("vcmi.statisticWindow.param.maxArmyStrength"), [this](PlayerColor color){ + int maxArmyStrength = 0; + for(const auto & val : playerDataFilter(color)) + if(maxArmyStrength < val.armyStrength) + maxArmyStrength = val.armyStrength; + return TextOperations::formatMetric(maxArmyStrength, 6); + } + }, + { + CGI->generaltexth->translate("vcmi.statisticWindow.param.tradeVolume") + " - " + CGI->generaltexth->translate(TextIdentifier("core.restypes", EGameResID::GOLD).get()), [this](PlayerColor color){ + auto val = playerDataFilter(color).back(); + return std::to_string(val.tradeVolume[EGameResID::GOLD]); + } + }, + { + CGI->generaltexth->translate("vcmi.statisticWindow.param.tradeVolume") + " - " + CGI->generaltexth->translate(TextIdentifier("core.restypes", EGameResID::WOOD).get()), [this](PlayerColor color){ + auto val = playerDataFilter(color).back(); + return std::to_string(val.tradeVolume[EGameResID::WOOD]); + } + }, + { + CGI->generaltexth->translate("vcmi.statisticWindow.param.tradeVolume") + " - " + CGI->generaltexth->translate(TextIdentifier("core.restypes", EGameResID::MERCURY).get()), [this](PlayerColor color){ + auto val = playerDataFilter(color).back(); + return std::to_string(val.tradeVolume[EGameResID::MERCURY]); + } + }, + { + CGI->generaltexth->translate("vcmi.statisticWindow.param.tradeVolume") + " - " + CGI->generaltexth->translate(TextIdentifier("core.restypes", EGameResID::ORE).get()), [this](PlayerColor color){ + auto val = playerDataFilter(color).back(); + return std::to_string(val.tradeVolume[EGameResID::ORE]); + } + }, + { + CGI->generaltexth->translate("vcmi.statisticWindow.param.tradeVolume") + " - " + CGI->generaltexth->translate(TextIdentifier("core.restypes", EGameResID::SULFUR).get()), [this](PlayerColor color){ + auto val = playerDataFilter(color).back(); + return std::to_string(val.tradeVolume[EGameResID::SULFUR]); + } + }, + { + CGI->generaltexth->translate("vcmi.statisticWindow.param.tradeVolume") + " - " + CGI->generaltexth->translate(TextIdentifier("core.restypes", EGameResID::CRYSTAL).get()), [this](PlayerColor color){ + auto val = playerDataFilter(color).back(); + return std::to_string(val.tradeVolume[EGameResID::CRYSTAL]); + } + }, + { + CGI->generaltexth->translate("vcmi.statisticWindow.param.tradeVolume") + " - " + CGI->generaltexth->translate(TextIdentifier("core.restypes", EGameResID::GEMS).get()), [this](PlayerColor color){ + auto val = playerDataFilter(color).back(); + return std::to_string(val.tradeVolume[EGameResID::GEMS]); + } + }, + }; + + int usedLines = dataExtract.size(); + + slider = std::make_shared(Point(pos.w - 16, Y_OFFS), pos.h - Y_OFFS, [this](int to){ update(to); setRedrawParent(true); redraw(); }, LINES - 1, usedLines, 0, Orientation::VERTICAL, CSlider::BLUE); + slider->setPanningStep(canvas->pos.h / LINES); + slider->setScrollBounds(Rect(-pos.w + slider->pos.w, 0, pos.w, canvas->pos.h)); + + fieldSize = Point(canvas->pos.w / (graphics->playerColors.size() + 2), canvas->pos.h / LINES); + for(int x = 0; x < graphics->playerColors.size() + 1; x++) + for(int y = 0; y < LINES; y++) + { + int xStart = (x + (x == 0 ? 0 : 1)) * fieldSize.x; + int yStart = y * fieldSize.y; + if(x == 0 || y == 0) + canvas->addBox(Point(xStart, yStart), Point(x == 0 ? 2 * fieldSize.x : fieldSize.x, fieldSize.y), ColorRGBA(0, 0, 0, 100)); + canvas->addRectangle(Point(xStart, yStart), Point(x == 0 ? 2 * fieldSize.x : fieldSize.x, fieldSize.y), ColorRGBA(127, 127, 127, 255)); + } + + update(0); +} + +std::vector OverviewPanel::playerDataFilter(PlayerColor color) +{ + std::vector tmpData; + std::copy_if(data.data.begin(), data.data.end(), std::back_inserter(tmpData), [color](const StatisticDataSetEntry & e){ return e.player == color; }); + return tmpData; +} + +void OverviewPanel::update(int to) +{ + OBJECT_CONSTRUCTION; + + content.clear(); + for(int y = to; y < LINES - 1 + to; y++) + { + if(y >= dataExtract.size()) + continue; + + for(int x = 0; x < PlayerColor::PLAYER_LIMIT_I + 1; x++) + { + if(y == to && x < PlayerColor::PLAYER_LIMIT_I) + content.emplace_back(std::make_shared(AnimationPath::builtin("ITGFLAGS"), x, 0, 180 + x * fieldSize.x, 35)); + int xStart = (x + (x == 0 ? 0 : 1)) * fieldSize.x + (x == 0 ? fieldSize.x : (fieldSize.x / 2)); + int yStart = Y_OFFS + (y + 1 - to) * fieldSize.y + (fieldSize.y / 2); + PlayerColor tmpColor(x - 1); + if(playerDataFilter(tmpColor).size() || x == 0) + content.emplace_back(std::make_shared(xStart, yStart, FONT_TINY, ETextAlignment::CENTER, Colors::WHITE, (x == 0 ? dataExtract[y].first : dataExtract[y].second(tmpColor)), x == 0 ? (fieldSize.x * 2) : fieldSize.x)); + } + } +} + +int computeGridStep(int maxAmount, int linesLimit) +{ + for (int lineInterval = 1;;lineInterval *= 10) + { + for (int factor : { 1, 2, 5 } ) + { + int lineIntervalToTest = lineInterval * factor; + if (maxAmount / lineIntervalToTest <= linesLimit) + return lineIntervalToTest; + } + } +} + +LineChart::LineChart(Rect position, std::string title, TData data, TIcons icons, float maxY) + : CIntObject(), maxVal(0), maxDay(0) +{ + OBJECT_CONSTRUCTION; + + addUsedEvents(LCLICK | MOVE | GESTURE); + + pos = position + pos.topLeft(); + + layout.emplace_back(std::make_shared(pos.w / 2, 20, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, title)); + + chartArea = pos.resize(-50); + chartArea.moveTo(Point(50, 50)); + + canvas = std::make_shared(Rect(0, 0, pos.w, pos.h)); + + statusBar = CGStatusBar::create(0, 0, ImagePath::builtin("radialMenu/statusBar")); + static_cast>(statusBar)->setEnabled(false); + + // additional calculations + bool skipMaxValCalc = maxY > 0; + maxVal = maxY; + for(const auto & line : data) + { + for(auto & val : line.second) + if(maxVal < val && !skipMaxValCalc) + maxVal = val; + if(maxDay < line.second.size()) + maxDay = line.second.size(); + } + + //calculate nice maxVal + int gridLineCount = 10; + int gridStep = computeGridStep(maxVal, gridLineCount); + niceMaxVal = gridStep * std::ceil(maxVal / gridStep); + niceMaxVal = std::max(1, niceMaxVal); // avoid zero size Y axis (if all values are 0) + + // calculate points in chart + auto getPoint = [this](int i, std::vector data){ + float x = (static_cast(chartArea.w) / static_cast(maxDay - 1)) * static_cast(i); + float y = static_cast(chartArea.h) - (static_cast(chartArea.h) / niceMaxVal) * data[i]; + return Point(x, y); + }; + + // draw grid (vertical lines) + int dayGridInterval = maxDay < 700 ? 7 : 28; + for(const auto & line : data) + { + for(int i = 0; i < line.second.size(); i += dayGridInterval) + { + Point p = getPoint(i, line.second) + chartArea.topLeft(); + canvas->addLine(Point(p.x, chartArea.topLeft().y), Point(p.x, chartArea.topLeft().y + chartArea.h), ColorRGBA(70, 70, 70)); + } + } + + // draw grid (horizontal lines) + if(maxVal > 0) + { + int gridStepPx = int((static_cast(chartArea.h) / niceMaxVal) * gridStep); + for(int i = 0; i < std::ceil(maxVal / gridStep) + 1; i++) + { + canvas->addLine(chartArea.topLeft() + Point(0, chartArea.h - gridStepPx * i), chartArea.topLeft() + Point(chartArea.w, chartArea.h - gridStepPx * i), ColorRGBA(70, 70, 70)); + layout.emplace_back(std::make_shared(chartArea.topLeft().x - 5, chartArea.topLeft().y + 10 + chartArea.h - gridStepPx * i, FONT_SMALL, ETextAlignment::CENTERRIGHT, Colors::WHITE, TextOperations::formatMetric(i * gridStep, 5))); + } + } + + // draw + for(const auto & line : data) + { + Point lastPoint(-1, -1); + for(int i = 0; i < line.second.size(); i++) + { + Point p = getPoint(i, line.second) + chartArea.topLeft(); + + if(lastPoint.x != -1) + canvas->addLine(lastPoint, p, line.first); + + // icons + for(auto & icon : icons) + if(std::get<0>(icon) == line.first && std::get<1>(icon) == i + 1) // color && day + { + pictures.emplace_back(std::make_shared(std::get<2>(icon), Point(p.x - (std::get<2>(icon)->width() / 2), p.y - (std::get<2>(icon)->height() / 2)))); + pictures.back()->addRClickCallback([icon](){ CRClickPopup::createAndPush(std::get<3>(icon)); }); + } + + lastPoint = p; + } + } + + // Axis + canvas->addLine(chartArea.topLeft() + Point(0, -10), chartArea.topLeft() + Point(0, chartArea.h + 10), Colors::WHITE); + canvas->addLine(chartArea.topLeft() + Point(-10, chartArea.h), chartArea.topLeft() + Point(chartArea.w + 10, chartArea.h), Colors::WHITE); + + Point p = chartArea.topLeft() + Point(chartArea.w + 10, chartArea.h + 10); + layout.emplace_back(std::make_shared(p.x, p.y, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, CStatisticScreen::getDay(maxDay))); + p = chartArea.bottomLeft() + Point(chartArea.w / 2, + 20); + layout.emplace_back(std::make_shared(p.x, p.y, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->translate("core.genrltxt.64"))); +} + +void LineChart::updateStatusBar(const Point & cursorPosition) +{ + statusBar->moveTo(cursorPosition + Point(-statusBar->pos.w / 2, 20)); + statusBar->fitToRect(pos, 10); + Rect r(pos.x + chartArea.x, pos.y + chartArea.y, chartArea.w, chartArea.h); + statusBar->setEnabled(r.isInside(cursorPosition)); + if(r.isInside(cursorPosition)) + { + float x = (static_cast(maxDay - 1) / static_cast(chartArea.w)) * (static_cast(cursorPosition.x) - static_cast(r.x)) + 1.0f; + float y = niceMaxVal - (niceMaxVal / static_cast(chartArea.h)) * (static_cast(cursorPosition.y) - static_cast(r.y)); + statusBar->write(CGI->generaltexth->translate("core.genrltxt.64") + ": " + CStatisticScreen::getDay(x) + " " + CGI->generaltexth->translate("vcmi.statisticWindow.value") + ": " + (static_cast(y) > 0 ? std::to_string(static_cast(y)) : std::to_string(y))); + } + setRedrawParent(true); + redraw(); +} + +void LineChart::mouseMoved(const Point & cursorPosition, const Point & lastUpdateDistance) +{ + updateStatusBar(cursorPosition); +} + +void LineChart::gesturePanning(const Point & initialPosition, const Point & currentPosition, const Point & lastUpdateDistance) +{ + updateStatusBar(currentPosition); +} diff --git a/client/mainmenu/CStatisticScreen.h b/client/mainmenu/CStatisticScreen.h new file mode 100644 index 000000000..1c28da504 --- /dev/null +++ b/client/mainmenu/CStatisticScreen.h @@ -0,0 +1,133 @@ +/* + * CStatisticScreen.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 "../windows/CWindowObject.h" +#include "../../lib/gameState/GameStatistics.h" + +class FilledTexturePlayerColored; +class CToggleButton; +class GraphicalPrimitiveCanvas; +class LineChart; +class CGStatusBar; +class ComboBox; +class CSlider; +class IImage; +class CPicture; + +using TData = std::vector>>; +using TIcons = std::vector, std::string>>; // Color, Day, Image, Helptext + +class CStatisticScreen : public CWindowObject +{ + enum Content { + OVERVIEW, + CHART_RESOURCES, + CHART_INCOME, + CHART_NUMBER_OF_HEROES, + CHART_NUMBER_OF_TOWNS, + CHART_NUMBER_OF_ARTIFACTS, + CHART_NUMBER_OF_DWELLINGS, + CHART_NUMBER_OF_MINES, + CHART_ARMY_STRENGTH, + CHART_EXPERIENCE, + CHART_RESOURCES_SPENT_ARMY, + CHART_RESOURCES_SPENT_BUILDINGS, + CHART_MAP_EXPLORED, + }; + std::map> contentInfo = { // tuple: textid, resource selection needed + { OVERVIEW, { "vcmi.statisticWindow.title.overview", false } }, + { CHART_RESOURCES, { "vcmi.statisticWindow.title.resources", true } }, + { CHART_INCOME, { "vcmi.statisticWindow.title.income", false } }, + { CHART_NUMBER_OF_HEROES, { "vcmi.statisticWindow.title.numberOfHeroes", false } }, + { CHART_NUMBER_OF_TOWNS, { "vcmi.statisticWindow.title.numberOfTowns", false } }, + { CHART_NUMBER_OF_ARTIFACTS, { "vcmi.statisticWindow.title.numberOfArtifacts", false } }, + { CHART_NUMBER_OF_DWELLINGS, { "vcmi.statisticWindow.title.numberOfDwellings", false } }, + { CHART_NUMBER_OF_MINES, { "vcmi.statisticWindow.title.numberOfMines", true } }, + { CHART_ARMY_STRENGTH, { "vcmi.statisticWindow.title.armyStrength", false } }, + { CHART_EXPERIENCE, { "vcmi.statisticWindow.title.experience", false } }, + { CHART_RESOURCES_SPENT_ARMY, { "vcmi.statisticWindow.title.resourcesSpentArmy", true } }, + { CHART_RESOURCES_SPENT_BUILDINGS, { "vcmi.statisticWindow.title.resourcesSpentBuildings", true } }, + { CHART_MAP_EXPLORED, { "vcmi.statisticWindow.title.mapExplored", false } }, + }; + + std::shared_ptr filledBackground; + std::vector> layout; + std::shared_ptr buttonCsvSave; + std::shared_ptr buttonSelect; + StatisticDataSet statistic; + std::shared_ptr mainContent; + Rect contentArea; + + using ExtractFunctor = std::function; + TData extractData(const StatisticDataSet & stat, const ExtractFunctor & selector) const; + TIcons extractIcons() const; + std::shared_ptr getContent(Content c, EGameResID res); + void onSelectButton(); +public: + CStatisticScreen(const StatisticDataSet & stat); + static std::string getDay(int day); +}; + +class StatisticSelector : public CWindowObject +{ + std::shared_ptr filledBackground; + std::vector> buttons; + std::shared_ptr slider; + + const int LINES = 10; + + std::vector texts; + std::function cb; + + void update(int to); +public: + StatisticSelector(const std::vector & texts, const std::function & cb); +}; + +class OverviewPanel : public CIntObject +{ + std::shared_ptr canvas; + std::vector> layout; + std::vector> content; + std::shared_ptr slider; + + Point fieldSize; + StatisticDataSet data; + + std::vector>> dataExtract; + + const int LINES = 15; + const int Y_OFFS = 30; + + std::vector playerDataFilter(PlayerColor color); + void update(int to); +public: + OverviewPanel(Rect position, std::string title, const StatisticDataSet & stat); +}; + +class LineChart : public CIntObject +{ + std::shared_ptr canvas; + std::vector> layout; + std::shared_ptr statusBar; + std::vector> pictures; + + Rect chartArea; + float maxVal; + int niceMaxVal; + int maxDay; + + void updateStatusBar(const Point & cursorPosition); +public: + LineChart(Rect position, std::string title, TData data, TIcons icons, float maxY); + + void mouseMoved(const Point & cursorPosition, const Point & lastUpdateDistance) override; + void gesturePanning(const Point & initialPosition, const Point & currentPosition, const Point & lastUpdateDistance) override; +}; diff --git a/client/mainmenu/CreditsScreen.cpp b/client/mainmenu/CreditsScreen.cpp index bd36ce98b..a83093ef6 100644 --- a/client/mainmenu/CreditsScreen.cpp +++ b/client/mainmenu/CreditsScreen.cpp @@ -22,12 +22,14 @@ #include "../../AUTHORS.h" CreditsScreen::CreditsScreen(Rect rect) - : CIntObject(LCLICK), positionCounter(0) + : CIntObject(LCLICK), timePassed(0) { pos.w = rect.w; pos.h = rect.h; setRedrawParent(true); - OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + OBJECT_CONSTRUCTION; + + addUsedEvents(TIME); std::string contributorsText = ""; std::string contributorsTask = ""; @@ -48,15 +50,15 @@ CreditsScreen::CreditsScreen(Rect rect) credits->scrollTextTo(-600); // move all text below the screen } -void CreditsScreen::show(Canvas & to) +void CreditsScreen::tick(uint32_t msPassed) { - CIntObject::show(to); - positionCounter++; - if(positionCounter % 2 == 0) - credits->scrollTextBy(1); + static const int timeToScrollByOnePx = 20; + timePassed += msPassed; + int scrollPosition = timePassed / timeToScrollByOnePx - 600; + credits->scrollTextTo(scrollPosition); //end of credits, close this screen - if(credits->textSize.y + 600 < positionCounter / 2) + if(credits->textSize.y < scrollPosition) clickPressed(GH.getCursorPosition()); } diff --git a/client/mainmenu/CreditsScreen.h b/client/mainmenu/CreditsScreen.h index d6055cfd0..d70287363 100644 --- a/client/mainmenu/CreditsScreen.h +++ b/client/mainmenu/CreditsScreen.h @@ -15,11 +15,11 @@ class CMultiLineLabel; class CreditsScreen : public CIntObject { - int positionCounter; + int timePassed; std::shared_ptr credits; public: CreditsScreen(Rect rect); - void show(Canvas & to) override; + void tick(uint32_t msPassed) override; void clickPressed(const Point & cursorPosition) override; }; diff --git a/client/mapView/IMapRendererContext.h b/client/mapView/IMapRendererContext.h index 1fab9394d..e1ae49f47 100644 --- a/client/mapView/IMapRendererContext.h +++ b/client/mapView/IMapRendererContext.h @@ -16,6 +16,7 @@ class Point; class CGObjectInstance; class ObjectInstanceID; struct TerrainTile; +class ColorRGBA; struct CGPath; VCMI_LIB_NAMESPACE_END @@ -67,6 +68,12 @@ public: /// returns index of image for overlay on specific tile, or numeric_limits::max if none virtual size_t overlayImageIndex(const int3 & coordinates) const = 0; + /// returns text that should be used as overlay for current tile + virtual std::string overlayText(const int3 & coordinates) const = 0; + + /// returns text that should be used as overlay for current tile + virtual ColorRGBA overlayTextColor(const int3 & coordinates) const = 0; + /// returns animation frame for terrain virtual size_t terrainImageIndex(size_t groupSize) const = 0; @@ -80,7 +87,10 @@ public: virtual bool showBorder() const = 0; /// if true, world view overlay will be shown - virtual bool showOverlay() const = 0; + virtual bool showImageOverlay() const = 0; + + // if true, new text overlay will be shown + virtual bool showTextOverlay() const = 0; /// if true, map grid should be visible on map virtual bool showGrid() const = 0; diff --git a/client/mapView/MapOverlayLogVisualizer.cpp b/client/mapView/MapOverlayLogVisualizer.cpp new file mode 100644 index 000000000..7e1678230 --- /dev/null +++ b/client/mapView/MapOverlayLogVisualizer.cpp @@ -0,0 +1,98 @@ +/* + * MapOverlayLogVisualizer.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 "MapOverlayLogVisualizer.h" +#include "MapViewModel.h" + +#include "../../lib/logging/VisualLogger.h" +#include "../render/Canvas.h" +#include "../render/Colors.h" +#include "../render/EFont.h" +#include "../render/IFont.h" +#include "../render/IRenderHandler.h" +#include "../render/Graphics.h" +#include "../gui/TextAlignment.h" +#include "../gui/CGuiHandler.h" + + +MapOverlayLogVisualizer::MapOverlayLogVisualizer(Canvas & target, std::shared_ptr model) + : target(target), model(model) +{ +} + +void MapOverlayLogVisualizer::drawLine(int3 start, int3 end) +{ + const Point offset = Point(30, 30); + + auto level = model->getLevel(); + + if(start.z != level || end.z != level) + return; + + auto pStart = model->getTargetTileArea(start).topLeft(); + auto pEnd = model->getTargetTileArea(end).topLeft(); + auto viewPort = target.getRenderArea(); + + pStart.x += 3; + pEnd.x -= 3; + + pStart += offset; + pEnd += offset; + + if(viewPort.isInside(pStart) && viewPort.isInside(pEnd)) + { + target.drawLine(pStart, pEnd, ColorRGBA(255, 255, 0), ColorRGBA(255, 0, 0)); + } +} + +void MapOverlayLogVisualizer::drawText( + int3 tile, + int lineNumber, + const std::string & text, + const std::optional & background) +{ + const Point offset = Point(6, 6); + + auto level = model->getLevel(); + + if(tile.z != level) + return; + + auto pStart = offset + model->getTargetTileArea(tile).topLeft(); + auto viewPort = target.getRenderArea(); + + ColorRGBA color = Colors::YELLOW; + + if(background) + { + color = ((background->b + background->r + background->g) < 300) + ? Colors::WHITE + : Colors::BLACK; + } + + if(viewPort.isInside(pStart)) + { + const auto & font = GH.renderHandler().loadFont(FONT_TINY); + + int w = font->getStringWidth(text); + int h = font->getLineHeight(); + + pStart.y += h * lineNumber; + + if(background) + { + target.drawColor(Rect(pStart, Point(w + 4, h)), *background); + pStart.x += 2; + } + + target.drawText(pStart, EFonts::FONT_TINY, color, ETextAlignment::TOPLEFT, text); + } +} diff --git a/client/mapView/MapOverlayLogVisualizer.h b/client/mapView/MapOverlayLogVisualizer.h new file mode 100644 index 000000000..c1d6bf254 --- /dev/null +++ b/client/mapView/MapOverlayLogVisualizer.h @@ -0,0 +1,33 @@ +/* + * MapOverlayLogVisualizer.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 "../../lib/logging/VisualLogger.h" + +VCMI_LIB_NAMESPACE_BEGIN + +class int3; + +VCMI_LIB_NAMESPACE_END + +class Canvas; +class MapViewModel; + +class MapOverlayLogVisualizer : public IMapOverlayLogVisualizer +{ +private: + Canvas & target; + std::shared_ptr model; + +public: + MapOverlayLogVisualizer(Canvas & target, std::shared_ptr model); + void drawLine(int3 start, int3 end) override; + void drawText(int3 tile, int lineNumber, const std::string & text, const std::optional & color) override; +}; diff --git a/client/mapView/MapRenderer.cpp b/client/mapView/MapRenderer.cpp index b541b30e3..1a2ae9089 100644 --- a/client/mapView/MapRenderer.cpp +++ b/client/mapView/MapRenderer.cpp @@ -21,6 +21,7 @@ #include "../render/IImage.h" #include "../render/IRenderHandler.h" #include "../render/Colors.h" +#include "../render/Graphics.h" #include "../../CCallback.h" @@ -104,31 +105,39 @@ void MapTileStorage::load(size_t index, const AnimationPath & filename, EImageBl for(auto & entry : terrainAnimations) { if (!filename.empty()) - { - entry = GH.renderHandler().loadAnimation(filename); - entry->preload(); - } - else - entry = GH.renderHandler().createAnimation(); - - for(size_t i = 0; i < entry->size(); ++i) - entry->getImage(i)->setBlitMode(blitMode); + entry = GH.renderHandler().loadAnimation(filename, blitMode); } - for(size_t i = 0; i < terrainAnimations[0]->size(); ++i) - { - terrainAnimations[1]->getImage(i)->verticalFlip(); - terrainAnimations[3]->getImage(i)->verticalFlip(); + if (terrainAnimations[1]) + terrainAnimations[1]->verticalFlip(); - terrainAnimations[2]->getImage(i)->horizontalFlip(); - terrainAnimations[3]->getImage(i)->horizontalFlip(); - } + if (terrainAnimations[3]) + terrainAnimations[3]->verticalFlip(); + + if (terrainAnimations[2]) + terrainAnimations[2]->horizontalFlip(); + + if (terrainAnimations[3]) + terrainAnimations[3]->horizontalFlip(); } -std::shared_ptr MapTileStorage::find(size_t fileIndex, size_t rotationIndex, size_t imageIndex) +std::shared_ptr MapTileStorage::find(size_t fileIndex, size_t rotationIndex, size_t imageIndex, size_t groupIndex) { const auto & animation = animations[fileIndex][rotationIndex]; - return animation->getImage(imageIndex); + if (animation) + return animation->getImage(imageIndex, groupIndex); + else + return nullptr; +} + +int MapTileStorage::groupCount(size_t fileIndex, size_t rotationIndex, size_t imageIndex) +{ + const auto & animation = animations[fileIndex][rotationIndex]; + if (animation) + for(int i = 0;; i++) + if(!animation->getImage(imageIndex, i, false)) + return i; + return 1; } MapRendererTerrain::MapRendererTerrain() @@ -136,7 +145,7 @@ MapRendererTerrain::MapRendererTerrain() { logGlobal->debug("Loading map terrains"); for(const auto & terrain : VLC->terrainTypeHandler->objects) - storage.load(terrain->getIndex(), terrain->tilesFilename, EImageBlitMode::OPAQUE); + storage.load(terrain->getIndex(), AnimationPath::builtin(terrain->tilesFilename.getName() + (terrain->paletteAnimation.size() ? "_Shifted": "")), EImageBlitMode::OPAQUE); logGlobal->debug("Done loading map terrains"); } @@ -144,22 +153,20 @@ 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; - const auto & image = storage.find(terrainIndex, rotationIndex, imageIndex); + int groupCount = storage.groupCount(terrainIndex, rotationIndex, imageIndex); + const auto & image = storage.find(terrainIndex, rotationIndex, imageIndex, groupCount > 1 ? context.terrainImageIndex(groupCount) : 0); 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) - image->shiftPalette(element.start, element.length, context.terrainImageIndex(element.length)); - target.draw(image, Point(0, 0)); } @@ -167,7 +174,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; } @@ -177,7 +184,7 @@ MapRendererRiver::MapRendererRiver() { logGlobal->debug("Loading map rivers"); for(const auto & river : VLC->riverTypeHandler->objects) - storage.load(river->getIndex(), river->tilesFilename, EImageBlitMode::COLORKEY); + storage.load(river->getIndex(), AnimationPath::builtin(river->tilesFilename.getName() + (river->paletteAnimation.size() ? "_Shifted": "")), EImageBlitMode::COLORKEY); logGlobal->debug("Done loading map rivers"); } @@ -185,17 +192,15 @@ 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) - image->shiftPalette(element.start, element.length, context.terrainImageIndex(element.length)); + int groupCount = storage.groupCount(terrainIndex, rotationIndex, imageIndex); + const auto & image = storage.find(terrainIndex, rotationIndex, imageIndex, groupCount > 1 ? context.terrainImageIndex(groupCount) : 0); target.draw(image, Point(0, 0)); } @@ -204,7 +209,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; } @@ -225,9 +230,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; @@ -237,9 +242,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; @@ -255,8 +260,7 @@ uint8_t MapRendererRoad::checksum(IMapRendererContext & context, const int3 & co MapRendererBorder::MapRendererBorder() { - animation = GH.renderHandler().loadAnimation(AnimationPath::builtin("EDG")); - animation->preload(); + animation = GH.renderHandler().loadAnimation(AnimationPath::builtin("EDG"), EImageBlitMode::OPAQUE); } size_t MapRendererBorder::getIndexForTile(IMapRendererContext & context, const int3 & tile) @@ -317,13 +321,8 @@ uint8_t MapRendererBorder::checksum(IMapRendererContext & context, const int3 & MapRendererFow::MapRendererFow() { - fogOfWarFullHide = GH.renderHandler().loadAnimation(AnimationPath::builtin("TSHRC")); - fogOfWarFullHide->preload(); - fogOfWarPartialHide = GH.renderHandler().loadAnimation(AnimationPath::builtin("TSHRE")); - fogOfWarPartialHide->preload(); - - for(size_t i = 0; i < fogOfWarFullHide->size(); ++i) - fogOfWarFullHide->getImage(i)->setBlitMode(EImageBlitMode::OPAQUE); + fogOfWarFullHide = GH.renderHandler().loadAnimation(AnimationPath::builtin("TSHRC"), EImageBlitMode::OPAQUE); + 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}; @@ -332,8 +331,7 @@ MapRendererFow::MapRendererFow() for(const int rotation : rotations) { fogOfWarPartialHide->duplicateImage(0, rotation, 0); - auto image = fogOfWarPartialHide->getImage(size, 0); - image->verticalFlip(); + fogOfWarPartialHide->verticalFlip(size, 0); size++; } } @@ -391,26 +389,26 @@ 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); + auto ret = GH.renderHandler().loadAnimation(filename, enableOverlay ? EImageBlitMode::WITH_SHADOW_AND_OVERLAY : EImageBlitMode::WITH_SHADOW); animations[filename] = ret; - ret->preload(); if(generateMovementGroups) { @@ -436,14 +434,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; @@ -456,7 +454,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; } @@ -487,23 +485,20 @@ void MapRendererObjects::renderImage(IMapRendererContext & context, Canvas & tar return; image->setAlpha(transparency); - image->setFlagColor(object->tempOwner); + if (object->ID != Obj::HERO) // heroes use separate image with flag instead of player-colored palette + { + if (object->getOwner().isValidPlayer()) + image->setOverlayColor(graphics->playerColors[object->getOwner().getNum()]); + + if (object->getOwner() == PlayerColor::NEUTRAL) + image->setOverlayColor(graphics->neutralColor); + } Point offsetPixels = context.objectImageOffset(object->id, coordinates); if ( offsetPixels.x < image->dimensions().x && offsetPixels.y < image->dimensions().y) { Point imagePos = image->dimensions() - offsetPixels - Point(32, 32); - - //if (transparency == 255) - //{ - // Canvas intermediate(Point(32,32)); - // intermediate.enableTransparency(true); - // image->setBlitMode(EImageBlitMode::OPAQUE); - // intermediate.draw(image, Point(0, 0), Rect(imagePos, Point(32,32))); - // target.draw(intermediate, Point(0,0)); - // return; - //} target.draw(image, Point(0, 0), Rect(imagePos, Point(32,32))); } } @@ -571,10 +566,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)) { } @@ -595,8 +590,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); } } @@ -630,9 +625,8 @@ uint8_t MapRendererOverlay::checksum(IMapRendererContext & context, const int3 & } MapRendererPath::MapRendererPath() - : pathNodes(GH.renderHandler().loadAnimation(AnimationPath::builtin("ADAG"))) + : pathNodes(GH.renderHandler().loadAnimation(AnimationPath::builtin("ADAG"), EImageBlitMode::SIMPLE)) { - pathNodes->preload(); } size_t MapRendererPath::selectImageReachability(bool reachableToday, size_t imageIndex) diff --git a/client/mapView/MapRenderer.h b/client/mapView/MapRenderer.h index 46bbca888..085846c63 100644 --- a/client/mapView/MapRenderer.h +++ b/client/mapView/MapRenderer.h @@ -23,7 +23,7 @@ class CAnimation; class IImage; class Canvas; class IMapRendererContext; -enum class EImageBlitMode; +enum class EImageBlitMode : uint8_t; class MapTileStorage { @@ -33,7 +33,8 @@ class MapTileStorage public: explicit MapTileStorage(size_t capacity); void load(size_t index, const AnimationPath & filename, EImageBlitMode blitMode); - std::shared_ptr find(size_t fileIndex, size_t rotationIndex, size_t imageIndex); + std::shared_ptr find(size_t fileIndex, size_t rotationIndex, size_t imageIndex, size_t groupIndex = 0); + int groupCount(size_t fileIndex, size_t rotationIndex, size_t imageIndex); }; class MapRendererTerrain @@ -77,7 +78,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 88348af09..c12c1ccc4 100644 --- a/client/mapView/MapRendererContext.cpp +++ b/client/mapView/MapRendererContext.cpp @@ -113,14 +113,14 @@ const CGPath * MapRendererBaseContext::currentPath() const size_t MapRendererBaseContext::objectGroupIndex(ObjectInstanceID objectID) const { - static const std::vector idleGroups = {0, 13, 0, 1, 2, 3, 4, 15, 14}; + static const std::array idleGroups = {0, 13, 0, 1, 2, 3, 4, 15, 14}; return idleGroups[getObjectRotation(objectID)]; } 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); } @@ -156,6 +156,16 @@ size_t MapRendererBaseContext::overlayImageIndex(const int3 & coordinates) const return std::numeric_limits::max(); } +std::string MapRendererBaseContext::overlayText(const int3 & coordinates) const +{ + return {}; +} + +ColorRGBA MapRendererBaseContext::overlayTextColor(const int3 & coordinates) const +{ + return {}; +} + double MapRendererBaseContext::viewTransitionProgress() const { return 0; @@ -181,7 +191,12 @@ bool MapRendererBaseContext::showBorder() const return false; } -bool MapRendererBaseContext::showOverlay() const +bool MapRendererBaseContext::showImageOverlay() const +{ + return false; +} + +bool MapRendererBaseContext::showTextOverlay() const { return false; } @@ -253,6 +268,59 @@ size_t MapRendererAdventureContext::terrainImageIndex(size_t groupSize) const return frameIndex; } +std::string MapRendererAdventureContext::overlayText(const int3 & coordinates) const +{ + if(!isVisible(coordinates)) + return {}; + + const auto & tile = getMapTile(coordinates); + + if (!tile.visitable()) + return {}; + + return tile.visitableObjects.back()->getObjectName(); +} + +ColorRGBA MapRendererAdventureContext::overlayTextColor(const int3 & coordinates) const +{ + if(!isVisible(coordinates)) + return {}; + + const auto & tile = getMapTile(coordinates); + + if (!tile.visitable()) + return {}; + + const auto * object = tile.visitableObjects.back(); + + if (object->getOwner() == LOCPLINT->playerID) + return { 0, 192, 0}; + + if (LOCPLINT->cb->getPlayerRelations(object->getOwner(), LOCPLINT->playerID) == PlayerRelations::ALLIES) + return { 0, 128, 255}; + + if (object->getOwner().isValidPlayer()) + return { 255, 0, 0}; + + if (object->ID == MapObjectID::MONSTER) + return { 255, 0, 0}; + + auto hero = LOCPLINT->localState->getCurrentHero(); + + if (hero) + { + if (object->wasVisited(hero)) + return { 160, 160, 160 }; + } + else + { + if (object->wasVisited(LOCPLINT->playerID)) + return { 160, 160, 160 }; + } + + return { 255, 192, 0 }; +} + bool MapRendererAdventureContext::showBorder() const { return true; @@ -273,6 +341,11 @@ bool MapRendererAdventureContext::showBlocked() const return settingShowBlocked; } +bool MapRendererAdventureContext::showTextOverlay() const +{ + return settingTextOverlay; +} + bool MapRendererAdventureContext::showSpellRange(const int3 & position) const { if (!settingSpellRange) @@ -330,7 +403,7 @@ size_t MapRendererAdventureMovingContext::objectGroupIndex(ObjectInstanceID obje { if(target == objectID) { - static const std::vector moveGroups = {0, 10, 5, 6, 7, 8, 9, 12, 11}; + static const std::array moveGroups = {0, 10, 5, 6, 7, 8, 9, 12, 11}; return moveGroups[getObjectRotation(objectID)]; } return MapRendererAdventureContext::objectGroupIndex(objectID); @@ -411,7 +484,7 @@ MapRendererWorldViewContext::MapRendererWorldViewContext(const MapRendererContex { } -bool MapRendererWorldViewContext::showOverlay() const +bool MapRendererWorldViewContext::showImageOverlay() const { return true; } @@ -425,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); @@ -475,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/MapRendererContext.h b/client/mapView/MapRendererContext.h index 742053fd1..b956f1f09 100644 --- a/client/mapView/MapRendererContext.h +++ b/client/mapView/MapRendererContext.h @@ -48,13 +48,16 @@ public: size_t objectImageIndex(ObjectInstanceID objectID, size_t groupSize) const override; size_t terrainImageIndex(size_t groupSize) const override; size_t overlayImageIndex(const int3 & coordinates) const override; + std::string overlayText(const int3 & coordinates) const override; + ColorRGBA overlayTextColor(const int3 & coordinates) const override; double viewTransitionProgress() const override; bool filterGrayscale() const override; bool showRoads() const override; bool showRivers() const override; bool showBorder() const override; - bool showOverlay() const override; + bool showImageOverlay() const override; + bool showTextOverlay() const override; bool showGrid() const override; bool showVisitable() const override; bool showBlocked() const override; @@ -69,6 +72,7 @@ public: bool settingShowVisitable = false; bool settingShowBlocked = false; bool settingSpellRange= false; + bool settingTextOverlay = false; bool settingsAdventureObjectAnimation = true; bool settingsAdventureTerrainAnimation = true; @@ -77,11 +81,14 @@ public: const CGPath * currentPath() const override; size_t objectImageIndex(ObjectInstanceID objectID, size_t groupSize) const override; size_t terrainImageIndex(size_t groupSize) const override; + std::string overlayText(const int3 & coordinates) const override; + ColorRGBA overlayTextColor(const int3 & coordinates) const override; bool showBorder() const override; bool showGrid() const override; bool showVisitable() const override; bool showBlocked() const override; + bool showTextOverlay() const override; bool showSpellRange(const int3 & position) const override; }; @@ -133,7 +140,7 @@ public: explicit MapRendererWorldViewContext(const MapRendererContextState & viewState); size_t overlayImageIndex(const int3 & coordinates) const override; - bool showOverlay() const override; + bool showImageOverlay() const override; }; class MapRendererSpellViewContext : public MapRendererWorldViewContext 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/MapView.cpp b/client/mapView/MapView.cpp index 8d6405fd5..522cca227 100644 --- a/client/mapView/MapView.cpp +++ b/client/mapView/MapView.cpp @@ -31,7 +31,8 @@ #include "../../lib/CConfigHandler.h" #include "../../lib/mapObjects/CGHeroInstance.h" -#include "../../lib/logging/VisualLogger.h" + +#include "MapOverlayLogVisualizer.h" BasicMapView::~BasicMapView() = default; @@ -52,56 +53,19 @@ BasicMapView::BasicMapView(const Point & offset, const Point & dimensions) , tilesCache(new MapViewCache(model)) , controller(new MapViewController(model, tilesCache)) { - OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + OBJECT_CONSTRUCTION; pos += offset; pos.w = dimensions.x; pos.h = dimensions.y; } -class VisualLoggerRenderer : public ILogVisualizer -{ -private: - Canvas & target; - std::shared_ptr model; - -public: - VisualLoggerRenderer(Canvas & target, std::shared_ptr model) : target(target), model(model) - { - } - - virtual void drawLine(int3 start, int3 end) override - { - const Point offset = Point(30, 30); - - auto level = model->getLevel(); - - if(start.z != level || end.z != level) - return; - - auto pStart = model->getTargetTileArea(start).topLeft(); - auto pEnd = model->getTargetTileArea(end).topLeft(); - auto viewPort = target.getRenderArea(); - - pStart.x += 3; - pEnd.x -= 3; - - pStart += offset; - pEnd += offset; - - if(viewPort.isInside(pStart) && viewPort.isInside(pEnd)) - { - target.drawLine(pStart, pEnd, ColorRGBA(255, 255, 0), ColorRGBA(255, 0, 0)); - } - } -}; - void BasicMapView::render(Canvas & target, bool fullUpdate) { Canvas targetClipped(target, pos); tilesCache->update(controller->getContext()); tilesCache->render(controller->getContext(), targetClipped, fullUpdate); - VisualLoggerRenderer r(target, model); + MapOverlayLogVisualizer r(target, model); logVisual->visualize(r); } @@ -141,7 +105,7 @@ void MapView::show(Canvas & to) MapView::MapView(const Point & offset, const Point & dimensions) : BasicMapView(offset, dimensions) { - OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + OBJECT_CONSTRUCTION; actions = std::make_shared(*this, model); actions->setContext(controller->getContext()); @@ -187,7 +151,7 @@ void MapView::postSwipe(uint32_t msPassed) std::pair firstAccepted; uint32_t now = GH.input().getTicks(); for (auto & x : swipeHistory) { - if(now - x.first < postSwipeCatchIntervalMs) { // only the last x ms are catched + if(now - x.first < postSwipeCatchIntervalMs) { // only the last x ms are caught if(firstAccepted.first == 0) firstAccepted = x; diff += x.second; diff --git a/client/mapView/MapViewActions.cpp b/client/mapView/MapViewActions.cpp index 6c87edba4..6b2041a57 100644 --- a/client/mapView/MapViewActions.cpp +++ b/client/mapView/MapViewActions.cpp @@ -34,7 +34,7 @@ MapViewActions::MapViewActions(MapView & owner, const std::shared_ptrgetPixelsVisibleDimensions().x; pos.h = model->getPixelsVisibleDimensions().y; - addUsedEvents(LCLICK | SHOW_POPUP | DRAG | GESTURE | HOVER | MOVE | WHEEL); + addUsedEvents(LCLICK | SHOW_POPUP | DRAG | DRAG_POPUP | GESTURE | HOVER | MOVE | WHEEL); } void MapViewActions::setContext(const std::shared_ptr & context) @@ -101,6 +101,11 @@ void MapViewActions::mouseDragged(const Point & cursorPosition, const Point & la owner.onMapSwiped(lastUpdateDistance); } +void MapViewActions::mouseDraggedPopup(const Point & cursorPosition, const Point & lastUpdateDistance) +{ + owner.onMapSwiped(lastUpdateDistance); +} + void MapViewActions::gesturePanning(const Point & initialPosition, const Point & currentPosition, const Point & lastUpdateDistance) { owner.onMapSwiped(lastUpdateDistance); diff --git a/client/mapView/MapViewActions.h b/client/mapView/MapViewActions.h index 02dd877aa..463f70780 100644 --- a/client/mapView/MapViewActions.h +++ b/client/mapView/MapViewActions.h @@ -42,6 +42,7 @@ public: void gesture(bool on, const Point & initialPosition, const Point & finalPosition) override; void mouseMoved(const Point & cursorPosition, const Point & lastUpdateDistance) override; void mouseDragged(const Point & cursorPosition, const Point & lastUpdateDistance) override; + void mouseDraggedPopup(const Point & cursorPosition, const Point & lastUpdateDistance) override; void wheelScrolled(int distance) override; bool dragActive; diff --git a/client/mapView/MapViewCache.cpp b/client/mapView/MapViewCache.cpp index 2bd21ce8d..34c06d0b7 100644 --- a/client/mapView/MapViewCache.cpp +++ b/client/mapView/MapViewCache.cpp @@ -18,9 +18,12 @@ #include "../render/CAnimation.h" #include "../render/Canvas.h" #include "../render/IImage.h" +#include "../render/IFont.h" #include "../render/IRenderHandler.h" +#include "../render/Graphics.h" #include "../gui/CGuiHandler.h" +#include "../widgets/TextControls.h" #include "../../lib/mapObjects/CObjectHandler.h" #include "../../lib/int3.h" @@ -30,16 +33,13 @@ MapViewCache::~MapViewCache() = default; MapViewCache::MapViewCache(const std::shared_ptr & model) : model(model) , cachedLevel(0) + , overlayWasVisible(false) , mapRenderer(new MapRenderer()) - , iconsStorage(GH.renderHandler().loadAnimation(AnimationPath::builtin("VwSymbol"))) - , intermediate(new Canvas(Point(32, 32))) - , terrain(new Canvas(model->getCacheDimensionsPixels())) - , terrainTransition(new Canvas(model->getPixelsVisibleDimensions())) + , iconsStorage(GH.renderHandler().loadAnimation(AnimationPath::builtin("VwSymbol"), EImageBlitMode::COLORKEY)) + , intermediate(new Canvas(Point(32, 32), CanvasScalingPolicy::AUTO)) + , terrain(new Canvas(model->getCacheDimensionsPixels(), CanvasScalingPolicy::AUTO)) + , terrainTransition(new Canvas(model->getPixelsVisibleDimensions(), CanvasScalingPolicy::AUTO)) { - iconsStorage->preload(); - for(size_t i = 0; i < iconsStorage->size(); ++i) - iconsStorage->getImage(i)->setBlitMode(EImageBlitMode::COLORKEY); - Point visibleSize = model->getTilesVisibleDimensions(); terrainChecksum.resize(boost::extents[visibleSize.x][visibleSize.y]); tilesUpToDate.resize(boost::extents[visibleSize.x][visibleSize.y]); @@ -141,7 +141,9 @@ void MapViewCache::update(const std::shared_ptr & context) void MapViewCache::render(const std::shared_ptr & context, Canvas & target, bool fullRedraw) { bool mapMoved = (cachedPosition != model->getMapViewCenter()); - bool lazyUpdate = !mapMoved && !fullRedraw && vstd::isAlmostZero(context->viewTransitionProgress()); + bool overlayVisible = context->showImageOverlay() || context->showTextOverlay(); + bool overlayVisibilityChanged = overlayVisible != overlayWasVisible; + bool lazyUpdate = !overlayVisibilityChanged && !mapMoved && !fullRedraw && vstd::isAlmostZero(context->viewTransitionProgress()); Rect dimensions = model->getTilesTotalRect(); @@ -165,18 +167,18 @@ void MapViewCache::render(const std::shared_ptr & context, } } - if(context->showOverlay()) + if(context->showImageOverlay()) { for(int y = dimensions.top(); y < dimensions.bottom(); ++y) { for(int x = dimensions.left(); x < dimensions.right(); ++x) { int3 tile(x, y, model->getLevel()); - Rect targetRect = model->getTargetTileArea(tile); auto overlay = getOverlayImageForTile(context, tile); if(overlay) { + Rect targetRect = model->getTargetTileArea(tile); Point position = targetRect.center() - overlay->dimensions() / 2; target.draw(overlay, position); } @@ -184,10 +186,42 @@ void MapViewCache::render(const std::shared_ptr & context, } } + if(context->showTextOverlay()) + { + const auto & font = GH.renderHandler().loadFont(FONT_TINY); + + for(int y = dimensions.top(); y < dimensions.bottom(); ++y) + { + for(int x = dimensions.left(); x < dimensions.right(); ++x) + { + int3 tile(x, y, model->getLevel()); + auto overlay = context->overlayText(tile); + + if(!overlay.empty()) + { + Rect targetRect = model->getTargetTileArea(tile); + Point position = targetRect.center(); + if (x % 2 == 0) + position.y += targetRect.h / 4; + else + position.y -= targetRect.h / 4; + + Point dimensions(font->getStringWidth(overlay), font->getLineHeight()); + Rect textRect = Rect(position - dimensions / 2, dimensions).resize(2); + + target.drawColor(textRect, context->overlayTextColor(tile)); + target.drawBorder(textRect, Colors::BRIGHT_YELLOW); + target.drawText(position, EFonts::FONT_TINY, Colors::BLACK, ETextAlignment::CENTER, overlay); + } + } + } + } + if(!vstd::isAlmostZero(context->viewTransitionProgress())) target.drawTransparent(*terrainTransition, Point(0, 0), 1.0 - context->viewTransitionProgress()); cachedPosition = model->getMapViewCenter(); + overlayWasVisible = overlayVisible; } void MapViewCache::createTransitionSnapshot(const std::shared_ptr & context) diff --git a/client/mapView/MapViewCache.h b/client/mapView/MapViewCache.h index 0435d3584..ad2ba2a7a 100644 --- a/client/mapView/MapViewCache.h +++ b/client/mapView/MapViewCache.h @@ -44,6 +44,7 @@ class MapViewCache Point cachedSize; Point cachedPosition; int cachedLevel; + bool overlayWasVisible; std::shared_ptr model; diff --git a/client/mapView/MapViewController.cpp b/client/mapView/MapViewController.cpp index 7ba38a162..fa653725b 100644 --- a/client/mapView/MapViewController.cpp +++ b/client/mapView/MapViewController.cpp @@ -224,6 +224,7 @@ void MapViewController::updateState() adventureContext->settingShowVisitable = settings["session"]["showVisitable"].Bool(); adventureContext->settingShowBlocked = settings["session"]["showBlocked"].Bool(); adventureContext->settingSpellRange = settings["session"]["showSpellRange"].Bool(); + adventureContext->settingTextOverlay = GH.isKeyboardAltDown(); } } @@ -316,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 f535fa61d..eb30680fd 100644 --- a/client/mapView/mapHandler.cpp +++ b/client/mapView/mapHandler.cpp @@ -17,7 +17,7 @@ #include "../CPlayerInterface.h" #include "../gui/CGuiHandler.h" -#include "../../lib/CGeneralTextHandler.h" +#include "../../lib/texts/CGeneralTextHandler.h" #include "../../lib/TerrainHandler.h" #include "../../lib/mapObjectConstructors/CObjectClassesHandler.h" #include "../../lib/mapObjects/CGHeroInstance.h" @@ -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/CAudioBase.cpp b/client/media/CAudioBase.cpp new file mode 100644 index 000000000..24bbc1eff --- /dev/null +++ b/client/media/CAudioBase.cpp @@ -0,0 +1,43 @@ +/* + * CAudioBase.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 "CAudioBase.h" + +#include + +int CAudioBase::initializationCounter = 0; +bool CAudioBase::initializeSuccess = false; + +CAudioBase::CAudioBase() +{ + if(initializationCounter == 0) + { + if(Mix_OpenAudio(44100, MIX_DEFAULT_FORMAT, 2, 1024) == -1) + logGlobal->error("Mix_OpenAudio error: %s", Mix_GetError()); + else + initializeSuccess = true; + } + ++initializationCounter; +} + +bool CAudioBase::isInitialized() const +{ + return initializeSuccess; +} + +CAudioBase::~CAudioBase() +{ + --initializationCounter; + + if(initializationCounter == 0 && initializeSuccess) + Mix_CloseAudio(); + + initializeSuccess = false; +} diff --git a/client/media/CAudioBase.h b/client/media/CAudioBase.h new file mode 100644 index 000000000..94437db51 --- /dev/null +++ b/client/media/CAudioBase.h @@ -0,0 +1,22 @@ +/* + * CAudioBase.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 + +class CAudioBase : boost::noncopyable +{ + static int initializationCounter; + static bool initializeSuccess; + +protected: + bool isInitialized() const; + + CAudioBase(); + ~CAudioBase(); +}; diff --git a/client/media/CEmptyVideoPlayer.h b/client/media/CEmptyVideoPlayer.h new file mode 100644 index 000000000..6497f20a2 --- /dev/null +++ b/client/media/CEmptyVideoPlayer.h @@ -0,0 +1,28 @@ +/* + * CEmptyVideoPlayer.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 "IVideoPlayer.h" + +class CEmptyVideoPlayer final : public IVideoPlayer +{ +public: + /// Load video from specified path + std::unique_ptr open(const VideoPath & name, float scaleFactor) override + { + return nullptr; + }; + + /// Extracts audio data from provided video in wav format + std::pair, si64> getAudio(const VideoPath & videoToOpen) override + { + return {nullptr, 0}; + }; +}; diff --git a/client/media/CMusicHandler.cpp b/client/media/CMusicHandler.cpp new file mode 100644 index 000000000..0a04b0633 --- /dev/null +++ b/client/media/CMusicHandler.cpp @@ -0,0 +1,367 @@ +/* + * CMusicHandler.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 "CMusicHandler.h" + +#include "../CGameInfo.h" +#include "../eventsSDL/InputHandler.h" +#include "../gui/CGuiHandler.h" +#include "../renderSDL/SDLRWwrapper.h" + +#include "../../lib/entities/faction/CFaction.h" +#include "../../lib/entities/faction/CTown.h" +#include "../../lib/entities/faction/CTownHandler.h" +#include "../../lib/CRandomGenerator.h" +#include "../../lib/TerrainHandler.h" +#include "../../lib/filesystem/Filesystem.h" + +#include + +void CMusicHandler::onVolumeChange(const JsonNode & volumeNode) +{ + setVolume(volumeNode.Integer()); +} + +CMusicHandler::CMusicHandler(): + listener(settings.listen["general"]["music"]) +{ + listener(std::bind(&CMusicHandler::onVolumeChange, this, _1)); + + auto mp3files = CResourceHandler::get()->getFilteredFiles([](const ResourcePath & id) -> bool + { + if(id.getType() != EResType::SOUND) + return false; + + if(!boost::algorithm::istarts_with(id.getName(), "MUSIC/")) + return false; + + logGlobal->trace("Found music file %s", id.getName()); + return true; + }); + + for(const ResourcePath & file : mp3files) + { + if(boost::algorithm::istarts_with(file.getName(), "MUSIC/Combat")) + addEntryToSet("battle", AudioPath::fromResource(file)); + else if(boost::algorithm::istarts_with(file.getName(), "MUSIC/AITheme")) + addEntryToSet("enemy-turn", AudioPath::fromResource(file)); + } + + if (isInitialized()) + { + Mix_HookMusicFinished([]() + { + CCS->musich->musicFinishedCallback(); + }); + } +} + +void CMusicHandler::loadTerrainMusicThemes() +{ + for(const auto & terrain : CGI->terrainTypeHandler->objects) + { + for(const auto & filename : terrain->musicFilename) + addEntryToSet("terrain_" + terrain->getJsonKey(), filename); + } + + for(const auto & faction : CGI->townh->objects) + { + if (!faction || !faction->hasTown()) + continue; + + for(const auto & filename : faction->town->clientInfo.musicTheme) + addEntryToSet("faction_" + faction->getJsonKey(), filename); + } +} + +void CMusicHandler::addEntryToSet(const std::string & set, const AudioPath & musicURI) +{ + musicsSet[set].push_back(musicURI); +} + +CMusicHandler::~CMusicHandler() +{ + if(isInitialized()) + { + boost::mutex::scoped_lock guard(mutex); + + Mix_HookMusicFinished(nullptr); + current->stop(); + + current.reset(); + next.reset(); + } +} + +void CMusicHandler::playMusic(const AudioPath & musicURI, bool loop, bool fromStart) +{ + boost::mutex::scoped_lock guard(mutex); + + if(current && current->isPlaying() && current->isTrack(musicURI)) + return; + + queueNext(this, "", musicURI, loop, fromStart); +} + +void CMusicHandler::playMusicFromSet(const std::string & musicSet, const std::string & entryID, bool loop, bool fromStart) +{ + playMusicFromSet(musicSet + "_" + entryID, loop, fromStart); +} + +void CMusicHandler::playMusicFromSet(const std::string & whichSet, bool loop, bool fromStart) +{ + boost::mutex::scoped_lock guard(mutex); + + auto selectedSet = musicsSet.find(whichSet); + if(selectedSet == musicsSet.end()) + { + logGlobal->error("Error: playing music from non-existing set: %s", whichSet); + return; + } + + if(current && current->isPlaying() && current->isSet(whichSet)) + return; + + // in this mode - play random track from set + queueNext(this, whichSet, AudioPath(), loop, fromStart); +} + +void CMusicHandler::queueNext(std::unique_ptr queued) +{ + if(!isInitialized()) + return; + + next = std::move(queued); + + if(current == nullptr || !current->stop(1000)) + { + current.reset(next.release()); + current->play(); + } +} + +void CMusicHandler::queueNext(CMusicHandler * owner, const std::string & setName, const AudioPath & musicURI, bool looped, bool fromStart) +{ + queueNext(std::make_unique(owner, setName, musicURI, looped, fromStart)); +} + +void CMusicHandler::stopMusic(int fade_ms) +{ + if(!isInitialized()) + return; + + boost::mutex::scoped_lock guard(mutex); + + if(current != nullptr) + current->stop(fade_ms); + next.reset(); +} + +ui32 CMusicHandler::getVolume() const +{ + return volume; +} + +void CMusicHandler::setVolume(ui32 percent) +{ + volume = std::min(100u, percent); + + if(isInitialized()) + Mix_VolumeMusic((MIX_MAX_VOLUME * volume) / 100); +} + +void CMusicHandler::musicFinishedCallback() +{ + // call music restart in separate thread to avoid deadlock in some cases + // It is possible for: + // 1) SDL thread to call this method on end of playback + // 2) VCMI code to call queueNext() method to queue new file + // this leads to: + // 1) SDL thread waiting to acquire music lock in this method (while keeping internal SDL mutex locked) + // 2) VCMI thread waiting to acquire internal SDL mutex (while keeping music mutex locked) + + GH.dispatchMainThread( + [this]() + { + boost::unique_lock lockGuard(mutex); + if(current != nullptr) + { + // if music is looped, play it again + if(current->play()) + return; + else + current.reset(); + } + + if(current == nullptr && next != nullptr) + { + current.reset(next.release()); + current->play(); + } + } + ); +} + +MusicEntry::MusicEntry(CMusicHandler * owner, std::string setName, const AudioPath & musicURI, bool looped, bool fromStart) + : owner(owner) + , music(nullptr) + , setName(std::move(setName)) + , startTime(static_cast(-1)) + , startPosition(0) + , loop(looped ? -1 : 1) + , fromStart(fromStart) + , playing(false) + +{ + if(!musicURI.empty()) + load(musicURI); +} + +MusicEntry::~MusicEntry() +{ + if(playing && loop > 0) + { + assert(0); + logGlobal->error("Attempt to delete music while playing!"); + Mix_HaltMusic(); + } + + if(loop == 0 && Mix_FadingMusic() != MIX_NO_FADING) + { + assert(0); + logGlobal->error("Attempt to delete music while fading out!"); + Mix_HaltMusic(); + } + + logGlobal->trace("Del-ing music file %s", currentName.getOriginalName()); + if(music) + Mix_FreeMusic(music); +} + +void MusicEntry::load(const AudioPath & musicURI) +{ + if(music) + { + logGlobal->trace("Del-ing music file %s", currentName.getOriginalName()); + Mix_FreeMusic(music); + music = nullptr; + } + + if(CResourceHandler::get()->existsResource(musicURI)) + currentName = musicURI; + else + currentName = musicURI.addPrefix("MUSIC/"); + + music = nullptr; + + logGlobal->trace("Loading music file %s", currentName.getOriginalName()); + + try + { + 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) + { + logGlobal->error("Failed to load music. setName=%s\tmusicURI=%s", setName, currentName.getOriginalName()); + logGlobal->error("Exception: %s", e.what()); + } + + if(!music) + { + logGlobal->warn("Warning: Cannot open %s: %s", currentName.getOriginalName(), Mix_GetError()); + return; + } +} + +bool MusicEntry::play() +{ + if(!(loop--) && music) //already played once - return + return false; + + if(!setName.empty()) + { + const auto & set = owner->musicsSet[setName]; + const auto & iter = RandomGeneratorUtil::nextItem(set, CRandomGenerator::getDefault()); + load(*iter); + } + + logGlobal->trace("Playing music file %s", currentName.getOriginalName()); + + if(!fromStart && owner->trackPositions.count(currentName) > 0 && owner->trackPositions[currentName] > 0) + { + float timeToStart = owner->trackPositions[currentName]; + startPosition = std::round(timeToStart * 1000); + + // erase stored position: + // if music track will be interrupted again - new position will be written in stop() method + // if music track is not interrupted and will finish by timeout/end of file - it will restart from beginning as it should + owner->trackPositions.erase(owner->trackPositions.find(currentName)); + + if(Mix_FadeInMusicPos(music, 1, 1000, timeToStart) == -1) + { + logGlobal->error("Unable to play music (%s)", Mix_GetError()); + return false; + } + } + else + { + startPosition = 0; + + if(Mix_PlayMusic(music, 1) == -1) + { + logGlobal->error("Unable to play music (%s)", Mix_GetError()); + return false; + } + } + + startTime = GH.input().getTicks(); + + playing = true; + return true; +} + +bool MusicEntry::stop(int fade_ms) +{ + if(Mix_PlayingMusic()) + { + playing = false; + loop = 0; + uint32_t endTime = GH.input().getTicks(); + assert(startTime != uint32_t(-1)); + float playDuration = (endTime - startTime + startPosition) / 1000.f; + owner->trackPositions[currentName] = playDuration; + logGlobal->trace("Stopping music file %s at %f", currentName.getOriginalName(), playDuration); + + Mix_FadeOutMusic(fade_ms); + return true; + } + return false; +} + +bool MusicEntry::isPlaying() const +{ + return playing; +} + +bool MusicEntry::isSet(const std::string & set) +{ + return !setName.empty() && set == setName; +} + +bool MusicEntry::isTrack(const AudioPath & track) +{ + return setName.empty() && track == currentName; +} diff --git a/client/media/CMusicHandler.h b/client/media/CMusicHandler.h new file mode 100644 index 000000000..b85463d4a --- /dev/null +++ b/client/media/CMusicHandler.h @@ -0,0 +1,95 @@ +/* + * CMusicHandler.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 "CAudioBase.h" +#include "IMusicPlayer.h" + +#include "../lib/CConfigHandler.h" + +struct _Mix_Music; +using Mix_Music = struct _Mix_Music; + +class CMusicHandler; + +//Class for handling one music file +class MusicEntry : boost::noncopyable +{ + CMusicHandler * owner; + Mix_Music * music; + + //if not null - set from which music will be randomly selected + std::string setName; + AudioPath currentName; + + uint32_t startTime; + uint32_t startPosition; + int loop; // -1 = indefinite + bool fromStart; + bool playing; + + void load(const AudioPath & musicURI); + +public: + MusicEntry(CMusicHandler * owner, std::string setName, const AudioPath & musicURI, bool looped, bool fromStart); + ~MusicEntry(); + + bool isSet(const std::string & setName); + bool isTrack(const AudioPath & trackName); + bool isPlaying() const; + + bool play(); + bool stop(int fade_ms = 0); +}; + +class CMusicHandler final : public CAudioBase, public IMusicPlayer +{ +private: + //update volume on configuration change + SettingsListener listener; + void onVolumeChange(const JsonNode & volumeNode); + + std::unique_ptr current; + std::unique_ptr next; + + boost::mutex mutex; + int volume = 0; // from 0 (mute) to 100 + + void queueNext(CMusicHandler * owner, const std::string & setName, const AudioPath & musicURI, bool looped, bool fromStart); + void queueNext(std::unique_ptr queued); + void musicFinishedCallback() final; + + /// map -> + std::map> musicsSet; + /// stored position, in seconds at which music player should resume playing this track + std::map trackPositions; + +public: + CMusicHandler(); + ~CMusicHandler(); + + /// add entry with URI musicURI in set. Track will have ID musicID + void addEntryToSet(const std::string & set, const AudioPath & musicURI); + + void loadTerrainMusicThemes() final; + void setVolume(ui32 percent) final; + ui32 getVolume() const final; + + /// play track by URI, if loop = true music will be looped + void playMusic(const AudioPath & musicURI, bool loop, bool fromStart) final; + /// play random track from this set + void playMusicFromSet(const std::string & musicSet, bool loop, bool fromStart) final; + /// play random track from set (musicSet, entryID) + void playMusicFromSet(const std::string & musicSet, const std::string & entryID, bool loop, bool fromStart) final; + /// stops currently playing music by fading out it over fade_ms and starts next scheduled track, if any + void stopMusic(int fade_ms) final; + + friend class MusicEntry; +}; diff --git a/client/media/CSoundHandler.cpp b/client/media/CSoundHandler.cpp new file mode 100644 index 000000000..bd099e728 --- /dev/null +++ b/client/media/CSoundHandler.cpp @@ -0,0 +1,406 @@ +/* + * CMusicHandler.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 "CSoundHandler.h" + +#include "../gui/CGuiHandler.h" +#include "../CGameInfo.h" + +#include "../lib/filesystem/Filesystem.h" +#include "../lib/CRandomGenerator.h" + +#include + +#define VCMI_SOUND_NAME(x) +#define VCMI_SOUND_FILE(y) #y, + +// sounds mapped to soundBase enum +static const std::string soundsList[] = { + "", // invalid + "", // todo + VCMI_SOUND_LIST +}; +#undef VCMI_SOUND_NAME +#undef VCMI_SOUND_FILE + +void CSoundHandler::onVolumeChange(const JsonNode & volumeNode) +{ + setVolume(volumeNode.Integer()); +} + +CSoundHandler::CSoundHandler(): + listener(settings.listen["general"]["sound"]), + ambientConfig(JsonPath::builtin("config/ambientSounds.json")) +{ + listener(std::bind(&CSoundHandler::onVolumeChange, this, _1)); + + if(ambientConfig["allocateChannels"].isNumber()) + Mix_AllocateChannels(ambientConfig["allocateChannels"].Integer()); + + if(isInitialized()) + { + Mix_ChannelFinished([](int channel) + { + if (CCS) + { + CCS->soundh->soundFinishedCallback(channel); + } + }); + } +} + +CSoundHandler::~CSoundHandler() +{ + if(isInitialized()) + { + Mix_HaltChannel(-1); + + for(auto & chunk : soundChunks) + { + if(chunk.second.first) + Mix_FreeChunk(chunk.second.first); + } + } +} + +// Allocate an SDL chunk and cache it. +Mix_Chunk * CSoundHandler::GetSoundChunk(const AudioPath & sound, bool cache) +{ + try + { + if(cache && soundChunks.find(sound) != soundChunks.end()) + return soundChunks[sound].first; + + auto data = CResourceHandler::get()->load(sound.addPrefix("SOUNDS/"))->readAll(); + SDL_RWops * ops = SDL_RWFromMem(data.first.get(), data.second); + Mix_Chunk * chunk = Mix_LoadWAV_RW(ops, 1); // will free ops + + if(cache) + soundChunks.insert({sound, std::make_pair(chunk, std::move(data.first))}); + + return chunk; + } + catch(std::exception & e) + { + logGlobal->warn("Cannot get sound %s chunk: %s", sound.getOriginalName(), e.what()); + return nullptr; + } +} + +Mix_Chunk * CSoundHandler::GetSoundChunk(std::pair, si64> & data, bool cache) +{ + try + { + std::vector startBytes = std::vector(data.first.get(), data.first.get() + std::min(static_cast(100), data.second)); + + if(cache && soundChunksRaw.find(startBytes) != soundChunksRaw.end()) + return soundChunksRaw[startBytes].first; + + SDL_RWops * ops = SDL_RWFromMem(data.first.get(), data.second); + Mix_Chunk * chunk = Mix_LoadWAV_RW(ops, 1); // will free ops + + if(cache) + soundChunksRaw.insert({startBytes, std::make_pair(chunk, std::move(data.first))}); + + return chunk; + } + catch(std::exception & e) + { + logGlobal->warn("Cannot get sound chunk: %s", e.what()); + return nullptr; + } +} + +int CSoundHandler::ambientDistToVolume(int distance) const +{ + const auto & distancesVector = ambientConfig["distances"].Vector(); + + if(distance >= distancesVector.size()) + return 0; + + int volumeByDistance = static_cast(distancesVector[distance].Integer()); + return volumeByDistance * ambientConfig["volume"].Integer() / 100; +} + +void CSoundHandler::ambientStopSound(const AudioPath & soundId) +{ + stopSound(ambientChannels[soundId]); + setChannelVolume(ambientChannels[soundId], volume); +} + +uint32_t CSoundHandler::getSoundDurationMilliseconds(const AudioPath & sound) +{ + if(!isInitialized() || sound.empty()) + return 0; + + auto resourcePath = sound.addPrefix("SOUNDS/"); + + if(!CResourceHandler::get()->existsResource(resourcePath)) + return 0; + + auto data = CResourceHandler::get()->load(resourcePath)->readAll(); + + uint32_t milliseconds = 0; + + 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) + { + Uint32 sampleSizeBytes = (fmt & 0xFF) / 8; + Uint32 samples = (chunk->alen / sampleSizeBytes); + Uint32 frames = (samples / channels); + milliseconds = ((frames * 1000) / freq); + + Mix_FreeChunk(chunk); + } + + return milliseconds; +} + +// Plays a sound, and return its channel so we can fade it out later +int CSoundHandler::playSound(soundBase::soundID soundID, int repeats) +{ + assert(soundID < soundBase::sound_after_last); + auto sound = AudioPath::builtin(soundsList[soundID]); + logGlobal->trace("Attempt to play sound %d with file name %s with cache", soundID, sound.getOriginalName()); + + return playSound(sound, repeats, true); +} + +int CSoundHandler::playSound(const AudioPath & sound, int repeats, bool cache) +{ + if(!isInitialized() || sound.empty()) + return -1; + + int channel; + Mix_Chunk * chunk = GetSoundChunk(sound, cache); + + if(chunk) + { + channel = Mix_PlayChannel(-1, chunk, repeats); + if(channel == -1) + { + logGlobal->error("Unable to play sound file %s , error %s", sound.getOriginalName(), Mix_GetError()); + if(!cache) + Mix_FreeChunk(chunk); + } + else if(cache) + initCallback(channel); + else + initCallback(channel, [chunk](){ Mix_FreeChunk(chunk);}); + } + else + channel = -1; + + return channel; +} + +int CSoundHandler::playSound(std::pair, si64> & data, int repeats, bool cache) +{ + int channel = -1; + if(Mix_Chunk * chunk = GetSoundChunk(data, cache)) + { + channel = Mix_PlayChannel(-1, chunk, repeats); + if(channel == -1) + { + logGlobal->error("Unable to play sound, error %s", Mix_GetError()); + if(!cache) + Mix_FreeChunk(chunk); + } + else if(cache) + initCallback(channel); + else + initCallback(channel, [chunk](){ Mix_FreeChunk(chunk);}); + } + return channel; +} + +// Helper. Randomly select a sound from an array and play it +int CSoundHandler::playSoundFromSet(std::vector & sound_vec) +{ + return playSound(*RandomGeneratorUtil::nextItem(sound_vec, CRandomGenerator::getDefault())); +} + +void CSoundHandler::stopSound(int handler) +{ + if(isInitialized() && handler != -1) + 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; +} + +// Sets the sound volume, from 0 (mute) to 100 +void CSoundHandler::setVolume(ui32 percent) +{ + volume = std::min(100u, percent); + + if(isInitialized()) + { + setChannelVolume(-1, volume); + + for(const auto & channel : channelVolumes) + updateChannelVolume(channel.first); + } +} + +void CSoundHandler::updateChannelVolume(int channel) +{ + if(channelVolumes.count(channel)) + setChannelVolume(channel, getVolume() * channelVolumes[channel] / 100); + else + setChannelVolume(channel, getVolume()); +} + +// Sets the sound volume, from 0 (mute) to 100 +void CSoundHandler::setChannelVolume(int channel, ui32 percent) +{ + Mix_Volume(channel, (MIX_MAX_VOLUME * percent) / 100); +} + +void CSoundHandler::setCallback(int channel, std::function function) +{ + boost::mutex::scoped_lock lockGuard(mutexCallbacks); + + auto iter = callbacks.find(channel); + + //channel not found. It may have finished so fire callback now + if(iter == callbacks.end()) + function(); + else + iter->second.push_back(function); +} + +void CSoundHandler::resetCallback(int channel) +{ + boost::mutex::scoped_lock lockGuard(mutexCallbacks); + + callbacks.erase(channel); +} + +void CSoundHandler::soundFinishedCallback(int channel) +{ + boost::mutex::scoped_lock lockGuard(mutexCallbacks); + + if(callbacks.count(channel) == 0) + return; + + // store callbacks from container locally - SDL might reuse this channel for another sound + // but do actually execution in separate thread, to avoid potential deadlocks in case if callback requires locks of its own + auto callback = callbacks.at(channel); + callbacks.erase(channel); + + if(!callback.empty()) + { + GH.dispatchMainThread( + [callback]() + { + for(const auto & entry : callback) + entry(); + } + ); + } +} + +void CSoundHandler::initCallback(int channel) +{ + boost::mutex::scoped_lock lockGuard(mutexCallbacks); + assert(callbacks.count(channel) == 0); + callbacks[channel] = {}; +} + +void CSoundHandler::initCallback(int channel, const std::function & function) +{ + boost::mutex::scoped_lock lockGuard(mutexCallbacks); + assert(callbacks.count(channel) == 0); + callbacks[channel].push_back(function); +} + +int CSoundHandler::ambientGetRange() const +{ + return ambientConfig["range"].Integer(); +} + +void CSoundHandler::ambientUpdateChannels(std::map soundsArg) +{ + boost::mutex::scoped_lock guard(mutex); + + std::vector stoppedSounds; + for(const auto & pair : ambientChannels) + { + const auto & soundId = pair.first; + const int channel = pair.second; + + if(!vstd::contains(soundsArg, soundId)) + { + ambientStopSound(soundId); + stoppedSounds.push_back(soundId); + } + else + { + int channelVolume = ambientDistToVolume(soundsArg[soundId]); + channelVolumes[channel] = channelVolume; + updateChannelVolume(channel); + } + } + for(const auto & soundId : stoppedSounds) + { + channelVolumes.erase(ambientChannels[soundId]); + ambientChannels.erase(soundId); + } + + for(const auto & pair : soundsArg) + { + const auto & soundId = pair.first; + const int distance = pair.second; + + if(!vstd::contains(ambientChannels, soundId)) + { + int channel = playSound(soundId, -1); + int channelVolume = ambientDistToVolume(distance); + channelVolumes[channel] = channelVolume; + + updateChannelVolume(channel); + ambientChannels[soundId] = channel; + } + } +} + +void CSoundHandler::ambientStopAllChannels() +{ + boost::mutex::scoped_lock guard(mutex); + + for(const auto & ch : ambientChannels) + { + ambientStopSound(ch.first); + } + channelVolumes.clear(); + ambientChannels.clear(); +} diff --git a/client/media/CSoundHandler.h b/client/media/CSoundHandler.h new file mode 100644 index 000000000..3450cbffb --- /dev/null +++ b/client/media/CSoundHandler.h @@ -0,0 +1,80 @@ +/* + * CSoundHandler.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 "CAudioBase.h" +#include "ISoundPlayer.h" + +#include "../lib/CConfigHandler.h" + +struct Mix_Chunk; + +class CSoundHandler final : public CAudioBase, public ISoundPlayer +{ +private: + //update volume on configuration change + SettingsListener listener; + void onVolumeChange(const JsonNode & volumeNode); + + using CachedChunk = std::pair>; + std::map soundChunks; + std::map, CachedChunk> soundChunksRaw; + + Mix_Chunk * GetSoundChunk(const AudioPath & sound, bool cache); + Mix_Chunk * GetSoundChunk(std::pair, si64> & data, bool cache); + + /// have entry for every currently active channel + /// vector will be empty if callback was not set + std::map>> callbacks; + + /// Protects access to callbacks member to avoid data races: + /// SDL calls sound finished callbacks from audio thread + boost::mutex mutexCallbacks; + + int ambientDistToVolume(int distance) const; + void ambientStopSound(const AudioPath & soundId); + void updateChannelVolume(int channel); + + const JsonNode ambientConfig; + + boost::mutex mutex; + std::map ambientChannels; + std::map channelVolumes; + int volume = 0; + + void initCallback(int channel, const std::function & function); + void initCallback(int channel); + +public: + CSoundHandler(); + ~CSoundHandler(); + + ui32 getVolume() const final; + void setVolume(ui32 percent) final; + void setChannelVolume(int channel, ui32 percent); + + // Sounds + uint32_t getSoundDurationMilliseconds(const AudioPath & sound) final; + int playSound(soundBase::soundID soundID, int repeats = 0) final; + int playSound(const AudioPath & sound, int repeats = 0, bool cache = false) final; + 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; + void soundFinishedCallback(int channel) final; + + int ambientGetRange() const final; + void ambientUpdateChannels(std::map currentSounds) final; + void ambientStopAllChannels() final; +}; diff --git a/client/media/CVideoHandler.cpp b/client/media/CVideoHandler.cpp new file mode 100644 index 000000000..8ba26d5e1 --- /dev/null +++ b/client/media/CVideoHandler.cpp @@ -0,0 +1,678 @@ +/* + * CVideoHandler.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 "CVideoHandler.h" + +#ifndef DISABLE_VIDEO + +#include "ISoundPlayer.h" + +#include "../CGameInfo.h" +#include "../CMT.h" +#include "../eventsSDL/InputHandler.h" +#include "../gui/CGuiHandler.h" +#include "../render/Canvas.h" +#include "../render/IScreenHandler.h" +#include "../renderSDL/SDL_Extensions.h" + +#include "../../lib/filesystem/CInputStream.h" +#include "../../lib/filesystem/Filesystem.h" +#include "../../lib/texts/CGeneralTextHandler.h" +#include "../../lib/texts/Languages.h" + +#include + +extern "C" { +#include +#include +#include +#include +#include +#include +} + +// Define a set of functions to read data +static int lodRead(void * opaque, uint8_t * buf, int size) +{ + auto * data = static_cast(opaque); + auto bytesRead = data->read(buf, size); + if(bytesRead == 0) + return AVERROR_EOF; + + return bytesRead; +} + +static si64 lodSeek(void * opaque, si64 pos, int whence) +{ + auto * data = static_cast(opaque); + + if(whence & AVSEEK_SIZE) + return data->getSize(); + + return data->seek(pos); +} + +[[noreturn]] static void throwFFmpegError(int errorCode) +{ + std::array errorMessage{}; + av_strerror(errorCode, errorMessage.data(), errorMessage.size()); + + throw std::runtime_error(errorMessage.data()); +} + +static std::unique_ptr findVideoData(const VideoPath & videoToOpen) +{ + if(CResourceHandler::get()->existsResource(videoToOpen)) + return CResourceHandler::get()->load(videoToOpen); + + auto highQualityVideoToOpenWithDir = videoToOpen.addPrefix("VIDEO/"); + auto lowQualityVideo = videoToOpen.toType(); + auto lowQualityVideoWithDir = highQualityVideoToOpenWithDir.toType(); + + if(CResourceHandler::get()->existsResource(highQualityVideoToOpenWithDir)) + return CResourceHandler::get()->load(highQualityVideoToOpenWithDir); + + if(CResourceHandler::get()->existsResource(lowQualityVideo)) + return CResourceHandler::get()->load(lowQualityVideo); + + if(CResourceHandler::get()->existsResource(lowQualityVideoWithDir)) + return CResourceHandler::get()->load(lowQualityVideoWithDir); + + return nullptr; +} + +bool FFMpegStream::openInput(const VideoPath & videoToOpen) +{ + input = findVideoData(videoToOpen); + + return input != nullptr; +} + +void FFMpegStream::openContext() +{ + static const int BUFFER_SIZE = 4096; + input->seek(0); + + auto * buffer = static_cast(av_malloc(BUFFER_SIZE)); // will be freed by ffmpeg + context = avio_alloc_context(buffer, BUFFER_SIZE, 0, input.get(), lodRead, nullptr, lodSeek); + + formatContext = avformat_alloc_context(); + formatContext->pb = context; + // filename is not needed - file was already open and stored in this->data; + int avfopen = avformat_open_input(&formatContext, "dummyFilename", nullptr, nullptr); + + if(avfopen != 0) + throwFFmpegError(avfopen); + + // Retrieve stream information + int findStreamInfo = avformat_find_stream_info(formatContext, nullptr); + + if(avfopen < 0) + throwFFmpegError(findStreamInfo); +} + +void FFMpegStream::openCodec(int desiredStreamIndex) +{ + streamIndex = desiredStreamIndex; + + // Find the decoder for the stream + codec = avcodec_find_decoder(formatContext->streams[streamIndex]->codecpar->codec_id); + + if(codec == nullptr) + throw std::runtime_error("Unsupported codec"); + + codecContext = avcodec_alloc_context3(codec); + if(codecContext == nullptr) + throw std::runtime_error("Failed to create codec context"); + + // Get a pointer to the codec context for the video stream + int ret = avcodec_parameters_to_context(codecContext, formatContext->streams[streamIndex]->codecpar); + if(ret < 0) + { + //We cannot get codec from parameters + avcodec_free_context(&codecContext); + throwFFmpegError(ret); + } + + // Open codec + ret = avcodec_open2(codecContext, codec, nullptr); + if(ret < 0) + { + // Could not open codec + codec = nullptr; + throwFFmpegError(ret); + } + + // Allocate video frame + frame = av_frame_alloc(); +} + +const AVCodecParameters * FFMpegStream::getCodecParameters() const +{ + return formatContext->streams[streamIndex]->codecpar; +} + +const AVCodecContext * FFMpegStream::getCodecContext() const +{ + return codecContext; +} + +const AVFrame * FFMpegStream::getCurrentFrame() const +{ + return frame; +} + +void CVideoInstance::openVideo() +{ + openContext(); + openCodec(findVideoStream()); +} + +void CVideoInstance::prepareOutput(float scaleFactor, bool useTextureOutput) +{ + //setup scaling + dimensions = Point(getCodecContext()->width * scaleFactor, getCodecContext()->height * scaleFactor) * GH.screenHandler().getScalingFactor(); + + // Allocate a place to put our YUV image on that screen + if (useTextureOutput) + { + std::array potentialFormats = { + AV_PIX_FMT_YUV420P, // -> SDL_PIXELFORMAT_IYUV - most of H3 videos use YUV format, so it is preferred to save some space & conversion time + AV_PIX_FMT_RGB32, // -> SDL_PIXELFORMAT_ARGB8888 - some .smk videos actually use palette, so RGB > YUV. This is also our screen texture format + AV_PIX_FMT_NONE + }; + + auto preferredFormat = avcodec_find_best_pix_fmt_of_list(potentialFormats.data(), getCodecContext()->pix_fmt, false, nullptr); + + if (preferredFormat == AV_PIX_FMT_YUV420P) + textureYUV = SDL_CreateTexture( mainRenderer, SDL_PIXELFORMAT_IYUV, SDL_TEXTUREACCESS_STREAMING, dimensions.x, dimensions.y); + else + textureRGB = SDL_CreateTexture( mainRenderer, SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_STREAMING, dimensions.x, dimensions.y); + sws = sws_getContext(getCodecContext()->width, getCodecContext()->height, getCodecContext()->pix_fmt, + dimensions.x, dimensions.y, preferredFormat, + SWS_BICUBIC, nullptr, nullptr, nullptr); + } + else + { + surface = CSDL_Ext::newSurface(dimensions); + sws = sws_getContext(getCodecContext()->width, getCodecContext()->height, getCodecContext()->pix_fmt, + dimensions.x, dimensions.y, AV_PIX_FMT_RGB32, + SWS_BICUBIC, nullptr, nullptr, nullptr); + } + + if (sws == nullptr) + throw std::runtime_error("Failed to create sws"); +} + +void FFMpegStream::decodeNextFrame() +{ + int rc = avcodec_receive_frame(codecContext, frame); + + // frame extracted - data that was sent to codecContext before was sufficient + if (rc == 0) + return; + + // returning AVERROR(EAGAIN) is legal - this indicates that codec requires more data from input stream to decode next frame + if(rc != AVERROR(EAGAIN)) + throwFFmpegError(rc); + + for(;;) + { + AVPacket packet; + + // codecContext does not have enough input data - read next packet from input stream + int ret = av_read_frame(formatContext, &packet); + if(ret < 0) + { + if(ret == AVERROR_EOF) + { + av_packet_unref(&packet); + av_frame_free(&frame); + frame = nullptr; + return; + } + throwFFmpegError(ret); + } + + // Is this a packet from the stream that needs decoding? + if(packet.stream_index == streamIndex) + { + // Decode read packet + // Note: this method may return AVERROR(EAGAIN). However this should never happen with ffmpeg API + // since there is guaranteed call to avcodec_receive_frame and ffmpeg API promises that *both* of these methods will never return AVERROR(EAGAIN). + int rc = avcodec_send_packet(codecContext, &packet); + if(rc < 0) + throwFFmpegError(rc); + + rc = avcodec_receive_frame(codecContext, frame); + if(rc == AVERROR(EAGAIN)) + { + // still need more data - read next packet + av_packet_unref(&packet); + continue; + } + else if(rc < 0) + { + throwFFmpegError(rc); + } + else + { + // read succesful. Exit the loop + av_packet_unref(&packet); + return; + } + } + av_packet_unref(&packet); + } +} + +bool CVideoInstance::loadNextFrame() +{ + decodeNextFrame(); + const AVFrame * frame = getCurrentFrame(); + + if(!frame) + return false; + + uint8_t * data[4] = {}; + int linesize[4] = {}; + + if(textureYUV) + { + av_image_alloc(data, linesize, dimensions.x, dimensions.y, AV_PIX_FMT_YUV420P, 1); + sws_scale(sws, frame->data, frame->linesize, 0, getCodecContext()->height, data, linesize); + SDL_UpdateYUVTexture(textureYUV, nullptr, data[0], linesize[0], data[1], linesize[1], data[2], linesize[2]); + av_freep(&data[0]); + } + if(textureRGB) + { + av_image_alloc(data, linesize, dimensions.x, dimensions.y, AV_PIX_FMT_RGB32, 1); + sws_scale(sws, frame->data, frame->linesize, 0, getCodecContext()->height, data, linesize); + SDL_UpdateTexture(textureRGB, nullptr, data[0], linesize[0]); + av_freep(&data[0]); + } + if(surface) + { + // Avoid buffer overflow caused by sws_scale(): + // http://trac.ffmpeg.org/ticket/9254 + + size_t pic_bytes = surface->pitch * surface->h; + size_t ffmped_pad = 1024; /* a few bytes of overflow will go here */ + void * for_sws = av_malloc(pic_bytes + ffmped_pad); + data[0] = (ui8 *)for_sws; + linesize[0] = surface->pitch; + + sws_scale(sws, frame->data, frame->linesize, 0, getCodecContext()->height, data, linesize); + memcpy(surface->pixels, for_sws, pic_bytes); + av_free(for_sws); + } + return true; +} + + +double CVideoInstance::timeStamp() +{ + return getCurrentFrameEndTime(); +} + +bool CVideoInstance::videoEnded() +{ + return getCurrentFrame() == nullptr; +} + +CVideoInstance::CVideoInstance() + : startTimeInitialized(false), deactivationStartTimeHandling(false) +{ +} + +CVideoInstance::~CVideoInstance() +{ + sws_freeContext(sws); + SDL_DestroyTexture(textureYUV); + SDL_DestroyTexture(textureRGB); + SDL_FreeSurface(surface); +} + +FFMpegStream::~FFMpegStream() +{ + av_frame_free(&frame); + +#if (LIBAVCODEC_VERSION_MAJOR < 61 ) + // deprecated, apparently no longer necessary - avcodec_free_context should suffice + avcodec_close(codecContext); +#endif + + avcodec_free_context(&codecContext); + + avformat_close_input(&formatContext); + av_free(context); +} + +Point CVideoInstance::size() +{ + return dimensions / GH.screenHandler().getScalingFactor(); +} + +void CVideoInstance::show(const Point & position, Canvas & canvas) +{ + if(sws == nullptr) + throw std::runtime_error("No video to show!"); + + CSDL_Ext::blitSurface(surface, canvas.getInternalSurface(), position * GH.screenHandler().getScalingFactor()); +} + +double FFMpegStream::getCurrentFrameEndTime() const +{ +#if(LIBAVUTIL_VERSION_MAJOR < 58) + auto packet_duration = frame->pkt_duration; +#else + auto packet_duration = frame->duration; +#endif + return (frame->pts + packet_duration) * av_q2d(formatContext->streams[streamIndex]->time_base); +} + +double FFMpegStream::getCurrentFrameDuration() const +{ +#if(LIBAVUTIL_VERSION_MAJOR < 58) + auto packet_duration = frame->pkt_duration; +#else + auto packet_duration = frame->duration; +#endif + return packet_duration * av_q2d(formatContext->streams[streamIndex]->time_base); +} + +void CVideoInstance::tick(uint32_t msPassed) +{ + if(sws == nullptr) + throw std::runtime_error("No video to show!"); + + if(videoEnded()) + throw std::runtime_error("Video already ended!"); + + if(!startTimeInitialized) + { + startTime = std::chrono::steady_clock::now(); + startTimeInitialized = true; + } + + 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(deactivationStartTimeHandling) + { + auto pauseDuration = std::chrono::steady_clock::now() - deactivationStartTime; + startTime += pauseDuration; + deactivationStartTimeHandling = false; + } +} + +void CVideoInstance::deactivate() +{ + deactivationStartTime = std::chrono::steady_clock::now(); + deactivationStartTimeHandling = true; +} + +struct FFMpegFormatDescription +{ + uint8_t sampleSizeBytes; + uint8_t wavFormatID; + bool isPlanar; +}; + +static FFMpegFormatDescription getAudioFormatProperties(int audioFormat) +{ + switch (audioFormat) + { + case AV_SAMPLE_FMT_U8: return { 1, 1, false}; + case AV_SAMPLE_FMT_U8P: return { 1, 1, true}; + case AV_SAMPLE_FMT_S16: return { 2, 1, false}; + case AV_SAMPLE_FMT_S16P: return { 2, 1, true}; + case AV_SAMPLE_FMT_S32: return { 4, 1, false}; + case AV_SAMPLE_FMT_S32P: return { 4, 1, true}; + case AV_SAMPLE_FMT_S64: return { 8, 1, false}; + case AV_SAMPLE_FMT_S64P: return { 8, 1, true}; + case AV_SAMPLE_FMT_FLT: return { 4, 3, false}; + case AV_SAMPLE_FMT_FLTP: return { 4, 3, true}; + case AV_SAMPLE_FMT_DBL: return { 8, 3, false}; + case AV_SAMPLE_FMT_DBLP: return { 8, 3, true}; + } + throw std::runtime_error("Invalid audio format"); +} + +int FFMpegStream::findAudioStream() const +{ + std::vector audioStreamIndices; + + for(int i = 0; i < formatContext->nb_streams; i++) + if(formatContext->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_AUDIO) + audioStreamIndices.push_back(i); + + if (audioStreamIndices.empty()) + return -1; + + if (audioStreamIndices.size() == 1) + return audioStreamIndices.front(); + + // multiple audio streams - try to pick best one based on language settings + std::map streamToLanguage; + + // Approach 1 - check if stream has language set in metadata + for (auto const & index : audioStreamIndices) + { + const AVDictionaryEntry *e = av_dict_get(formatContext->streams[index]->metadata, "language", nullptr, 0); + if (e) + streamToLanguage[index] = e->value; + } + + // Approach 2 - no metadata found. This may be video from Chronicles which have predefined (presumably hardcoded) list of languages + if (streamToLanguage.empty()) + { + if (audioStreamIndices.size() == 2) + { + streamToLanguage[audioStreamIndices[0]] = Languages::getLanguageOptions(Languages::ELanguages::ENGLISH).tagISO2; + streamToLanguage[audioStreamIndices[1]] = Languages::getLanguageOptions(Languages::ELanguages::GERMAN).tagISO2; + } + + if (audioStreamIndices.size() == 5) + { + streamToLanguage[audioStreamIndices[0]] = Languages::getLanguageOptions(Languages::ELanguages::ENGLISH).tagISO2; + streamToLanguage[audioStreamIndices[1]] = Languages::getLanguageOptions(Languages::ELanguages::FRENCH).tagISO2; + streamToLanguage[audioStreamIndices[2]] = Languages::getLanguageOptions(Languages::ELanguages::GERMAN).tagISO2; + streamToLanguage[audioStreamIndices[3]] = Languages::getLanguageOptions(Languages::ELanguages::ITALIAN).tagISO2; + streamToLanguage[audioStreamIndices[4]] = Languages::getLanguageOptions(Languages::ELanguages::SPANISH).tagISO2; + } + } + + std::string preferredLanguageName = CGI->generaltexth->getPreferredLanguage(); + std::string preferredTag = Languages::getLanguageOptions(preferredLanguageName).tagISO2; + + for (auto const & entry : streamToLanguage) + if (entry.second == preferredTag) + return entry.first; + + return audioStreamIndices.front(); +} + +int FFMpegStream::findVideoStream() const +{ + for(int i = 0; i < formatContext->nb_streams; i++) + if(formatContext->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) + return i; + + return -1; +} + +std::pair, si64> CAudioInstance::extractAudio(const VideoPath & videoToOpen) +{ + if (!openInput(videoToOpen)) + return { nullptr, 0}; + openContext(); + + int audioStreamIndex = findAudioStream(); + if (audioStreamIndex == -1) + return { nullptr, 0}; + openCodec(audioStreamIndex); + + const auto * codecpar = getCodecParameters(); + + std::vector samples; + + auto formatProperties = getAudioFormatProperties(codecpar->format); +#if(LIBAVUTIL_VERSION_MAJOR < 58) + int numChannels = codecpar->channels; +#else + int numChannels = codecpar->ch_layout.nb_channels; +#endif + + samples.reserve(44100 * 5); // arbitrary 5-second buffer to reduce reallocations + + if (formatProperties.isPlanar && numChannels > 1) + { + // Format is 'planar', which is not supported by wav / SDL + // Use swresample part of ffmpeg to deplanarize audio into format supported by wav / SDL + + auto sourceFormat = static_cast(codecpar->format); + auto targetFormat = av_get_alt_sample_fmt(sourceFormat, false); + + SwrContext * swr_ctx = swr_alloc(); + +#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 (;;) + { + 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); + } + 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); + } + } + + struct WavHeader { + ui8 RIFF[4] = {'R', 'I', 'F', 'F'}; + ui32 ChunkSize; + ui8 WAVE[4] = {'W', 'A', 'V', 'E'}; + ui8 fmt[4] = {'f', 'm', 't', ' '}; + ui32 Subchunk1Size = 16; + ui16 AudioFormat = 1; + ui16 NumOfChan = 2; + ui32 SamplesPerSec = 22050; + ui32 bytesPerSec = 22050 * 2; + ui16 blockAlign = 1; + ui16 bitsPerSample = 32; + ui8 Subchunk2ID[4] = {'d', 'a', 't', 'a'}; + ui32 Subchunk2Size; + }; + + WavHeader wav; + wav.ChunkSize = samples.size() + sizeof(WavHeader) - 8; + wav.AudioFormat = formatProperties.wavFormatID; // 1 = PCM, 3 = IEEE float + wav.NumOfChan = numChannels; + wav.SamplesPerSec = codecpar->sample_rate; + wav.bytesPerSec = codecpar->sample_rate * formatProperties.sampleSizeBytes; + wav.bitsPerSample = formatProperties.sampleSizeBytes * 8; + wav.Subchunk2Size = samples.size() + sizeof(WavHeader) - 44; + auto * wavPtr = reinterpret_cast(&wav); + + auto dat = std::make_pair(std::make_unique(samples.size() + sizeof(WavHeader)), samples.size() + sizeof(WavHeader)); + std::copy(wavPtr, wavPtr + sizeof(WavHeader), dat.first.get()); + std::copy(samples.begin(), samples.end(), dat.first.get() + sizeof(WavHeader)); + + return dat; +} + +std::unique_ptr CVideoPlayer::open(const VideoPath & name, float scaleFactor) +{ + auto result = std::make_unique(); + + if (!result->openInput(name)) + return nullptr; + + result->openVideo(); + result->prepareOutput(scaleFactor, false); + result->loadNextFrame(); // prepare 1st frame + + return result; +} + +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); +} + +#endif diff --git a/client/media/CVideoHandler.h b/client/media/CVideoHandler.h new file mode 100644 index 000000000..5047ba01a --- /dev/null +++ b/client/media/CVideoHandler.h @@ -0,0 +1,116 @@ +/* + * CVideoHandler.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 + +#ifndef DISABLE_VIDEO + +#include "../lib/Point.h" +#include "IVideoPlayer.h" + +struct SDL_Surface; +struct SDL_Texture; +struct AVFormatContext; +struct AVCodecContext; +struct AVCodecParameters; +struct AVCodec; +struct AVFrame; +struct AVIOContext; + +VCMI_LIB_NAMESPACE_BEGIN +class CInputStream; +class Point; +VCMI_LIB_NAMESPACE_END + +class FFMpegStream : boost::noncopyable +{ + std::unique_ptr input; + + AVIOContext * context = nullptr; + AVFormatContext * formatContext = nullptr; + + const AVCodec * codec = nullptr; + AVCodecContext * codecContext = nullptr; + int streamIndex = -1; + + AVFrame * frame = nullptr; + +protected: + void openContext(); + void openCodec(int streamIndex); + + int findVideoStream() const; + int findAudioStream() const; + + const AVCodecParameters * getCodecParameters() const; + const AVCodecContext * getCodecContext() const; + void decodeNextFrame(); + const AVFrame * getCurrentFrame() const; + double getCurrentFrameEndTime() const; + double getCurrentFrameDuration() const; + +public: + virtual ~FFMpegStream(); + + bool openInput(const VideoPath & fname); +}; + +class CAudioInstance final : public FFMpegStream +{ +public: + std::pair, si64> extractAudio(const VideoPath & videoToOpen); +}; + +class CVideoInstance final : public IVideoInstance, public FFMpegStream +{ + friend class CVideoPlayer; + + struct SwsContext * sws = nullptr; + SDL_Texture * textureRGB = nullptr; + SDL_Texture * textureYUV = nullptr; + SDL_Surface * surface = nullptr; + Point dimensions; + + /// video playback start time point + bool startTimeInitialized; + bool deactivationStartTimeHandling; + 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(); + ~CVideoInstance(); + + 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 +{ + void openVideoFile(CVideoInstance & state, const VideoPath & fname); + +public: + std::unique_ptr open(const VideoPath & name, float scaleFactor) final; + std::pair, si64> getAudio(const VideoPath & videoToOpen) final; +}; + +#endif diff --git a/client/media/IMusicPlayer.h b/client/media/IMusicPlayer.h new file mode 100644 index 000000000..1dde50ed7 --- /dev/null +++ b/client/media/IMusicPlayer.h @@ -0,0 +1,33 @@ +/* + * IMusicPlayer.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 "../lib/filesystem/ResourcePath.h" + +class IMusicPlayer +{ +public: + virtual ~IMusicPlayer() = default; + + virtual void loadTerrainMusicThemes() = 0; + virtual void setVolume(ui32 percent) = 0; + virtual ui32 getVolume() const = 0; + + virtual void musicFinishedCallback() = 0; + + /// play track by URI, if loop = true music will be looped + virtual void playMusic(const AudioPath & musicURI, bool loop, bool fromStart) = 0; + /// play random track from this set + virtual void playMusicFromSet(const std::string & musicSet, bool loop, bool fromStart) = 0; + /// play random track from set (musicSet, entryID) + virtual void playMusicFromSet(const std::string & musicSet, const std::string & entryID, bool loop, bool fromStart) = 0; + /// stops currently playing music by fading out it over fade_ms and starts next scheduled track, if any + virtual void stopMusic(int fade_ms = 1000) = 0; +}; diff --git a/client/media/ISoundPlayer.h b/client/media/ISoundPlayer.h new file mode 100644 index 000000000..ffcb90dce --- /dev/null +++ b/client/media/ISoundPlayer.h @@ -0,0 +1,37 @@ +/* + * ISoundPlayer.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 "../lib/CSoundBase.h" +#include "../lib/filesystem/ResourcePath.h" + +class ISoundPlayer +{ +public: + virtual ~ISoundPlayer() = default; + + virtual int playSound(soundBase::soundID soundID, int repeats = 0) = 0; + virtual int playSound(const AudioPath & sound, int repeats = 0, bool cache = false) = 0; + 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; + virtual uint32_t getSoundDurationMilliseconds(const AudioPath & sound) = 0; + virtual void setCallback(int channel, std::function function) = 0; + virtual void resetCallback(int channel) = 0; + virtual void soundFinishedCallback(int channel) = 0; + virtual void ambientUpdateChannels(std::map currentSounds) = 0; + virtual void ambientStopAllChannels() = 0; + virtual int ambientGetRange() const = 0; +}; diff --git a/client/media/IVideoPlayer.h b/client/media/IVideoPlayer.h new file mode 100644 index 000000000..35f385bc1 --- /dev/null +++ b/client/media/IVideoPlayer.h @@ -0,0 +1,55 @@ +/* + * IVideoPlayer.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 "../lib/filesystem/ResourcePath.h" + +class Canvas; + +VCMI_LIB_NAMESPACE_BEGIN +class Point; +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; + + /// Returns dimensions of the video + virtual Point size() = 0; + + /// Displays current frame at specified position + virtual void show(const Point & position, Canvas & canvas) = 0; + + /// 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: + /// Load video from specified path. Returns nullptr on failure + virtual std::unique_ptr open(const VideoPath & name, float scaleFactor) = 0; + + /// Extracts audio data from provided video in wav format + virtual std::pair, si64> getAudio(const VideoPath & videoToOpen) = 0; + + virtual ~IVideoPlayer() = default; +}; diff --git a/client/render/AssetGenerator.cpp b/client/render/AssetGenerator.cpp new file mode 100644 index 000000000..a1e4befe7 --- /dev/null +++ b/client/render/AssetGenerator.cpp @@ -0,0 +1,436 @@ +/* + * AssetGenerator.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 "AssetGenerator.h" + +#include "../gui/CGuiHandler.h" +#include "../render/IImage.h" +#include "../render/IImageLoader.h" +#include "../render/Canvas.h" +#include "../render/ColorFilter.h" +#include "../render/IRenderHandler.h" +#include "../render/CAnimation.h" + +#include "../lib/filesystem/Filesystem.h" +#include "../lib/GameSettings.h" +#include "../lib/IGameSettings.h" +#include "../lib/json/JsonNode.h" +#include "../lib/VCMI_Lib.h" +#include "../lib/RiverHandler.h" +#include "../lib/RoadHandler.h" +#include "../lib/TerrainHandler.h" + +void AssetGenerator::generateAll() +{ + createBigSpellBook(); + createAdventureOptionsCleanBackground(); + for (int i = 0; i < PlayerColor::PLAYER_LIMIT_I; ++i) + createPlayerColoredBackground(PlayerColor(i)); + createCombatUnitNumberWindow(); + createCampaignBackground(); + createChroniclesCampaignImages(); + createPaletteShiftedSprites(); +} + +void AssetGenerator::createAdventureOptionsCleanBackground() +{ + std::string filename = "data/AdventureOptionsBackgroundClear.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("ADVOPTBK")); + locator.scalingFactor = 1; + + std::shared_ptr img = GH.renderHandler().loadImage(locator, EImageBlitMode::OPAQUE); + + Canvas canvas = Canvas(Point(575, 585), CanvasScalingPolicy::IGNORE); + canvas.draw(img, Point(0, 0), Rect(0, 0, 575, 585)); + canvas.draw(img, Point(54, 121), Rect(54, 123, 335, 1)); + canvas.draw(img, Point(158, 84), Rect(156, 84, 2, 37)); + canvas.draw(img, Point(234, 84), Rect(232, 84, 2, 37)); + canvas.draw(img, Point(310, 84), Rect(308, 84, 2, 37)); + canvas.draw(img, Point(53, 567), Rect(53, 520, 339, 3)); + canvas.draw(img, Point(53, 520), Rect(53, 264, 339, 47)); + + std::shared_ptr image = GH.renderHandler().createImage(canvas.getInternalSurface()); + + image->exportBitmap(*CResourceHandler::get("local")->getResourceName(savePath)); +} + +void AssetGenerator::createBigSpellBook() +{ + std::string filename = "data/SpellBookLarge.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("SpelBack")); + locator.scalingFactor = 1; + + std::shared_ptr img = GH.renderHandler().loadImage(locator, EImageBlitMode::OPAQUE); + Canvas canvas = Canvas(Point(800, 600), CanvasScalingPolicy::IGNORE); + // edges + canvas.draw(img, Point(0, 0), Rect(15, 38, 90, 45)); + canvas.draw(img, Point(0, 460), Rect(15, 400, 90, 141)); + canvas.draw(img, Point(705, 0), Rect(509, 38, 95, 45)); + canvas.draw(img, Point(705, 460), Rect(509, 400, 95, 141)); + // left / right + Canvas tmp1 = Canvas(Point(90, 355 - 45), CanvasScalingPolicy::IGNORE); + tmp1.draw(img, Point(0, 0), Rect(15, 38 + 45, 90, 355 - 45)); + canvas.drawScaled(tmp1, Point(0, 45), Point(90, 415)); + Canvas tmp2 = Canvas(Point(95, 355 - 45), CanvasScalingPolicy::IGNORE); + tmp2.draw(img, Point(0, 0), Rect(509, 38 + 45, 95, 355 - 45)); + canvas.drawScaled(tmp2, Point(705, 45), Point(95, 415)); + // top / bottom + Canvas tmp3 = Canvas(Point(409, 45), CanvasScalingPolicy::IGNORE); + tmp3.draw(img, Point(0, 0), Rect(100, 38, 409, 45)); + canvas.drawScaled(tmp3, Point(90, 0), Point(615, 45)); + Canvas tmp4 = Canvas(Point(409, 141), CanvasScalingPolicy::IGNORE); + tmp4.draw(img, Point(0, 0), Rect(100, 400, 409, 141)); + canvas.drawScaled(tmp4, Point(90, 460), Point(615, 141)); + // middle + Canvas tmp5 = Canvas(Point(409, 141), CanvasScalingPolicy::IGNORE); + tmp5.draw(img, Point(0, 0), Rect(100, 38 + 45, 509 - 15, 400 - 38)); + canvas.drawScaled(tmp5, Point(90, 45), Point(615, 415)); + // carpet + Canvas tmp6 = Canvas(Point(590, 59), CanvasScalingPolicy::IGNORE); + tmp6.draw(img, Point(0, 0), Rect(15, 484, 590, 59)); + canvas.drawScaled(tmp6, Point(0, 545), Point(800, 59)); + // remove bookmarks + for (int i = 0; i < 56; i++) + canvas.draw(Canvas(canvas, Rect(i < 30 ? 268 : 327, 464, 1, 46)), Point(269 + i, 464)); + for (int i = 0; i < 56; i++) + canvas.draw(Canvas(canvas, Rect(469, 464, 1, 42)), Point(470 + i, 464)); + for (int i = 0; i < 57; i++) + canvas.draw(Canvas(canvas, Rect(i < 30 ? 564 : 630, 464, 1, 44)), Point(565 + i, 464)); + for (int i = 0; i < 56; i++) + canvas.draw(Canvas(canvas, Rect(656, 464, 1, 47)), Point(657 + i, 464)); + // draw bookmarks + canvas.draw(img, Point(278, 464), Rect(220, 405, 37, 47)); + canvas.draw(img, Point(481, 465), Rect(354, 406, 37, 41)); + canvas.draw(img, Point(575, 465), Rect(417, 406, 37, 45)); + canvas.draw(img, Point(667, 465), Rect(478, 406, 37, 47)); + + std::shared_ptr image = GH.renderHandler().createImage(canvas.getInternalSurface()); + + image->exportBitmap(*CResourceHandler::get("local")->getResourceName(savePath)); +} + +void AssetGenerator::createPlayerColoredBackground(const PlayerColor & player) +{ + std::string filename = "data/DialogBoxBackground_" + player.toString() + ".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("DiBoxBck")); + locator.scalingFactor = 1; + + std::shared_ptr texture = GH.renderHandler().loadImage(locator, EImageBlitMode::OPAQUE); + + // 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( 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!", player.getNum()); + return; + } + + texture->adjustPalette(filters[player.getNum()], 0); + texture->exportBitmap(*CResourceHandler::get("local")->getResourceName(savePath)); +} + +void AssetGenerator::createCombatUnitNumberWindow() +{ + std::string filenameToSave = "data/combatUnitNumberWindow"; + + ResourcePath savePathDefault(filenameToSave + "Default.png", EResType::IMAGE); + ResourcePath savePathNeutral(filenameToSave + "Neutral.png", EResType::IMAGE); + ResourcePath savePathPositive(filenameToSave + "Positive.png", EResType::IMAGE); + ResourcePath savePathNegative(filenameToSave + "Negative.png", EResType::IMAGE); + + if(CResourceHandler::get()->existsResource(savePathDefault)) // overridden by mod, no generation + return; + + if(!CResourceHandler::get("local")->createResource(savePathDefault.getOriginalName() + ".png") || + !CResourceHandler::get("local")->createResource(savePathNeutral.getOriginalName() + ".png") || + !CResourceHandler::get("local")->createResource(savePathPositive.getOriginalName() + ".png") || + !CResourceHandler::get("local")->createResource(savePathNegative.getOriginalName() + ".png")) + return; + + auto locator = ImageLocator(ImagePath::builtin("CMNUMWIN")); + locator.scalingFactor = 1; + + std::shared_ptr texture = GH.renderHandler().loadImage(locator, EImageBlitMode::OPAQUE); + + static const auto shifterNormal = ColorFilter::genRangeShifter( 0.f, 0.f, 0.f, 0.6f, 0.2f, 1.0f ); + static const auto shifterPositive = ColorFilter::genRangeShifter( 0.f, 0.f, 0.f, 0.2f, 1.0f, 0.2f ); + static const auto shifterNegative = ColorFilter::genRangeShifter( 0.f, 0.f, 0.f, 1.0f, 0.2f, 0.2f ); + static const auto shifterNeutral = ColorFilter::genRangeShifter( 0.f, 0.f, 0.f, 1.0f, 1.0f, 0.2f ); + + // do not change border color + static const int32_t ignoredMask = 1 << 26; + + texture->adjustPalette(shifterNormal, ignoredMask); + texture->exportBitmap(*CResourceHandler::get("local")->getResourceName(savePathDefault)); + texture->adjustPalette(shifterPositive, ignoredMask); + texture->exportBitmap(*CResourceHandler::get("local")->getResourceName(savePathPositive)); + texture->adjustPalette(shifterNegative, ignoredMask); + texture->exportBitmap(*CResourceHandler::get("local")->getResourceName(savePathNegative)); + 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)); + } +} + +void AssetGenerator::createPaletteShiftedSprites() +{ + std::vector tiles; + std::vector>> paletteAnimations; + for(auto entity : VLC->terrainTypeHandler->objects) + { + if(entity->paletteAnimation.size()) + { + tiles.push_back(entity->tilesFilename.getName()); + std::vector> tmpAnim; + for(auto & animEntity : entity->paletteAnimation) + tmpAnim.push_back(animEntity); + paletteAnimations.push_back(tmpAnim); + } + } + for(auto entity : VLC->riverTypeHandler->objects) + { + if(entity->paletteAnimation.size()) + { + tiles.push_back(entity->tilesFilename.getName()); + std::vector> tmpAnim; + for(auto & animEntity : entity->paletteAnimation) + tmpAnim.push_back(animEntity); + paletteAnimations.push_back(tmpAnim); + } + } + + for(int i = 0; i < tiles.size(); i++) + { + auto sprite = tiles[i]; + + JsonNode config; + config["basepath"].String() = sprite + "_Shifted/"; + config["images"].Vector(); + + auto filename = AnimationPath::builtin(sprite).addPrefix("SPRITES/"); + auto filenameNew = AnimationPath::builtin(sprite + "_Shifted").addPrefix("SPRITES/"); + + if(CResourceHandler::get()->existsResource(ResourcePath(filenameNew.getName(), EResType::JSON))) // overridden by mod, no generation + return; + + auto anim = GH.renderHandler().loadAnimation(filename, EImageBlitMode::COLORKEY); + for(int j = 0; j < anim->size(); j++) + { + int maxLen = 1; + for(int k = 0; k < paletteAnimations[i].size(); k++) + { + auto element = paletteAnimations[i][k]; + if(std::holds_alternative(element)) + maxLen = std::lcm(maxLen, std::get(element).length); + else + maxLen = std::lcm(maxLen, std::get(element).length); + } + for(int l = 0; l < maxLen; l++) + { + std::string spriteName = sprite + boost::str(boost::format("%02d") % j) + "_" + std::to_string(l) + ".png"; + std::string filenameNewImg = "sprites/" + sprite + "_Shifted" + "/" + spriteName; + ResourcePath savePath(filenameNewImg, EResType::IMAGE); + + if(!CResourceHandler::get("local")->createResource(filenameNewImg)) + return; + + auto imgLoc = anim->getImageLocator(j, 0); + imgLoc.scalingFactor = 1; + auto img = GH.renderHandler().loadImage(imgLoc, EImageBlitMode::COLORKEY); + for(int k = 0; k < paletteAnimations[i].size(); k++) + { + auto element = paletteAnimations[i][k]; + if(std::holds_alternative(element)) + { + auto tmp = std::get(element); + img->shiftPalette(tmp.start, tmp.length, l % tmp.length); + } + else + { + auto tmp = std::get(element); + img->shiftPalette(tmp.start, tmp.length, l % tmp.length); + } + } + + Canvas canvas = Canvas(Point(32, 32), CanvasScalingPolicy::IGNORE); + canvas.draw(img, Point((32 - img->dimensions().x) / 2, (32 - img->dimensions().y) / 2)); + std::shared_ptr image = GH.renderHandler().createImage(canvas.getInternalSurface()); + image->exportBitmap(*CResourceHandler::get("local")->getResourceName(savePath)); + + JsonNode node(JsonMap{ + { "group", JsonNode(l) }, + { "frame", JsonNode(j) }, + { "file", JsonNode(spriteName) } + }); + config["images"].Vector().push_back(node); + } + } + + ResourcePath savePath(filenameNew.getOriginalName(), EResType::JSON); + if(!CResourceHandler::get("local")->createResource(filenameNew.getOriginalName() + ".json")) + return; + + std::fstream file(CResourceHandler::get("local")->getResourceName(savePath)->c_str(), std::ofstream::out | std::ofstream::trunc); + file << config.toString(); + } +} diff --git a/client/render/AssetGenerator.h b/client/render/AssetGenerator.h new file mode 100644 index 000000000..8ca08aae6 --- /dev/null +++ b/client/render/AssetGenerator.h @@ -0,0 +1,27 @@ +/* + * AssetGenerator.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 PlayerColor; +VCMI_LIB_NAMESPACE_END + +class AssetGenerator +{ +public: + static void generateAll(); + static void createAdventureOptionsCleanBackground(); + static void createBigSpellBook(); + static void createPlayerColoredBackground(const PlayerColor & player); + static void createCombatUnitNumberWindow(); + static void createCampaignBackground(); + static void createChroniclesCampaignImages(); + static void createPaletteShiftedSprites(); +}; diff --git a/client/render/CAnimation.cpp b/client/render/CAnimation.cpp index 93549f333..50276cf5e 100644 --- a/client/render/CAnimation.cpp +++ b/client/render/CAnimation.cpp @@ -10,77 +10,43 @@ #include "StdInc.h" #include "CAnimation.h" -#include "CDefFile.h" +#include "../gui/CGuiHandler.h" +#include "../render/IImage.h" +#include "../render/IRenderHandler.h" +#include "../render/IScreenHandler.h" -#include "Graphics.h" #include "../../lib/filesystem/Filesystem.h" #include "../../lib/json/JsonUtils.h" -#include "../renderSDL/SDLImage.h" -std::shared_ptr CAnimation::getFromExtraDef(std::string filename) -{ - size_t pos = filename.find(':'); - if (pos == -1) - return nullptr; - CAnimation anim(AnimationPath::builtinTODO(filename.substr(0, pos))); - pos++; - size_t frame = atoi(filename.c_str()+pos); - size_t group = 0; - pos = filename.find(':', pos); - if (pos != -1) - { - pos++; - group = frame; - frame = atoi(filename.c_str()+pos); - } - anim.load(frame ,group); - auto ret = anim.images[group][frame]; - anim.images.clear(); - return ret; -} - -bool CAnimation::loadFrame(size_t frame, size_t group) +bool CAnimation::loadFrame(size_t frame, size_t group, bool verbose) { if(size(group) <= frame) { - printError(frame, group, "LoadFrame"); + if(verbose) + printError(frame, group, "LoadFrame"); return false; } - auto image = getImage(frame, group, false); + if(auto image = getImageImpl(frame, group, false)) + return true; + + std::shared_ptr image = GH.renderHandler().loadImage(getImageLocator(frame, group), mode); + if(image) { + images[group][frame] = image; + + if (player.isValidPlayer()) + image->playerColored(player); return true; } - - //try to get image from def - if(source[group][frame].getType() == JsonNode::JsonType::DATA_NULL) + else { - if(defFile) - { - auto frameList = defFile->getEntries(); - - if(vstd::contains(frameList, group) && frameList.at(group) > frame) // frame is present - { - images[group][frame] = std::make_shared(defFile.get(), frame, group); - return true; - } - } - // still here? image is missing - + // image is missing printError(frame, group, "LoadFrame"); - images[group][frame] = std::make_shared(ImagePath::builtin("DEFAULT"), EImageBlitMode::ALPHA); + images[group][frame] = GH.renderHandler().loadImage(ImagePath::builtin("DEFAULT"), EImageBlitMode::OPAQUE); + return false; } - else //load from separate file - { - auto img = getFromExtraDef(source[group][frame]["file"].String()); - if(!img) - img = std::make_shared(source[group][frame], EImageBlitMode::ALPHA); - - images[group][frame] = img; - return true; - } - return false; } bool CAnimation::unloadFrame(size_t frame, size_t group) @@ -97,45 +63,6 @@ bool CAnimation::unloadFrame(size_t frame, size_t group) return false; } -void CAnimation::initFromJson(const JsonNode & config) -{ - std::string basepath; - basepath = config["basepath"].String(); - - JsonNode base; - base["margins"] = config["margins"]; - base["width"] = config["width"]; - base["height"] = config["height"]; - - for(const JsonNode & group : config["sequences"].Vector()) - { - size_t groupID = group["group"].Integer();//TODO: string-to-value conversion("moving" -> MOVING) - source[groupID].clear(); - - for(const JsonNode & frame : group["frames"].Vector()) - { - JsonNode toAdd; - JsonUtils::inherit(toAdd, base); - toAdd["file"].String() = basepath + frame.String(); - source[groupID].push_back(toAdd); - } - } - - for(const JsonNode & node : config["images"].Vector()) - { - size_t group = node["group"].Integer(); - size_t frame = node["frame"].Integer(); - - if (source[group].size() <= frame) - source[group].resize(frame+1); - - JsonNode toAdd; - JsonUtils::inherit(toAdd, base); - toAdd["file"].String() = basepath + node["file"].String(); - source[group][frame] = toAdd; - } -} - void CAnimation::exportBitmaps(const boost::filesystem::path& path) const { if(images.empty()) @@ -169,109 +96,36 @@ void CAnimation::exportBitmaps(const boost::filesystem::path& path) const logGlobal->info("Exported %d frames to %s", counter, actualPath.string()); } -void CAnimation::init() -{ - if(defFile) - { - const std::map defEntries = defFile->getEntries(); - - for (auto & defEntry : defEntries) - source[defEntry.first].resize(defEntry.second); - } - - if (vstd::contains(graphics->imageLists, name.getName())) - initFromJson(graphics->imageLists[name.getName()]); - - auto jsonResource = name.toType(); - auto configList = CResourceHandler::get()->getResourcesWithName(jsonResource); - - for(auto & loader : configList) - { - auto stream = loader->load(jsonResource); - std::unique_ptr textData(new ui8[stream->getSize()]); - stream->read(textData.get(), stream->getSize()); - - const JsonNode config(reinterpret_cast(textData.get()), stream->getSize()); - - initFromJson(config); - } -} - void CAnimation::printError(size_t frame, size_t group, std::string type) const { logGlobal->error("%s error: Request for frame not present in CAnimation! File name: %s, Group: %d, Frame: %d", type, name.getOriginalName(), group, frame); } -CAnimation::CAnimation(const AnimationPath & Name): +CAnimation::CAnimation(const AnimationPath & Name, std::map > layout, EImageBlitMode mode): name(boost::starts_with(Name.getName(), "SPRITES") ? Name : Name.addPrefix("SPRITES/")), - preloaded(false) + source(layout), + mode(mode) { - if(CResourceHandler::get()->existsResource(name)) - { - try - { - defFile = std::make_shared(name); - } - catch ( const std::runtime_error & e) - { - logAnim->error("Def file %s failed to load! Reason: %s", Name.getOriginalName(), e.what()); - } - } - - init(); - if(source.empty()) logAnim->error("Animation %s failed to load", Name.getOriginalName()); } -CAnimation::CAnimation(): - preloaded(false) -{ - init(); -} - CAnimation::~CAnimation() = default; void CAnimation::duplicateImage(const size_t sourceGroup, const size_t sourceFrame, const size_t targetGroup) { - if(!source.count(sourceGroup)) - { - logAnim->error("Group %d missing in %s", sourceGroup, name.getName()); - return; - } - - if(source[sourceGroup].size() <= sourceFrame) - { - logAnim->error("Frame [%d %d] missing in %s", sourceGroup, sourceFrame, name.getName()); - return; - } - - //todo: clone actual loaded Image object - JsonNode clone(source[sourceGroup][sourceFrame]); - - if(clone.getType() == JsonNode::JsonType::DATA_NULL) - { - std::string temp = name.getName()+":"+std::to_string(sourceGroup)+":"+std::to_string(sourceFrame); - clone["file"].String() = temp; - } - + ImageLocator clone(getImageLocator(sourceFrame, sourceGroup)); source[targetGroup].push_back(clone); - - size_t index = source[targetGroup].size() - 1; - - if(preloaded) - load(index, targetGroup); } -void CAnimation::setCustom(std::string filename, size_t frame, size_t group) +std::shared_ptr CAnimation::getImage(size_t frame, size_t group, bool verbose) { - if (source[group].size() <= frame) - source[group].resize(frame+1); - source[group][frame]["file"].String() = filename; - //FIXME: update image if already loaded + if (!loadFrame(frame, group, verbose)) + return nullptr; + return getImageImpl(frame, group, verbose); } -std::shared_ptr CAnimation::getImage(size_t frame, size_t group, bool verbose) const +std::shared_ptr CAnimation::getImageImpl(size_t frame, size_t group, bool verbose) { auto groupIter = images.find(group); if (groupIter != images.end()) @@ -285,54 +139,6 @@ std::shared_ptr CAnimation::getImage(size_t frame, size_t group, bool ve return nullptr; } -void CAnimation::load() -{ - for (auto & elem : source) - for (size_t image=0; image < elem.second.size(); image++) - loadFrame(image, elem.first); -} - -void CAnimation::unload() -{ - for (auto & elem : source) - for (size_t image=0; image < elem.second.size(); image++) - unloadFrame(image, elem.first); - -} - -void CAnimation::preload() -{ - if(!preloaded) - { - preloaded = true; - load(); - } -} - -void CAnimation::loadGroup(size_t group) -{ - if (vstd::contains(source, group)) - for (size_t image=0; image < source[group].size(); image++) - loadFrame(image, group); -} - -void CAnimation::unloadGroup(size_t group) -{ - if (vstd::contains(source, group)) - for (size_t image=0; image < source[group].size(); image++) - unloadFrame(image, group); -} - -void CAnimation::load(size_t frame, size_t group) -{ - loadFrame(frame, group); -} - -void CAnimation::unload(size_t frame, size_t group) -{ - unloadFrame(frame, group); -} - size_t CAnimation::size(size_t group) const { auto iter = source.find(group); @@ -343,20 +149,53 @@ size_t CAnimation::size(size_t group) const void CAnimation::horizontalFlip() { - for(auto & group : images) - for(auto & image : group.second) - image.second->horizontalFlip(); + for(auto & group : source) + for(size_t i = 0; i < group.second.size(); ++i) + horizontalFlip(i, group.first); } void CAnimation::verticalFlip() { - for(auto & group : images) - for(auto & image : group.second) - image.second->verticalFlip(); + for(auto & group : source) + for(size_t i = 0; i < group.second.size(); ++i) + verticalFlip(i, group.first); } -void CAnimation::playerColored(PlayerColor player) +void CAnimation::horizontalFlip(size_t frame, size_t group) { + auto i1 = images.find(group); + if(i1 != images.end()) + { + auto i2 = i1->second.find(frame); + + if(i2 != i1->second.end()) + i2->second = nullptr; + } + + auto locator = getImageLocator(frame, group); + locator.horizontalFlip = !locator.horizontalFlip; + source[group][frame] = locator; +} + +void CAnimation::verticalFlip(size_t frame, size_t group) +{ + auto i1 = images.find(group); + if(i1 != images.end()) + { + auto i2 = i1->second.find(frame); + + if(i2 != i1->second.end()) + i2->second = nullptr; + } + + auto locator = getImageLocator(frame, group); + locator.verticalFlip = !locator.verticalFlip; + source[group][frame] = locator; +} + +void CAnimation::playerColored(PlayerColor targetPlayer) +{ + player = targetPlayer; for(auto & group : images) for(auto & image : group.second) image.second->playerColored(player); @@ -367,9 +206,16 @@ void CAnimation::createFlippedGroup(const size_t sourceGroup, const size_t targe for(size_t frame = 0; frame < size(sourceGroup); ++frame) { duplicateImage(sourceGroup, frame, targetGroup); - - auto image = getImage(frame, targetGroup); - image->verticalFlip(); + verticalFlip(frame, targetGroup); } } +ImageLocator CAnimation::getImageLocator(size_t frame, size_t group) const +{ + const ImageLocator & locator = source.at(group).at(frame); + + if (!locator.empty()) + return locator; + + return ImageLocator(name, frame, group); +} diff --git a/client/render/CAnimation.h b/client/render/CAnimation.h index efb66f602..6b9e66272 100644 --- a/client/render/CAnimation.h +++ b/client/render/CAnimation.h @@ -9,6 +9,9 @@ */ #pragma once +#include "IImage.h" +#include "ImageLocator.h" + #include "../../lib/GameConstants.h" #include "../../lib/filesystem/ResourcePath.h" @@ -17,15 +20,14 @@ class JsonNode; VCMI_LIB_NAMESPACE_END class CDefFile; -class IImage; class RenderHandler; /// Class for handling animation class CAnimation { private: - //source[group][position] - file with this frame, if string is empty - image located in def file - std::map > source; + //source[group][position] - location of this frame + std::map > source; //bitmap[group][position], store objects with loaded bitmaps std::map > > images; @@ -33,63 +35,44 @@ private: //animation file name AnimationPath name; - bool preloaded; + EImageBlitMode mode; - std::shared_ptr defFile; + // current player color, if any + PlayerColor player = PlayerColor::CANNOT_DETERMINE; //loader, will be called by load(), require opened def file for loading from it. Returns true if image is loaded - bool loadFrame(size_t frame, size_t group); + bool loadFrame(size_t frame, size_t group, bool verbose = true); //unloadFrame, returns true if image has been unloaded ( either deleted or decreased refCount) bool unloadFrame(size_t frame, size_t group); - //initialize animation from file - void initFromJson(const JsonNode & input); - void init(); - //to get rid of copy-pasting error message :] void printError(size_t frame, size_t group, std::string type) const; - //not a very nice method to get image from another def file - //TODO: remove after implementing resource manager - std::shared_ptr getFromExtraDef(std::string filename); - + std::shared_ptr getImageImpl(size_t frame, size_t group=0, bool verbose=true); public: - CAnimation(const AnimationPath & Name); - CAnimation(); + CAnimation(const AnimationPath & Name, std::map > layout, EImageBlitMode mode); ~CAnimation(); //duplicates frame at [sourceGroup, sourceFrame] as last frame in targetGroup //and loads it if animation is preloaded void duplicateImage(const size_t sourceGroup, const size_t sourceFrame, const size_t targetGroup); - //add custom surface to the selected position. - void setCustom(std::string filename, size_t frame, size_t group=0); - - std::shared_ptr getImage(size_t frame, size_t group=0, bool verbose=true) const; + std::shared_ptr getImage(size_t frame, size_t group=0, bool verbose=true); void exportBitmaps(const boost::filesystem::path & path) const; - //all available frames - void load (); - void unload(); - void preload(); - - //all frames from group - void loadGroup (size_t group); - void unloadGroup(size_t group); - - //single image - void load (size_t frame, size_t group=0); - void unload(size_t frame, size_t group=0); - //total count of frames in group (including not loaded) size_t size(size_t group=0) const; + void horizontalFlip(size_t frame, size_t group=0); + void verticalFlip(size_t frame, size_t group=0); void horizontalFlip(); void verticalFlip(); void playerColored(PlayerColor player); void createFlippedGroup(const size_t sourceGroup, const size_t targetGroup); + + ImageLocator getImageLocator(size_t frame, size_t group) const; }; diff --git a/client/render/CBitmapHandler.cpp b/client/render/CBitmapHandler.cpp index a6e0836f0..1318ca1eb 100644 --- a/client/render/CBitmapHandler.cpp +++ b/client/render/CBitmapHandler.cpp @@ -12,6 +12,7 @@ #include "../renderSDL/SDL_Extensions.h" +#include "../lib/ExceptionsCommon.h" #include "../lib/filesystem/Filesystem.h" #include "../lib/vcmi_endian.h" @@ -112,40 +113,47 @@ SDL_Surface * BitmapHandler::loadBitmapFromDir(const ImagePath & path) SDL_Surface * ret=nullptr; - auto readFile = CResourceHandler::get()->load(path)->readAll(); + try { + auto readFile = CResourceHandler::get()->load(path)->readAll(); - if (isPCX(readFile.first.get())) - {//H3-style PCX - ret = loadH3PCX(readFile.first.get(), readFile.second); - if (!ret) - { - logGlobal->error("Failed to open %s as H3 PCX!", path.getOriginalName()); - return nullptr; - } - } - else - { //loading via SDL_Image - ret = IMG_Load_RW( - //create SDL_RW with our data (will be deleted by SDL) - SDL_RWFromConstMem((void*)readFile.first.get(), (int)readFile.second), - 1); // mark it for auto-deleting - if (ret) - { - if (ret->format->palette) + if (isPCX(readFile.first.get())) + {//H3-style PCX + ret = loadH3PCX(readFile.first.get(), readFile.second); + if (!ret) { - // set correct value for alpha\unused channel - // NOTE: might be unnecessary with SDL2 - for (int i=0; i < ret->format->palette->ncolors; i++) - ret->format->palette->colors[i].a = SDL_ALPHA_OPAQUE; + logGlobal->error("Failed to open %s as H3 PCX!", path.getOriginalName()); + return nullptr; } } else - { - logGlobal->error("Failed to open %s via SDL_Image", path.getOriginalName()); - logGlobal->error("Reason: %s", IMG_GetError()); - return nullptr; + { //loading via SDL_Image + ret = IMG_Load_RW( + //create SDL_RW with our data (will be deleted by SDL) + SDL_RWFromConstMem((void*)readFile.first.get(), (int)readFile.second), + 1); // mark it for auto-deleting + if (ret) + { + if (ret->format->palette) + { + // set correct value for alpha\unused channel + // NOTE: might be unnecessary with SDL2 + for (int i=0; i < ret->format->palette->ncolors; i++) + ret->format->palette->colors[i].a = SDL_ALPHA_OPAQUE; + } + } + else + { + logGlobal->error("Failed to open %s via SDL_Image", path.getOriginalName()); + logGlobal->error("Reason: %s", IMG_GetError()); + return nullptr; + } } } + catch (const DataLoadingException & e) + { + logGlobal->error("%s", e.what()); + return nullptr; + } // When modifying anything here please check two use cases: // 1) Vampire mansion in Necropolis (not 1st color is transparent) diff --git a/client/render/CDefFile.cpp b/client/render/CDefFile.cpp index 23377c4e5..4501b40d2 100644 --- a/client/render/CDefFile.cpp +++ b/client/render/CDefFile.cpp @@ -18,50 +18,6 @@ #include -// Extremely simple file cache. TODO: smarter, more general solution -class CFileCache -{ - static const int cacheSize = 50; //Max number of cached files - struct FileData - { - AnimationPath name; - size_t size; - std::unique_ptr data; - - std::unique_ptr getCopy() - { - auto ret = std::unique_ptr(new ui8[size]); - std::copy(data.get(), data.get() + size, ret.get()); - return ret; - } - FileData(AnimationPath name_, size_t size_, std::unique_ptr data_): - name{std::move(name_)}, - size{size_}, - data{std::move(data_)} - {} - }; - - std::deque cache; -public: - std::unique_ptr getCachedFile(AnimationPath rid) - { - for(auto & file : cache) - { - if (file.name == rid) - return file.getCopy(); - } - // Still here? Cache miss - if (cache.size() > cacheSize) - cache.pop_front(); - - auto data = CResourceHandler::get()->load(rid)->readAll(); - - cache.emplace_back(std::move(rid), data.second, std::move(data.first)); - - return cache.back().getCopy(); - } -}; - enum class DefType : uint32_t { SPELL = 0x40, @@ -76,55 +32,15 @@ enum class DefType : uint32_t BATTLE_HERO = 0x49 }; -static CFileCache animationCache; - /************************************************************************* * DefFile, class used for def loading * *************************************************************************/ -static bool colorsSimilar (const SDL_Color & lhs, const SDL_Color & 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 = static_cast(lhs.r) - rhs.r; - int diffG = static_cast(lhs.g) - rhs.g; - int diffB = static_cast(lhs.b) - rhs.b; - int diffA = static_cast(lhs.a) - rhs.a; - - return std::abs(diffR) < threshold && std::abs(diffG) < threshold && std::abs(diffB) < threshold && std::abs(diffA) < threshold; -} - CDefFile::CDefFile(const AnimationPath & Name): data(nullptr), palette(nullptr) { - //First 8 colors in def palette used for transparency - static const SDL_Color sourcePalette[8] = { - {0, 255, 255, SDL_ALPHA_OPAQUE}, - {255, 150, 255, SDL_ALPHA_OPAQUE}, - {255, 100, 255, SDL_ALPHA_OPAQUE}, - {255, 50, 255, SDL_ALPHA_OPAQUE}, - {255, 0, 255, SDL_ALPHA_OPAQUE}, - {255, 255, 0, SDL_ALPHA_OPAQUE}, - {180, 0, 255, SDL_ALPHA_OPAQUE}, - {0, 255, 0, SDL_ALPHA_OPAQUE} - }; - - static const SDL_Color targetPalette[8] = { - {0, 0, 0, 0 }, // transparency ( used in most images ) - {0, 0, 0, 64 }, // shadow border ( used in battle, adventure map def's ) - {0, 0, 0, 64 }, // shadow border ( used in fog-of-war def's ) - {0, 0, 0, 128}, // shadow body ( used in fog-of-war def's ) - {0, 0, 0, 128}, // shadow body ( used in battle, adventure map def's ) - {0, 0, 0, 0 }, // selection / owner flag ( used in battle, adventure map def's ) - {0, 0, 0, 128}, // shadow body below selection ( used in battle def's ) - {0, 0, 0, 64 } // shadow border below selection ( used in battle def's ) - }; - - data = animationCache.getCachedFile(Name); + data = CResourceHandler::get()->load(Name)->readAll().first; palette = std::unique_ptr(new SDL_Color[256]); int it = 0; @@ -145,18 +61,6 @@ CDefFile::CDefFile(const AnimationPath & Name): palette[i].a = SDL_ALPHA_OPAQUE; } - // these colors seems to be used unconditionally - palette[0] = targetPalette[0]; - palette[1] = targetPalette[1]; - palette[4] = targetPalette[4]; - - // rest of special colors are used only if their RGB values are close to H3 - for (uint32_t i = 0; i < 8; ++i) - { - if (colorsSimilar(sourcePalette[i], palette[i])) - palette[i] = targetPalette[i]; - } - for (ui32 i=0; i #include -Canvas::Canvas(SDL_Surface * surface): +Canvas::Canvas(SDL_Surface * surface, CanvasScalingPolicy scalingPolicy): + scalingPolicy(scalingPolicy), surface(surface), renderArea(0,0, surface->w, surface->h) { @@ -27,6 +31,7 @@ Canvas::Canvas(SDL_Surface * surface): } Canvas::Canvas(const Canvas & other): + scalingPolicy(other.scalingPolicy), surface(other.surface), renderArea(other.renderArea) { @@ -34,6 +39,7 @@ Canvas::Canvas(const Canvas & other): } Canvas::Canvas(Canvas && other): + scalingPolicy(other.scalingPolicy), surface(other.surface), renderArea(other.renderArea) { @@ -43,20 +49,39 @@ Canvas::Canvas(Canvas && other): Canvas::Canvas(const Canvas & other, const Rect & newClipRect): Canvas(other) { - renderArea = other.renderArea.intersect(newClipRect + other.renderArea.topLeft()); + Rect scaledClipRect( transformPos(newClipRect.topLeft()), transformPos(newClipRect.dimensions())); + renderArea = other.renderArea.intersect(scaledClipRect + other.renderArea.topLeft()); } -Canvas::Canvas(const Point & size): - renderArea(Point(0,0), size), - surface(CSDL_Ext::newSurface(size.x, size.y)) +Canvas::Canvas(const Point & size, CanvasScalingPolicy scalingPolicy): + scalingPolicy(scalingPolicy), + surface(CSDL_Ext::newSurface(size * getScalingFactor())), + renderArea(Point(0,0), size * getScalingFactor()) { CSDL_Ext::fillSurface(surface, CSDL_Ext::toSDL(Colors::TRANSPARENCY) ); SDL_SetSurfaceBlendMode(surface, SDL_BLENDMODE_NONE); } -Canvas Canvas::createFromSurface(SDL_Surface * surface) +int Canvas::getScalingFactor() const { - return Canvas(surface); + if (scalingPolicy == CanvasScalingPolicy::IGNORE) + return 1; + return GH.screenHandler().getScalingFactor(); +} + +Point Canvas::transformPos(const Point & input) +{ + return renderArea.topLeft() + input * getScalingFactor(); +} + +Point Canvas::transformSize(const Point & input) +{ + return input * getScalingFactor(); +} + +Canvas Canvas::createFromSurface(SDL_Surface * surface, CanvasScalingPolicy scalingPolicy) +{ + return Canvas(surface, scalingPolicy); } void Canvas::applyTransparency(bool on) @@ -81,19 +106,20 @@ void Canvas::draw(const std::shared_ptr& image, const Point & pos) { assert(image); if (image) - image->draw(surface, renderArea.x + pos.x, renderArea.y + pos.y); + image->draw(surface, transformPos(pos)); } void Canvas::draw(const std::shared_ptr& image, const Point & pos, const Rect & sourceRect) { + Rect realSourceRect = sourceRect * getScalingFactor(); assert(image); if (image) - image->draw(surface, renderArea.x + pos.x, renderArea.y + pos.y, &sourceRect); + image->draw(surface, transformPos(pos), &realSourceRect); } void Canvas::draw(const Canvas & image, const Point & pos) { - CSDL_Ext::blitSurface(image.surface, image.renderArea, surface, renderArea.topLeft() + pos); + CSDL_Ext::blitSurface(image.surface, image.renderArea, surface, transformPos(pos)); } void Canvas::drawTransparent(const Canvas & image, const Point & pos, double transparency) @@ -103,42 +129,38 @@ void Canvas::drawTransparent(const Canvas & image, const Point & pos, double tra SDL_GetSurfaceBlendMode(image.surface, &oldMode); SDL_SetSurfaceBlendMode(image.surface, SDL_BLENDMODE_BLEND); SDL_SetSurfaceAlphaMod(image.surface, 255 * transparency); - CSDL_Ext::blitSurface(image.surface, image.renderArea, surface, renderArea.topLeft() + pos); + CSDL_Ext::blitSurface(image.surface, image.renderArea, surface, transformPos(pos)); SDL_SetSurfaceAlphaMod(image.surface, 255); SDL_SetSurfaceBlendMode(image.surface, oldMode); } void Canvas::drawScaled(const Canvas & image, const Point & pos, const Point & targetSize) { - SDL_Rect targetRect = CSDL_Ext::toSDL(Rect(pos + renderArea.topLeft(), targetSize)); + SDL_Rect targetRect = CSDL_Ext::toSDL(Rect(transformPos(pos), transformSize(targetSize))); SDL_BlitScaled(image.surface, nullptr, surface, &targetRect); } void Canvas::drawPoint(const Point & dest, const ColorRGBA & color) { - CSDL_Ext::putPixelWithoutRefreshIfInSurf(surface, dest.x, dest.y, color.r, color.g, color.b, color.a); + Point point = transformPos(dest); + CSDL_Ext::putPixelWithoutRefreshIfInSurf(surface, point.x, point.y, color.r, color.g, color.b, color.a); } void Canvas::drawLine(const Point & from, const Point & dest, const ColorRGBA & colorFrom, const ColorRGBA & colorDest) { - CSDL_Ext::drawLine(surface, renderArea.topLeft() + from, renderArea.topLeft() + dest, CSDL_Ext::toSDL(colorFrom), CSDL_Ext::toSDL(colorDest)); -} - -void Canvas::drawLineDashed(const Point & from, const Point & dest, const ColorRGBA & color) -{ - CSDL_Ext::drawLineDashed(surface, renderArea.topLeft() + from, renderArea.topLeft() + dest, CSDL_Ext::toSDL(color)); + CSDL_Ext::drawLine(surface, transformPos(from), transformPos(dest), CSDL_Ext::toSDL(colorFrom), CSDL_Ext::toSDL(colorDest), getScalingFactor()); } void Canvas::drawBorder(const Rect & target, const ColorRGBA & color, int width) { - Rect realTarget = target + renderArea.topLeft(); + Rect realTarget = target * getScalingFactor() + renderArea.topLeft(); - CSDL_Ext::drawBorder(surface, realTarget.x, realTarget.y, realTarget.w, realTarget.h, CSDL_Ext::toSDL(color), width); + CSDL_Ext::drawBorder(surface, realTarget.x, realTarget.y, realTarget.w, realTarget.h, CSDL_Ext::toSDL(color), width * getScalingFactor()); } void Canvas::drawBorderDashed(const Rect & target, const ColorRGBA & color) { - Rect realTarget = target + renderArea.topLeft(); + Rect realTarget = target * getScalingFactor() + renderArea.topLeft(); CSDL_Ext::drawLineDashed(surface, realTarget.topLeft(), realTarget.topRight(), CSDL_Ext::toSDL(color)); CSDL_Ext::drawLineDashed(surface, realTarget.bottomLeft(), realTarget.bottomRight(), CSDL_Ext::toSDL(color)); @@ -148,36 +170,40 @@ void Canvas::drawBorderDashed(const Rect & target, const ColorRGBA & color) void Canvas::drawText(const Point & position, const EFonts & font, const ColorRGBA & colorDest, ETextAlignment alignment, const std::string & text ) { + const auto & fontPtr = GH.renderHandler().loadFont(font); + switch (alignment) { - case ETextAlignment::TOPLEFT: return graphics->fonts[font]->renderTextLeft (surface, text, colorDest, renderArea.topLeft() + position); - case ETextAlignment::TOPCENTER: return graphics->fonts[font]->renderTextCenter(surface, text, colorDest, renderArea.topLeft() + position); - case ETextAlignment::CENTER: return graphics->fonts[font]->renderTextCenter(surface, text, colorDest, renderArea.topLeft() + position); - case ETextAlignment::BOTTOMRIGHT: return graphics->fonts[font]->renderTextRight (surface, text, colorDest, renderArea.topLeft() + position); + case ETextAlignment::TOPLEFT: return fontPtr->renderTextLeft (surface, text, colorDest, transformPos(position)); + case ETextAlignment::TOPCENTER: return fontPtr->renderTextCenter(surface, text, colorDest, transformPos(position)); + case ETextAlignment::CENTER: return fontPtr->renderTextCenter(surface, text, colorDest, transformPos(position)); + case ETextAlignment::BOTTOMRIGHT: return fontPtr->renderTextRight (surface, text, colorDest, transformPos(position)); } } void Canvas::drawText(const Point & position, const EFonts & font, const ColorRGBA & colorDest, ETextAlignment alignment, const std::vector & text ) { + const auto & fontPtr = GH.renderHandler().loadFont(font); + switch (alignment) { - case ETextAlignment::TOPLEFT: return graphics->fonts[font]->renderTextLinesLeft (surface, text, colorDest, renderArea.topLeft() + position); - case ETextAlignment::TOPCENTER: return graphics->fonts[font]->renderTextLinesCenter(surface, text, colorDest, renderArea.topLeft() + position); - case ETextAlignment::CENTER: return graphics->fonts[font]->renderTextLinesCenter(surface, text, colorDest, renderArea.topLeft() + position); - case ETextAlignment::BOTTOMRIGHT: return graphics->fonts[font]->renderTextLinesRight (surface, text, colorDest, renderArea.topLeft() + position); + case ETextAlignment::TOPLEFT: return fontPtr->renderTextLinesLeft (surface, text, colorDest, transformPos(position)); + case ETextAlignment::TOPCENTER: return fontPtr->renderTextLinesCenter(surface, text, colorDest, transformPos(position)); + case ETextAlignment::CENTER: return fontPtr->renderTextLinesCenter(surface, text, colorDest, transformPos(position)); + case ETextAlignment::BOTTOMRIGHT: return fontPtr->renderTextLinesRight (surface, text, colorDest, transformPos(position)); } } void Canvas::drawColor(const Rect & target, const ColorRGBA & color) { - Rect realTarget = target + renderArea.topLeft(); + Rect realTarget = target * getScalingFactor() + renderArea.topLeft(); CSDL_Ext::fillRect(surface, realTarget, CSDL_Ext::toSDL(color)); } void Canvas::drawColorBlended(const Rect & target, const ColorRGBA & color) { - Rect realTarget = target + renderArea.topLeft(); + Rect realTarget = target * getScalingFactor() + renderArea.topLeft(); CSDL_Ext::fillRectBlended(surface, realTarget, CSDL_Ext::toSDL(color)); } @@ -192,7 +218,7 @@ void Canvas::fillTexture(const std::shared_ptr& image) for (int y=0; y < surface->h; y+= imageArea.h) { for (int x=0; x < surface->w; x+= imageArea.w) - image->draw(surface, renderArea.x + x, renderArea.y + y); + image->draw(surface, Point(renderArea.x + x, renderArea.y + y)); } } diff --git a/client/render/Canvas.h b/client/render/Canvas.h index 647c1ddde..a421ab232 100644 --- a/client/render/Canvas.h +++ b/client/render/Canvas.h @@ -15,11 +15,21 @@ struct SDL_Surface; class IImage; -enum EFonts : int; +enum EFonts : int8_t; + +enum class CanvasScalingPolicy +{ + AUTO, // automatically scale canvas operations by global scaling factor + IGNORE // disable any scaling processing. Scaling factor will be set to 1 + +}; /// Class that represents surface for drawing on class Canvas { + /// Upscaler awareness. Must be first member for initialization + CanvasScalingPolicy scalingPolicy; + /// Target surface SDL_Surface * surface; @@ -27,27 +37,30 @@ class Canvas Rect renderArea; /// constructs canvas using existing surface. Caller maintains ownership on the surface - explicit Canvas(SDL_Surface * surface); + explicit Canvas(SDL_Surface * surface, CanvasScalingPolicy scalingPolicy); - /// copy contructor + /// copy constructor Canvas(const Canvas & other); + Point transformPos(const Point & input); + Point transformSize(const Point & input); + public: Canvas & operator = (const Canvas & other) = delete; Canvas & operator = (Canvas && other) = delete; - /// move contructor + /// move constructor Canvas(Canvas && other); /// creates canvas that only covers specified subsection of a surface Canvas(const Canvas & other, const Rect & clipRect); /// constructs canvas of specified size - explicit Canvas(const Point & size); + explicit Canvas(const Point & size, CanvasScalingPolicy scalingPolicy); /// constructs canvas using existing surface. Caller maintains ownership on the surface /// Compatibility method. AVOID USAGE. To be removed once SDL abstraction layer is finished. - static Canvas createFromSurface(SDL_Surface * surface); + static Canvas createFromSurface(SDL_Surface * surface, CanvasScalingPolicy scalingPolicy); ~Canvas(); @@ -78,9 +91,6 @@ public: /// renders continuous, 1-pixel wide line with color gradient void drawLine(const Point & from, const Point & dest, const ColorRGBA & colorFrom, const ColorRGBA & colorDest); - /// renders dashed, 1-pixel wide line with specified color - void drawLineDashed(const Point & from, const Point & dest, const ColorRGBA & color); - /// renders rectangular, solid-color border in specified location void drawBorder(const Rect & target, const ColorRGBA & color, int width = 1); @@ -102,6 +112,8 @@ public: /// fills canvas with texture void fillTexture(const std::shared_ptr& image); + int getScalingFactor() const; + /// Compatibility method. AVOID USAGE. To be removed once SDL abstraction layer is finished. SDL_Surface * getInternalSurface(); 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/Colors.cpp b/client/render/Colors.cpp index d0bd8e23f..1b14130a9 100644 --- a/client/render/Colors.cpp +++ b/client/render/Colors.cpp @@ -15,6 +15,7 @@ const ColorRGBA Colors::YELLOW = { 229, 215, 123, ColorRGBA::ALPHA_OPAQUE }; const ColorRGBA Colors::WHITE = { 255, 243, 222, ColorRGBA::ALPHA_OPAQUE }; +const ColorRGBA Colors::WHITE_TRUE = { 255, 255, 255, ColorRGBA::ALPHA_OPAQUE }; const ColorRGBA Colors::METALLIC_GOLD = { 173, 142, 66, ColorRGBA::ALPHA_OPAQUE }; const ColorRGBA Colors::GREEN = { 0, 255, 0, ColorRGBA::ALPHA_OPAQUE }; const ColorRGBA Colors::CYAN = { 0, 255, 255, ColorRGBA::ALPHA_OPAQUE }; diff --git a/client/render/Colors.h b/client/render/Colors.h index ef046bd8f..cd643381b 100644 --- a/client/render/Colors.h +++ b/client/render/Colors.h @@ -23,6 +23,9 @@ public: /** the standard h3 white color */ static const ColorRGBA WHITE; + /** actual 100% white color */ + static const ColorRGBA WHITE_TRUE; + /** the metallic gold color used mostly as a border around buttons */ static const ColorRGBA METALLIC_GOLD; diff --git a/client/render/EFont.h b/client/render/EFont.h index 7f5dfee41..316f3028f 100644 --- a/client/render/EFont.h +++ b/client/render/EFont.h @@ -9,7 +9,15 @@ */ #pragma once -enum EFonts : int +enum EFonts : int8_t { - FONT_BIG, FONT_CALLI, FONT_CREDITS, FONT_HIGH_SCORE, FONT_MEDIUM, FONT_SMALL, FONT_TIMES, FONT_TINY, FONT_VERD + FONT_BIG, + FONT_CALLI, + FONT_CREDITS, + FONT_HIGH_SCORE, + FONT_MEDIUM, + FONT_SMALL, + FONT_TIMES, + FONT_TINY, + FONT_VERD }; diff --git a/client/render/Graphics.cpp b/client/render/Graphics.cpp index ad8f90d92..9792e83e7 100644 --- a/client/render/Graphics.cpp +++ b/client/render/Graphics.cpp @@ -19,26 +19,15 @@ #include #include "../renderSDL/SDL_Extensions.h" -#include "../renderSDL/CBitmapFont.h" -#include "../renderSDL/CBitmapHanFont.h" -#include "../renderSDL/CTrueTypeFont.h" #include "../render/CAnimation.h" #include "../render/IImage.h" -#include "../render/IRenderHandler.h" -#include "../gui/CGuiHandler.h" #include "../lib/filesystem/Filesystem.h" #include "../lib/filesystem/CBinaryReader.h" #include "../../lib/json/JsonNode.h" #include "../lib/modding/CModHandler.h" #include "../lib/modding/ModScope.h" -#include "CGameInfo.h" #include "../lib/VCMI_Lib.h" -#include "../CCallback.h" -#include "../lib/CGeneralTextHandler.h" -#include "../lib/vcmi_endian.h" -#include "../lib/CStopWatch.h" -#include "../lib/CHeroHandler.h" #include @@ -127,103 +116,41 @@ void Graphics::initializeBattleGraphics() } Graphics::Graphics() { - loadFonts(); loadPaletteAndColors(); initializeBattleGraphics(); loadErmuToPicture(); - initializeImageLists(); //(!) do not load any CAnimation here } -void Graphics::blueToPlayersAdv(SDL_Surface * sur, PlayerColor player) +void Graphics::setPlayerPalette(SDL_Palette * targetPalette, PlayerColor player) { - if(sur->format->palette) + SDL_Color palette[32]; + if(player.isValidPlayer()) { - SDL_Color palette[32]; - if(player.isValidPlayer()) - { - for(int i=0; i<32; ++i) - palette[i] = CSDL_Ext::toSDL(playerColorPalette[player][i]); - } - else if(player == PlayerColor::NEUTRAL) - { - for(int i=0; i<32; ++i) - palette[i] = CSDL_Ext::toSDL(neutralColorPalette[i]); - } - else - { - logGlobal->error("Wrong player id in blueToPlayersAdv (%s)!", player.toString()); - return; - } -//FIXME: not all player colored images have player palette at last 32 indexes -//NOTE: following code is much more correct but still not perfect (bugged with status bar) - CSDL_Ext::setColors(sur, palette, 224, 32); - - -#if 0 - - SDL_Color * bluePalette = playerColorPalette + 32; - - SDL_Palette * oldPalette = sur->format->palette; - - SDL_Palette * newPalette = SDL_AllocPalette(256); - - for(size_t destIndex = 0; destIndex < 256; destIndex++) - { - SDL_Color old = oldPalette->colors[destIndex]; - - bool found = false; - - for(size_t srcIndex = 0; srcIndex < 32; srcIndex++) - { - if(old.b == bluePalette[srcIndex].b && old.g == bluePalette[srcIndex].g && old.r == bluePalette[srcIndex].r) - { - found = true; - newPalette->colors[destIndex] = palette[srcIndex]; - break; - } - } - if(!found) - newPalette->colors[destIndex] = old; - } - - SDL_SetSurfacePalette(sur, newPalette); - - SDL_FreePalette(newPalette); - -#endif // 0 + for(int i=0; i<32; ++i) + palette[i] = CSDL_Ext::toSDL(playerColorPalette[player][i]); } else { - //TODO: implement. H3 method works only for images with palettes. - // Add some kind of player-colored overlay? - // Or keep palette approach here and replace only colors of specific value(s) - // Or just wait for OpenGL support? - logGlobal->warn("Image must have palette to be player-colored!"); + for(int i=0; i<32; ++i) + palette[i] = CSDL_Ext::toSDL(neutralColorPalette[i]); } + + SDL_SetPaletteColors(targetPalette, palette, 224, 32); } -void Graphics::loadFonts() +void Graphics::setPlayerFlagColor(SDL_Palette * targetPalette, PlayerColor player) { - const JsonNode config(JsonPath::builtin("config/fonts.json")); - - const JsonVector & bmpConf = config["bitmap"].Vector(); - const JsonNode & ttfConf = config["trueType"]; - const JsonNode & hanConf = config["bitmapHan"]; - - assert(bmpConf.size() == FONTS_NUMBER); - - for (size_t i=0; i(hanConf[filename]); - else if (!ttfConf[filename].isNull()) // no ttf override - fonts[i] = std::make_shared(ttfConf[filename]); - else - fonts[i] = std::make_shared(filename); + SDL_Color color = CSDL_Ext::toSDL(playerColors[player.getNum()]); + SDL_SetPaletteColors(targetPalette, &color, 5, 1); + } + else + { + SDL_Color color = CSDL_Ext::toSDL(neutralColor); + SDL_SetPaletteColors(targetPalette, &color, 5, 1); } } @@ -244,51 +171,3 @@ void Graphics::loadErmuToPicture() } assert (etp_idx == 44); } - -void Graphics::addImageListEntry(size_t index, size_t group, const std::string & listName, const std::string & imageName) -{ - if (!imageName.empty()) - { - JsonNode entry; - if (group != 0) - entry["group"].Integer() = group; - entry["frame"].Integer() = index; - entry["file"].String() = imageName; - - imageLists["SPRITES/" + listName]["images"].Vector().push_back(entry); - } -} - -void Graphics::addImageListEntries(const EntityService * service) -{ - auto cb = std::bind(&Graphics::addImageListEntry, this, _1, _2, _3, _4); - - auto loopCb = [&](const Entity * entity, bool & stop) - { - entity->registerIcons(cb); - }; - - service->forEachBase(loopCb); -} - -void Graphics::initializeImageLists() -{ - addImageListEntries(CGI->creatures()); - addImageListEntries(CGI->heroTypes()); - addImageListEntries(CGI->artifacts()); - addImageListEntries(CGI->factions()); - addImageListEntries(CGI->spells()); - addImageListEntries(CGI->skills()); -} - -std::shared_ptr Graphics::getAnimation(const AnimationPath & path) -{ - if (cachedAnimations.count(path) != 0) - return cachedAnimations.at(path); - - auto newAnimation = GH.renderHandler().loadAnimation(path); - - newAnimation->preload(); - cachedAnimations[path] = newAnimation; - return newAnimation; -} diff --git a/client/render/Graphics.h b/client/render/Graphics.h index 4170f8e1a..017f8e1b4 100644 --- a/client/render/Graphics.h +++ b/client/render/Graphics.h @@ -9,49 +9,20 @@ */ #pragma once -#include "../lib/GameConstants.h" +#include "../lib/constants/NumericConstants.h" +#include "../lib/constants/EntityIdentifiers.h" #include "../lib/Color.h" -#include "../lib/filesystem/ResourcePath.h" -VCMI_LIB_NAMESPACE_BEGIN - -class CGHeroInstance; -class CGTownInstance; -class CHeroClass; -struct InfoAboutHero; -struct InfoAboutTown; -class CGObjectInstance; -class ObjectTemplate; -class EntityService; -class JsonNode; - -VCMI_LIB_NAMESPACE_END - -struct SDL_Surface; -class CAnimation; -class IFont; +struct SDL_Palette; /// Handles fonts, hero images, town images, various graphics class Graphics { - void addImageListEntry(size_t index, size_t group, const std::string & listName, const std::string & imageName); - void addImageListEntries(const EntityService * service); - void initializeBattleGraphics(); void loadPaletteAndColors(); void loadErmuToPicture(); - void loadFonts(); - void initializeImageLists(); - - std::map> cachedAnimations; public: - std::shared_ptr getAnimation(const AnimationPath & path); - - //Fonts - static const int FONTS_NUMBER = 9; - std::array< std::shared_ptr, FONTS_NUMBER> fonts; - using PlayerPalette = std::array; //various graphics @@ -61,8 +32,6 @@ public: PlayerPalette neutralColorPalette; ColorRGBA neutralColor; - std::map imageLists; - //towns std::map ERMUtoPicture[GameConstants::F_NUMBER]; //maps building ID to it's picture's name for each town type //for battles @@ -71,7 +40,8 @@ public: //functions Graphics(); - void blueToPlayersAdv(SDL_Surface * sur, PlayerColor player); //replaces blue interface colour with a color of player + void setPlayerPalette(SDL_Palette * sur, PlayerColor player); + void setPlayerFlagColor(SDL_Palette * sur, PlayerColor player); }; extern Graphics * graphics; diff --git a/client/render/IFont.cpp b/client/render/IFont.cpp index 5cc60928b..5e773de04 100644 --- a/client/render/IFont.cpp +++ b/client/render/IFont.cpp @@ -11,16 +11,45 @@ #include "StdInc.h" #include "IFont.h" +#include "../gui/CGuiHandler.h" + +#include "../render/IScreenHandler.h" + #include "../../lib/Point.h" -#include "../../lib/TextOperations.h" +#include "../../lib/texts/TextOperations.h" + +int IFont::getScalingFactor() const +{ + return GH.screenHandler().getScalingFactor(); +} + +size_t IFont::getLineHeight() const +{ + return getLineHeightScaled() / getScalingFactor(); +} + +size_t IFont::getGlyphWidth(const char * data) const +{ + return getGlyphWidthScaled(data) / getScalingFactor(); +} size_t IFont::getStringWidth(const std::string & data) const +{ + return getStringWidthScaled(data) / getScalingFactor(); +} + +size_t IFont::getFontAscent() const +{ + return getFontAscentScaled() / getScalingFactor(); +} + +size_t IFont::getStringWidthScaled(const std::string & data) const { size_t width = 0; for(size_t i=0; i & data, const ColorRGBA & color, const Point & pos) const { Point currPos = pos; - currPos.y -= (int)data.size() * (int)getLineHeight(); + currPos.y -= data.size() * getLineHeight() * getScalingFactor(); for(const std::string & line : data) { renderTextRight(surface, line, color, currPos); - currPos.y += (int)getLineHeight(); + currPos.y += getLineHeight() * getScalingFactor(); } } void IFont::renderTextLinesCenter(SDL_Surface * surface, const std::vector & data, const ColorRGBA & color, const Point & pos) const { Point currPos = pos; - currPos.y -= (int)data.size() * (int)getLineHeight() / 2; + currPos.y -= data.size() * getLineHeight() / 2 * getScalingFactor(); for(const std::string & line : data) { renderTextCenter(surface, line, color, currPos); - currPos.y += (int)getLineHeight(); + currPos.y += getLineHeight() * getScalingFactor(); } } diff --git a/client/render/IFont.h b/client/render/IFont.h index 80575de93..a54ea8294 100644 --- a/client/render/IFont.h +++ b/client/render/IFont.h @@ -16,22 +16,38 @@ VCMI_LIB_NAMESPACE_END struct SDL_Surface; -class IFont +class IFont : boost::noncopyable { protected: - /// Internal function to render font, see renderTextLeft - virtual void renderText(SDL_Surface * surface, const std::string & data, const ColorRGBA & color, const Point & pos) const = 0; + + int getScalingFactor() const; public: virtual ~IFont() {} /// Returns height of font - virtual size_t getLineHeight() const = 0; + virtual size_t getLineHeightScaled() const = 0; /// Returns width, in pixels of a character glyph. Pointer must contain at least characterSize valid bytes - virtual size_t getGlyphWidth(const char * data) const = 0; + virtual size_t getGlyphWidthScaled(const char * data) const = 0; /// Return width of the string - virtual size_t getStringWidth(const std::string & data) const; + virtual size_t getStringWidthScaled(const std::string & data) const; + /// Returns distance from top of the font glyphs to baseline + virtual size_t getFontAscentScaled() const = 0; + + /// Returns height of font + size_t getLineHeight() const; + /// Returns width, in pixels of a character glyph. Pointer must contain at least characterSize valid bytes + size_t getGlyphWidth(const char * data) const; + /// Return width of the string + size_t getStringWidth(const std::string & data) const; + /// Returns distance from top of the font glyphs to baseline + size_t getFontAscent() const; + + /// Internal function to render font, see renderTextLeft + virtual void renderText(SDL_Surface * surface, const std::string & data, const ColorRGBA & color, const Point & pos) const = 0; + + virtual bool canRepresentCharacter(const char * data) const = 0; /** * @param surface - destination to print text on diff --git a/client/render/IImage.h b/client/render/IImage.h index adf5126cd..cc7cf6889 100644 --- a/client/render/IImage.h +++ b/client/render/IImage.h @@ -21,43 +21,62 @@ class ColorRGBA; VCMI_LIB_NAMESPACE_END struct SDL_Surface; +struct SDL_Palette; class ColorFilter; +class ISharedImage; /// Defines which blit method will be selected when image is used for rendering -enum class EImageBlitMode +enum class EImageBlitMode : uint8_t { - /// Image can have no transparency and can be only used as background + /// Preferred for images that don't need any background + /// Indexed or RGBA: Image can have no transparency and can be only used as background OPAQUE, - /// Image can have only a single color as transparency and has no semi-transparent areas + /// Preferred for images that may need transparency + /// Indexed: Image can have only a single color as transparency and has no semi-transparent areas + /// RGBA: full alpha transparency range, e.g. shadows COLORKEY, - /// 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, can be used for non-animation pictures as well - */ +/// Base class for images for use in client code. +/// This class represents current state of image, with potential transformations applied, such as player coloring class IImage { public: - using SpecialPalette = std::vector; - static constexpr int32_t SPECIAL_PALETTE_MASK_CREATURES = 0b11110011; - //draws image on surface "where" at position - virtual void draw(SDL_Surface * where, int posX = 0, int posY = 0, const Rect * src = nullptr) const = 0; - virtual void draw(SDL_Surface * where, const Rect * dest, const Rect * src) const = 0; + virtual void draw(SDL_Surface * where, const Point & pos, const Rect * src = nullptr) const = 0; - virtual std::shared_ptr scaleFast(const Point & size) const = 0; + virtual void scaleTo(const Point & size) = 0; + virtual void scaleInteger(int factor) = 0; virtual void exportBitmap(const boost::filesystem::path & path) const = 0; //Change palette to specific player - virtual void playerColored(PlayerColor player)=0; - - //set special color for flag - virtual void setFlagColor(PlayerColor player)=0; + virtual void playerColored(PlayerColor player) = 0; //test transparency of specific pixel virtual bool isTransparent(const Point & coords) const = 0; @@ -69,20 +88,36 @@ public: //only indexed bitmaps, 16 colors maximum virtual void shiftPalette(uint32_t firstColorID, uint32_t colorsToMove, uint32_t distanceToMove) = 0; virtual void adjustPalette(const ColorFilter & shifter, uint32_t colorsToSkipMask) = 0; - virtual void resetPalette(int colorID) = 0; - virtual void resetPalette() = 0; virtual void setAlpha(uint8_t value) = 0; virtual void setBlitMode(EImageBlitMode mode) = 0; //only indexed bitmaps with 7 special colors - virtual void setSpecialPallete(const SpecialPalette & SpecialPalette, uint32_t colorsToSkipMask) = 0; + virtual void setOverlayColor(const ColorRGBA & color) = 0; - virtual void horizontalFlip() = 0; - virtual void verticalFlip() = 0; - virtual void doubleFlip() = 0; + virtual std::shared_ptr getSharedImage() const = 0; - IImage() = default; virtual ~IImage() = default; }; +/// Base class for image data, mostly for internal use +/// Represents unmodified pixel data, usually loaded from file +/// This image can be shared between multiple image handlers (IImage instances) +class ISharedImage +{ +public: + virtual Point dimensions() const = 0; + virtual void exportBitmap(const boost::filesystem::path & path, SDL_Palette * palette) const = 0; + 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; + + [[nodiscard]] virtual std::shared_ptr createImageReference(EImageBlitMode mode) const = 0; + + [[nodiscard]] virtual std::shared_ptr horizontalFlip() const = 0; + [[nodiscard]] virtual std::shared_ptr verticalFlip() const = 0; + [[nodiscard]] virtual std::shared_ptr scaleInteger(int factor, SDL_Palette * palette, EImageBlitMode blitMode) const = 0; + [[nodiscard]] virtual std::shared_ptr scaleTo(const Point & size, SDL_Palette * palette) const = 0; + + + virtual ~ISharedImage() = default; +}; diff --git a/client/render/IImageLoader.h b/client/render/IImageLoader.h index 7cd950091..b4368e5f5 100644 --- a/client/render/IImageLoader.h +++ b/client/render/IImageLoader.h @@ -14,8 +14,6 @@ VCMI_LIB_NAMESPACE_BEGIN class Point; VCMI_LIB_NAMESPACE_END -class SDLImage; - struct SDL_Color; class IImageLoader diff --git a/client/render/IRenderHandler.h b/client/render/IRenderHandler.h index 880295170..b3b0deb19 100644 --- a/client/render/IRenderHandler.h +++ b/client/render/IRenderHandler.h @@ -9,30 +9,40 @@ */ #pragma once -#include "../../lib/filesystem/ResourcePath.h" +#include "ImageLocator.h" + +VCMI_LIB_NAMESPACE_BEGIN +class Services; +VCMI_LIB_NAMESPACE_END struct SDL_Surface; +class IFont; class IImage; class CAnimation; -enum class EImageBlitMode; +enum class EImageBlitMode : uint8_t; +enum EFonts : int8_t; class IRenderHandler : public boost::noncopyable { public: virtual ~IRenderHandler() = default; + /// Must be called once VLC loading is over to initialize icons + virtual void onLibraryLoadingFinished(const Services * services) = 0; + /// Loads image using given path - virtual std::shared_ptr loadImage(const ImagePath & path) = 0; + virtual std::shared_ptr loadImage(const ImageLocator & locator, EImageBlitMode mode) = 0; virtual std::shared_ptr loadImage(const ImagePath & path, EImageBlitMode mode) = 0; + virtual std::shared_ptr loadImage(const AnimationPath & path, int frame, int group, EImageBlitMode mode) = 0; /// temporary compatibility method. Creates IImage from existing SDL_Surface /// Surface will be shared, caller must still free it with SDL_FreeSurface virtual std::shared_ptr createImage(SDL_Surface * source) = 0; /// Loads animation using given path - virtual std::shared_ptr loadAnimation(const AnimationPath & path) = 0; + virtual std::shared_ptr loadAnimation(const AnimationPath & path, EImageBlitMode mode) = 0; - /// Creates empty CAnimation - virtual std::shared_ptr createAnimation() = 0; + /// Returns font with specified identifer + virtual std::shared_ptr loadFont(EFonts font) = 0; }; diff --git a/client/render/IScreenHandler.h b/client/render/IScreenHandler.h index 49e5cd95e..f3b6d5eee 100644 --- a/client/render/IScreenHandler.h +++ b/client/render/IScreenHandler.h @@ -41,6 +41,13 @@ public: /// Dimensions of render output virtual Point getRenderResolution() const = 0; + /// 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 virtual bool hasFocus() = 0; }; diff --git a/client/render/ImageLocator.cpp b/client/render/ImageLocator.cpp new file mode 100644 index 000000000..515e767a0 --- /dev/null +++ b/client/render/ImageLocator.cpp @@ -0,0 +1,137 @@ +/* + * ImageLocator.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 "ImageLocator.h" + +#include "../gui/CGuiHandler.h" +#include "IScreenHandler.h" + +#include "../../lib/json/JsonNode.h" + +ImageLocator::ImageLocator(const JsonNode & config) + : 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) + : image(path) +{ +} + +ImageLocator::ImageLocator(const AnimationPath & path, int frame, int group) + : defFile(path) + , defFrame(frame) + , defGroup(group) +{ +} + +bool ImageLocator::operator<(const ImageLocator & other) const +{ + if(image != other.image) + return image < other.image; + if(defFile != other.defFile) + return defFile < other.defFile; + if(defGroup != other.defGroup) + return defGroup < other.defGroup; + if(defFrame != other.defFrame) + return defFrame < other.defFrame; + if(verticalFlip != other.verticalFlip) + return verticalFlip < other.verticalFlip; + if(horizontalFlip != other.horizontalFlip) + return horizontalFlip < other.horizontalFlip; + if(scalingFactor != other.scalingFactor) + return scalingFactor < other.scalingFactor; + if(playerColored != other.playerColored) + return playerColored < other.playerColored; + if(layer != other.layer) + return layer < other.layer; + + return false; +} + +bool ImageLocator::empty() const +{ + return !image.has_value() && !defFile.has_value(); +} + +ImageLocator ImageLocator::copyFile() const +{ + ImageLocator result; + result.scalingFactor = 1; + result.preScaledFactor = preScaledFactor; + result.image = image; + result.defFile = defFile; + result.defFrame = defFrame; + result.defGroup = defGroup; + return result; +} + +ImageLocator ImageLocator::copyFileTransform() const +{ + ImageLocator result = copyFile(); + result.horizontalFlip = horizontalFlip; + result.verticalFlip = verticalFlip; + return result; +} + +ImageLocator ImageLocator::copyFileTransformScale() const +{ + return *this; // full copy +} + +std::string ImageLocator::toString() const +{ + std::string result; + if (empty()) + return "invalid"; + + if (image) + { + result += image->getOriginalName(); + assert(!result.empty()); + } + + if (defFile) + { + result += defFile->getOriginalName(); + assert(!result.empty()); + result += "-" + std::to_string(defGroup); + result += "-" + std::to_string(defFrame); + } + + if (verticalFlip) + result += "-vflip"; + + if (horizontalFlip) + result += "-hflip"; + + if (scalingFactor > 1) + result += "-scale" + std::to_string(scalingFactor); + + if (playerColored.isValidPlayer()) + result += "-player" + playerColored.toString(); + + 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 new file mode 100644 index 000000000..1868caaf2 --- /dev/null +++ b/client/render/ImageLocator.h @@ -0,0 +1,48 @@ +/* + * ImageLocator.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 "IImage.h" + +#include "../../lib/filesystem/ResourcePath.h" +#include "../../lib/constants/EntityIdentifiers.h" + +struct ImageLocator +{ + std::optional image; + std::optional defFile; + int defFrame = -1; + int defGroup = -1; + + 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 + int8_t preScaledFactor = 1; + EImageBlitMode layer = EImageBlitMode::OPAQUE; + + ImageLocator() = default; + ImageLocator(const AnimationPath & path, int frame, int group); + explicit ImageLocator(const JsonNode & config); + explicit ImageLocator(const ImagePath & path); + + bool operator < (const ImageLocator & other) const; + bool empty() const; + + ImageLocator copyFile() const; + ImageLocator copyFileTransform() const; + ImageLocator copyFileTransformScale() const; + + // generates string representation of this image locator + // guaranteed to be a valid file path with no extension + // but may contain '/' if source file is in directory + std::string toString() const; +}; diff --git a/client/renderSDL/CBitmapFont.cpp b/client/renderSDL/CBitmapFont.cpp index 36a29d174..4287221e7 100644 --- a/client/renderSDL/CBitmapFont.cpp +++ b/client/renderSDL/CBitmapFont.cpp @@ -12,33 +12,96 @@ #include "SDL_Extensions.h" #include "../CGameInfo.h" +#include "../gui/CGuiHandler.h" #include "../render/Colors.h" +#include "../render/IScreenHandler.h" -#include "../../lib/Languages.h" +#include "../../lib/CConfigHandler.h" #include "../../lib/Rect.h" -#include "../../lib/TextOperations.h" +#include "../../lib/VCMI_Lib.h" #include "../../lib/filesystem/Filesystem.h" #include "../../lib/modding/CModHandler.h" +#include "../../lib/texts/Languages.h" +#include "../../lib/texts/TextOperations.h" #include "../../lib/vcmi_endian.h" -#include "../../lib/VCMI_Lib.h" #include +#include -void CBitmapFont::loadModFont(const std::string & modName, const ResourcePath & resource) +struct AtlasLayout { - if (!CResourceHandler::get(modName)->existsResource(resource)) + Point dimensions; + std::map images; +}; + +/// Attempts to pack provided list of images into 2d box of specified size +/// Returns resulting layout on success and empty optional on failure +static std::optional tryAtlasPacking(Point dimensions, const std::map & images) +{ + // Simple atlas packing algorithm. Can be extended if needed, however optimal solution is NP-complete problem, so 'perfect' solution is too costly + + AtlasLayout result; + result.dimensions = dimensions; + + // a little interval to prevent potential 'bleeding' into adjacent symbols + // should be unnecessary for base game, but may be needed for upscaled filters + constexpr int interval = 1; + int currentHeight = 0; + int nextHeight = 0; + int currentWidth = 0; + + for (auto const & image : images) { - logGlobal->error("Failed to load font %s from mod %s", resource.getName(), modName); - return; + int nextWidth = currentWidth + image.second.x + interval; + + if (nextWidth > dimensions.x) + { + currentHeight = nextHeight; + currentWidth = 0; + nextWidth = currentWidth + image.second.x + interval; + } + + nextHeight = std::max(nextHeight, currentHeight + image.second.y + interval); + if (nextHeight > dimensions.y) + return std::nullopt; // failure - ran out of space + + result.images[image.first] = Rect(Point(currentWidth, currentHeight), image.second); + + currentWidth = nextWidth; } - auto data = CResourceHandler::get(modName)->load(resource)->readAll(); - std::string modLanguage = CGI->modh->getModLanguage(modName); - std::string modEncoding = Languages::getLanguageOptions(modLanguage).encoding; + return result; +} - uint32_t dataHeight = data.first[5]; +/// Arranges images to fit into texture atlas with automatic selection of image size +/// Returns images arranged into 2d box +static AtlasLayout doAtlasPacking(const std::map & images) +{ + // initial size of an atlas. Smaller size won't even fit tiniest H3 font + Point dimensions(128, 128); - maxHeight = std::max(maxHeight, dataHeight); + for (;;) + { + auto result = tryAtlasPacking(dimensions, images); + + if (result) + return *result; + + // else - packing failed. Increase atlas size and try again + // increase width and height in alternating form: (64,64) -> (128,64) -> (128,128) ... + if (dimensions.x > dimensions.y) + dimensions.y *= 2; + else + dimensions.x *= 2; + } +} + +void CBitmapFont::loadFont(const ResourcePath & resource, std::unordered_map & loadedChars) +{ + auto data = CResourceHandler::get()->load(resource)->readAll(); + std::string modEncoding = VLC->modh->findResourceEncoding(resource); + + height = data.first[5]; constexpr size_t symbolsInFile = 0x100; constexpr size_t baseIndex = 32; @@ -49,15 +112,15 @@ void CBitmapFont::loadModFont(const std::string & modName, const ResourcePath & { CodePoint codepoint = TextOperations::getUnicodeCodepoint(static_cast(charIndex), modEncoding); - BitmapChar symbol; + EntryFNT symbol; symbol.leftOffset = read_le_u32(data.first.get() + baseIndex + charIndex * 12 + 0); symbol.width = read_le_u32(data.first.get() + baseIndex + charIndex * 12 + 4); symbol.rightOffset = read_le_u32(data.first.get() + baseIndex + charIndex * 12 + 8); - symbol.height = dataHeight; + symbol.height = height; uint32_t pixelDataOffset = read_le_u32(data.first.get() + offsetIndex + charIndex * 4); - uint32_t pixelsCount = dataHeight * symbol.width; + uint32_t pixelsCount = height * symbol.width; symbol.pixels.resize(pixelsCount); @@ -65,30 +128,107 @@ void CBitmapFont::loadModFont(const std::string & modName, const ResourcePath & std::copy_n(pixelData, pixelsCount, symbol.pixels.data() ); - chars[codepoint] = symbol; + loadedChars[codepoint] = symbol; } + + // Try to use symbol 'L' to detect font 'ascent' - number of pixels above text baseline + const auto & symbolL = loadedChars['L']; + uint32_t lastNonEmptyRow = 0; + for (uint32_t row = 0; row < symbolL.height; ++row) + { + for (uint32_t col = 0; col < symbolL.width; ++col) + if (symbolL.pixels.at(row * symbolL.width + col) == 255) + lastNonEmptyRow = std::max(lastNonEmptyRow, row); + } + + ascent = lastNonEmptyRow + 1; } CBitmapFont::CBitmapFont(const std::string & filename): - maxHeight(0) + height(0) { ResourcePath resource("data/" + filename, EResType::BMP_FONT); - loadModFont("core", resource); + std::unordered_map loadedChars; + loadFont(resource, loadedChars); - for(const auto & modName : VLC->modh->getActiveMods()) + std::map atlasSymbol; + for (auto const & symbol : loadedChars) + atlasSymbol[symbol.first] = Point(symbol.second.width, symbol.second.height); + + auto atlas = doAtlasPacking(atlasSymbol); + + atlasImage = SDL_CreateRGBSurface(0, atlas.dimensions.x, atlas.dimensions.y, 8, 0, 0, 0, 0); + + assert(atlasImage->format->palette != nullptr); + assert(atlasImage->format->palette->ncolors == 256); + + atlasImage->format->palette->colors[0] = { 0, 255, 255, SDL_ALPHA_OPAQUE }; // transparency + atlasImage->format->palette->colors[1] = { 0, 0, 0, SDL_ALPHA_OPAQUE }; // black shadow + + CSDL_Ext::fillSurface(atlasImage, CSDL_Ext::toSDL(Colors::CYAN)); + CSDL_Ext::setColorKey(atlasImage, CSDL_Ext::toSDL(Colors::CYAN)); + + for (size_t i = 2; i < atlasImage->format->palette->ncolors; ++i) + atlasImage->format->palette->colors[i] = { 255, 255, 255, SDL_ALPHA_OPAQUE }; + + for (auto const & symbol : loadedChars) { - if (CResourceHandler::get(modName)->existsResource(resource)) - loadModFont(modName, resource); + BitmapChar storedEntry; + + storedEntry.leftOffset = symbol.second.leftOffset; + storedEntry.rightOffset = symbol.second.rightOffset; + storedEntry.positionInAtlas = atlas.images.at(symbol.first); + + // Copy pixel data to atlas + uint8_t *dstPixels = static_cast(atlasImage->pixels); + uint8_t *dstLine = dstPixels + storedEntry.positionInAtlas.y * atlasImage->pitch; + uint8_t *dst = dstLine + storedEntry.positionInAtlas.x; + + for (size_t i = 0; i < storedEntry.positionInAtlas.h; ++i) + { + const uint8_t *srcPtr = symbol.second.pixels.data() + i * storedEntry.positionInAtlas.w; + uint8_t * dstPtr = dst + i * atlasImage->pitch; + + std::copy_n(srcPtr, storedEntry.positionInAtlas.w, dstPtr); + } + + chars[symbol.first] = storedEntry; } + + if (GH.screenHandler().getScalingFactor() != 1) + { + static const std::map filterNameToEnum = { + { "nearest", EScalingAlgorithm::NEAREST}, + { "bilinear", EScalingAlgorithm::BILINEAR}, + { "xbrz", EScalingAlgorithm::XBRZ_ALPHA} + }; + + auto filterName = settings["video"]["fontUpscalingFilter"].String(); + EScalingAlgorithm algorithm = filterNameToEnum.at(filterName); + auto scaledSurface = CSDL_Ext::scaleSurfaceIntegerFactor(atlasImage, GH.screenHandler().getScalingFactor(), algorithm); + SDL_FreeSurface(atlasImage); + atlasImage = scaledSurface; + } + + logGlobal->debug("Loaded BMP font: '%s', height %d, ascent %d", + filename, + getLineHeightScaled(), + getFontAscentScaled() + ); } -size_t CBitmapFont::getLineHeight() const +CBitmapFont::~CBitmapFont() { - return maxHeight; + SDL_FreeSurface(atlasImage); } -size_t CBitmapFont::getGlyphWidth(const char * data) const +size_t CBitmapFont::getLineHeightScaled() const +{ + return height * getScalingFactor(); +} + +size_t CBitmapFont::getGlyphWidthScaled(const char * data) const { CodePoint localChar = TextOperations::getUnicodeCodepoint(data, 4); @@ -97,7 +237,12 @@ size_t CBitmapFont::getGlyphWidth(const char * data) const if (iter == chars.end()) return 0; - return iter->second.leftOffset + iter->second.width + iter->second.rightOffset; + return (iter->second.leftOffset + iter->second.positionInAtlas.w + iter->second.rightOffset) * getScalingFactor(); +} + +size_t CBitmapFont::getFontAscentScaled() const +{ + return ascent * getScalingFactor(); } bool CBitmapFont::canRepresentCharacter(const char *data) const @@ -120,52 +265,21 @@ bool CBitmapFont::canRepresentString(const std::string & data) const void CBitmapFont::renderCharacter(SDL_Surface * surface, const BitmapChar & character, const ColorRGBA & color, int &posX, int &posY) const { - Rect clipRect; - CSDL_Ext::getClipRect(surface, clipRect); + int scalingFactor = GH.screenHandler().getScalingFactor(); - posX += character.leftOffset; + posX += character.leftOffset * scalingFactor; - CSDL_Ext::TColorPutter colorPutter = CSDL_Ext::getPutterFor(surface, 0); + auto sdlColor = CSDL_Ext::toSDL(color); - uint8_t bpp = surface->format->BytesPerPixel; + if (atlasImage->format->palette) + SDL_SetPaletteColors(atlasImage->format->palette, &sdlColor, 255, 1); + else + SDL_SetSurfaceColorMod(atlasImage, color.r, color.g, color.b); - // start of line, may differ from 0 due to end of surface or clipped surface - int lineBegin = std::max(0, clipRect.y - posY); - int lineEnd = std::min(character.height, clipRect.y + clipRect.h - posY - 1); + CSDL_Ext::blitSurface(atlasImage, character.positionInAtlas * scalingFactor, surface, Point(posX, posY)); - // start end end of each row, may differ from 0 - int rowBegin = std::max(0, clipRect.x - posX); - int rowEnd = std::min(character.width, clipRect.x + clipRect.w - posX - 1); - - //for each line in symbol - for(int dy = lineBegin; dy pixels; - const uint8_t *srcLine = character.pixels.data(); - - // shift source\destination pixels to current position - dstLine += (posY+dy) * surface->pitch + posX * bpp; - srcLine += dy * character.width; - - //for each column in line - for(int dx = rowBegin; dx < rowEnd; dx++) - { - uint8_t* dstPixel = dstLine + dx*bpp; - switch(srcLine[dx]) - { - case 1: //black "shadow" - colorPutter(dstPixel, 0, 0, 0); - break; - case 255: //text colour - colorPutter(dstPixel, color.r, color.g, color.b); - break; - default : - break; //transparency - } - } - } - posX += character.width; - posX += character.rightOffset; + posX += character.positionInAtlas.w * scalingFactor; + posX += character.rightOffset * scalingFactor; } void CBitmapFont::renderText(SDL_Surface * surface, const std::string & data, const ColorRGBA & color, const Point & pos) const @@ -178,12 +292,6 @@ void CBitmapFont::renderText(SDL_Surface * surface, const std::string & data, co int posX = pos.x; int posY = pos.y; - // Should be used to detect incorrect text parsing. Disabled right now due to some old UI code (mostly pregame and battles) - //assert(data[0] != '{'); - //assert(data[data.size()-1] != '}'); - - SDL_LockSurface(surface); - for(size_t i=0; isecond, color, posX, posY); } - SDL_UnlockSurface(surface); } diff --git a/client/renderSDL/CBitmapFont.h b/client/renderSDL/CBitmapFont.h index b5fa2b4f4..78bb6aad2 100644 --- a/client/renderSDL/CBitmapFont.h +++ b/client/renderSDL/CBitmapFont.h @@ -11,38 +11,52 @@ #include "../render/IFont.h" +#include "../../lib/Rect.h" + VCMI_LIB_NAMESPACE_BEGIN class ResourcePath; VCMI_LIB_NAMESPACE_END -class CBitmapFont : public IFont +class CBitmapFont final : public IFont { + SDL_Surface * atlasImage; + using CodePoint = uint32_t; - struct BitmapChar + struct EntryFNT { int32_t leftOffset; uint32_t width; uint32_t height; int32_t rightOffset; - std::vector pixels; // pixels of this character, part of BitmapFont::data + std::vector pixels; + }; + + struct BitmapChar + { + Rect positionInAtlas; + int32_t leftOffset; + int32_t rightOffset; }; std::unordered_map chars; - uint32_t maxHeight; + uint32_t height; + uint32_t ascent; - void loadModFont(const std::string & modName, const ResourcePath & resource); + void loadFont(const ResourcePath & resource, std::unordered_map & loadedChars); void renderCharacter(SDL_Surface * surface, const BitmapChar & character, const ColorRGBA & color, int &posX, int &posY) const; void renderText(SDL_Surface * surface, const std::string & data, const ColorRGBA & color, const Point & pos) const override; public: explicit CBitmapFont(const std::string & filename); + ~CBitmapFont(); - size_t getLineHeight() const override; - size_t getGlyphWidth(const char * data) const override; + size_t getFontAscentScaled() const override; + size_t getLineHeightScaled() const override; + size_t getGlyphWidthScaled(const char * data) const override; /// returns true if this font contains provided utf-8 character - bool canRepresentCharacter(const char * data) const; + bool canRepresentCharacter(const char * data) const override; bool canRepresentString(const std::string & data) const; friend class CBitmapHanFont; diff --git a/client/renderSDL/CBitmapHanFont.cpp b/client/renderSDL/CBitmapHanFont.cpp deleted file mode 100644 index ac1684185..000000000 --- a/client/renderSDL/CBitmapHanFont.cpp +++ /dev/null @@ -1,127 +0,0 @@ -/* - * CBitmapHanFont.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 "CBitmapHanFont.h" - -#include "CBitmapFont.h" -#include "SDL_Extensions.h" - -#include "../../lib/filesystem/Filesystem.h" -#include "../../lib/json/JsonNode.h" -#include "../../lib/TextOperations.h" -#include "../../lib/Rect.h" - -#include - -size_t CBitmapHanFont::getCharacterDataOffset(size_t index) const -{ - size_t rowSize = (size + 7) / 8; // 1 bit per pixel, rounded up - size_t charSize = rowSize * size; // glyph contains "size" rows - return index * charSize; -} - -size_t CBitmapHanFont::getCharacterIndex(ui8 first, ui8 second) const -{ - if (second > 0x7f ) - second--; - - return (first - 0x81) * (12*16 - 2) + (second - 0x40); -} - -void CBitmapHanFont::renderCharacter(SDL_Surface * surface, int characterIndex, const ColorRGBA & color, int &posX, int &posY) const -{ - //TODO: somewhat duplicated with CBitmapFont::renderCharacter(); - Rect clipRect; - CSDL_Ext::getClipRect(surface, clipRect); - - CSDL_Ext::TColorPutter colorPutter = CSDL_Ext::getPutterFor(surface, 0); - uint8_t bpp = surface->format->BytesPerPixel; - - // start of line, may differ from 0 due to end of surface or clipped surface - int lineBegin = std::max(0, clipRect.y - posY); - int lineEnd = std::min((int)size, clipRect.y + clipRect.h - posY); - - // start end end of each row, may differ from 0 - int rowBegin = std::max(0, clipRect.x - posX); - int rowEnd = std::min((int)size, clipRect.x + clipRect.w - posX); - - //for each line in symbol - for(int dy = lineBegin; dy pixels; - uint8_t *source = data.first.get() + getCharacterDataOffset(characterIndex); - - // shift source\destination pixels to current position - dstLine += (posY+dy) * surface->pitch + posX * bpp; - source += ((size + 7) / 8) * dy; - - //for each column in line - for(int dx = rowBegin; dx < rowEnd; dx++) - { - // select current bit in bitmap - int bit = (source[dx / 8] << (dx % 8)) & 0x80; - - uint8_t* dstPixel = dstLine + dx*bpp; - if (bit != 0) - colorPutter(dstPixel, color.r, color.g, color.b); - } - } - posX += (int)size + 1; -} - -void CBitmapHanFont::renderText(SDL_Surface * surface, const std::string & data, const ColorRGBA & color, const Point & pos) const -{ - int posX = pos.x; - int posY = pos.y; - - SDL_LockSurface(surface); - - for(size_t i=0; irenderCharacter(surface, fallback->chars[ui8(localChar[0])], color, posX, posY); - - if (localChar.size() == 2) - renderCharacter(surface, (int)getCharacterIndex(localChar[0], localChar[1]), color, posX, posY); - } - SDL_UnlockSurface(surface); -} - -CBitmapHanFont::CBitmapHanFont(const JsonNode &config): - fallback(new CBitmapFont(config["fallback"].String())), - data(CResourceHandler::get()->load(ResourcePath("data/" + config["name"].String(), EResType::OTHER))->readAll()), - size((size_t)config["size"].Float()) -{ - // basic tests to make sure that fonts are OK - // 1) fonts must contain 190 "sections", 126 symbols each. - assert(getCharacterIndex(0xfe, 0xff) == 190*126); - // 2) ensure that font size is correct - enough to fit all possible symbols - assert(getCharacterDataOffset(getCharacterIndex(0xfe, 0xff)) == data.second); -} - -size_t CBitmapHanFont::getLineHeight() const -{ - return std::max(size + 1, fallback->getLineHeight()); -} - -size_t CBitmapHanFont::getGlyphWidth(const char * data) const -{ - std::string localChar = TextOperations::fromUnicode(std::string(data, TextOperations::getUnicodeCharacterSize(data[0])), "GBK"); - - if (localChar.size() == 1) - return fallback->getGlyphWidth(data); - - if (localChar.size() == 2) - return size + 1; - - return 0; -} diff --git a/client/renderSDL/CBitmapHanFont.h b/client/renderSDL/CBitmapHanFont.h deleted file mode 100644 index 707c360b2..000000000 --- a/client/renderSDL/CBitmapHanFont.h +++ /dev/null @@ -1,40 +0,0 @@ -/* - * CBitmapHanFont.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 "../render/IFont.h" - -VCMI_LIB_NAMESPACE_BEGIN -class JsonNode; -VCMI_LIB_NAMESPACE_END - -class CBitmapFont; - -/// supports multi-byte characters for such languages like Chinese -class CBitmapHanFont : public IFont -{ - std::unique_ptr fallback; - // data, directly copied from file - const std::pair, ui64> data; - - // size of the font. Not available in file but needed for proper rendering - const size_t size; - - size_t getCharacterDataOffset(size_t index) const; - size_t getCharacterIndex(ui8 first, ui8 second) const; - - void renderCharacter(SDL_Surface * surface, int characterIndex, const ColorRGBA & color, int &posX, int &posY) const; - void renderText(SDL_Surface * surface, const std::string & data, const ColorRGBA & color, const Point & pos) const override; -public: - CBitmapHanFont(const JsonNode & config); - - size_t getLineHeight() const override; - size_t getGlyphWidth(const char * data) const override; -}; diff --git a/client/renderSDL/CTrueTypeFont.cpp b/client/renderSDL/CTrueTypeFont.cpp index e977a6c4c..94faf6502 100644 --- a/client/renderSDL/CTrueTypeFont.cpp +++ b/client/renderSDL/CTrueTypeFont.cpp @@ -15,9 +15,10 @@ #include "../render/Colors.h" #include "../renderSDL/SDL_Extensions.h" -#include "../../lib/TextOperations.h" +#include "../../lib/CConfigHandler.h" #include "../../lib/json/JsonNode.h" #include "../../lib/filesystem/Filesystem.h" +#include "../../lib/texts/TextOperations.h" #include @@ -27,17 +28,26 @@ std::pair, ui64> CTrueTypeFont::loadData(const JsonNode & return CResourceHandler::get()->load(ResourcePath(filename, EResType::TTF_FONT))->readAll(); } +int CTrueTypeFont::getPointSize(const JsonNode & config) const +{ + float fontScale = settings["video"]["fontScalingFactor"].Float(); + int scalingFactor = getScalingFactor(); + + if (config.isNumber()) + return std::round(config.Integer() * scalingFactor * fontScale); + else + return std::round(config[scalingFactor-1].Integer() * fontScale); +} + TTF_Font * CTrueTypeFont::loadFont(const JsonNode &config) { - int pointSize = static_cast(config["size"].Float()); - if(!TTF_WasInit() && TTF_Init()==-1) throw std::runtime_error(std::string("Failed to initialize true type support: ") + TTF_GetError() + "\n"); - return TTF_OpenFontRW(SDL_RWFromConstMem(data.first.get(), (int)data.second), 1, pointSize); + return TTF_OpenFontRW(SDL_RWFromConstMem(data.first.get(), data.second), 1, getPointSize(config["size"])); } -int CTrueTypeFont::getFontStyle(const JsonNode &config) +int CTrueTypeFont::getFontStyle(const JsonNode &config) const { const JsonVector & names = config["style"].Vector(); int ret = 0; @@ -54,60 +64,79 @@ int CTrueTypeFont::getFontStyle(const JsonNode &config) CTrueTypeFont::CTrueTypeFont(const JsonNode & fontConfig): data(loadData(fontConfig)), font(loadFont(fontConfig), TTF_CloseFont), - dropShadow(fontConfig["blend"].Bool()), - blended(fontConfig["blend"].Bool()) + blended(true), + outline(fontConfig["outline"].Bool()), + dropShadow(!fontConfig["noShadow"].Bool()) { assert(font); TTF_SetFontStyle(font.get(), getFontStyle(fontConfig)); + TTF_SetFontHinting(font.get(),TTF_HINTING_MONO); - std::string fallbackName = fontConfig["fallback"].String(); - - if (!fallbackName.empty()) - fallbackFont = std::make_unique(fallbackName); + 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; -size_t CTrueTypeFont::getLineHeight() const +size_t CTrueTypeFont::getFontAscentScaled() const { - if (fallbackFont) - return fallbackFont->getLineHeight(); + return TTF_FontAscent(font.get()); +} +size_t CTrueTypeFont::getLineHeightScaled() const +{ return TTF_FontHeight(font.get()); } -size_t CTrueTypeFont::getGlyphWidth(const char *data) const +size_t CTrueTypeFont::getGlyphWidthScaled(const char *text) const { - if (fallbackFont && fallbackFont->canRepresentCharacter(data)) - return fallbackFont->getGlyphWidth(data); - - return getStringWidth(std::string(data, TextOperations::getUnicodeCharacterSize(*data))); - int advance; - TTF_GlyphMetrics(font.get(), *data, nullptr, nullptr, nullptr, nullptr, &advance); - return advance; + return getStringWidthScaled(std::string(text, TextOperations::getUnicodeCharacterSize(*text))); } -size_t CTrueTypeFont::getStringWidth(const std::string & data) const +bool CTrueTypeFont::canRepresentCharacter(const char * text) const { - if (fallbackFont && fallbackFont->canRepresentString(data)) - return fallbackFont->getStringWidth(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) + if (codepoint <= 0xffff) + return TTF_GlyphIsProvided(font.get(), codepoint); + return true; +#else + return true; +#endif +} +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; } void CTrueTypeFont::renderText(SDL_Surface * surface, const std::string & data, const ColorRGBA & color, const Point & pos) const { - if (fallbackFont && fallbackFont->canRepresentString(data)) + if (color.r != 0 && color.g != 0 && color.b != 0) // not black - add shadow { - fallbackFont->renderText(surface, data, color, pos); - return; - } + if (outline) + renderText(surface, data, Colors::BLACK, pos - Point(1,1) * getScalingFactor()); - if (dropShadow && color.r != 0 && color.g != 0 && color.b != 0) // not black - add shadow - renderText(surface, data, Colors::BLACK, pos + Point(1,1)); + if (dropShadow || outline) + renderText(surface, data, Colors::BLACK, pos + Point(1,1) * getScalingFactor()); + } if (!data.empty()) { diff --git a/client/renderSDL/CTrueTypeFont.h b/client/renderSDL/CTrueTypeFont.h index 804217a0b..87a9ac484 100644 --- a/client/renderSDL/CTrueTypeFont.h +++ b/client/renderSDL/CTrueTypeFont.h @@ -19,25 +19,29 @@ class CBitmapFont; using TTF_Font = struct _TTF_Font; -class CTrueTypeFont : public IFont +class CTrueTypeFont final : public IFont { - std::unique_ptr fallbackFont; const std::pair, ui64> data; const std::unique_ptr font; const bool blended; + const bool outline; const bool dropShadow; std::pair, ui64> loadData(const JsonNode & config); TTF_Font * loadFont(const JsonNode & config); - int getFontStyle(const JsonNode & config); + int getPointSize(const JsonNode & config) const; + int getFontStyle(const JsonNode & config) const; void renderText(SDL_Surface * surface, const std::string & data, const ColorRGBA & color, const Point & pos) const override; + size_t getFontAscentScaled() const override; public: CTrueTypeFont(const JsonNode & fontConfig); ~CTrueTypeFont(); - size_t getLineHeight() const override; - size_t getGlyphWidth(const char * data) const override; - size_t getStringWidth(const std::string & data) const override; + bool canRepresentCharacter(const char * data) const override; + + size_t getLineHeightScaled() const override; + size_t getGlyphWidthScaled(const char * data) const override; + size_t getStringWidthScaled(const std::string & data) const override; }; diff --git a/client/renderSDL/CursorHardware.cpp b/client/renderSDL/CursorHardware.cpp index a4a10f2f8..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 "../renderSDL/ScreenHandler.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().x, image->dimensions().y); + 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); + 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/CursorHardware.h b/client/renderSDL/CursorHardware.h index c4e311778..02b75fca5 100644 --- a/client/renderSDL/CursorHardware.h +++ b/client/renderSDL/CursorHardware.h @@ -9,7 +9,6 @@ */ #pragma once -class CAnimation; class IImage; struct SDL_Surface; struct SDL_Texture; diff --git a/client/renderSDL/CursorSoftware.cpp b/client/renderSDL/CursorSoftware.cpp index 78b9e1250..e5d5a9c09 100644 --- a/client/renderSDL/CursorSoftware.cpp +++ b/client/renderSDL/CursorSoftware.cpp @@ -11,6 +11,8 @@ #include "StdInc.h" #include "CursorSoftware.h" +#include "../gui/CGuiHandler.h" +#include "../render/IScreenHandler.h" #include "../render/Colors.h" #include "../render/IImage.h" #include "../CMT.h" @@ -30,8 +32,8 @@ void CursorSoftware::render() SDL_Rect destRect; destRect.x = renderPos.x; destRect.y = renderPos.y; - destRect.w = 40; - destRect.h = 40; + destRect.w = cursorSurface->w; + destRect.h = cursorSurface->h; SDL_RenderCopy(mainRenderer, cursorTexture, nullptr, &destRect); } @@ -44,7 +46,7 @@ void CursorSoftware::createTexture(const Point & dimensions) if (cursorSurface) SDL_FreeSurface(cursorSurface); - cursorSurface = CSDL_Ext::newSurface(dimensions.x, dimensions.y); + cursorSurface = CSDL_Ext::newSurface(dimensions); cursorTexture = SDL_CreateTexture(mainRenderer, SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_STREAMING, dimensions.x, dimensions.y); SDL_SetSurfaceBlendMode(cursorSurface, SDL_BLENDMODE_NONE); @@ -53,12 +55,17 @@ void CursorSoftware::createTexture(const Point & dimensions) void CursorSoftware::updateTexture() { - if (!cursorSurface || Point(cursorSurface->w, cursorSurface->h) != cursorImage->dimensions()) - createTexture(cursorImage->dimensions()); + if (!cursorSurface) + createTexture(cursorImage->dimensions() * GH.screenHandler().getScalingFactor()); + + Point currentSize = Point(cursorSurface->w, cursorSurface->h); + + if (currentSize != cursorImage->dimensions() * GH.screenHandler().getScalingFactor()) + createTexture(cursorImage->dimensions() * GH.screenHandler().getScalingFactor()); CSDL_Ext::fillSurface(cursorSurface, CSDL_Ext::toSDL(Colors::TRANSPARENCY)); - cursorImage->draw(cursorSurface); + cursorImage->draw(cursorSurface, Point(0,0)); SDL_UpdateTexture(cursorTexture, nullptr, cursorSurface->pixels, cursorSurface->pitch); needUpdate = false; } diff --git a/client/renderSDL/CursorSoftware.h b/client/renderSDL/CursorSoftware.h index 44080e3e2..cb43bb1e5 100644 --- a/client/renderSDL/CursorSoftware.h +++ b/client/renderSDL/CursorSoftware.h @@ -9,7 +9,6 @@ */ #pragma once -class CAnimation; class IImage; struct SDL_Surface; struct SDL_Texture; diff --git a/client/renderSDL/FontChain.cpp b/client/renderSDL/FontChain.cpp new file mode 100644 index 000000000..331d1c71a --- /dev/null +++ b/client/renderSDL/FontChain.cpp @@ -0,0 +1,152 @@ +/* + * FontChain.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 "FontChain.h" + +#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 +{ + auto chunks = splitTextToChunks(data); + int maxAscent = getFontAscentScaled(); + Point currentPos = pos; + for (auto const & chunk : chunks) + { + Point chunkPos = currentPos; + int currAscent = chunk.font->getFontAscentScaled(); + chunkPos.y += maxAscent - currAscent; + chunk.font->renderText(surface, chunk.text, color, chunkPos); + currentPos.x += chunk.font->getStringWidthScaled(chunk.text); + } +} + +size_t FontChain::getFontAscentScaled() const +{ + size_t maxHeight = 0; + for(const auto & font : chain) + maxHeight = std::max(maxHeight, font->getFontAscentScaled()); + return maxHeight; +} + +bool FontChain::bitmapFontsPrioritized(const std::string & bitmapFontName) const +{ + const std::string & fontType = settings["video"]["fontsType"].String(); + if (fontType == "original") + return true; + if (fontType == "scalable") + return false; + + // else - autoselection. + + if (getScalingFactor() != 1) + return false; // If xbrz in use ttf/scalable fonts are preferred + + 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 +} + +void FontChain::addTrueTypeFont(const JsonNode & trueTypeConfig) +{ + chain.insert(chain.begin(), std::make_unique(trueTypeConfig)); +} + +void FontChain::addBitmapFont(const std::string & bitmapFilename) +{ + if (bitmapFontsPrioritized(bitmapFilename)) + chain.insert(chain.begin(), std::make_unique(bitmapFilename)); + else + chain.push_back(std::make_unique(bitmapFilename)); +} + +bool FontChain::canRepresentCharacter(const char * data) const +{ + for(const auto & font : chain) + if (font->canRepresentCharacter(data)) + return true; + return false; +} + +size_t FontChain::getLineHeightScaled() const +{ + size_t maxHeight = 0; + for(const auto & font : chain) + maxHeight = std::max(maxHeight, font->getLineHeightScaled()); + return maxHeight; +} + +size_t FontChain::getGlyphWidthScaled(const char * data) const +{ + for(const auto & font : chain) + if (font->canRepresentCharacter(data)) + return font->getGlyphWidthScaled(data); + return 0; +} + +std::vector FontChain::splitTextToChunks(const std::string & data) const +{ + std::vector chunks; + + for (size_t i = 0; i < data.size(); i += TextOperations::getUnicodeCharacterSize(data[i])) + { + const IFont * currentFont = nullptr; + for(const auto & font : chain) + { + if (font->canRepresentCharacter(data.data() + i)) + { + currentFont = font.get(); + break; + } + } + + if (currentFont == nullptr) + continue; // not representable + + std::string symbol = data.substr(i, TextOperations::getUnicodeCharacterSize(data[i])); + + if (chunks.empty() || chunks.back().font != currentFont) + chunks.push_back({currentFont, symbol}); + else + chunks.back().text += symbol; + } + + return chunks; +} + +size_t FontChain::getStringWidthScaled(const std::string & data) const +{ + size_t result = 0; + auto chunks = splitTextToChunks(data); + for (auto const & chunk : chunks) + result += chunk.font->getStringWidthScaled(chunk.text); + + return result; +} diff --git a/client/renderSDL/FontChain.h b/client/renderSDL/FontChain.h new file mode 100644 index 000000000..b66d5cce2 --- /dev/null +++ b/client/renderSDL/FontChain.h @@ -0,0 +1,43 @@ +/* + * FontChain.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 "../render/IFont.h" + +VCMI_LIB_NAMESPACE_BEGIN +class JsonNode; +VCMI_LIB_NAMESPACE_END + +class FontChain final : public IFont +{ + struct TextChunk + { + const IFont * font; + std::string text; + }; + + std::vector splitTextToChunks(const std::string & data) const; + + std::vector> chain; + + 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 std::string & bitmapFontName) const; +public: + FontChain() = default; + + void addTrueTypeFont(const JsonNode & trueTypeConfig); + void addBitmapFont(const std::string & bitmapFilename); + + size_t getLineHeightScaled() const override; + size_t getGlyphWidthScaled(const char * data) const override; + size_t getStringWidthScaled(const std::string & data) const override; + bool canRepresentCharacter(const char * data) const override; +}; diff --git a/client/renderSDL/ImageScaled.cpp b/client/renderSDL/ImageScaled.cpp new file mode 100644 index 000000000..82c02fee3 --- /dev/null +++ b/client/renderSDL/ImageScaled.cpp @@ -0,0 +1,167 @@ +/* + * ImageScaled.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 "ImageScaled.h" + +#include "SDLImage.h" +#include "SDL_Extensions.h" + +#include "../gui/CGuiHandler.h" +#include "../render/IScreenHandler.h" +#include "../render/Colors.h" + +#include "../../lib/constants/EntityIdentifiers.h" + +#include + +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) +{ + prepareImages(); +} + +std::shared_ptr ImageScaled::getSharedImage() const +{ + return body; +} + +void ImageScaled::scaleInteger(int factor) +{ + assert(0); +} + +void ImageScaled::scaleTo(const Point & size) +{ + if (source) + source = source->scaleTo(size, nullptr); + + if (body) + body = body->scaleTo(size * GH.screenHandler().getScalingFactor(), nullptr); +} + +void ImageScaled::exportBitmap(const boost::filesystem::path &path) const +{ + source->exportBitmap(path, nullptr); +} + +bool ImageScaled::isTransparent(const Point &coords) const +{ + return source->isTransparent(coords); +} + +Point ImageScaled::dimensions() const +{ + return source->dimensions(); +} + +void ImageScaled::setAlpha(uint8_t value) +{ + alphaValue = value; +} + +void ImageScaled::setBlitMode(EImageBlitMode mode) +{ + blitMode = mode; +} + +void ImageScaled::draw(SDL_Surface *where, const Point &pos, const Rect *src) const +{ + if (shadow) + shadow->draw(where, nullptr, pos, src, Colors::WHITE_TRUE, alphaValue, blitMode); + if (body) + body->draw(where, nullptr, pos, src, Colors::WHITE_TRUE, alphaValue, blitMode); + if (overlay) + overlay->draw(where, nullptr, pos, src, colorMultiplier, colorMultiplier.a * alphaValue / 255, blitMode); +} + +void ImageScaled::setOverlayColor(const ColorRGBA & color) +{ + colorMultiplier = color; +} + +void ImageScaled::playerColored(PlayerColor player) +{ + playerColor = player; + prepareImages(); +} + +void ImageScaled::shiftPalette(uint32_t firstColorID, uint32_t colorsToMove, uint32_t distanceToMove) +{ + // TODO: implement +} + +void ImageScaled::adjustPalette(const ColorFilter &shifter, uint32_t colorsToSkipMask) +{ + // TODO: implement +} + +void ImageScaled::prepareImages() +{ + switch(blitMode) + { + case EImageBlitMode::OPAQUE: + case EImageBlitMode::COLORKEY: + case EImageBlitMode::SIMPLE: + locator.layer = blitMode; + locator.playerColored = playerColor; + body = GH.renderHandler().loadImage(locator, blitMode)->getSharedImage(); + break; + + 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) + { + 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; + } + + switch(blitMode) + { + 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; + } +} diff --git a/client/renderSDL/ImageScaled.h b/client/renderSDL/ImageScaled.h new file mode 100644 index 000000000..40f4b2c7e --- /dev/null +++ b/client/renderSDL/ImageScaled.h @@ -0,0 +1,65 @@ +/* + * ImageScaled.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 "../render/IImage.h" +#include "../render/IRenderHandler.h" + +#include "../../lib/Color.h" +#include "../../lib/constants/EntityIdentifiers.h" + +struct SDL_Palette; + +class SDLImageShared; + +// Upscaled image with several mechanisms to emulate H3 palette effects +class ImageScaled final : public IImage +{ +private: + + /// Original unscaled image + std::shared_ptr source; + + /// Upscaled shadow of our image, may be null + std::shared_ptr shadow; + + /// Upscaled main part of our image, may be null + std::shared_ptr body; + + /// Upscaled overlay (player color, selection highlight) of our image, may be null + std::shared_ptr overlay; + + ImageLocator locator; + + ColorRGBA colorMultiplier; + PlayerColor playerColor = PlayerColor::CANNOT_DETERMINE; + + uint8_t alphaValue; + EImageBlitMode blitMode; + + void prepareImages(); +public: + ImageScaled(const ImageLocator & locator, const std::shared_ptr & source, EImageBlitMode mode); + + void scaleInteger(int factor) override; + void scaleTo(const Point & size) override; + void exportBitmap(const boost::filesystem::path & path) const override; + bool isTransparent(const Point & coords) const override; + Point dimensions() const override; + void setAlpha(uint8_t value) override; + void setBlitMode(EImageBlitMode mode) override; + void draw(SDL_Surface * where, const Point & pos, const Rect * src) const override; + void setOverlayColor(const ColorRGBA & color) override; + void playerColored(PlayerColor player) override; + void shiftPalette(uint32_t firstColorID, uint32_t colorsToMove, uint32_t distanceToMove) override; + void adjustPalette(const ColorFilter & shifter, uint32_t colorsToSkipMask) override; + + std::shared_ptr getSharedImage() const override; +}; diff --git a/client/renderSDL/RenderHandler.cpp b/client/renderSDL/RenderHandler.cpp index 87eb99091..0447c9786 100644 --- a/client/renderSDL/RenderHandler.cpp +++ b/client/renderSDL/RenderHandler.cpp @@ -10,31 +10,455 @@ #include "StdInc.h" #include "RenderHandler.h" -#include "../render/CAnimation.h" #include "SDLImage.h" +#include "ImageScaled.h" +#include "FontChain.h" +#include "../gui/CGuiHandler.h" -std::shared_ptr RenderHandler::loadImage(const ImagePath & path) +#include "../render/CAnimation.h" +#include "../render/CDefFile.h" +#include "../render/Colors.h" +#include "../render/ColorFilter.h" +#include "../render/IScreenHandler.h" +#include "../../lib/json/JsonUtils.h" +#include "../../lib/filesystem/Filesystem.h" +#include "../../lib/VCMIDirs.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +std::shared_ptr RenderHandler::getAnimationFile(const AnimationPath & path) { - return loadImage(path, EImageBlitMode::ALPHA); + AnimationPath actualPath = boost::starts_with(path.getName(), "SPRITES") ? path : path.addPrefix("SPRITES/"); + + auto it = animationFiles.find(actualPath); + + if (it != animationFiles.end()) + return it->second; + + if (!CResourceHandler::get()->existsResource(actualPath)) + { + animationFiles[actualPath] = nullptr; + return nullptr; + } + + auto result = std::make_shared(actualPath); + + animationFiles[actualPath] = result; + 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; + basepath = config["basepath"].String(); + + JsonNode base; + base["margins"] = config["margins"]; + base["width"] = config["width"]; + base["height"] = config["height"]; + + for(const JsonNode & group : config["sequences"].Vector()) + { + size_t groupID = group["group"].Integer();//TODO: string-to-value conversion("moving" -> MOVING) + source[groupID].clear(); + + for(const JsonNode & frame : group["frames"].Vector()) + { + JsonNode toAdd = frame; + JsonUtils::inherit(toAdd, base); + toAdd["file"].String() = basepath + frame.String(); + source[groupID].emplace_back(toAdd); + } + } + + for(const JsonNode & node : config["images"].Vector()) + { + size_t group = node["group"].Integer(); + size_t frame = node["frame"].Integer(); + + if (source[group].size() <= frame) + source[group].resize(frame+1); + + JsonNode toAdd = node; + JsonUtils::inherit(toAdd, base); + + 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) +{ + 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); + + if (it != animationLayouts.end()) + return it->second; + + AnimationLayoutMap result; + + auto defFile = getAnimationFile(actualPath); + if(defFile) + { + const std::map defEntries = defFile->getEntries(); + + for (const auto & defEntry : defEntries) + result[defEntry.first].resize(defEntry.second); + } + + auto jsonResource = actualPath.toType(); + auto configList = CResourceHandler::get()->getResourcesWithName(jsonResource); + + for(auto & loader : configList) + { + auto stream = loader->load(jsonResource); + std::unique_ptr textData(new ui8[stream->getSize()]); + stream->read(textData.get(), stream->getSize()); + + 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]; +} + +int RenderHandler::getScalingFactor() const +{ + return GH.screenHandler().getScalingFactor(); +} + +ImageLocator RenderHandler::getLocatorForAnimationFrame(const AnimationPath & path, int frame, int group) +{ + const auto & layout = getAnimationLayout(path); + if (!layout.count(group)) + return ImageLocator(ImagePath::builtin("DEFAULT")); + + if (frame >= layout.at(group).size()) + return ImageLocator(ImagePath::builtin("DEFAULT")); + + const auto & locator = layout.at(group).at(frame); + if (locator.image || locator.defFile) + return locator; + + return ImageLocator(path, frame, group); +} + +std::shared_ptr RenderHandler::loadImageImpl(const ImageLocator & locator) +{ + auto it = imageFiles.find(locator); + if (it != imageFiles.end()) + return it->second; + + // TODO: order should be different: + // 1) try to find correctly scaled image + // 2) if fails -> try to find correctly transformed + // 3) if also fails -> try to find image from correct file + // 4) load missing part of the sequence + // TODO: check whether (load -> transform -> scale) or (load -> scale -> transform) order should be used for proper loading of pre-scaled data + auto imageFromFile = loadImageFromFile(locator.copyFile()); + auto transformedImage = transformImage(locator.copyFileTransform(), imageFromFile); + auto scaledImage = scaleImage(locator.copyFileTransformScale(), transformedImage); + + return scaledImage; +} + +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, locator.preScaledFactor); + } + + if (locator.defFile) + { + auto defFile = getAnimationFile(*locator.defFile); + 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) +{ + imageFiles[locator] = image; + +#if 0 + const boost::filesystem::path outPath = VCMIDirs::get().userExtractedPath() / "imageCache" / (locator.toString() + ".png"); + boost::filesystem::path outDir = outPath; + outDir.remove_filename(); + boost::filesystem::create_directories(outDir); + image->exportBitmap(outPath , nullptr); +#endif +} + +std::shared_ptr RenderHandler::loadImageFromFile(const ImageLocator & locator) +{ + if (imageFiles.count(locator)) + return imageFiles.at(locator); + + auto result = loadImageFromFileUncached(locator); + storeCachedImage(locator, result); + return result; +} + +std::shared_ptr RenderHandler::transformImage(const ImageLocator & locator, std::shared_ptr image) +{ + if (imageFiles.count(locator)) + return imageFiles.at(locator); + + auto result = image; + + if (locator.verticalFlip) + result = result->verticalFlip(); + + if (locator.horizontalFlip) + result = result->horizontalFlip(); + + storeCachedImage(locator, result); + return result; +} + +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); + + assert(locator.scalingFactor != 1); // should be filtered-out before + if (locator.playerColored != PlayerColor::CANNOT_DETERMINE) + handle->playerColored(locator.playerColored); + + handle->scaleInteger(locator.scalingFactor); + + auto result = handle->getSharedImage(); + storeCachedImage(locator, result); + return result; +} + +std::shared_ptr RenderHandler::loadImage(const ImageLocator & locator, EImageBlitMode mode) +{ + ImageLocator adjustedLocator = locator; + + if(adjustedLocator.image) + { + 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(); + auto unscaledImage = loadImageImpl(unscaledLocator); + + return std::make_shared(scaledLocator, unscaledImage, mode); + } + + if (adjustedLocator.scalingFactor == 0) + { + auto scaledLocator = adjustedLocator; + scaledLocator.scalingFactor = getScalingFactor(); + + return loadImageImpl(scaledLocator)->createImageReference(mode); + } + else + return loadImageImpl(adjustedLocator)->createImageReference(mode); +} + +std::shared_ptr RenderHandler::loadImage(const AnimationPath & path, int frame, int group, EImageBlitMode mode) +{ + auto tmp = getScalePath(path); + ImageLocator locator = getLocatorForAnimationFrame(AnimationPath::builtin(tmp.first.getName()), frame, group); + locator.preScaledFactor = tmp.second; + return loadImage(locator, mode); } std::shared_ptr RenderHandler::loadImage(const ImagePath & path, EImageBlitMode mode) { - return std::make_shared(path, mode); + ImageLocator locator(path); + return loadImage(locator, mode); } std::shared_ptr RenderHandler::createImage(SDL_Surface * source) { - return std::make_shared(source, EImageBlitMode::ALPHA); + return std::make_shared(source)->createImageReference(EImageBlitMode::SIMPLE); } -std::shared_ptr RenderHandler::loadAnimation(const AnimationPath & path) +std::shared_ptr RenderHandler::loadAnimation(const AnimationPath & path, EImageBlitMode mode) { - return std::make_shared(path); + return std::make_shared(path, getAnimationLayout(path), mode); } -std::shared_ptr RenderHandler::createAnimation() +void RenderHandler::addImageListEntries(const EntityService * service) { - return std::make_shared(); + service->forEachBase([this](const Entity * entity, bool & stop) + { + entity->registerIcons([this](size_t index, size_t group, const std::string & listName, const std::string & imageName) + { + if (imageName.empty()) + return; + + auto & layout = getAnimationLayout(AnimationPath::builtin("SPRITES/" + listName)); + + JsonNode entry; + entry["file"].String() = imageName; + + if (index >= layout[group].size()) + layout[group].resize(index + 1); + + layout[group][index] = ImageLocator(entry); + }); + }); +} + +void RenderHandler::onLibraryLoadingFinished(const Services * services) +{ + addImageListEntries(services->creatures()); + addImageListEntries(services->heroTypes()); + addImageListEntries(services->artifacts()); + addImageListEntries(services->factions()); + addImageListEntries(services->spells()); + addImageListEntries(services->skills()); +} + +std::shared_ptr RenderHandler::loadFont(EFonts font) +{ + if (fonts.count(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; + + for(auto & loader : configList) + { + auto stream = loader->load(JsonPath::builtin("config/fonts.json")); + std::unique_ptr textData(new ui8[stream->getSize()]); + stream->read(textData.get(), stream->getSize()); + const JsonNode config(reinterpret_cast(textData.get()), stream->getSize(), "config/fonts.json"); + const JsonVector & bmpConf = config["bitmap"].Vector(); + const JsonNode & ttfConf = config["trueType"]; + + bitmapPath = bmpConf[index].String(); + if (!ttfConf[bitmapPath].isNull()) + loadedFont->addTrueTypeFont(ttfConf[bitmapPath]); + } + loadedFont->addBitmapFont(bitmapPath); + + fonts[font] = loadedFont; + return loadedFont; } diff --git a/client/renderSDL/RenderHandler.h b/client/renderSDL/RenderHandler.h index 76e30ee9c..43df617a1 100644 --- a/client/renderSDL/RenderHandler.h +++ b/client/renderSDL/RenderHandler.h @@ -11,15 +11,58 @@ #include "../render/IRenderHandler.h" +VCMI_LIB_NAMESPACE_BEGIN +class EntityService; +VCMI_LIB_NAMESPACE_END + +class CDefFile; +class SDLImageShared; +class ISharedImage; + class RenderHandler : public IRenderHandler { + using AnimationLayoutMap = std::map>; + + std::map> animationFiles; + std::map animationLayouts; + 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); + + std::shared_ptr loadImageImpl(const ImageLocator & config); + + 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); + + ImageLocator getLocatorForAnimationFrame(const AnimationPath & path, int frame, int group); + + int getScalingFactor() const; + public: - std::shared_ptr loadImage(const ImagePath & path) override; + + // IRenderHandler implementation + void onLibraryLoadingFinished(const Services * services) override; + + std::shared_ptr loadImage(const ImageLocator & locator, EImageBlitMode mode) override; std::shared_ptr loadImage(const ImagePath & path, EImageBlitMode mode) override; + std::shared_ptr loadImage(const AnimationPath & path, int frame, int group, EImageBlitMode mode) override; + + std::shared_ptr loadAnimation(const AnimationPath & path, EImageBlitMode mode) override; std::shared_ptr createImage(SDL_Surface * source) override; - std::shared_ptr loadAnimation(const AnimationPath & path) override; - - std::shared_ptr createAnimation() override; + /// Returns font with specified identifer + std::shared_ptr loadFont(EFonts font) override; }; diff --git a/client/renderSDL/SDLImage.cpp b/client/renderSDL/SDLImage.cpp index 6dbe8db94..1f5932094 100644 --- a/client/renderSDL/SDLImage.cpp +++ b/client/renderSDL/SDLImage.cpp @@ -1,4 +1,4 @@ -/* +/* * SDLImage.cpp, part of VCMI engine * * Authors: listed in file AUTHORS in main folder @@ -14,16 +14,73 @@ #include "SDL_Extensions.h" #include "../render/ColorFilter.h" +#include "../render/Colors.h" #include "../render/CBitmapHandler.h" #include "../render/CDefFile.h" #include "../render/Graphics.h" +#include "../xBRZ/xbrz.h" +#include "../gui/CGuiHandler.h" +#include "../render/IScreenHandler.h" -#include "../../lib/json/JsonNode.h" - +#include #include +#include class SDLImageLoader; +//First 8 colors in def palette used for transparency +static constexpr std::array sourcePalette = {{ + {0, 255, 255, SDL_ALPHA_OPAQUE}, + {255, 150, 255, SDL_ALPHA_OPAQUE}, + {255, 100, 255, SDL_ALPHA_OPAQUE}, + {255, 50, 255, SDL_ALPHA_OPAQUE}, + {255, 0, 255, SDL_ALPHA_OPAQUE}, + {255, 255, 0, SDL_ALPHA_OPAQUE}, + {180, 0, 255, SDL_ALPHA_OPAQUE}, + {0, 255, 0, SDL_ALPHA_OPAQUE} +}}; + +static constexpr std::array targetPalette = {{ + {0, 0, 0, 0 }, // 0 - transparency ( used in most images ) + {0, 0, 0, 64 }, // 1 - shadow border ( used in battle, adventure map def's ) + {0, 0, 0, 64 }, // 2 - shadow border ( used in fog-of-war def's ) + {0, 0, 0, 128}, // 3 - shadow body ( used in fog-of-war def's ) + {0, 0, 0, 128}, // 4 - shadow body ( used in battle, adventure map def's ) + {0, 0, 0, 0 }, // 5 - selection / owner flag ( used in battle, adventure map def's ) + {0, 0, 0, 128}, // 6 - shadow body below selection ( used in battle def's ) + {0, 0, 0, 64 } // 7 - shadow border below selection ( used in battle def's ) +}}; + +static ui8 mixChannels(ui8 c1, ui8 c2, ui8 a1, ui8 a2) +{ + return c1*a1 / 256 + c2*a2*(255 - a1) / 256 / 256; +} + +static ColorRGBA addColors(const ColorRGBA & base, const ColorRGBA & over) +{ + return ColorRGBA( + mixChannels(over.r, base.r, over.a, base.a), + mixChannels(over.g, base.g, over.a, base.a), + mixChannels(over.b, base.b, over.a, base.a), + static_cast(over.a + base.a * (255 - over.a) / 256) + ); +} + +static bool colorsSimilar (const SDL_Color & lhs, const SDL_Color & 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 = static_cast(lhs.r) - rhs.r; + int diffG = static_cast(lhs.g) - rhs.g; + int diffB = static_cast(lhs.b) - rhs.b; + int diffA = static_cast(lhs.a) - rhs.a; + + return std::abs(diffR) < threshold && std::abs(diffG) < threshold && std::abs(diffB) < threshold && std::abs(diffA) < threshold; +} + int IImage::width() const { return dimensions().x; @@ -34,75 +91,43 @@ int IImage::height() const return dimensions().y; } -SDLImage::SDLImage(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); savePalette(); - setBlitMode(EImageBlitMode::ALPHA); } -SDLImage::SDLImage(SDL_Surface * from, EImageBlitMode mode) +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) return; savePalette(); - setBlitMode(mode); surf->refcount++; fullSize.x = surf->w; fullSize.y = surf->h; } -SDLImage::SDLImage(const JsonNode & conf, EImageBlitMode mode) +SDLImageShared::SDLImageShared(const ImagePath & filename, int preScaleFactor) : surf(nullptr), margins(0, 0), fullSize(0, 0), - originalPalette(nullptr) -{ - surf = BitmapHandler::loadBitmap(ImagePath::fromJson(conf["file"])); - - if(surf == nullptr) - return; - - savePalette(); - setBlitMode(mode); - - const JsonNode & jsonMargins = conf["margins"]; - - margins.x = static_cast(jsonMargins["left"].Integer()); - margins.y = static_cast(jsonMargins["top"].Integer()); - - fullSize.x = static_cast(conf["width"].Integer()); - fullSize.y = static_cast(conf["height"].Integer()); - - if(fullSize.x == 0) - { - fullSize.x = margins.x + surf->w + (int)jsonMargins["right"].Integer(); - } - - if(fullSize.y == 0) - { - fullSize.y = margins.y + surf->h + (int)jsonMargins["bottom"].Integer(); - } -} - -SDLImage::SDLImage(const ImagePath & filename, EImageBlitMode mode) - : surf(nullptr), - margins(0, 0), - fullSize(0, 0), - originalPalette(nullptr) + originalPalette(nullptr), + preScaleFactor(preScaleFactor) { surf = BitmapHandler::loadBitmap(filename); @@ -114,22 +139,15 @@ SDLImage::SDLImage(const ImagePath & filename, EImageBlitMode mode) else { savePalette(); - setBlitMode(mode); fullSize.x = surf->w; fullSize.y = surf->h; + + optimizeSurface(); } } -void SDLImage::draw(SDL_Surface *where, int posX, int posY, const Rect *src) const -{ - if(!surf) - return; - Rect destRect(posX, posY, surf->w, surf->h); - draw(where, &destRect, src); -} - -void SDLImage::draw(SDL_Surface* where, const Rect * dest, const Rect* src) const +void SDLImageShared::draw(SDL_Surface * where, SDL_Palette * palette, const Point & dest, const Rect * src, const ColorRGBA & colorMultiplier, uint8_t alpha, EImageBlitMode mode) const { if (!surf) return; @@ -153,29 +171,173 @@ void SDLImage::draw(SDL_Surface* where, const Rect * dest, const Rect* src) cons else destShift = margins; - if(dest) - destShift += dest->topLeft(); + destShift += dest; - uint8_t perSurfaceAlpha; - if (SDL_GetSurfaceAlphaMod(surf, &perSurfaceAlpha) != 0) - logGlobal->error("SDL_GetSurfaceAlphaMod faied! %s", SDL_GetError()); + SDL_SetSurfaceColorMod(surf, colorMultiplier.r, colorMultiplier.g, colorMultiplier.b); + SDL_SetSurfaceAlphaMod(surf, alpha); - if(surf->format->BitsPerPixel == 8 && perSurfaceAlpha == SDL_ALPHA_OPAQUE && blitMode == EImageBlitMode::ALPHA) + if (alpha != SDL_ALPHA_OPAQUE || (mode != EImageBlitMode::OPAQUE && surf->format->Amask != 0)) + SDL_SetSurfaceBlendMode(surf, SDL_BLENDMODE_BLEND); + else + SDL_SetSurfaceBlendMode(surf, SDL_BLENDMODE_NONE); + + if (palette && surf->format->palette) + SDL_SetSurfacePalette(surf, palette); + + if(surf->format->palette && mode != EImageBlitMode::OPAQUE && mode != EImageBlitMode::COLORKEY) { - CSDL_Ext::blit8bppAlphaTo24bpp(surf, sourceRect, where, destShift); + CSDL_Ext::blit8bppAlphaTo24bpp(surf, sourceRect, where, destShift, alpha); } else { CSDL_Ext::blitSurface(surf, sourceRect, where, destShift); } + + if (surf->format->palette) + SDL_SetSurfacePalette(surf, originalPalette); } -std::shared_ptr SDLImage::scaleFast(const Point & size) const +void SDLImageShared::optimizeSurface() { - float scaleX = float(size.x) / width(); - float scaleY = float(size.y) / height(); + if (!surf) + return; - auto scaled = CSDL_Ext::scaleSurfaceFast(surf, (int)(surf->w * scaleX), (int)(surf->h * scaleY)); + int left = surf->w; + int top = surf->h; + int right = 0; + int bottom = 0; + + // locate fully-transparent area around image + // H3 hadles this on format level, but mods or images scaled in runtime do not + if (surf->format->palette) + { + for (int y = 0; y < surf->h; ++y) + { + const uint8_t * row = static_cast(surf->pixels) + y * surf->pitch; + for (int x = 0; x < surf->w; ++x) + { + if (row[x] != 0) + { + // opaque or can be opaque (e.g. disabled shadow) + top = std::min(top, y); + left = std::min(left, x); + right = std::max(right, x); + bottom = std::max(bottom, y); + } + } + } + } + else + { + for (int y = 0; y < surf->h; ++y) + { + for (int x = 0; x < surf->w; ++x) + { + ColorRGBA color; + SDL_GetRGBA(CSDL_Ext::getPixel(surf, x, y), surf->format, &color.r, &color.g, &color.b, &color.a); + + if (color.a != SDL_ALPHA_TRANSPARENT) + { + // opaque + top = std::min(top, y); + left = std::min(left, x); + right = std::max(right, x); + bottom = std::max(bottom, y); + } + } + } + } + + if (left == surf->w) + { + // empty image - simply delete it + SDL_FreeSurface(surf); + surf = nullptr; + return; + } + + if (left != 0 || top != 0 || right != surf->w - 1 || bottom != surf->h - 1) + { + // non-zero border found + Rect newDimensions(left, top, right - left + 1, bottom - top + 1); + SDL_Rect rectSDL = CSDL_Ext::toSDL(newDimensions); + auto newSurface = CSDL_Ext::newSurface(newDimensions.dimensions(), surf); + 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; + + margins.x += left; + margins.y += top; + } + + if(preScaleFactor > 1 && preScaleFactor != GH.screenHandler().getScalingFactor()) + { + margins.x = margins.x * GH.screenHandler().getScalingFactor() / preScaleFactor; + margins.y = margins.y * GH.screenHandler().getScalingFactor() / preScaleFactor; + } +} + +std::shared_ptr SDLImageShared::scaleInteger(int factor, SDL_Palette * palette, EImageBlitMode mode) const +{ + if (factor <= 0) + throw std::runtime_error("Unable to scale by integer value of " + std::to_string(factor)); + + if (!surf) + return shared_from_this(); + + if (palette && surf->format->palette) + SDL_SetSurfacePalette(surf, palette); + + SDL_Surface * scaled = nullptr; + if(preScaleFactor == factor) + return shared_from_this(); + else if(preScaleFactor == 1) + { + // dump heuristics to differentiate tileable UI elements from map object / combat assets + if (mode == EImageBlitMode::OPAQUE || mode == EImageBlitMode::COLORKEY || mode == EImageBlitMode::SIMPLE) + scaled = CSDL_Ext::scaleSurfaceIntegerFactor(surf, factor, EScalingAlgorithm::XBRZ_OPAQUE); + else + scaled = CSDL_Ext::scaleSurfaceIntegerFactor(surf, factor, EScalingAlgorithm::XBRZ_ALPHA); + } + else + scaled = CSDL_Ext::scaleSurface(surf, (surf->w / preScaleFactor) * factor, (surf->h / preScaleFactor) * factor); + + auto ret = std::make_shared(scaled, preScaleFactor); + + ret->fullSize.x = fullSize.x * factor; + ret->fullSize.y = fullSize.y * factor; + + ret->margins.x = margins.x * factor; + ret->margins.y = margins.y * factor; + ret->optimizeSurface(); + + // erase our own reference + SDL_FreeSurface(scaled); + + if (surf->format->palette) + SDL_SetSurfacePalette(surf, originalPalette); + + return ret; +} + +std::shared_ptr SDLImageShared::scaleTo(const Point & size, SDL_Palette * palette) const +{ + 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); + + auto scaled = CSDL_Ext::scaleSurface(surf, (int)(surf->w * scaleX), (int)(surf->h * scaleY)); if (scaled->format && scaled->format->palette) // fix color keying, because SDL loses it at this point CSDL_Ext::setColorKey(scaled, scaled->format->palette->colors[0]); @@ -184,7 +346,7 @@ std::shared_ptr SDLImage::scaleFast(const Point & size) const else CSDL_Ext::setDefaultColorKey(scaled);//just in case - auto * ret = new SDLImage(scaled, EImageBlitMode::ALPHA); + 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); @@ -195,167 +357,295 @@ std::shared_ptr SDLImage::scaleFast(const Point & size) const // erase our own reference SDL_FreeSurface(scaled); - return std::shared_ptr(ret); + if (surf->format->palette) + SDL_SetSurfacePalette(surf, originalPalette); + + return ret; } -void SDLImage::exportBitmap(const boost::filesystem::path& path) const -{ - SDL_SaveBMP(surf, path.string().c_str()); -} - -void SDLImage::playerColored(PlayerColor player) +void SDLImageShared::exportBitmap(const boost::filesystem::path& path, SDL_Palette * palette) const { if (!surf) return; - graphics->blueToPlayersAdv(surf, player); + + if (palette && surf->format->palette) + SDL_SetSurfacePalette(surf, palette); + IMG_SavePNG(surf, path.string().c_str()); + if (palette && surf->format->palette) + SDL_SetSurfacePalette(surf, originalPalette); } -void SDLImage::setAlpha(uint8_t value) +void SDLImageIndexed::playerColored(PlayerColor player) { - CSDL_Ext::setAlpha (surf, value); - if (value != 255) - SDL_SetSurfaceBlendMode(surf, SDL_BLENDMODE_BLEND); + graphics->setPlayerPalette(currentPalette, player); } -void SDLImage::setBlitMode(EImageBlitMode mode) -{ - blitMode = mode; - - if (blitMode != EImageBlitMode::OPAQUE && surf->format->Amask != 0) - SDL_SetSurfaceBlendMode(surf, SDL_BLENDMODE_BLEND); - else - SDL_SetSurfaceBlendMode(surf, SDL_BLENDMODE_NONE); -} - -void SDLImage::setFlagColor(PlayerColor player) -{ - if(player.isValidPlayer() || player==PlayerColor::NEUTRAL) - CSDL_Ext::setPlayerColor(surf, player); -} - -bool SDLImage::isTransparent(const Point & coords) const +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 SDLImage::dimensions() const +Point SDLImageShared::dimensions() const { - return fullSize; + return fullSize / preScaleFactor; } -void SDLImage::horizontalFlip() +std::shared_ptr SDLImageShared::createImageReference(EImageBlitMode mode) const { - margins.y = fullSize.y - surf->h - margins.y; + if (surf && surf->format->palette) + return std::make_shared(shared_from_this(), originalPalette, mode); + else + return std::make_shared(shared_from_this(), mode); +} - //todo: modify in-place +std::shared_ptr SDLImageShared::horizontalFlip() const +{ SDL_Surface * flipped = CSDL_Ext::horizontalFlip(surf); - SDL_FreeSurface(surf); - surf = 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; + ret->fullSize = fullSize; + + return ret; } -void SDLImage::verticalFlip() +std::shared_ptr SDLImageShared::verticalFlip() const { - margins.x = fullSize.x - surf->w - margins.x; - - //todo: modify in-place SDL_Surface * flipped = CSDL_Ext::verticalFlip(surf); - SDL_FreeSurface(surf); - surf = 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; + ret->fullSize = fullSize; -void SDLImage::doubleFlip() -{ - horizontalFlip(); - verticalFlip(); + return ret; } // Keep the original palette, in order to do color switching operation -void SDLImage::savePalette() +void SDLImageShared::savePalette() { // For some images that don't have palette, skip this if(surf->format->palette == nullptr) return; if(originalPalette == nullptr) - originalPalette = SDL_AllocPalette(DEFAULT_PALETTE_COLORS); + originalPalette = SDL_AllocPalette(surf->format->palette->ncolors); - SDL_SetPaletteColors(originalPalette, surf->format->palette->colors, 0, DEFAULT_PALETTE_COLORS); + SDL_SetPaletteColors(originalPalette, surf->format->palette->colors, 0, surf->format->palette->ncolors); } -void SDLImage::shiftPalette(uint32_t firstColorID, uint32_t colorsToMove, uint32_t distanceToMove) +void SDLImageIndexed::shiftPalette(uint32_t firstColorID, uint32_t colorsToMove, uint32_t distanceToMove) { - if(surf->format->palette) - { - std::vector shifterColors(colorsToMove); + std::vector shifterColors(colorsToMove); - for(uint32_t i=0; icolors[firstColorID + i]; - } - CSDL_Ext::setColors(surf, shifterColors.data(), firstColorID, colorsToMove); - } + for(uint32_t i=0; icolors[firstColorID + i]; + + SDL_SetPaletteColors(currentPalette, shifterColors.data(), firstColorID, colorsToMove); } -void SDLImage::adjustPalette(const ColorFilter & shifter, uint32_t colorsToSkipMask) +void SDLImageIndexed::adjustPalette(const ColorFilter & shifter, uint32_t colorsToSkipMask) { - if(originalPalette == nullptr) - return; - - SDL_Palette* palette = surf->format->palette; + // If shadow is enabled, following colors must be skipped unconditionally + 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 - for(int i = 0; i < palette->ncolors; i++) + for(int i = 0; i < currentPalette->ncolors; i++) { + if (i < std::size(sourcePalette) && colorsSimilar(sourcePalette[i], originalPalette->colors[i])) + continue; + if(i < std::numeric_limits::digits && ((colorsToSkipMask >> i) & 1) == 1) continue; - palette->colors[i] = CSDL_Ext::toSDL(shifter.shiftColor(CSDL_Ext::fromSDL(originalPalette->colors[i]))); + currentPalette->colors[i] = CSDL_Ext::toSDL(shifter.shiftColor(CSDL_Ext::fromSDL(originalPalette->colors[i]))); } } -void SDLImage::resetPalette() +SDLImageIndexed::SDLImageIndexed(const std::shared_ptr & image, SDL_Palette * originalPalette, EImageBlitMode mode) + :SDLImageBase::SDLImageBase(image, mode) + ,originalPalette(originalPalette) { - if(originalPalette == nullptr) - return; - - // Always keept the original palette not changed, copy a new palette to assign to surface - SDL_SetPaletteColors(surf->format->palette, originalPalette->colors, 0, originalPalette->ncolors); + currentPalette = SDL_AllocPalette(originalPalette->ncolors); + SDL_SetPaletteColors(currentPalette, originalPalette->colors, 0, originalPalette->ncolors); + + preparePalette(); } -void SDLImage::resetPalette( int colorID ) +SDLImageIndexed::~SDLImageIndexed() { - if(originalPalette == nullptr) - return; - - // Always keept the original palette not changed, copy a new palette to assign to surface - SDL_SetPaletteColors(surf->format->palette, originalPalette->colors + colorID, colorID, 1); + SDL_FreePalette(currentPalette); } -void SDLImage::setSpecialPallete(const IImage::SpecialPalette & specialPalette, uint32_t colorsToSkipMask) +void SDLImageIndexed::setShadowTransparency(float factor) { - if(surf->format->palette) + ColorRGBA shadow50(0, 0, 0, 128 * factor); + ColorRGBA shadow25(0, 0, 0, 64 * factor); + + std::array colorsSDL = { + originalPalette->colors[0], + originalPalette->colors[1], + originalPalette->colors[2], + originalPalette->colors[3], + originalPalette->colors[4] + }; + + // seems to be used unconditionally + colorsSDL[0] = CSDL_Ext::toSDL(Colors::TRANSPARENCY); + colorsSDL[1] = CSDL_Ext::toSDL(shadow25); + colorsSDL[4] = CSDL_Ext::toSDL(shadow50); + + // seems to be used only if color matches + if (colorsSimilar(originalPalette->colors[2], sourcePalette[2])) + colorsSDL[2] = CSDL_Ext::toSDL(shadow25); + + if (colorsSimilar(originalPalette->colors[3], sourcePalette[3])) + colorsSDL[3] = CSDL_Ext::toSDL(shadow50); + + SDL_SetPaletteColors(currentPalette, colorsSDL.data(), 0, colorsSDL.size()); +} + +void SDLImageIndexed::setOverlayColor(const ColorRGBA & color) +{ + currentPalette->colors[5] = CSDL_Ext::toSDL(addColors(targetPalette[5], color)); + + for (int i : {6,7}) { - size_t last = std::min(specialPalette.size(), surf->format->palette->ncolors); - - for (size_t i = 0; i < last; ++i) - { - if(i < std::numeric_limits::digits && ((colorsToSkipMask >> i) & 1) == 1) - surf->format->palette->colors[i] = CSDL_Ext::toSDL(specialPalette[i]); - } + if (colorsSimilar(originalPalette->colors[i], sourcePalette[i])) + currentPalette->colors[i] = CSDL_Ext::toSDL(addColors(targetPalette[i], color)); } } -SDLImage::~SDLImage() +void SDLImageIndexed::preparePalette() +{ + switch(blitMode) + { + case EImageBlitMode::ONLY_SHADOW: + case EImageBlitMode::ONLY_OVERLAY: + adjustPalette(ColorFilter::genAlphaShifter(0), 0); + break; + } + + 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; + } + + 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() { SDL_FreeSurface(surf); - - if(originalPalette != nullptr) - { - SDL_FreePalette(originalPalette); - originalPalette = nullptr; - } + SDL_FreePalette(originalPalette); } +SDLImageBase::SDLImageBase(const std::shared_ptr & image, EImageBlitMode mode) + :image(image) + , alphaValue(SDL_ALPHA_OPAQUE) + , blitMode(mode) +{} + +std::shared_ptr SDLImageBase::getSharedImage() const +{ + return image; +} + +void SDLImageRGB::draw(SDL_Surface * where, const Point & pos, const Rect * src) const +{ + image->draw(where, nullptr, pos, src, Colors::WHITE_TRUE, alphaValue, blitMode); +} + +void SDLImageIndexed::draw(SDL_Surface * where, const Point & pos, const Rect * src) const +{ + image->draw(where, currentPalette, pos, src, Colors::WHITE_TRUE, alphaValue, blitMode); +} + +void SDLImageIndexed::exportBitmap(const boost::filesystem::path & path) const +{ + image->exportBitmap(path, currentPalette); +} + +void SDLImageIndexed::scaleTo(const Point & size) +{ + image = image->scaleTo(size, currentPalette); +} + +void SDLImageRGB::scaleTo(const Point & size) +{ + image = image->scaleTo(size, nullptr); +} + +void SDLImageIndexed::scaleInteger(int factor) +{ + image = image->scaleInteger(factor, currentPalette, blitMode); +} + +void SDLImageRGB::scaleInteger(int factor) +{ + image = image->scaleInteger(factor, nullptr, blitMode); +} + +void SDLImageRGB::exportBitmap(const boost::filesystem::path & path) const +{ + image->exportBitmap(path, nullptr); +} + +bool SDLImageBase::isTransparent(const Point & coords) const +{ + return image->isTransparent(coords); +} + +Point SDLImageBase::dimensions() const +{ + return image->dimensions(); +} + +void SDLImageBase::setAlpha(uint8_t value) +{ + alphaValue = value; +} + +void SDLImageBase::setBlitMode(EImageBlitMode mode) +{ + blitMode = mode; +} + +void SDLImageRGB::setOverlayColor(const ColorRGBA & color) +{} + +void SDLImageRGB::playerColored(PlayerColor player) +{} + +void SDLImageRGB::shiftPalette(uint32_t firstColorID, uint32_t colorsToMove, uint32_t distanceToMove) +{} + +void SDLImageRGB::adjustPalette(const ColorFilter & shifter, uint32_t colorsToSkipMask) +{} + + diff --git a/client/renderSDL/SDLImage.h b/client/renderSDL/SDLImage.h index 05c516fa2..096eccf58 100644 --- a/client/renderSDL/SDLImage.h +++ b/client/renderSDL/SDLImage.h @@ -24,60 +24,98 @@ struct SDL_Palette; /* * Wrapper around SDL_Surface */ -class SDLImage : public IImage +class SDLImageShared final : public ISharedImage, public std::enable_shared_from_this, boost::noncopyable { -public: - - const static int DEFAULT_PALETTE_COLORS = 256; - //Surface without empty borders SDL_Surface * surf; + + SDL_Palette * originalPalette; //size of left and top borders Point margins; //total size including borders Point fullSize; - EImageBlitMode blitMode; - -public: - //Load image from def file - SDLImage(CDefFile *data, size_t frame, size_t group=0); - //Load from bitmap file - SDLImage(const ImagePath & filename, EImageBlitMode blitMode); - - SDLImage(const JsonNode & conf, EImageBlitMode blitMode); - //Create using existing surface, extraRef will increase refcount on SDL_Surface - SDLImage(SDL_Surface * from, EImageBlitMode blitMode); - ~SDLImage(); + //pre scaled image + int preScaleFactor; // Keep the original palette, in order to do color switching operation void savePalette(); - void draw(SDL_Surface * where, int posX=0, int posY=0, const Rect *src=nullptr) const override; - void draw(SDL_Surface * where, const Rect * dest, const Rect * src) const override; - std::shared_ptr scaleFast(const Point & size) const override; - void exportBitmap(const boost::filesystem::path & path) const override; - void playerColored(PlayerColor player) override; - void setFlagColor(PlayerColor player) override; - bool isTransparent(const Point & coords) const override; + void optimizeSurface(); + +public: + //Load image from def file + SDLImageShared(const CDefFile *data, size_t frame, size_t group=0, int preScaleFactor=1); + //Load from bitmap file + SDLImageShared(const ImagePath & filename, int preScaleFactor=1); + //Create using existing surface, extraRef will increase refcount on SDL_Surface + 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; + + void exportBitmap(const boost::filesystem::path & path, SDL_Palette * palette) const override; Point dimensions() const override; - - void horizontalFlip() override; - void verticalFlip() override; - void doubleFlip() override; - - void shiftPalette(uint32_t firstColorID, uint32_t colorsToMove, uint32_t distanceToMove) override; - void adjustPalette(const ColorFilter & shifter, uint32_t colorsToSkipMask) override; - void resetPalette(int colorID) override; - void resetPalette() override; - - void setAlpha(uint8_t value) override; - void setBlitMode(EImageBlitMode mode) override; - - void setSpecialPallete(const SpecialPalette & SpecialPalette, uint32_t colorsToSkipMask) override; + bool isTransparent(const Point & coords) const override; + [[nodiscard]] std::shared_ptr createImageReference(EImageBlitMode mode) const override; + [[nodiscard]] std::shared_ptr horizontalFlip() const override; + [[nodiscard]] std::shared_ptr verticalFlip() const override; + [[nodiscard]] std::shared_ptr scaleInteger(int factor, SDL_Palette * palette, EImageBlitMode blitMode) const override; + [[nodiscard]] std::shared_ptr scaleTo(const Point & size, SDL_Palette * palette) const override; friend class SDLImageLoader; - -private: - SDL_Palette * originalPalette; +}; + +class SDLImageBase : public IImage, boost::noncopyable +{ +protected: + std::shared_ptr image; + + uint8_t alphaValue; + EImageBlitMode blitMode; + +public: + 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; +}; + +class SDLImageIndexed final : public SDLImageBase +{ + SDL_Palette * currentPalette = nullptr; + SDL_Palette * originalPalette = nullptr; + + void setShadowTransparency(float factor); + void preparePalette(); +public: + 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; + void setOverlayColor(const ColorRGBA & color) override; + void playerColored(PlayerColor player) override; + void shiftPalette(uint32_t firstColorID, uint32_t colorsToMove, uint32_t distanceToMove) override; + void adjustPalette(const ColorFilter & shifter, uint32_t colorsToSkipMask) override; + void scaleInteger(int factor) override; + void scaleTo(const Point & size) override; + void exportBitmap(const boost::filesystem::path & path) const override; +}; + +class SDLImageRGB final : public SDLImageBase +{ +public: + using SDLImageBase::SDLImageBase; + + void draw(SDL_Surface * where, const Point & pos, const Rect * src) const override; + void setOverlayColor(const ColorRGBA & color) override; + void playerColored(PlayerColor player) override; + void shiftPalette(uint32_t firstColorID, uint32_t colorsToMove, uint32_t distanceToMove) override; + void adjustPalette(const ColorFilter & shifter, uint32_t colorsToSkipMask) override; + void scaleInteger(int factor) override; + void scaleTo(const Point & size) override; + void exportBitmap(const boost::filesystem::path & path) const override; }; diff --git a/client/renderSDL/SDLImageLoader.cpp b/client/renderSDL/SDLImageLoader.cpp index 30e288f38..a2467a44e 100644 --- a/client/renderSDL/SDLImageLoader.cpp +++ b/client/renderSDL/SDLImageLoader.cpp @@ -17,7 +17,7 @@ #include -SDLImageLoader::SDLImageLoader(SDLImage * Img): +SDLImageLoader::SDLImageLoader(SDLImageShared * Img): image(Img), lineStart(nullptr), position(nullptr) @@ -32,8 +32,8 @@ void SDLImageLoader::init(Point SpriteSize, Point Margins, Point FullSize, SDL_C image->fullSize = FullSize; //Prepare surface - SDL_Palette * p = SDL_AllocPalette(SDLImage::DEFAULT_PALETTE_COLORS); - SDL_SetPaletteColors(p, pal, 0, SDLImage::DEFAULT_PALETTE_COLORS); + SDL_Palette * p = SDL_AllocPalette(DEFAULT_PALETTE_COLORS); + SDL_SetPaletteColors(p, pal, 0, DEFAULT_PALETTE_COLORS); SDL_SetSurfacePalette(image->surf, p); SDL_FreePalette(p); diff --git a/client/renderSDL/SDLImageLoader.h b/client/renderSDL/SDLImageLoader.h index 6e4cca11b..caa1101af 100644 --- a/client/renderSDL/SDLImageLoader.h +++ b/client/renderSDL/SDLImageLoader.h @@ -11,9 +11,13 @@ #include "../render/IImageLoader.h" +class SDLImageShared; + class SDLImageLoader : public IImageLoader { - SDLImage * image; + static constexpr int DEFAULT_PALETTE_COLORS = 256; + + SDLImageShared * image; ui8 * lineStart; ui8 * position; public: @@ -25,7 +29,7 @@ public: //init image with these sizes and palette void init(Point SpriteSize, Point Margins, Point FullSize, SDL_Color *pal); - SDLImageLoader(SDLImage * Img); + SDLImageLoader(SDLImageShared * Img); ~SDLImageLoader(); }; diff --git a/client/renderSDL/SDL_Extensions.cpp b/client/renderSDL/SDL_Extensions.cpp index 9000473c4..63a9089de 100644 --- a/client/renderSDL/SDL_Extensions.cpp +++ b/client/renderSDL/SDL_Extensions.cpp @@ -12,13 +12,20 @@ #include "SDL_PixelAccess.h" +#include "../gui/CGuiHandler.h" #include "../render/Graphics.h" +#include "../render/IScreenHandler.h" #include "../render/Colors.h" #include "../CMT.h" +#include "../xBRZ/xbrz.h" #include "../../lib/GameConstants.h" +#include + #include +#include +#include Rect CSDL_Ext::fromSDL(const SDL_Rect & rect) { @@ -61,34 +68,21 @@ void CSDL_Ext::setAlpha(SDL_Surface * bg, int value) SDL_SetSurfaceAlphaMod(bg, value); } -void CSDL_Ext::updateRect(SDL_Surface *surface, const Rect & rect ) +SDL_Surface * CSDL_Ext::newSurface(const Point & dimensions) { - SDL_Rect rectSDL = CSDL_Ext::toSDL(rect); - if(0 !=SDL_UpdateTexture(screenTexture, &rectSDL, surface->pixels, surface->pitch)) - logGlobal->error("%sSDL_UpdateTexture %s", __FUNCTION__, SDL_GetError()); - - SDL_RenderClear(mainRenderer); - if(0 != SDL_RenderCopy(mainRenderer, screenTexture, nullptr, nullptr)) - logGlobal->error("%sSDL_RenderCopy %s", __FUNCTION__, SDL_GetError()); - SDL_RenderPresent(mainRenderer); - + return newSurface(dimensions, screen); } -SDL_Surface * CSDL_Ext::newSurface(int w, int h) +SDL_Surface * CSDL_Ext::newSurface(const Point & dimensions, SDL_Surface * mod) //creates new surface, with flags/format same as in surface given { - return newSurface(w, h, screen); -} - -SDL_Surface * CSDL_Ext::newSurface(int w, int h, SDL_Surface * mod) //creates new surface, with flags/format same as in surface given -{ - SDL_Surface * ret = SDL_CreateRGBSurface(0,w,h,mod->format->BitsPerPixel,mod->format->Rmask,mod->format->Gmask,mod->format->Bmask,mod->format->Amask); + SDL_Surface * ret = SDL_CreateRGBSurface(0,dimensions.x,dimensions.y,mod->format->BitsPerPixel,mod->format->Rmask,mod->format->Gmask,mod->format->Bmask,mod->format->Amask); if(ret == nullptr) { const char * error = SDL_GetError(); std::string messagePattern = "Failed to create SDL Surface of size %d x %d, %d bpp. Reason: %s"; - std::string message = boost::str(boost::format(messagePattern) % w % h % mod->format->BitsPerPixel % error); + std::string message = boost::str(boost::format(messagePattern) % dimensions.x % dimensions.y % mod->format->BitsPerPixel % error); handleFatalError(message, true); } @@ -96,7 +90,7 @@ SDL_Surface * CSDL_Ext::newSurface(int w, int h, SDL_Surface * mod) //creates ne 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; @@ -221,8 +215,8 @@ uint32_t CSDL_Ext::getPixel(SDL_Surface *surface, const int & x, const int & y, } } -template -int CSDL_Ext::blit8bppAlphaTo24bppT(const SDL_Surface * src, const Rect & srcRectInput, SDL_Surface * dst, const Point & dstPointInput) +template +int CSDL_Ext::blit8bppAlphaTo24bppT(const SDL_Surface * src, const Rect & srcRectInput, SDL_Surface * dst, const Point & dstPointInput, [[maybe_unused]] uint8_t alpha) { SDL_Rect srcRectInstance = CSDL_Ext::toSDL(srcRectInput); SDL_Rect dstRectInstance = CSDL_Ext::toSDL(Rect(dstPointInput, srcRectInput.dimensions())); @@ -337,15 +331,20 @@ int CSDL_Ext::blit8bppAlphaTo24bppT(const SDL_Surface * src, const Rect & srcRec uint8_t *colory = (uint8_t*)src->pixels + srcy*src->pitch + srcx; uint8_t *py = (uint8_t*)dst->pixels + dstRect->y*dst->pitch + dstRect->x*bpp; - for(int y=h; y; y--, colory+=src->pitch, py+=dst->pitch) + for(int y=0; ypitch, py+=dst->pitch) { uint8_t *color = colory; uint8_t *p = py; - for(int x = w; x; x--) + for(int x = 0; x < w; ++x) { const SDL_Color &tbc = colors[*color++]; //color to blit - ColorPutter::PutColorAlphaSwitch(p, tbc.r, tbc.g, tbc.b, tbc.a); + if constexpr (useAlpha) + ColorPutter::PutColorAlphaSwitch(p, tbc.r, tbc.g, tbc.b, int(alpha) * tbc.a / 255 ); + else + ColorPutter::PutColorAlphaSwitch(p, tbc.r, tbc.g, tbc.b, tbc.a); + + p += bpp; } } SDL_UnlockSurface(dst); @@ -354,17 +353,27 @@ int CSDL_Ext::blit8bppAlphaTo24bppT(const SDL_Surface * src, const Rect & srcRec return 0; } -int CSDL_Ext::blit8bppAlphaTo24bpp(const SDL_Surface * src, const Rect & srcRect, SDL_Surface * dst, const Point & dstPoint) +int CSDL_Ext::blit8bppAlphaTo24bpp(const SDL_Surface * src, const Rect & srcRect, SDL_Surface * dst, const Point & dstPoint, uint8_t alpha) { - switch(dst->format->BytesPerPixel) + if (alpha == SDL_ALPHA_OPAQUE) { - case 2: return blit8bppAlphaTo24bppT<2>(src, srcRect, dst, dstPoint); - case 3: return blit8bppAlphaTo24bppT<3>(src, srcRect, dst, dstPoint); - case 4: return blit8bppAlphaTo24bppT<4>(src, srcRect, dst, dstPoint); - default: - logGlobal->error("%d bpp is not supported!", (int)dst->format->BitsPerPixel); - return -1; + switch(dst->format->BytesPerPixel) + { + case 3: return blit8bppAlphaTo24bppT<3, false>(src, srcRect, dst, dstPoint, alpha); + case 4: return blit8bppAlphaTo24bppT<4, false>(src, srcRect, dst, dstPoint, alpha); + } } + else + { + switch(dst->format->BytesPerPixel) + { + case 3: return blit8bppAlphaTo24bppT<3, true>(src, srcRect, dst, dstPoint, alpha); + case 4: return blit8bppAlphaTo24bppT<4, true>(src, srcRect, dst, dstPoint, alpha); + } + } + + logGlobal->error("%d bpp is not supported!", (int)dst->format->BitsPerPixel); + return -1; } uint32_t CSDL_Ext::colorTouint32_t(const SDL_Color * color) @@ -422,7 +431,7 @@ static void drawLineX(SDL_Surface * sur, int x1, int y1, int x2, int y2, const S uint8_t a = vstd::lerp(color1.a, color2.a, f); uint8_t *p = CSDL_Ext::getPxPtr(sur, x, y); - ColorPutter<4, 0>::PutColor(p, r,g,b,a); + ColorPutter<4>::PutColor(p, r,g,b,a); } } @@ -440,36 +449,39 @@ static void drawLineY(SDL_Surface * sur, int x1, int y1, int x2, int y2, const S uint8_t a = vstd::lerp(color1.a, color2.a, f); uint8_t *p = CSDL_Ext::getPxPtr(sur, x, y); - ColorPutter<4, 0>::PutColor(p, r,g,b,a); + ColorPutter<4>::PutColor(p, r,g,b,a); } } -void CSDL_Ext::drawLine(SDL_Surface * sur, const Point & from, const Point & dest, const SDL_Color & color1, const SDL_Color & color2) +void CSDL_Ext::drawLine(SDL_Surface * sur, const Point & from, const Point & dest, const SDL_Color & color1, const SDL_Color & color2, int thickness) { //FIXME: duplicated code with drawLineDashed - int width = std::abs(from.x - dest.x); + int width = std::abs(from.x - dest.x); int height = std::abs(from.y - dest.y); - if ( width == 0 && height == 0) + if(width == 0 && height == 0) { - uint8_t *p = CSDL_Ext::getPxPtr(sur, from.x, from.y); - ColorPutter<4, 0>::PutColorAlpha(p, color1); + uint8_t * p = CSDL_Ext::getPxPtr(sur, from.x, from.y); + ColorPutter<4>::PutColorAlpha(p, color1); return; } - if (width > height) + for(int i = 0; i < thickness; ++i) { - if ( from.x < dest.x) - drawLineX(sur, from.x, from.y, dest.x, dest.y, color1, color2); + if(width > height) + { + if(from.x < dest.x) + drawLineX(sur, from.x, from.y + i, dest.x, dest.y + i, color1, color2); + else + drawLineX(sur, dest.x, dest.y + i, from.x, from.y + i, color2, color1); + } else - drawLineX(sur, dest.x, dest.y, from.x, from.y, color2, color1); - } - else - { - if ( from.y < dest.y) - drawLineY(sur, from.x, from.y, dest.x, dest.y, color1, color2); - else - drawLineY(sur, dest.x, dest.y, from.x, from.y, color2, color1); + { + if(from.y < dest.y) + drawLineY(sur, from.x + i, from.y, dest.x + i, dest.y, color1, color2); + else + drawLineY(sur, dest.x + i, dest.y, from.x + i, from.y, color2, color1); + } } } @@ -524,59 +536,18 @@ void CSDL_Ext::drawBorder( SDL_Surface * sur, const Rect &r, const SDL_Color &co drawBorder(sur, r.x, r.y, r.w, r.h, color, depth); } -void CSDL_Ext::setPlayerColor(SDL_Surface * sur, const PlayerColor & player) +CSDL_Ext::TColorPutter CSDL_Ext::getPutterFor(SDL_Surface * const &dest) { - if(player==PlayerColor::UNFLAGGABLE) - return; - if(sur->format->BitsPerPixel==8) - { - ColorRGBA color = (player == PlayerColor::NEUTRAL - ? graphics->neutralColor - : graphics->playerColors[player.getNum()]); - - SDL_Color colorSDL = toSDL(color); - CSDL_Ext::setColors(sur, &colorSDL, 5, 1); - } - else - logGlobal->warn("Warning, setPlayerColor called on not 8bpp surface!"); -} - -CSDL_Ext::TColorPutter CSDL_Ext::getPutterFor(SDL_Surface * const &dest, int incrementing) -{ -#define CASE_BPP(BytesPerPixel) \ -case BytesPerPixel: \ - if(incrementing > 0) \ - return ColorPutter::PutColor; \ - else if(incrementing == 0) \ - return ColorPutter::PutColor; \ - else \ - return ColorPutter::PutColor;\ - break; - switch(dest->format->BytesPerPixel) { - CASE_BPP(2) - CASE_BPP(3) - CASE_BPP(4) + case 3: + return ColorPutter<3>::PutColor; + case 4: + return ColorPutter<4>::PutColor; default: logGlobal->error("%d bpp is not supported!", (int)dest->format->BitsPerPixel); return nullptr; } - -} - -CSDL_Ext::TColorPutterAlpha CSDL_Ext::getPutterAlphaFor(SDL_Surface * const &dest, int incrementing) -{ - switch(dest->format->BytesPerPixel) - { - CASE_BPP(2) - CASE_BPP(3) - CASE_BPP(4) - default: - logGlobal->error("%d bpp is not supported!", (int)dest->format->BitsPerPixel); - return nullptr; - } -#undef CASE_BPP } uint8_t * CSDL_Ext::getPxPtr(const SDL_Surface * const &srf, const int x, const int y) @@ -607,11 +578,10 @@ bool CSDL_Ext::isTransparent( SDL_Surface * srf, int x, int y ) void CSDL_Ext::putPixelWithoutRefresh(SDL_Surface *ekran, const int & x, const int & y, const uint8_t & R, const uint8_t & G, const uint8_t & B, uint8_t A) { uint8_t *p = getPxPtr(ekran, x, y); - getPutterFor(ekran, false)(p, R, G, B); + getPutterFor(ekran)(p, R, G, B); switch(ekran->format->BytesPerPixel) { - case 2: Channels::px<2>::a.set(p, A); break; case 3: Channels::px<3>::a.set(p, A); break; case 4: Channels::px<4>::a.set(p, A); break; } @@ -655,127 +625,81 @@ void CSDL_Ext::convertToGrayscale( SDL_Surface * surf, const Rect & rect ) { switch(surf->format->BytesPerPixel) { - case 2: convertToGrayscaleBpp<2>(surf, rect); break; case 3: convertToGrayscaleBpp<3>(surf, rect); break; case 4: convertToGrayscaleBpp<4>(surf, rect); break; } } -template -void scaleSurfaceFastInternal(SDL_Surface *surf, SDL_Surface *ret) -{ - const float factorX = static_cast(surf->w) / static_cast(ret->w); - const float factorY = static_cast(surf->h) / static_cast(ret->h); - - for(int y = 0; y < ret->h; y++) - { - for(int x = 0; x < ret->w; x++) - { - //coordinates we want to calculate - auto origX = static_cast(floor(factorX * x)); - auto origY = static_cast(floor(factorY * y)); - - // Get pointers to source pixels - uint8_t *srcPtr = (uint8_t*)surf->pixels + origY * surf->pitch + origX * bpp; - uint8_t *destPtr = (uint8_t*)ret->pixels + y * ret->pitch + x * bpp; - - memcpy(destPtr, srcPtr, bpp); - } - } -} - -SDL_Surface * CSDL_Ext::scaleSurfaceFast(SDL_Surface *surf, int width, int height) -{ - if (!surf || !width || !height) - return nullptr; - - //Same size? return copy - this should more be faster - if (width == surf->w && height == surf->h) - return copySurface(surf); - - SDL_Surface *ret = newSurface(width, height, surf); - - switch(surf->format->BytesPerPixel) - { - case 1: scaleSurfaceFastInternal<1>(surf, ret); break; - case 2: scaleSurfaceFastInternal<2>(surf, ret); break; - case 3: scaleSurfaceFastInternal<3>(surf, ret); break; - case 4: scaleSurfaceFastInternal<4>(surf, ret); break; - } - return ret; -} - -template -void scaleSurfaceInternal(SDL_Surface *surf, SDL_Surface *ret) -{ - const float factorX = float(surf->w - 1) / float(ret->w), - factorY = float(surf->h - 1) / float(ret->h); - - for(int y = 0; y < ret->h; y++) - { - for(int x = 0; x < ret->w; x++) - { - //coordinates we want to interpolate - float origX = factorX * x, - origY = factorY * y; - - float x1 = floor(origX), x2 = floor(origX+1), - y1 = floor(origY), y2 = floor(origY+1); - //assert( x1 >= 0 && y1 >= 0 && x2 < surf->w && y2 < surf->h);//All pixels are in range - - // Calculate weights of each source pixel - float w11 = ((origX - x1) * (origY - y1)); - float w12 = ((origX - x1) * (y2 - origY)); - float w21 = ((x2 - origX) * (origY - y1)); - float w22 = ((x2 - origX) * (y2 - origY)); - //assert( w11 + w12 + w21 + w22 > 0.99 && w11 + w12 + w21 + w22 < 1.01);//total weight is ~1.0 - - // Get pointers to source pixels - uint8_t *p11 = (uint8_t*)surf->pixels + int(y1) * surf->pitch + int(x1) * bpp; - uint8_t *p12 = p11 + bpp; - uint8_t *p21 = p11 + surf->pitch; - uint8_t *p22 = p21 + bpp; - // Calculate resulting channels -#define PX(X, PTR) Channels::px::X.get(PTR) - int resR = static_cast(PX(r, p11) * w11 + PX(r, p12) * w12 + PX(r, p21) * w21 + PX(r, p22) * w22); - int resG = static_cast(PX(g, p11) * w11 + PX(g, p12) * w12 + PX(g, p21) * w21 + PX(g, p22) * w22); - int resB = static_cast(PX(b, p11) * w11 + PX(b, p12) * w12 + PX(b, p21) * w21 + PX(b, p22) * w22); - int resA = static_cast(PX(a, p11) * w11 + PX(a, p12) * w12 + PX(a, p21) * w21 + PX(a, p22) * w22); - //assert(resR < 256 && resG < 256 && resB < 256 && resA < 256); -#undef PX - uint8_t *dest = (uint8_t*)ret->pixels + y * ret->pitch + x * bpp; - Channels::px::r.set(dest, resR); - Channels::px::g.set(dest, resG); - Channels::px::b.set(dest, resB); - Channels::px::a.set(dest, resA); - } - } -} - // scaling via bilinear interpolation algorithm. // NOTE: best results are for scaling in range 50%...200%. // And upscaling looks awful right now - should be fixed somehow -SDL_Surface * CSDL_Ext::scaleSurface(SDL_Surface *surf, int width, int height) +SDL_Surface * CSDL_Ext::scaleSurface(SDL_Surface * surf, int width, int height) { - if (!surf || !width || !height) + if(!surf || !width || !height) return nullptr; - if (surf->format->palette) - return scaleSurfaceFast(surf, width, height); + // TODO: use xBRZ if possible? E.g. when scaling to 150% do 100% -> 200% via xBRZ and then linear downscale 200% -> 150%? + // Need to investigate which is optimal for performance and for visuals - //Same size? return copy - this should more be faster - if (width == surf->w && height == surf->h) - return copySurface(surf); + SDL_Surface * intermediate = SDL_ConvertSurface(surf, screen->format, 0); + SDL_Surface * ret = newSurface(Point(width, height), intermediate); - SDL_Surface *ret = newSurface(width, height, surf); +#if SDL_VERSION_ATLEAST(2,0,16) + SDL_SoftStretchLinear(intermediate, nullptr, ret, nullptr); +#else + SDL_SoftStretch(intermediate, nullptr, ret, nullptr); +#endif + SDL_FreeSurface(intermediate); - switch(surf->format->BytesPerPixel) + return ret; +} + +SDL_Surface * CSDL_Ext::scaleSurfaceIntegerFactor(SDL_Surface * surf, int factor, EScalingAlgorithm algorithm) +{ + if(surf == nullptr || factor == 0) + return nullptr; + + int newWidth = surf->w * factor; + int newHight = surf->h * factor; + + SDL_Surface * intermediate = SDL_ConvertSurfaceFormat(surf, SDL_PIXELFORMAT_ARGB8888, 0); + SDL_Surface * ret = newSurface(Point(newWidth, newHight), intermediate); + + assert(intermediate->pitch == intermediate->w * 4); + assert(ret->pitch == ret->w * 4); + + const uint32_t * srcPixels = static_cast(intermediate->pixels); + uint32_t * dstPixels = static_cast(ret->pixels); + + // avoid excessive granulation - xBRZ prefers at least 8-16 lines per task + // TODO: compare performance and size of images, recheck values for potentially better parameters + const int granulation = std::clamp(surf->h / 64 * 8, 8, 64); + + switch (algorithm) { - case 2: scaleSurfaceInternal<2>(surf, ret); break; - case 3: scaleSurfaceInternal<3>(surf, ret); break; - case 4: scaleSurfaceInternal<4>(surf, ret); break; + case EScalingAlgorithm::NEAREST: + xbrz::nearestNeighborScale(srcPixels, intermediate->w, intermediate->h, dstPixels, ret->w, ret->h); + break; + case EScalingAlgorithm::BILINEAR: + xbrz::bilinearScale(srcPixels, intermediate->w, intermediate->h, dstPixels, ret->w, ret->h); + break; + case EScalingAlgorithm::XBRZ_ALPHA: + case EScalingAlgorithm::XBRZ_OPAQUE: + { + auto format = algorithm == EScalingAlgorithm::XBRZ_OPAQUE ? xbrz::ColorFormat::ARGB_CLAMPED : xbrz::ColorFormat::ARGB; + tbb::parallel_for(tbb::blocked_range(0, intermediate->h, granulation), [factor, srcPixels, dstPixels, intermediate, format](const tbb::blocked_range & r) + { + + xbrz::scale(factor, srcPixels, dstPixels, intermediate->w, intermediate->h, format, {}, r.begin(), r.end()); + }); + break; + } + default: + throw std::runtime_error("invalid scaling algorithm!"); } + SDL_FreeSurface(intermediate); + return ret; } @@ -868,7 +792,10 @@ void CSDL_Ext::getClipRect(SDL_Surface * src, Rect & other) other = CSDL_Ext::fromSDL(rect); } -template SDL_Surface * CSDL_Ext::createSurfaceWithBpp<2>(int, int); +int CSDL_Ext::CClipRectGuard::getScalingFactor() const +{ + return GH.screenHandler().getScalingFactor(); +} + template SDL_Surface * CSDL_Ext::createSurfaceWithBpp<3>(int, int); template SDL_Surface * CSDL_Ext::createSurfaceWithBpp<4>(int, int); - diff --git a/client/renderSDL/SDL_Extensions.h b/client/renderSDL/SDL_Extensions.h index c85458c09..acdb0dc5a 100644 --- a/client/renderSDL/SDL_Extensions.h +++ b/client/renderSDL/SDL_Extensions.h @@ -27,6 +27,14 @@ class Point; VCMI_LIB_NAMESPACE_END +enum class EScalingAlgorithm : int8_t +{ + NEAREST, + BILINEAR, + XBRZ_OPAQUE, // xbrz, image edges are considered to have same color as pixel inside image + XBRZ_ALPHA // xbrz, image edges are considered to be transparent +}; + namespace CSDL_Ext { @@ -61,8 +69,6 @@ using TColorPutterAlpha = void (*)(uint8_t *&, const uint8_t &, const uint8_t &, void fillRect(SDL_Surface * dst, const Rect & dstrect, const SDL_Color & color); void fillRectBlended(SDL_Surface * dst, const Rect & dstrect, const SDL_Color & color); - void updateRect(SDL_Surface * surface, const Rect & rect); - void putPixelWithoutRefresh(SDL_Surface * ekran, const int & x, const int & y, const uint8_t & R, const uint8_t & G, const uint8_t & B, uint8_t A = 255); void putPixelWithoutRefreshIfInSurf(SDL_Surface *ekran, const int & x, const int & y, const uint8_t & R, const uint8_t & G, const uint8_t & B, uint8_t A = 255); @@ -73,32 +79,28 @@ using TColorPutterAlpha = void (*)(uint8_t *&, const uint8_t &, const uint8_t &, bool isTransparent(SDL_Surface * srf, const Point & position); //checks if surface is transparent at given position uint8_t * getPxPtr(const SDL_Surface * const & srf, const int x, const int y); - TColorPutter getPutterFor(SDL_Surface * const & dest, int incrementing); //incrementing: -1, 0, 1 - TColorPutterAlpha getPutterAlphaFor(SDL_Surface * const & dest, int incrementing); //incrementing: -1, 0, 1 + TColorPutter getPutterFor(SDL_Surface * const & dest); - template - int blit8bppAlphaTo24bppT(const SDL_Surface * src, const Rect & srcRect, SDL_Surface * dst, const Point & dstPoint); //blits 8 bpp surface with alpha channel to 24 bpp surface - int blit8bppAlphaTo24bpp(const SDL_Surface * src, const Rect & srcRect, SDL_Surface * dst, const Point & dstPoint); //blits 8 bpp surface with alpha channel to 24 bpp surface + template + int blit8bppAlphaTo24bppT(const SDL_Surface * src, const Rect & srcRect, SDL_Surface * dst, const Point & dstPoint, uint8_t alpha); //blits 8 bpp surface with alpha channel to 24 bpp surface + int blit8bppAlphaTo24bpp(const SDL_Surface * src, const Rect & srcRect, SDL_Surface * dst, const Point & dstPoint, uint8_t alpha); //blits 8 bpp surface with alpha channel to 24 bpp surface uint32_t colorTouint32_t(const SDL_Color * color); //little endian only - void drawLine(SDL_Surface * sur, const Point & from, const Point & dest, const SDL_Color & color1, const SDL_Color & color2); + void drawLine(SDL_Surface * sur, const Point & from, const Point & dest, const SDL_Color & color1, const SDL_Color & color2, int width); void drawLineDashed(SDL_Surface * sur, const Point & from, const Point & dest, const SDL_Color & color); void drawBorder(SDL_Surface * sur, int x, int y, int w, int h, const SDL_Color & color, int depth = 1); void drawBorder(SDL_Surface * sur, const Rect & r, const SDL_Color & color, int depth = 1); - void setPlayerColor(SDL_Surface * sur, const PlayerColor & player); //sets correct color of flags; -1 for neutral - SDL_Surface * newSurface(int w, int h, SDL_Surface * mod); //creates new surface, with flags/format same as in surface given - SDL_Surface * newSurface(int w, int h); //creates new surface, with flags/format same as in screen surface + SDL_Surface * newSurface(const Point & dimensions, SDL_Surface * mod); //creates new surface, with flags/format same as in surface given + SDL_Surface * newSurface(const Point & dimensions); //creates new surface, with flags/format same as in screen surface SDL_Surface * copySurface(SDL_Surface * mod); //returns copy of given surface template SDL_Surface * createSurfaceWithBpp(int width, int height); //create surface with give bits per pixels value - //scale surface to required size. - //nearest neighbour algorithm - SDL_Surface * scaleSurfaceFast(SDL_Surface * surf, int width, int height); - // bilinear filtering. Uses fallback to scaleSurfaceFast in case of indexed surfaces + // bilinear filtering. Always returns rgba surface SDL_Surface * scaleSurface(SDL_Surface * surf, int width, int height); + SDL_Surface * scaleSurfaceIntegerFactor(SDL_Surface * surf, int factor, EScalingAlgorithm scaler); template void convertToGrayscaleBpp(SDL_Surface * surf, const Rect & rect); @@ -117,11 +119,13 @@ using TColorPutterAlpha = void (*)(uint8_t *&, const uint8_t &, const uint8_t &, SDL_Surface * surf; Rect oldRect; + int getScalingFactor() const; + public: CClipRectGuard(SDL_Surface * surface, const Rect & rect): surf(surface) { CSDL_Ext::getClipRect(surf, oldRect); - CSDL_Ext::setClipRect(surf, rect); + CSDL_Ext::setClipRect(surf, rect * getScalingFactor()); } ~CClipRectGuard() diff --git a/client/renderSDL/SDL_PixelAccess.h b/client/renderSDL/SDL_PixelAccess.h index ddd91a663..2cf41ff28 100644 --- a/client/renderSDL/SDL_PixelAccess.h +++ b/client/renderSDL/SDL_PixelAccess.h @@ -109,58 +109,29 @@ namespace Channels }; #endif - - template<> - struct px<2> - { - static channel_subpx<5, 0xF800, 11> r; - static channel_subpx<6, 0x07E0, 5 > g; - static channel_subpx<5, 0x001F, 0 > b; - static channel_empty a; - }; } -template +template struct ColorPutter { static STRONG_INLINE void PutColor(uint8_t *&ptr, const uint8_t & R, const uint8_t & G, const uint8_t & B); static STRONG_INLINE void PutColor(uint8_t *&ptr, const uint8_t & R, const uint8_t & G, const uint8_t & B, const uint8_t & A); static STRONG_INLINE void PutColorAlphaSwitch(uint8_t *&ptr, const uint8_t & R, const uint8_t & G, const uint8_t & B, const uint8_t & A); - static STRONG_INLINE void PutColor(uint8_t *&ptr, const SDL_Color & Color); static STRONG_INLINE void PutColorAlpha(uint8_t *&ptr, const SDL_Color & Color); - static STRONG_INLINE void PutColorRow(uint8_t *&ptr, const SDL_Color & Color, size_t count); }; -template -struct ColorPutter<2, incrementPtr> -{ - static STRONG_INLINE void PutColor(uint8_t *&ptr, const uint8_t & R, const uint8_t & G, const uint8_t & B); - static STRONG_INLINE void PutColor(uint8_t *&ptr, const uint8_t & R, const uint8_t & G, const uint8_t & B, const uint8_t & A); - static STRONG_INLINE void PutColorAlphaSwitch(uint8_t *&ptr, const uint8_t & R, const uint8_t & G, const uint8_t & B, const uint8_t & A); - static STRONG_INLINE void PutColor(uint8_t *&ptr, const SDL_Color & Color); - static STRONG_INLINE void PutColorAlpha(uint8_t *&ptr, const SDL_Color & Color); - static STRONG_INLINE void PutColorRow(uint8_t *&ptr, const SDL_Color & Color, size_t count); -}; - -template -STRONG_INLINE void ColorPutter::PutColorAlpha(uint8_t *&ptr, const SDL_Color & Color) +template +STRONG_INLINE void ColorPutter::PutColorAlpha(uint8_t *&ptr, const SDL_Color & Color) { PutColor(ptr, Color.r, Color.g, Color.b, Color.a); } -template -STRONG_INLINE void ColorPutter::PutColor(uint8_t *&ptr, const SDL_Color & Color) -{ - PutColor(ptr, Color.r, Color.g, Color.b); -} - -template -STRONG_INLINE void ColorPutter::PutColorAlphaSwitch(uint8_t *&ptr, const uint8_t & R, const uint8_t & G, const uint8_t & B, const uint8_t & A) +template +STRONG_INLINE void ColorPutter::PutColorAlphaSwitch(uint8_t *&ptr, const uint8_t & R, const uint8_t & G, const uint8_t & B, const uint8_t & A) { switch (A) { case 0: - ptr += bpp * incrementPtr; return; case 255: PutColor(ptr, R, G, B); @@ -177,124 +148,19 @@ STRONG_INLINE void ColorPutter::PutColorAlphaSwitch(uint8_t * } } -template -STRONG_INLINE void ColorPutter::PutColor(uint8_t *&ptr, const uint8_t & R, const uint8_t & G, const uint8_t & B, const uint8_t & A) +template +STRONG_INLINE void ColorPutter::PutColor(uint8_t *&ptr, const uint8_t & R, const uint8_t & G, const uint8_t & B, const uint8_t & A) { PutColor(ptr, ((((uint32_t)R - (uint32_t)Channels::px::r.get(ptr))*(uint32_t)A) >> 8 ) + (uint32_t)Channels::px::r.get(ptr), ((((uint32_t)G - (uint32_t)Channels::px::g.get(ptr))*(uint32_t)A) >> 8 ) + (uint32_t)Channels::px::g.get(ptr), ((((uint32_t)B - (uint32_t)Channels::px::b.get(ptr))*(uint32_t)A) >> 8 ) + (uint32_t)Channels::px::b.get(ptr)); } - -template -STRONG_INLINE void ColorPutter::PutColor(uint8_t *&ptr, const uint8_t & R, const uint8_t & G, const uint8_t & B) +template +STRONG_INLINE void ColorPutter::PutColor(uint8_t *&ptr, const uint8_t & R, const uint8_t & G, const uint8_t & B) { - static_assert(incrementPtr >= -1 && incrementPtr <= +1, "Invalid incrementPtr value!"); - - if (incrementPtr < 0) - ptr -= bpp; - Channels::px::r.set(ptr, R); Channels::px::g.set(ptr, G); Channels::px::b.set(ptr, B); Channels::px::a.set(ptr, 255); - - if (incrementPtr > 0) - ptr += bpp; - -} - -template -STRONG_INLINE void ColorPutter::PutColorRow(uint8_t *&ptr, const SDL_Color & Color, size_t count) -{ - if (count) - { - uint8_t *pixel = ptr; - PutColor(ptr, Color.r, Color.g, Color.b); - - for (size_t i=0; i -STRONG_INLINE void ColorPutter<2, incrementPtr>::PutColor(uint8_t *&ptr, const uint8_t & R, const uint8_t & G, const uint8_t & B) -{ - if(incrementPtr == -1) - ptr -= 2; - - auto * const px = (uint16_t *)ptr; - *px = (B>>3) + ((G>>2) << 5) + ((R>>3) << 11); //drop least significant bits of 24 bpp encoded color - - if(incrementPtr == 1) - ptr += 2; //bpp -} - -template -STRONG_INLINE void ColorPutter<2, incrementPtr>::PutColorAlphaSwitch(uint8_t *&ptr, const uint8_t & R, const uint8_t & G, const uint8_t & B, const uint8_t & A) -{ - switch (A) - { - case 0: - ptr += 2 * incrementPtr; - return; - case 255: - PutColor(ptr, R, G, B); - return; - default: - PutColor(ptr, R, G, B, A); - return; - } -} - -template -STRONG_INLINE void ColorPutter<2, incrementPtr>::PutColor(uint8_t *&ptr, const uint8_t & R, const uint8_t & G, const uint8_t & B, const uint8_t & A) -{ - const int rbit = 5, gbit = 6, bbit = 5; //bits per color - const int rmask = 0xF800, gmask = 0x7E0, bmask = 0x1F; - const int rshift = 11, gshift = 5, bshift = 0; - - const uint8_t r5 = (*((uint16_t *)ptr) & rmask) >> rshift, - b5 = (*((uint16_t *)ptr) & bmask) >> bshift, - g5 = (*((uint16_t *)ptr) & gmask) >> gshift; - - const uint32_t r8 = (r5 << (8 - rbit)) | (r5 >> (2*rbit - 8)), - g8 = (g5 << (8 - gbit)) | (g5 >> (2*gbit - 8)), - b8 = (b5 << (8 - bbit)) | (b5 >> (2*bbit - 8)); - - PutColor(ptr, - (((R-r8)*A) >> 8) + r8, - (((G-g8)*A) >> 8) + g8, - (((B-b8)*A) >> 8) + b8); -} - -template -STRONG_INLINE void ColorPutter<2, incrementPtr>::PutColorAlpha(uint8_t *&ptr, const SDL_Color & Color) -{ - PutColor(ptr, Color.r, Color.g, Color.b, Color.a); -} - -template -STRONG_INLINE void ColorPutter<2, incrementPtr>::PutColor(uint8_t *&ptr, const SDL_Color & Color) -{ - PutColor(ptr, Color.r, Color.g, Color.b); -} - -template -STRONG_INLINE void ColorPutter<2, incrementPtr>::PutColorRow(uint8_t *&ptr, const SDL_Color & Color, size_t count) -{ - //drop least significant bits of 24 bpp encoded color - uint16_t pixel = (Color.b>>3) + ((Color.g>>2) << 5) + ((Color.r>>3) << 11); - - for (size_t i=0; i ScreenHandler::getSupportedScalingRange() const { // H3 resolution, any resolution smaller than that is not correctly supported - static const Point minResolution = {800, 600}; + static constexpr Point minResolution = heroes3Resolution; // arbitrary limit on *downscaling*. Allow some downscaling, if requested by user. Should be generally limited to 100+ for all but few devices - static const double minimalScaling = 50; + static constexpr double minimalScaling = 50; Point renderResolution = getRenderResolution(); double reservedAreaWidth = settings["video"]["reservedWidth"].Float(); @@ -83,22 +84,60 @@ 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; } +int ScreenHandler::getScalingFactor() const +{ + switch (upscalingFilter) + { + case EUpscalingFilter::NONE: return 1; + case EUpscalingFilter::XBRZ_2: return 2; + case EUpscalingFilter::XBRZ_3: return 3; + case EUpscalingFilter::XBRZ_4: return 4; + } + + throw std::runtime_error("invalid upscaling filter"); +} + +Point ScreenHandler::getLogicalResolution() const +{ + return Point(screen->w, screen->h) / getScalingFactor(); +} + Point ScreenHandler::getRenderResolution() const { assert(mainRenderer != nullptr); @@ -291,12 +330,61 @@ void ScreenHandler::initializeWindow() handleFatalError(message, true); } + selectUpscalingFilter(); + selectDownscalingFilter(); + SDL_RendererInfo info; SDL_GetRendererInfo(mainRenderer, &info); - SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, settings["video"]["scalingMode"].String().c_str()); logGlobal->info("Created renderer %s", info.name); } +EUpscalingFilter ScreenHandler::loadUpscalingFilter() const +{ + static const std::map upscalingFilterTypes = + { + {"auto", EUpscalingFilter::AUTO }, + {"none", EUpscalingFilter::NONE }, + {"xbrz2", EUpscalingFilter::XBRZ_2 }, + {"xbrz3", EUpscalingFilter::XBRZ_3 }, + {"xbrz4", EUpscalingFilter::XBRZ_4 } + }; + + auto filterName = settings["video"]["upscalingFilter"].String(); + auto filter = upscalingFilterTypes.at(filterName); + + if (filter != EUpscalingFilter::AUTO) + return filter; + + // 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.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() +{ + upscalingFilter = loadUpscalingFilter(); + logGlobal->debug("Selected upscaling filter %d", static_cast(upscalingFilter)); +} + +void ScreenHandler::selectDownscalingFilter() +{ + SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, settings["video"]["downscalingFilter"].String().c_str()); + logGlobal->debug("Selected downscaling filter %s", settings["video"]["downscalingFilter"].String()); +} + void ScreenHandler::initializeScreenBuffers() { #ifdef VCMI_ENDIAN_BIG @@ -311,7 +399,7 @@ void ScreenHandler::initializeScreenBuffers() int amask = 0xFF000000; #endif - auto logicalSize = getPreferredLogicalResolution(); + auto logicalSize = getPreferredLogicalResolution() * getScalingFactor(); SDL_RenderSetLogicalSize(mainRenderer, logicalSize.x, logicalSize.y); screen = SDL_CreateRGBSurface(0, logicalSize.x, logicalSize.y, 32, rmask, gmask, bmask, amask); diff --git a/client/renderSDL/ScreenHandler.h b/client/renderSDL/ScreenHandler.h index fb3d6a334..6a9026d7b 100644 --- a/client/renderSDL/ScreenHandler.h +++ b/client/renderSDL/ScreenHandler.h @@ -29,9 +29,23 @@ enum class EWindowMode FULLSCREEN_EXCLUSIVE }; +enum class EUpscalingFilter +{ + AUTO, // used only for loading from config, replaced with autoselected value on init + NONE, + //BILINEAR, // TODO? + //BICUBIC, // TODO? + XBRZ_2, + XBRZ_3, + XBRZ_4, + // NOTE: xbrz also provides x5 and x6 filters, but those would require high-end gaming PC's due to huge memory usage with no visible gain +}; + /// This class is responsible for management of game window and its main rendering surface class ScreenHandler final : public IScreenHandler { + EUpscalingFilter upscalingFilter = EUpscalingFilter::AUTO; + /// Dimensions of target surfaces/textures, this value is what game logic views as screen size Point getPreferredLogicalResolution() const; @@ -69,6 +83,11 @@ class ScreenHandler final : public IScreenHandler /// Performs validation of settings and updates them to valid values if necessary void validateSettings(); + + EUpscalingFilter loadUpscalingFilter() const; + + void selectDownscalingFilter(); + void selectUpscalingFilter(); public: /// Creates and initializes screen, window and SDL state @@ -89,6 +108,12 @@ public: /// Window has focus bool hasFocus() final; + Point getLogicalResolution() const final; + + 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 b69cc6199..9725b628b 100644 --- a/client/widgets/Buttons.cpp +++ b/client/widgets/Buttons.cpp @@ -13,7 +13,6 @@ #include "Images.h" #include "TextControls.h" -#include "../CMusicHandler.h" #include "../CGameInfo.h" #include "../CPlayerInterface.h" #include "../battle/BattleInterface.h" @@ -23,13 +22,13 @@ #include "../gui/MouseButton.h" #include "../gui/Shortcut.h" #include "../gui/InterfaceObjectConfigurable.h" +#include "../media/ISoundPlayer.h" #include "../windows/InfoWindows.h" -#include "../render/CAnimation.h" #include "../render/Canvas.h" #include "../render/IRenderHandler.h" #include "../../lib/CConfigHandler.h" -#include "../../lib/CGeneralTextHandler.h" +#include "../../lib/texts/CGeneralTextHandler.h" #include "../../lib/filesystem/Filesystem.h" void ButtonBase::update() @@ -50,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()) @@ -67,9 +67,14 @@ void CButton::addCallback(const std::function & callback) this->callback += callback; } +void CButton::addPopupCallback(const std::function & callback) +{ + this->callbackPopup += callback; +} + void ButtonBase::setTextOverlay(const std::string & Text, EFonts font, ColorRGBA color) { - OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255-DISPOSE); + OBJECT_CONSTRUCTION; setOverlay(std::make_shared(pos.w/2, pos.h/2, font, ETextAlignment::CENTER, color, Text)); update(); } @@ -88,14 +93,14 @@ void ButtonBase::setOverlay(const std::shared_ptr& newOverlay) void ButtonBase::setImage(const AnimationPath & defName, bool playerColoredButton) { - OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255-DISPOSE); + OBJECT_CONSTRUCTION; configurable.reset(); image = std::make_shared(defName, vstd::to_underlying(getState())); pos = image->pos; if (playerColoredButton) - image->playerColored(LOCPLINT->playerID); + image->setPlayerColor(LOCPLINT->playerID); } const JsonNode & ButtonBase::getCurrentConfig() const @@ -121,7 +126,7 @@ const JsonNode & ButtonBase::getCurrentConfig() const void ButtonBase::setConfigurable(const JsonPath & jsonName, bool playerColoredButton) { - OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255-DISPOSE); + OBJECT_CONSTRUCTION; config = std::make_unique(jsonName); @@ -130,7 +135,7 @@ void ButtonBase::setConfigurable(const JsonPath & jsonName, bool playerColoredBu pos = configurable->pos; if (playerColoredButton) - image->playerColored(LOCPLINT->playerID); + image->setPlayerColor(LOCPLINT->playerID); } void CButton::addHoverText(EButtonState state, const std::string & text) @@ -158,7 +163,7 @@ void ButtonBase::setStateImpl(EButtonState newState) if (configurable) { - OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255-DISPOSE); + OBJECT_CONSTRUCTION; configurable = std::make_shared(getCurrentConfig()); pos = configurable->pos; @@ -289,6 +294,8 @@ void CButton::clickCancel(const Point & cursorPosition) void CButton::showPopupWindow(const Point & cursorPosition) { + callbackPopup(); + if(!helpBox.empty()) //there is no point to show window with nothing inside... CRClickPopup::createAndPush(helpBox); } @@ -350,7 +357,6 @@ CButton::CButton(Point position, const AnimationPath &defName, const std::pairisPlayerColored()) - image->playerColored(player); + image->setPlayerColor(player); } void CButton::showAll(Canvas & to) @@ -441,10 +447,13 @@ void CToggleBase::setAllowDeselection(bool on) } CToggleButton::CToggleButton(Point position, const AnimationPath &defName, const std::pair &help, - CFunctionList callback, EShortcut key, bool playerColoredButton): + CFunctionList callback, EShortcut key, bool playerColoredButton, + CFunctionList callbackSelected): CButton(position, defName, help, 0, key, playerColoredButton), - CToggleBase(callback) + CToggleBase(callback), + callbackSelected(callbackSelected) { + addUsedEvents(DOUBLECLICK); } void CToggleButton::doSelect(bool on) @@ -511,6 +520,12 @@ void CToggleButton::clickCancel(const Point & cursorPosition) doSelect(isSelected()); } +void CToggleButton::clickDouble(const Point & cursorPosition) +{ + if(callbackSelected) + callbackSelected(); +} + void CToggleGroup::addCallback(const std::function & callback) { onChange += callback; diff --git a/client/widgets/Buttons.h b/client/widgets/Buttons.h index a3797d49b..f369473f8 100644 --- a/client/widgets/Buttons.h +++ b/client/widgets/Buttons.h @@ -69,6 +69,7 @@ public: class CButton : public ButtonBase { CFunctionList callback; + CFunctionList callbackPopup; std::array hoverTexts; //texts for statusbar, if empty - first entry will be used std::optional borderColor; // mapping of button state to border color @@ -90,6 +91,7 @@ public: /// adds one more callback to on-click actions void addCallback(const std::function & callback); + void addPopupCallback(const std::function & callback); void addHoverText(EButtonState state, const std::string & text); @@ -154,7 +156,7 @@ public: void addCallback(const std::function & callback); - /// Set whether the toggle is currently enabled for user to use, this is only inplemented in ToggleButton, not for other toggles yet. + /// Set whether the toggle is currently enabled for user to use, this is only implemented in ToggleButton, not for other toggles yet. virtual void setEnabled(bool enabled); }; @@ -164,13 +166,17 @@ class CToggleButton : public CButton, public CToggleBase void doSelect(bool on) override; void setEnabled(bool enabled) override; + CFunctionList callbackSelected; + public: CToggleButton(Point position, const AnimationPath &defName, const std::pair &help, - CFunctionList Callback = 0, EShortcut key = {}, bool playerColoredButton = false ); + CFunctionList Callback = nullptr, EShortcut key = {}, bool playerColoredButton = false, + CFunctionList CallbackSelected = nullptr ); void clickPressed(const Point & cursorPosition) override; void clickReleased(const Point & cursorPosition) override; void clickCancel(const Point & cursorPosition) override; + void clickDouble(const Point & cursorPosition) override; // bring overrides into scope //using CButton::addCallback; diff --git a/client/widgets/CArtPlace.cpp b/client/widgets/CArtPlace.cpp deleted file mode 100644 index 0f9e3656f..000000000 --- a/client/widgets/CArtPlace.cpp +++ /dev/null @@ -1,303 +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/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_CAPTURING(255 - DISPOSE); - - 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(); -} - -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(); - } -} - -bool ArtifactUtilsClient::askToAssemble(const CGHeroInstance * hero, const ArtifactPosition & slot) -{ - assert(hero); - const auto art = hero->getArt(slot); - assert(art); - - if(hero->tempOwner != LOCPLINT->playerID) - return false; - - auto assemblyPossibilities = ArtifactUtils::assemblyPossibilities(hero, art->getTypeId()); - if(!assemblyPossibilities.empty()) - { - auto askThread = new boost::thread([hero, art, slot, assemblyPossibilities]() -> void - { - boost::mutex::scoped_lock interfaceLock(GH.interfaceMutex); - for(const auto combinedArt : assemblyPossibilities) - { - bool assembleConfirmed = false; - CFunctionList onYesHandlers([&assembleConfirmed]() -> void {assembleConfirmed = true; }); - onYesHandlers += std::bind(&CCallback::assembleArtifacts, LOCPLINT->cb.get(), hero, slot, true, combinedArt->getId()); - - LOCPLINT->showArtifactAssemblyDialog(art->artType, combinedArt, onYesHandlers); - LOCPLINT->waitWhileDialog(); - if(assembleConfirmed) - break; - } - }); - askThread->detach(); - return true; - } - return false; -} - -bool ArtifactUtilsClient::askToDisassemble(const CGHeroInstance * hero, const ArtifactPosition & slot) -{ - assert(hero); - const auto art = hero->getArt(slot); - assert(art); - - if(hero->tempOwner != LOCPLINT->playerID) - return false; - - if(art->isCombined()) - { - if(ArtifactUtils::isSlotBackpack(slot) && !ArtifactUtils::isBackpackFreeSlots(hero, art->artType->getConstituents().size() - 1)) - return false; - - LOCPLINT->showArtifactAssemblyDialog( - art->artType, - nullptr, - std::bind(&CCallback::assembleArtifacts, LOCPLINT->cb.get(), hero, slot, false, ArtifactID())); - return true; - } - return false; -} diff --git a/client/widgets/CArtifactsOfHeroAltar.cpp b/client/widgets/CArtifactsOfHeroAltar.cpp index 19e79f02d..795243eca 100644 --- a/client/widgets/CArtifactsOfHeroAltar.cpp +++ b/client/widgets/CArtifactsOfHeroAltar.cpp @@ -21,12 +21,10 @@ CArtifactsOfHeroAltar::CArtifactsOfHeroAltar(const Point & position) { - init( - std::bind(&CArtifactsOfHeroBase::clickPrassedArtPlace, this, _1, _2), - std::bind(&CArtifactsOfHeroBase::showPopupArtPlace, this, _1, _2), - position, - std::bind(&CArtifactsOfHeroBase::scrollBackpack, this, _1)); - + 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) slot->moveBy(Point(2, -1)); diff --git a/client/widgets/CArtifactsOfHeroAltar.h b/client/widgets/CArtifactsOfHeroAltar.h index ad291dbc2..a7d82894a 100644 --- a/client/widgets/CArtifactsOfHeroAltar.h +++ b/client/widgets/CArtifactsOfHeroAltar.h @@ -16,6 +16,8 @@ class CArtifactsOfHeroAltar : public CArtifactsOfHeroBase { public: + ObjectInstanceID altarId; + CArtifactsOfHeroAltar(const Point & position); void deactivate() override; }; diff --git a/client/widgets/CArtifactsOfHeroBackpack.cpp b/client/widgets/CArtifactsOfHeroBackpack.cpp index 6a10f29d3..4cfde13ec 100644 --- a/client/widgets/CArtifactsOfHeroBackpack.cpp +++ b/client/widgets/CArtifactsOfHeroBackpack.cpp @@ -13,7 +13,7 @@ #include "../gui/CGuiHandler.h" #include "Images.h" -#include "GameSettings.h" +#include "IGameSettings.h" #include "ObjectLists.h" #include "../CPlayerInterface.h" @@ -34,12 +34,14 @@ CArtifactsOfHeroBackpack::CArtifactsOfHeroBackpack(size_t slotsColumnsMax, size_ CArtifactsOfHeroBackpack::CArtifactsOfHeroBackpack() : CArtifactsOfHeroBackpack(8, 8) { - const auto backpackCap = VLC->settings()->getInteger(EGameSettings::HEROES_BACKPACK_CAP); + const auto backpackCap = LOCPLINT->cb->getSettings().getInteger(EGameSettings::HEROES_BACKPACK_CAP); auto visibleCapacityMax = slotsRowsMax * slotsColumnsMax; if(backpackCap >= 0) 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) @@ -73,7 +75,7 @@ size_t CArtifactsOfHeroBackpack::getSlotsNum() void CArtifactsOfHeroBackpack::initAOHbackpack(size_t slots, bool slider) { - OBJECT_CONSTRUCTION_CAPTURING(255 - DISPOSE); + OBJECT_CONSTRUCTION; backpack.resize(slots); size_t artPlaceIdx = 0; @@ -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(filterBySlot != ArtifactPosition::FIRST_AVAILABLE); - 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,11 +173,12 @@ 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->getSlotByInstance(art.second)); + setSlotData(*artPlace++, curHero->getArtPos(art.second)); for(auto & art : filteredScrolls) - setSlotData(*artPlace++, curHero->getSlotByInstance(art.second)); + setSlotData(*artPlace++, curHero->getArtPos(art.second)); } } @@ -204,4 +204,4 @@ void CArtifactsOfHeroQuickBackpack::swapSelected() } if(backpackLoc.slot != ArtifactPosition::PRE_FIRST && filterBySlot != ArtifactPosition::PRE_FIRST && curHero) LOCPLINT->cb->swapArtifacts(backpackLoc, ArtifactLocation(curHero->id, filterBySlot)); -} \ No newline at end of file +} diff --git a/client/widgets/CArtifactsOfHeroBase.cpp b/client/widgets/CArtifactsOfHeroBase.cpp index c345b5155..36c0690f4 100644 --- a/client/widgets/CArtifactsOfHeroBase.cpp +++ b/client/widgets/CArtifactsOfHeroBase.cpp @@ -32,9 +32,9 @@ CArtifactsOfHeroBase::CArtifactsOfHeroBase() void CArtifactsOfHeroBase::putBackPickedArtifact() { // Artifact located in artifactsTransitionPos should be returned - if(getPickedArtifact()) + if(const auto art = getPickedArtifact()) { - auto slot = ArtifactUtils::getArtAnyPosition(curHero, curHero->artifactsTransitionPos.begin()->artifact->getTypeId()); + auto slot = ArtifactUtils::getArtAnyPosition(curHero, art->getTypeId()); if(slot == ArtifactPosition::PRE_FIRST) { LOCPLINT->cb->eraseArtifactByClient(ArtifactLocation(curHero->id, ArtifactPosition::TRANSITION_POS)); @@ -47,13 +47,11 @@ void CArtifactsOfHeroBase::putBackPickedArtifact() } void CArtifactsOfHeroBase::init( - const CArtPlace::ClickFunctor & onClickPressedCallback, - const CArtPlace::ClickFunctor & onShowPopupCallback, const Point & position, const BpackScrollFunctor & scrollCallback) { - // CArtifactsOfHeroBase::init may be transform to CArtifactsOfHeroBase::CArtifactsOfHeroBase if OBJECT_CONSTRUCTION_CAPTURING is removed - OBJECT_CONSTRUCTION_CAPTURING(255 - DISPOSE); + // CArtifactsOfHeroBase::init may be transform to CArtifactsOfHeroBase::CArtifactsOfHeroBase if OBJECT_CONSTRUCTION is removed + OBJECT_CONSTRUCTION; pos += position; for(int g = 0; g < ArtifactPosition::BACKPACK_START; g++) { @@ -65,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(onClickPressedCallback); - artPlace.second->setShowPopupCallback(onShowPopupCallback); + artPlace.second->setArtifact(ArtifactID(ArtifactID::NONE)); } - for(auto artPlace : backpack) + for(const auto & artPlace : backpack) { - artPlace->setArtifact(nullptr); - artPlace->setClickPressedCallback(onClickPressedCallback); - artPlace->setShowPopupCallback(onShowPopupCallback); + artPlace->setArtifact(ArtifactID(ArtifactID::NONE)); } leftBackpackRoll = std::make_shared(Point(379, 364), AnimationPath::builtin("hsbtns3.def"), CButton::tooltip(), [scrollCallback](){scrollCallback(true);}, EShortcut::MOVE_LEFT); @@ -91,27 +85,63 @@ void CArtifactsOfHeroBase::init( setRedrawParent(true); } -void CArtifactsOfHeroBase::clickPrassedArtPlace(CArtPlace & artPlace, const Point & cursorPosition) +void CArtifactsOfHeroBase::setClickPressedArtPlacesCallback(const CArtPlace::ClickFunctor & callback) const { - if(clickPressedCallback) - clickPressedCallback(*this, 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(showPopupCallback) - showPopupCallback(*this, 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(gestureCallback) - gestureCallback(*this, artPlace, cursorPosition); + if(auto ownedPlace = getArtPlace(cursorPosition)) + { + if(ownedPlace->isLocked()) + return; + + 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) { @@ -130,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)); } @@ -147,26 +177,27 @@ 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)) + 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) +{ + for(const auto & [slot, artPlace] : artWorn) { - for(ArtPlacePtr artPlace : backpack) - if(artPlace->slot == slot) - return artPlace; - return nullptr; + if(artPlace->pos.isInside(cursorPosition)) + return artPlace; } - else + for(const auto & artPlace : backpack) { - return nullptr; + if(artPlace->pos.isInside(cursorPosition)) + return artPlace; } + return nullptr; } void CArtifactsOfHeroBase::updateWornSlots() @@ -201,21 +232,31 @@ void CArtifactsOfHeroBase::updateSlot(const ArtifactPosition & slot) const CArtifactInstance * CArtifactsOfHeroBase::getPickedArtifact() { // Returns only the picked up artifact. Not just highlighted like in the trading window. - if(!curHero || curHero->artifactsTransitionPos.empty()) - return nullptr; - else + if(curHero) return curHero->getArt(ArtifactPosition::TRANSITION_POS); + else + return nullptr; } -void CArtifactsOfHeroBase::addGestureCallback(CArtPlace::ClickFunctor callback) +void CArtifactsOfHeroBase::enableGesture() { for(auto & artPlace : artWorn) { - artPlace.second->setGestureCallback(callback); + artPlace.second->setGestureCallback(std::bind(&CArtifactsOfHeroBase::gestureArtPlace, this, _1, _2)); artPlace.second->addUsedEvents(GESTURE); } } +const CArtifactInstance * CArtifactsOfHeroBase::getArt(const ArtifactPosition & slot) const +{ + return curHero ? curHero->getArt(slot) : nullptr; +} + +void CArtifactsOfHeroBase::enableKeyboardShortcuts() +{ + addUsedEvents(AEventsReceiver::KEYBOARD); +} + void CArtifactsOfHeroBase::setSlotData(ArtPlacePtr artPlace, const ArtifactPosition & slot) { // Spurious call from artifactMoved in attempt to update hidden backpack slot @@ -228,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 94dd241f8..2ded1efa7 100644 --- a/client/widgets/CArtifactsOfHeroBase.h +++ b/client/widgets/CArtifactsOfHeroBase.h @@ -9,13 +9,15 @@ */ #pragma once -#include "CArtPlace.h" +#include "CComponentHolder.h" #include "Scrollable.h" +#include "../gui/Shortcut.h" + class CButton; class BackpackScroller; -class CArtifactsOfHeroBase : virtual public CIntObject +class CArtifactsOfHeroBase : virtual public CIntObject, public CKeyShortcut { protected: using ArtPlacePtr = std::shared_ptr; @@ -23,7 +25,7 @@ protected: public: using ArtPlaceMap = std::map; - using ClickFunctor = std::function; + using ClickFunctor = std::function; ClickFunctor clickPressedCallback; ClickFunctor showPopupCallback; @@ -31,22 +33,26 @@ 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); virtual void updateWornSlots(); virtual void updateBackpackSlots(); virtual void updateSlot(const ArtifactPosition & slot); virtual const CArtifactInstance * getPickedArtifact(); - void addGestureCallback(CArtPlace::ClickFunctor callback); + 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; -protected: const CGHeroInstance * curHero; ArtPlaceMap artWorn; std::vector backpack; @@ -65,8 +71,8 @@ protected: Point(381,295) //18 }; - virtual void init(const CArtPlace::ClickFunctor & lClickCallback, const CArtPlace::ClickFunctor & showPopupCallback, - const Point & position, const BpackScrollFunctor & scrollCallback); +protected: + virtual void init(const Point & position, const BpackScrollFunctor & scrollCallback); // Assigns an artifacts to an artifact place depending on it's new slot ID virtual void setSlotData(ArtPlacePtr artPlace, const ArtifactPosition & slot); }; diff --git a/client/widgets/CArtifactsOfHeroKingdom.cpp b/client/widgets/CArtifactsOfHeroKingdom.cpp index c72bf8a00..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)); } - addGestureCallback(std::bind(&CArtifactsOfHeroBase::gestureArtPlace, 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 16dc0294d..34458351e 100644 --- a/client/widgets/CArtifactsOfHeroMain.cpp +++ b/client/widgets/CArtifactsOfHeroMain.cpp @@ -20,22 +20,16 @@ CArtifactsOfHeroMain::CArtifactsOfHeroMain(const Point & position) { - init( - std::bind(&CArtifactsOfHeroBase::clickPrassedArtPlace, this, _1, _2), - std::bind(&CArtifactsOfHeroBase::showPopupArtPlace, this, _1, _2), - position, - std::bind(&CArtifactsOfHeroBase::scrollBackpack, this, _1)); - addGestureCallback(std::bind(&CArtifactsOfHeroBase::gestureArtPlace, this, _1, _2)); + 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(); -} - -void CArtifactsOfHeroMain::enableArtifactsCostumeSwitcher() -{ - addUsedEvents(AEventsReceiver::KEYBOARD); + if(curHero) + CArtifactsOfHeroBase::putBackPickedArtifact(); } void CArtifactsOfHeroMain::keyPressed(EShortcut key) diff --git a/client/widgets/CArtifactsOfHeroMain.h b/client/widgets/CArtifactsOfHeroMain.h index f4b829278..fe4840fbf 100644 --- a/client/widgets/CArtifactsOfHeroMain.h +++ b/client/widgets/CArtifactsOfHeroMain.h @@ -11,14 +11,11 @@ #include "CArtifactsOfHeroBase.h" -#include "../gui/Shortcut.h" - -class CArtifactsOfHeroMain : public CArtifactsOfHeroBase, public CKeyShortcut +class CArtifactsOfHeroMain : public CArtifactsOfHeroBase { public: CArtifactsOfHeroMain(const Point & position); ~CArtifactsOfHeroMain() override; - void enableArtifactsCostumeSwitcher(); void keyPressed(EShortcut key) override; void keyReleased(EShortcut key) override; diff --git a/client/widgets/CArtifactsOfHeroMarket.cpp b/client/widgets/CArtifactsOfHeroMarket.cpp index ec57b9d4c..fac271b46 100644 --- a/client/widgets/CArtifactsOfHeroMarket.cpp +++ b/client/widgets/CArtifactsOfHeroMarket.cpp @@ -14,14 +14,34 @@ CArtifactsOfHeroMarket::CArtifactsOfHeroMarket(const Point & position, const int selectionWidth) { - init( - std::bind(&CArtifactsOfHeroBase::clickPrassedArtPlace, this, _1, _2), - std::bind(&CArtifactsOfHeroBase::showPopupArtPlace, this, _1, _2), - position, - std::bind(&CArtifactsOfHeroBase::scrollBackpack, this, _1)); - + 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::clickPressedArtPlace(CComponentHolder & artPlace, const Point & cursorPosition) +{ + if(auto ownedPlace = getArtPlace(cursorPosition)) + { + if(ownedPlace->isLocked()) + return; + + if(const auto art = getArt(ownedPlace->slot)) + { + 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 e66eeb0ee..84a80a831 100644 --- a/client/widgets/CArtifactsOfHeroMarket.h +++ b/client/widgets/CArtifactsOfHeroMarket.h @@ -14,7 +14,9 @@ class CArtifactsOfHeroMarket : public CArtifactsOfHeroBase { public: - std::function selectArtCallback; + std::function onSelectArtCallback; + std::function onClickNotTradableCallback; CArtifactsOfHeroMarket(const Point & position, const int selectionWidth); + void clickPressedArtPlace(CComponentHolder & artPlace, const Point & cursorPosition) override; }; diff --git a/client/widgets/CComponent.cpp b/client/widgets/CComponent.cpp index 98b1f87c8..80b800492 100644 --- a/client/widgets/CComponent.cpp +++ b/client/widgets/CComponent.cpp @@ -12,15 +12,13 @@ #include "Images.h" -#include -#include - #include "../gui/CGuiHandler.h" #include "../gui/CursorHandler.h" #include "../gui/TextAlignment.h" #include "../gui/Shortcut.h" #include "../render/Canvas.h" #include "../render/IFont.h" +#include "../render/IRenderHandler.h" #include "../render/Graphics.h" #include "../windows/CMessage.h" #include "../windows/InfoWindows.h" @@ -28,16 +26,23 @@ #include "../CGameInfo.h" #include "../../lib/ArtifactUtils.h" -#include "../../lib/CTownHandler.h" -#include "../../lib/CHeroHandler.h" +#include "../../lib/entities/building/CBuilding.h" +#include "../../lib/entities/faction/CFaction.h" +#include "../../lib/entities/faction/CTown.h" +#include "../../lib/entities/faction/CTownHandler.h" #include "../../lib/networkPacks/Component.h" #include "../../lib/spells/CSpellHandler.h" #include "../../lib/CCreatureHandler.h" #include "../../lib/CSkillHandler.h" -#include "../../lib/CGeneralTextHandler.h" +#include "../../lib/texts/CGeneralTextHandler.h" #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, ""); @@ -55,7 +60,7 @@ CComponent::CComponent(const Component & c, ESize imageSize, EFonts font) void CComponent::init(ComponentType Type, ComponentSubType Subtype, std::optional Val, ESize imageSize, EFonts fnt, const std::string & ValText) { - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + OBJECT_CONSTRUCTION; addUsedEvents(SHOW_POPUP); @@ -66,6 +71,7 @@ void CComponent::init(ComponentType Type, ComponentSubType Subtype, std::optiona customSubtitle = ValText; size = imageSize; font = fnt; + newLine = false; assert(size < sizeInvalid); @@ -92,9 +98,11 @@ void CComponent::init(ComponentType Type, ComponentSubType Subtype, std::optiona max = 80; std::vector textLines = CMessage::breakText(getSubtitle(), std::max(max, pos.w), font); + const auto & fontPtr = GH.renderHandler().loadFont(font); + const int height = static_cast(fontPtr->getLineHeight()); + for(auto & line : textLines) { - int height = static_cast(graphics->fonts[font]->getLineHeight()); auto label = std::make_shared(pos.w/2, pos.h + height/2, font, ETextAlignment::CENTER, Colors::WHITE, line); pos.h += height; @@ -222,15 +230,15 @@ std::string CComponent::getDescription() const case ComponentType::CREATURE: return ""; case ComponentType::ARTIFACT: - return VLC->artifacts()->getById(data.subType.as())->getDescriptionTranslated(); + return CGI->artifacts()->getById(data.subType.as())->getDescriptionTranslated(); case ComponentType::SPELL_SCROLL: { - auto description = VLC->arth->objects[ArtifactID::SPELL_SCROLL]->getDescriptionTranslated(); + auto description = ArtifactID(ArtifactID::SPELL_SCROLL).toEntity(CGI)->getDescriptionTranslated(); ArtifactUtils::insertScrrollSpellName(description, data.subType.as()); return description; } case ComponentType::SPELL: - return VLC->spells()->getById(data.subType.as())->getDescriptionTranslated(std::max(0, data.value.value_or(0))); + return CGI->spells()->getById(data.subType.as())->getDescriptionTranslated(std::max(0, data.value.value_or(0))); case ComponentType::MORALE: return CGI->generaltexth->heroscrn[ 4 - (data.value.value_or(0)>0) + (data.value.value_or(0)<0)]; case ComponentType::LUCK: @@ -320,7 +328,7 @@ std::string CComponent::getSubtitle() const void CComponent::setSurface(const AnimationPath & defName, int imgPos) { - OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255-DISPOSE); + OBJECT_CONSTRUCTION; image = std::make_shared(defName, imgPos); } @@ -431,7 +439,7 @@ int CComponentBox::getDistance(CComponent *left, CComponent *right) void CComponentBox::placeComponents(bool selectable) { - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + OBJECT_CONSTRUCTION; if (components.empty()) return; @@ -439,7 +447,6 @@ void CComponentBox::placeComponents(bool selectable) for(auto & comp : components) { addChild(comp.get()); - comp->recActions = defActions; //FIXME: for some reason, received component might have recActions set to 0 comp->moveTo(Point(pos.x, pos.y)); } @@ -466,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 4d62eca97..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,13 +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; }; -namespace ArtifactUtilsClient +class CSecSkillPlace : public CComponentHolder { - bool askToAssemble(const CGHeroInstance * hero, const ArtifactPosition & slot); - bool askToDisassemble(const CGHeroInstance * hero, const ArtifactPosition & slot); -} +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/CExchangeController.cpp b/client/widgets/CExchangeController.cpp index 0b2c25683..eb640a46c 100644 --- a/client/widgets/CExchangeController.cpp +++ b/client/widgets/CExchangeController.cpp @@ -79,6 +79,10 @@ void CExchangeController::moveArmy(bool leftToRight, std::optional heldS }); heldSlot = weakestSlot->first; } + + if (source->getCreature(heldSlot.value()) == nullptr) + return; + LOCPLINT->cb->bulkMoveArmy(source->id, target->id, heldSlot.value()); } diff --git a/client/widgets/CGarrisonInt.cpp b/client/widgets/CGarrisonInt.cpp index 6acf43b61..78a809e72 100644 --- a/client/widgets/CGarrisonInt.cpp +++ b/client/widgets/CGarrisonInt.cpp @@ -17,7 +17,6 @@ #include "../gui/CGuiHandler.h" #include "../gui/WindowHandler.h" #include "../render/IImage.h" -#include "../render/Graphics.h" #include "../windows/CCreatureWindow.h" #include "../windows/CWindowWithArtifacts.h" #include "../windows/GUIClasses.h" @@ -27,12 +26,12 @@ #include "../../CCallback.h" #include "../../lib/ArtifactUtils.h" -#include "../../lib/CGeneralTextHandler.h" +#include "../../lib/texts/CGeneralTextHandler.h" +#include "../../lib/texts/TextOperations.h" #include "../../lib/CCreatureHandler.h" #include "../../lib/CConfigHandler.h" #include "../../lib/mapObjects/CGHeroInstance.h" #include "../../lib/networkPacks/ArtifactLocation.h" -#include "../../lib/TextOperations.h" #include "../../lib/gameState/CGameState.h" void CGarrisonSlot::setHighlight(bool on) @@ -275,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) @@ -371,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(); @@ -399,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 { @@ -427,20 +429,20 @@ 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_CAPTURING(255-DISPOSE); + OBJECT_CONSTRUCTION; pos.x += x; pos.y += y; AnimationPath imgName = AnimationPath::builtin(owner->smallIcons ? "cprsmall" : "TWCRPORT"); - creatureImage = std::make_shared(graphics->getAnimation(imgName), 0); + creatureImage = std::make_shared(imgName, 0); creatureImage->disable(); - selectionImage = std::make_shared(graphics->getAnimation(imgName), 1); + selectionImage = std::make_shared(imgName, 1); selectionImage->disable(); selectionImage->center(creatureImage->pos.center()); @@ -535,7 +537,6 @@ bool CGarrisonSlot::handleSplittingShortcuts() void CGarrisonInt::addSplitBtn(std::shared_ptr button) { addChild(button.get()); - button->recActions &= ~DISPOSE; splitButtons.push_back(button); button->block(getSelection() == nullptr); } @@ -716,7 +717,7 @@ CGarrisonInt::CGarrisonInt(const Point & position, int inx, const Point & garsOf , removableUnits(_removableUnits) , layout(_layout) { - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + OBJECT_CONSTRUCTION; setArmy(s1, EGarrisonType::UPPER); setArmy(s2, EGarrisonType::LOWER); diff --git a/client/widgets/CTextInput.cpp b/client/widgets/CTextInput.cpp index 938a739a8..723d89a6a 100644 --- a/client/widgets/CTextInput.cpp +++ b/client/widgets/CTextInput.cpp @@ -17,8 +17,9 @@ #include "../gui/Shortcut.h" #include "../render/Graphics.h" #include "../render/IFont.h" +#include "../render/IRenderHandler.h" -#include "../../lib/TextOperations.h" +#include "../../lib/texts/TextOperations.h" std::list CFocusable::focusables; CFocusable * CFocusable::inputWithFocus; @@ -30,12 +31,12 @@ CTextInput::CTextInput(const Rect & Pos) pos.h = Pos.h; pos.w = Pos.w; - addUsedEvents(LCLICK | KEYBOARD | TEXTINPUT); + addUsedEvents(LCLICK | SHOW_POPUP | KEYBOARD | TEXTINPUT); } void CTextInput::createLabel(bool giveFocusToInput) { - OBJ_CONSTRUCTION; + OBJECT_CONSTRUCTION; label = std::make_shared(); label->pos = pos; label->alignment = originalAlignment; @@ -59,7 +60,7 @@ CTextInput::CTextInput(const Rect & Pos, EFonts font, ETextAlignment alignment, CTextInput::CTextInput(const Rect & Pos, const Point & bgOffset, const ImagePath & bgName) : CTextInput(Pos) { - OBJ_CONSTRUCTION; + OBJECT_CONSTRUCTION; if (!bgName.empty()) background = std::make_shared(bgName, bgOffset.x, bgOffset.y); else @@ -71,7 +72,7 @@ CTextInput::CTextInput(const Rect & Pos, const Point & bgOffset, const ImagePath CTextInput::CTextInput(const Rect & Pos, std::shared_ptr srf) : CTextInput(Pos) { - OBJ_CONSTRUCTION; + OBJECT_CONSTRUCTION; background = std::make_shared(srf, Pos); pos.w = background->pos.w; pos.h = background->pos.h; @@ -106,6 +107,11 @@ void CTextInput::setCallback(const TextEditedCallback & cb) onTextEdited = cb; } +void CTextInput::setPopupCallback(const std::function & cb) +{ + callbackPopup = cb; +} + void CTextInput::setFilterFilename() { assert(!onTextFiltering); @@ -117,11 +123,17 @@ void CTextInput::setFilterNumber(int minValue, int maxValue) onTextFiltering = std::bind(&CTextInput::numberFilter, _1, _2, minValue, maxValue); } -std::string CTextInput::getVisibleText() +std::string CTextInput::getVisibleText() const { return hasFocus() ? currentText + composedText + "_" : currentText; } +void CTextInput::showPopupWindow(const Point & cursorPosition) +{ + if(callbackPopup) + callbackPopup(); +} + void CTextInput::clickPressed(const Point & cursorPosition) { // attempt to give focus unconditionally, even if we already have it @@ -179,8 +191,9 @@ void CTextInput::updateLabel() std::string visibleText = getVisibleText(); label->alignment = originalAlignment; + const auto & font = GH.renderHandler().loadFont(label->font); - while (graphics->fonts[label->font]->getStringWidth(visibleText) > pos.w) + while (font->getStringWidth(visibleText) > pos.w) { label->alignment = ETextAlignment::CENTERRIGHT; visibleText = visibleText.substr(TextOperations::getUnicodeCharacterSize(visibleText[0])); @@ -189,7 +202,7 @@ void CTextInput::updateLabel() label->setText(visibleText); } -void CTextInput::textInputed(const std::string & enteredText) +void CTextInput::textInputted(const std::string & enteredText) { if(!hasFocus()) return; @@ -216,7 +229,6 @@ void CTextInput::textEdited(const std::string & enteredText) composedText = enteredText; updateLabel(); - //onTextEdited(currentText + composedText); } void CTextInput::filenameFilter(std::string & text, const std::string &oldText) diff --git a/client/widgets/CTextInput.h b/client/widgets/CTextInput.h index 72c02b1c0..67639d098 100644 --- a/client/widgets/CTextInput.h +++ b/client/widgets/CTextInput.h @@ -14,6 +14,7 @@ #include "../render/EFont.h" #include "../../lib/filesystem/ResourcePath.h" +#include "../../lib/FunctionList.h" class CLabel; class IImage; @@ -49,7 +50,6 @@ class CTextInput final : public CFocusable using TextEditedCallback = std::function; using TextFilterCallback = std::function; -private: std::string currentText; std::string composedText; ETextAlignment originalAlignment; @@ -59,6 +59,7 @@ private: TextEditedCallback onTextEdited; TextFilterCallback onTextFiltering; + CFunctionList callbackPopup; //Filter that will block all characters not allowed in filenames static void filenameFilter(std::string & text, const std::string & oldText); @@ -66,15 +67,16 @@ private: //min-max should be set via something like std::bind static void numberFilter(std::string & text, const std::string & oldText, int minValue, int maxValue); - std::string getVisibleText(); + std::string getVisibleText() const; void createLabel(bool giveFocusToInput); void updateLabel(); void clickPressed(const Point & cursorPosition) final; - void textInputed(const std::string & enteredText) final; + void textInputted(const std::string & enteredText) final; void textEdited(const std::string & enteredText) final; void onFocusGot() final; void onFocusLost() final; + void showPopupWindow(const Point & cursorPosition) final; CTextInput(const Rect & Pos); public: @@ -90,6 +92,9 @@ public: /// Set callback that will be called whenever player enters new text void setCallback(const TextEditedCallback & cb); + /// Set callback when player want to open popup + void setPopupCallback(const std::function & cb); + /// Enables filtering entered text that ensures that text is valid filename (existing or not) void setFilterFilename(); /// Enable filtering entered text that ensures that text is valid number in provided range [min, max] diff --git a/client/widgets/CreatureCostBox.cpp b/client/widgets/CreatureCostBox.cpp index c7a651bfa..c6429192c 100644 --- a/client/widgets/CreatureCostBox.cpp +++ b/client/widgets/CreatureCostBox.cpp @@ -15,7 +15,7 @@ CreatureCostBox::CreatureCostBox(Rect position, std::string titleText) { - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + OBJECT_CONSTRUCTION; setRedrawParent(true); pos = position + pos.topLeft(); @@ -33,7 +33,7 @@ void CreatureCostBox::createItems(TResources res) { resources.clear(); - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + OBJECT_CONSTRUCTION; TResources::nziterator iter(res); while(iter.valid()) @@ -49,14 +49,15 @@ void CreatureCostBox::createItems(TResources res) { int curx = pos.w / 2; int spacing = 48; + int resourcesCount = static_cast(resources.size()); if (resources.size() > 2) { spacing = 32; - curx -= (15 + 16 * ((int)resources.size() - 1)); + curx -= (15 + 16 * (resourcesCount - 1)); } else { - curx -= ((16 * (int)resources.size()) + (8 * ((int)resources.size() - 1))); + curx -= ((16 * resourcesCount) + (8 * (resourcesCount - 1))); } //reverse to display gold as first resource for(auto & currentRes : boost::adaptors::reverse(resources)) diff --git a/client/widgets/IVideoHolder.h b/client/widgets/IVideoHolder.h new file mode 100644 index 000000000..dbe03f2d2 --- /dev/null +++ b/client/widgets/IVideoHolder.h @@ -0,0 +1,17 @@ +/* + * IVideoHolder.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 + +class IVideoHolder +{ +public: + virtual ~IVideoHolder() = default; + virtual void onVideoPlaybackFinished() = 0; +}; diff --git a/client/widgets/Images.cpp b/client/widgets/Images.cpp index 6699476da..4cbb6be1a 100644 --- a/client/widgets/Images.cpp +++ b/client/widgets/Images.cpp @@ -14,24 +14,24 @@ #include "../gui/CGuiHandler.h" #include "../renderSDL/SDL_Extensions.h" +#include "../render/AssetGenerator.h" #include "../render/IImage.h" #include "../render/IRenderHandler.h" #include "../render/CAnimation.h" #include "../render/Canvas.h" #include "../render/ColorFilter.h" -#include "../render/Graphics.h" +#include "../render/Colors.h" #include "../battle/BattleInterface.h" #include "../battle/BattleInterfaceClasses.h" #include "../CGameInfo.h" #include "../CPlayerInterface.h" -#include "../CMusicHandler.h" #include "../../CCallback.h" #include "../../lib/CConfigHandler.h" -#include "../../lib/CGeneralTextHandler.h" //for Unicode related stuff +#include "../../lib/texts/CGeneralTextHandler.h" //for Unicode related stuff #include "../../lib/CRandomGenerator.h" CPicture::CPicture(std::shared_ptr image, const Point & position) @@ -41,6 +41,8 @@ CPicture::CPicture(std::shared_ptr image, const Point & position) pos += position; pos.w = bg->width(); pos.h = bg->height(); + + addUsedEvents(SHOW_POPUP); } CPicture::CPicture( const ImagePath &bmpname, int x, int y ) @@ -52,7 +54,7 @@ CPicture::CPicture( const ImagePath & bmpname ) {} CPicture::CPicture( const ImagePath & bmpname, const Point & position ) - : bg(GH.renderHandler().loadImage(bmpname)) + : bg(GH.renderHandler().loadImage(bmpname, EImageBlitMode::COLORKEY)) , needRefresh(false) { pos.x += position.x; @@ -68,6 +70,18 @@ CPicture::CPicture( const ImagePath & bmpname, const Point & position ) { pos.w = pos.h = 0; } + + addUsedEvents(SHOW_POPUP); +} + +CPicture::CPicture(const ImagePath & bmpname, const Rect &SrcRect, int x, int y) + : CPicture(bmpname, Point(x,y)) +{ + srcRect = SrcRect; + pos.w = srcRect->w; + pos.h = srcRect->h; + + addUsedEvents(SHOW_POPUP); } CPicture::CPicture(std::shared_ptr image, const Rect &SrcRect, int x, int y) @@ -76,6 +90,8 @@ CPicture::CPicture(std::shared_ptr image, const Rect &SrcRect, int x, in srcRect = SrcRect; pos.w = srcRect->w; pos.h = srcRect->h; + + addUsedEvents(SHOW_POPUP); } void CPicture::show(Canvas & to) @@ -102,29 +118,40 @@ void CPicture::setAlpha(uint8_t value) void CPicture::scaleTo(Point size) { - bg = bg->scaleFast(size); + bg->scaleTo(size); pos.w = bg->width(); pos.h = bg->height(); } -void CPicture::colorize(PlayerColor player) +void CPicture::setPlayerColor(PlayerColor player) { bg->playerColored(player); } -CFilledTexture::CFilledTexture(const ImagePath & imageName, Rect position): - CIntObject(0, position.topLeft()), - texture(GH.renderHandler().loadImage(imageName)) +void CPicture::addRClickCallback(const std::function & callback) +{ + rCallback = callback; +} + +void CPicture::showPopupWindow(const Point & cursorPosition) +{ + if(rCallback) + rCallback(); +} + +CFilledTexture::CFilledTexture(const ImagePath & imageName, Rect position) + : CIntObject(0, position.topLeft()) + , texture(GH.renderHandler().loadImage(imageName, EImageBlitMode::COLORKEY)) { pos.w = position.w; pos.h = position.h; imageArea = Rect(Point(), texture->dimensions()); } -CFilledTexture::CFilledTexture(std::shared_ptr image, Rect position, Rect imageArea) +CFilledTexture::CFilledTexture(const ImagePath & imageName, Rect position, Rect imageArea) : CIntObject(0, position.topLeft()) - , texture(image) + , texture(GH.renderHandler().loadImage(imageName, EImageBlitMode::COLORKEY)) , imageArea(imageArea) { pos.w = position.w; @@ -142,33 +169,23 @@ void CFilledTexture::showAll(Canvas & to) } } -FilledTexturePlayerColored::FilledTexturePlayerColored(const ImagePath & imageName, Rect position) - : CFilledTexture(imageName, position) +void FilledTexturePlayerIndexed::setPlayerColor(PlayerColor player) +{ + texture->playerColored(player); +} + +FilledTexturePlayerColored::FilledTexturePlayerColored(Rect position) + :CFilledTexture(ImagePath::builtin("DiBoxBck"), position) { } -void FilledTexturePlayerColored::playerColored(PlayerColor player) +void FilledTexturePlayerColored::setPlayerColor(PlayerColor player) { - // Color transform to make color of brown DIBOX.PCX texture match color of specified player - 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 - }; + AssetGenerator::createPlayerColoredBackground(player); - assert(player.isValidPlayer()); - if (!player.isValidPlayer()) - { - logGlobal->error("Unable to colorize to invalid player color %d!", static_cast(player.getNum())); - return; - } + ImagePath imagePath = ImagePath::builtin("DialogBoxBackground_" + player.toString() + ".bmp"); - texture->adjustPalette(filters[player.getNum()], 0); + texture = GH.renderHandler().loadImage(imagePath, EImageBlitMode::COLORKEY); } CAnimImage::CAnimImage(const AnimationPath & name, size_t Frame, size_t Group, int x, int y, ui8 Flags): @@ -178,23 +195,12 @@ CAnimImage::CAnimImage(const AnimationPath & name, size_t Frame, size_t Group, i { pos.x += x; pos.y += y; - anim = graphics->getAnimation(name); + anim = GH.renderHandler().loadAnimation(name, (Flags & CCreatureAnim::CREATURE_MODE) ? EImageBlitMode::WITH_SHADOW_AND_OVERLAY : EImageBlitMode::COLORKEY); init(); } -CAnimImage::CAnimImage(std::shared_ptr Anim, size_t Frame, size_t Group, int x, int y, ui8 Flags): - anim(Anim), - frame(Frame), - group(Group), - flags(Flags) -{ - pos.x += x; - pos.y += y; - init(); -} - -CAnimImage::CAnimImage(std::shared_ptr Anim, size_t Frame, Rect targetPos, size_t Group, ui8 Flags): - anim(Anim), +CAnimImage::CAnimImage(const AnimationPath & name, size_t Frame, Rect targetPos, size_t Group, ui8 Flags): + anim(GH.renderHandler().loadAnimation(name, (Flags & CCreatureAnim::CREATURE_MODE) ? EImageBlitMode::WITH_SHADOW_AND_OVERLAY : EImageBlitMode::COLORKEY)), frame(Frame), group(Group), flags(Flags), @@ -234,10 +240,6 @@ void CAnimImage::setSizeFromImage(const IImage &img) void CAnimImage::init() { visible = true; - anim->load(frame, group); - if (flags & CShowableAnim::BASE) - anim->load(0,group); - auto img = anim->getImage(frame, group); if (img) setSizeFromImage(*img); @@ -264,12 +266,9 @@ void CAnimImage::showAll(Canvas & to) if(auto img = anim->getImage(targetFrame, group)) { if(isScaled()) - { - auto scaled = img->scaleFast(scaledSize); - to.draw(scaled, pos.topLeft()); - } - else - to.draw(img, pos.topLeft()); + img->scaleTo(scaledSize); + + to.draw(img, pos.topLeft()); } } } @@ -277,7 +276,7 @@ void CAnimImage::showAll(Canvas & to) void CAnimImage::setAnimationPath(const AnimationPath & name, size_t frame) { this->frame = frame; - anim = GH.renderHandler().loadAnimation(name); + anim = GH.renderHandler().loadAnimation(name, EImageBlitMode::COLORKEY); init(); } @@ -292,7 +291,6 @@ void CAnimImage::setFrame(size_t Frame, size_t Group) return; if (anim->size(Group) > Frame) { - anim->load(Frame, Group); frame = Frame; group = Group; if(auto img = anim->getImage(frame, group)) @@ -306,7 +304,7 @@ void CAnimImage::setFrame(size_t Frame, size_t Group) logGlobal->error("Error: accessing unavailable frame %d:%d in CAnimation!", Group, Frame); } -void CAnimImage::playerColored(PlayerColor currPlayer) +void CAnimImage::setPlayerColor(PlayerColor currPlayer) { player = currPlayer; anim->getImage(frame, group)->playerColored(*player); @@ -320,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)), + anim(GH.renderHandler().loadAnimation(name, (Flags & CREATURE_MODE) ? EImageBlitMode::WITH_SHADOW_AND_OVERLAY : EImageBlitMode::COLORKEY)), group(Group), frame(0), first(0), @@ -331,7 +329,6 @@ CShowableAnim::CShowableAnim(int x, int y, const AnimationPath & name, ui8 Flags yOffset(0), alpha(alpha) { - anim->loadGroup(group); last = anim->size(group); auto image = anim->getImage(0, group); @@ -346,11 +343,6 @@ CShowableAnim::CShowableAnim(int x, int y, const AnimationPath & name, ui8 Flags addUsedEvents(TIME); } -CShowableAnim::~CShowableAnim() -{ - anim->unloadGroup(group); -} - void CShowableAnim::setAlpha(ui32 alphaValue) { alpha = std::min(alphaValue, 255); @@ -366,9 +358,6 @@ bool CShowableAnim::set(size_t Group, size_t from, size_t to) if (max < from || max == 0) return false; - anim->unloadGroup(group); - anim->loadGroup(Group); - group = Group; frame = first = from; last = max; @@ -382,9 +371,6 @@ bool CShowableAnim::set(size_t Group) return false; if (group != Group) { - anim->unloadGroup(group); - anim->loadGroup(Group); - first = 0; group = Group; last = anim->size(Group); @@ -446,6 +432,7 @@ void CShowableAnim::blitImage(size_t frame, size_t group, Canvas & to) if(img) { img->setAlpha(alpha); + img->setOverlayColor(Colors::TRANSPARENCY); to.draw(img, pos.topLeft(), src); } } @@ -465,7 +452,7 @@ void CShowableAnim::setDuration(int durationMs) } CCreatureAnim::CCreatureAnim(int x, int y, const AnimationPath & name, ui8 flags, ECreatureAnimType type): - CShowableAnim(x, y, name, flags, 100, size_t(type)) // H3 uses 100 ms per frame, irregardless of battle speed settings + CShowableAnim(x, y, name, flags | CREATURE_MODE, 100, size_t(type)) // H3 uses 100 ms per frame, irregardless of battle speed settings { xOffset = 0; yOffset = 0; diff --git a/client/widgets/Images.h b/client/widgets/Images.h index 94fef0c1c..80b100fc4 100644 --- a/client/widgets/Images.h +++ b/client/widgets/Images.h @@ -26,12 +26,13 @@ class IImage; class CPicture : public CIntObject { std::shared_ptr bg; + std::function rCallback; public: /// if set, only specified section of internal image will be rendered std::optional srcRect; - /// If set to true, iamge will be redrawn on each frame + /// If set to true, image will be redrawn on each frame bool needRefresh; std::shared_ptr getSurface() @@ -44,6 +45,7 @@ public: /// wrap section of an existing Image CPicture(std::shared_ptr image, const Rect &SrcRext, int x = 0, int y = 0); //wrap subrect of given surface + CPicture(const ImagePath & bmpname, const Rect &SrcRext, int x = 0, int y = 0); //wrap subrect of given surface /// Loads image from specified file name CPicture(const ImagePath & bmpname); @@ -54,10 +56,13 @@ public: /// 0=transparent, 255=opaque void setAlpha(uint8_t value); void scaleTo(Point size); - void colorize(PlayerColor player); + void setPlayerColor(PlayerColor player); + + void addRClickCallback(const std::function & callback); void show(Canvas & to) override; void showAll(Canvas & to) override; + void showPopupWindow(const Point & cursorPosition) override; }; /// area filled with specific texture @@ -68,18 +73,28 @@ protected: Rect imageArea; public: + CFilledTexture(const ImagePath & imageName, Rect position, Rect imageArea); CFilledTexture(const ImagePath & imageName, Rect position); - CFilledTexture(std::shared_ptr image, Rect position, Rect imageArea); void showAll(Canvas & to) override; }; +/// area filled with specific texture, colorized to player color if image is indexed +class FilledTexturePlayerIndexed : public CFilledTexture +{ +public: + using CFilledTexture::CFilledTexture; + + void setPlayerColor(PlayerColor player); +}; + +/// area filled with specific texture, with applied color filter to colorize it to specific player class FilledTexturePlayerColored : public CFilledTexture { public: - FilledTexturePlayerColored(const ImagePath & imageName, Rect position); + FilledTexturePlayerColored(Rect position); - void playerColored(PlayerColor player); + void setPlayerColor(PlayerColor player); }; /// Class for displaying one image from animation @@ -103,8 +118,7 @@ public: bool visible; CAnimImage(const AnimationPath & name, size_t Frame, size_t Group=0, int x=0, int y=0, ui8 Flags=0); - CAnimImage(std::shared_ptr Anim, size_t Frame, size_t Group=0, int x=0, int y=0, ui8 Flags=0); - CAnimImage(std::shared_ptr Anim, size_t Frame, Rect targetPos, size_t Group=0, ui8 Flags=0); + CAnimImage(const AnimationPath & name, size_t Frame, Rect targetPos, size_t Group=0, ui8 Flags=0); ~CAnimImage(); /// size of animation @@ -114,7 +128,7 @@ public: void setFrame(size_t Frame, size_t Group=0); /// makes image player-colored to specific player - void playerColored(PlayerColor player); + void setPlayerColor(PlayerColor player); /// returns true if image has player-colored effect applied bool isPlayerColored() const; @@ -135,6 +149,7 @@ public: BASE=1, //base frame will be blitted before current one HORIZONTAL_FLIP=2, //TODO: will be displayed rotated VERTICAL_FLIP=4, //TODO: will be displayed rotated + CREATURE_MODE=8, // use alpha channel for images with palette. Required for creatures in battle and map objects PLAY_ONCE=32 //play animation only once and stop at last frame }; protected: @@ -168,7 +183,6 @@ public: void setAlpha(ui32 alphaValue); CShowableAnim(int x, int y, const AnimationPath & name, ui8 flags, ui32 frameTime, size_t Group=0, uint8_t alpha = UINT8_MAX); - ~CShowableAnim(); //set animation to group or part of group bool set(size_t Group); diff --git a/client/widgets/MiscWidgets.cpp b/client/widgets/MiscWidgets.cpp index 1bdba2a75..c6e888571 100644 --- a/client/widgets/MiscWidgets.cpp +++ b/client/widgets/MiscWidgets.cpp @@ -28,18 +28,18 @@ #include "../windows/CCastleInterface.h" #include "../windows/InfoWindows.h" #include "../render/Canvas.h" -#include "../render/Graphics.h" #include "../../CCallback.h" #include "../../lib/CConfigHandler.h" +#include "../../lib/IGameSettings.h" +#include "../../lib/entities/faction/CTownHandler.h" #include "../../lib/gameState/InfoAboutArmy.h" -#include "../../lib/CGeneralTextHandler.h" -#include "../../lib/GameSettings.h" -#include "../../lib/TextOperations.h" #include "../../lib/mapObjects/CGCreature.h" #include "../../lib/mapObjects/CGHeroInstance.h" #include "../../lib/mapObjects/CGTownInstance.h" +#include "../../lib/texts/CGeneralTextHandler.h" +#include "../../lib/texts/TextOperations.h" void CHoverableArea::hover (bool on) { @@ -128,7 +128,7 @@ CHeroArea::CHeroArea(int x, int y, const CGHeroInstance * hero) clickFunctor(nullptr), clickRFunctor(nullptr) { - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + OBJECT_CONSTRUCTION; pos.x += x; pos.w = 58; @@ -243,13 +243,13 @@ void CMinorResDataBar::showAll(Canvas & to) CMinorResDataBar::CMinorResDataBar() { - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + OBJECT_CONSTRUCTION; pos.x = 7; pos.y = 575; background = std::make_shared(ImagePath::builtin("KRESBAR.bmp")); - background->colorize(LOCPLINT->playerID); + background->setPlayerColor(LOCPLINT->playerID); pos.w = background->pos.w; pos.h = background->pos.h; @@ -259,7 +259,7 @@ CMinorResDataBar::~CMinorResDataBar() = default; void CArmyTooltip::init(const InfoAboutArmy &army) { - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + OBJECT_CONSTRUCTION; title = std::make_shared(66, 2, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE, army.name); @@ -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) @@ -322,7 +322,7 @@ CArmyTooltip::CArmyTooltip(Point pos, const CArmedInstance * army): void CHeroTooltip::init(const InfoAboutHero & hero) { - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + OBJECT_CONSTRUCTION; portrait = std::make_shared(AnimationPath::builtin("PortraitsLarge"), hero.getIconIndex(), 0, 3, 2); if(hero.details) @@ -354,13 +354,13 @@ CInteractableHeroTooltip::CInteractableHeroTooltip(Point pos, const CGHeroInstan { init(InfoAboutHero(hero, InfoAboutHero::EInfoLevel::DETAILED)); - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + OBJECT_CONSTRUCTION; garrison = std::make_shared(pos + Point(0, 73), 4, Point(0, 0), hero, nullptr, true, true, CGarrisonInt::ESlotsLayout::REVERSED_TWO_ROWS); } void CInteractableHeroTooltip::init(const InfoAboutHero & hero) { - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + OBJECT_CONSTRUCTION; portrait = std::make_shared(AnimationPath::builtin("PortraitsLarge"), hero.getIconIndex(), 0, 3, 2); title = std::make_shared(66, 2, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE, hero.name); @@ -379,7 +379,7 @@ void CInteractableHeroTooltip::init(const InfoAboutHero & hero) void CTownTooltip::init(const InfoAboutTown & town) { - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + OBJECT_CONSTRUCTION; //order of icons in def: fort, citadel, castle, no fort size_t fortIndex = town.fortLevel ? town.fortLevel - 1 : 3; @@ -388,7 +388,7 @@ void CTownTooltip::init(const InfoAboutTown & town) assert(town.tType); - size_t iconIndex = town.tType->clientInfo.icons[town.fortLevel > 0][town.built >= CGI->settings()->getInteger(EGameSettings::TOWNS_BUILDINGS_PER_TURN_CAP)]; + size_t iconIndex = town.tType->clientInfo.icons[town.fortLevel > 0][town.built >= LOCPLINT->cb->getSettings().getInteger(EGameSettings::TOWNS_BUILDINGS_PER_TURN_CAP)]; build = std::make_shared(AnimationPath::builtin("itpt"), iconIndex, 0, 3, 2); @@ -435,13 +435,13 @@ CInteractableTownTooltip::CInteractableTownTooltip(Point pos, const CGTownInstan { init(town); - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + OBJECT_CONSTRUCTION; garrison = std::make_shared(pos + Point(0, 73), 4, Point(0, 0), town->getUpperArmy(), nullptr, true, true, CGarrisonInt::ESlotsLayout::REVERSED_TWO_ROWS); } void CInteractableTownTooltip::init(const CGTownInstance * town) { - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + OBJECT_CONSTRUCTION; const InfoAboutTown townInfo = InfoAboutTown(town, true); int townId = town->id; @@ -464,19 +464,19 @@ void CInteractableTownTooltip::init(const CGTownInstance * town) std::vector towns = LOCPLINT->cb->getTownsInfo(true); for(auto & town : towns) { - if(town->id == townId && town->builtBuildings.count(BuildingID::TAVERN)) + if(town->id == townId && town->hasBuilt(BuildingID::TAVERN)) 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), []() { std::vector towns = LOCPLINT->cb->getTownsInfo(true); for(auto & town : towns) { - if(town->builtBuildings.count(BuildingID::MARKETPLACE)) + if(town->hasBuilt(BuildingID::MARKETPLACE)) { GH.windows().createAndPushWindow(town, nullptr, nullptr, EMarketMode::RESOURCE_RESOURCE); return; @@ -487,7 +487,7 @@ void CInteractableTownTooltip::init(const CGTownInstance * town) assert(townInfo.tType); - size_t iconIndex = townInfo.tType->clientInfo.icons[townInfo.fortLevel > 0][townInfo.built >= CGI->settings()->getInteger(EGameSettings::TOWNS_BUILDINGS_PER_TURN_CAP)]; + size_t iconIndex = townInfo.tType->clientInfo.icons[townInfo.fortLevel > 0][townInfo.built >= LOCPLINT->cb->getSettings().getInteger(EGameSettings::TOWNS_BUILDINGS_PER_TURN_CAP)]; build = std::make_shared(AnimationPath::builtin("itpt"), iconIndex, 0, 3, 2); title = std::make_shared(66, 2, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE, townInfo.name); @@ -530,12 +530,11 @@ void CInteractableTownTooltip::init(const CGTownInstance * town) CreatureTooltip::CreatureTooltip(Point pos, const CGCreature * creature) { - OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + OBJECT_CONSTRUCTION; - auto creatureID = creature->getCreature(); - int32_t creatureIconIndex = CGI->creatures()->getById(creatureID)->getIconIndex(); + int32_t creatureIconIndex = creature->getCreature()->getIconIndex(); - creatureImage = std::make_shared(graphics->getAnimation(AnimationPath::builtin("TWCRPORT")), creatureIconIndex); + 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)); bool isHeroSelected = LOCPLINT->localState->getCurrentHero() != nullptr; @@ -553,7 +552,7 @@ CreatureTooltip::CreatureTooltip(Point pos, const CGCreature * creature) void MoraleLuckBox::set(const AFactionMember * node) { - OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255-DISPOSE); + OBJECT_CONSTRUCTION; const std::array textId = {62, 88}; //eg %s \n\n\n {Current Luck Modifiers:} const int noneTxtId = 108; //Russian version uses same text for neutral morale\luck @@ -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'; @@ -625,16 +625,15 @@ MoraleLuckBox::MoraleLuckBox(bool Morale, const Rect &r, bool Small) small(Small) { pos = r + pos.topLeft(); - defActions = 255-DISPOSE; } CCreaturePic::CCreaturePic(int x, int y, const CCreature * cre, bool Big, bool Animated) { - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + OBJECT_CONSTRUCTION; pos.x+=x; pos.y+=y; - auto faction = cre->getFaction(); + auto faction = cre->getFactionID(); assert(CGI->townh->size() > faction); @@ -681,7 +680,7 @@ SelectableSlot::SelectableSlot(Rect area, Point oversize, const int width) : LRClickableAreaWTextComp(area) , selected(false) { - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + OBJECT_CONSTRUCTION; selection = std::make_shared( Rect(-oversize, area.dimensions() + oversize * 2), Colors::TRANSPARENCY, Colors::YELLOW, width); selectSlot(false); @@ -710,7 +709,12 @@ bool SelectableSlot::isSelected() const void SelectableSlot::setSelectionWidth(int width) { - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + OBJECT_CONSTRUCTION; selection = std::make_shared( selection->pos - pos.topLeft(), Colors::TRANSPARENCY, Colors::YELLOW, width); selectSlot(selected); } + +void SelectableSlot::moveSelectionForeground() +{ + moveChildForeground(selection.get()); +} diff --git a/client/widgets/MiscWidgets.h b/client/widgets/MiscWidgets.h index 36a741755..926d54f32 100644 --- a/client/widgets/MiscWidgets.h +++ b/client/widgets/MiscWidgets.h @@ -261,4 +261,5 @@ public: void selectSlot(bool on); bool isSelected() const; void setSelectionWidth(int width); + void moveSelectionForeground(); }; diff --git a/client/widgets/ObjectLists.cpp b/client/widgets/ObjectLists.cpp index ec3e1560e..6470df4a5 100644 --- a/client/widgets/ObjectLists.cpp +++ b/client/widgets/ObjectLists.cpp @@ -28,12 +28,11 @@ void CObjectList::deleteItem(std::shared_ptr item) std::shared_ptr CObjectList::createItem(size_t index) { - OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255-DISPOSE); + OBJECT_CONSTRUCTION; std::shared_ptr item = createObject(index); if(!item) item = std::make_shared(); - item->recActions = defActions; addChild(item.get()); if (isActive()) item->activate(); @@ -45,7 +44,6 @@ CTabbedInt::CTabbedInt(CreateFunc create, Point position, size_t ActiveID) activeTab(nullptr), activeID(ActiveID) { - defActions &= ~DISPOSE; pos += position; reset(); } @@ -92,7 +90,7 @@ CListBox::CListBox(CreateFunc create, Point Pos, Point ItemOffset, size_t Visibl if(Slider & 1) { - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + OBJECT_CONSTRUCTION; slider = std::make_shared( SliderPos.topLeft(), SliderPos.w, @@ -118,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/ObjectLists.h b/client/widgets/ObjectLists.h index 07b81c1be..11703130c 100644 --- a/client/widgets/ObjectLists.h +++ b/client/widgets/ObjectLists.h @@ -18,7 +18,6 @@ VCMI_LIB_NAMESPACE_END class CAnimImage; class CSlider; class CLabel; -class CAnimation; /// Used as base for Tabs and List classes class CObjectList : public CIntObject @@ -77,7 +76,7 @@ public: //ItemOffset - distance between items in the list //VisibleSize - maximal number of displayable at once items //TotalSize - //Slider - slider style, bit field: 1 = present(disabled), 2=horisontal(vertical), 4=blue(brown) + //Slider - slider style, bit field: 1 = present(disabled), 2=horizontal(vertical), 4=blue(brown) //SliderPos - position of slider, if present CListBox(CreateFunc create, Point Pos, Point ItemOffset, size_t VisibleSize, size_t TotalSize, size_t InitialPos=0, int Slider=0, Rect SliderPos=Rect() ); diff --git a/client/widgets/RadialMenu.cpp b/client/widgets/RadialMenu.cpp index ea8743b91..7e20c92c3 100644 --- a/client/widgets/RadialMenu.cpp +++ b/client/widgets/RadialMenu.cpp @@ -19,13 +19,13 @@ #include "../render/IImage.h" #include "../CGameInfo.h" -#include "../../lib/CGeneralTextHandler.h" +#include "../../lib/texts/CGeneralTextHandler.h" RadialMenuItem::RadialMenuItem(const std::string & imageName, const std::string & hoverText, const std::function & callback, bool alternativeLayout) : callback(callback) , hoverText(hoverText) { - OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + OBJECT_CONSTRUCTION; inactiveImage = std::make_shared(ImagePath::builtin(alternativeLayout ? "radialMenu/itemInactiveAlt" : "radialMenu/itemInactive"), Point(0, 0)); selectedImage = std::make_shared(ImagePath::builtin(alternativeLayout ? "radialMenu/itemEmptyAlt" : "radialMenu/itemEmpty"), Point(0, 0)); @@ -45,7 +45,7 @@ void RadialMenuItem::setSelected(bool selected) RadialMenu::RadialMenu(const Point & positionToCenter, const std::vector & menuConfig, bool alternativeLayout): centerPosition(positionToCenter), alternativeLayout(alternativeLayout) { - OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + OBJECT_CONSTRUCTION; pos += positionToCenter; Point itemSize = alternativeLayout ? Point(80, 70) : Point(70, 80); diff --git a/client/widgets/Slider.cpp b/client/widgets/Slider.cpp index e51896cdc..d5c19ebac 100644 --- a/client/widgets/Slider.cpp +++ b/client/widgets/Slider.cpp @@ -183,7 +183,7 @@ CSlider::CSlider(Point position, int totalw, const SliderMovingFunctor & Moved, value(Value), moved(Moved) { - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + OBJECT_CONSTRUCTION; setAmount(amount); vstd::amax(value, 0); vstd::amin(value, positions); diff --git a/client/widgets/TextControls.cpp b/client/widgets/TextControls.cpp index 5c9fdf7d5..16745214f 100644 --- a/client/widgets/TextControls.cpp +++ b/client/widgets/TextControls.cpp @@ -22,8 +22,9 @@ #include "../render/Canvas.h" #include "../render/Graphics.h" #include "../render/IFont.h" +#include "../render/IRenderHandler.h" -#include "../../lib/TextOperations.h" +#include "../../lib/texts/TextOperations.h" #ifdef VCMI_ANDROID #include "lib/CAndroidVMHelper.h" @@ -56,8 +57,9 @@ CLabel::CLabel(int x, int y, EFonts Font, ETextAlignment Align, const ColorRGBA if(alignment == ETextAlignment::TOPLEFT) // causes issues for MIDDLE { - pos.w = (int)graphics->fonts[font]->getStringWidth(visibleText().c_str()); - pos.h = (int)graphics->fonts[font]->getLineHeight(); + const auto & fontPtr = GH.renderHandler().loadFont(font); + pos.w = fontPtr->getStringWidth(visibleText().c_str()); + pos.h = fontPtr->getLineHeight(); } } @@ -114,8 +116,12 @@ void CLabel::setMaxWidth(int width) void CLabel::trimText() { if(maxWidth > 0) - while ((int)graphics->fonts[font]->getStringWidth(visibleText().c_str()) > maxWidth) + { + const auto & fontPtr = GH.renderHandler().loadFont(font); + + while (fontPtr->getStringWidth(visibleText().c_str()) > maxWidth) TextOperations::trimRightUnicode(text); + } } void CLabel::setColor(const ColorRGBA & Color) @@ -132,7 +138,8 @@ void CLabel::setColor(const ColorRGBA & Color) size_t CLabel::getWidth() { - return graphics->fonts[font]->getStringWidth(visibleText()); + const auto & fontPtr = GH.renderHandler().loadFont(font); + return fontPtr->getStringWidth(visibleText()); } CMultiLineLabel::CMultiLineLabel(Rect position, EFonts Font, ETextAlignment Align, const ColorRGBA & Color, const std::string & Text) : @@ -169,17 +176,22 @@ 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 = graphics->fonts[font]; + const auto f = GH.renderHandler().loadFont(font); Point where = destRect.topLeft(); - const std::string delimeters = "{}"; - auto delimitersCount = std::count_if(what.cbegin(), what.cend(), [&delimeters](char c) + const std::string delimiters = "{}"; + auto delimitersCount = std::count_if(what.cbegin(), what.cend(), [&delimiters](char c) { - return delimeters.find(c) != std::string::npos; + return delimiters.find(c) != std::string::npos; }); //We should count delimiters length from string to correct centering later. - delimitersCount *= f->getStringWidth(delimeters)/2; + delimitersCount *= f->getStringWidth(delimiters)/2; std::smatch match; std::regex expr("\\{(.*?)\\|"); @@ -214,16 +226,16 @@ void CTextContainer::blitLine(Canvas & to, Rect destRect, std::string what) where.y += getBorderSize().y + destRect.h - static_cast(f->getLineHeight()); size_t begin = 0; - size_t currDelimeter = 0; + size_t currDelimiter = 0; do { - size_t end = what.find_first_of(delimeters[currDelimeter % 2], begin); + size_t end = what.find_first_of(delimiters[currDelimiter % 2], begin); if(begin != end) { std::string toPrint = what.substr(begin, end - begin); - if(currDelimeter % 2) // Enclosed in {} text - set to yellow or defined color + if(currDelimiter % 2) // Enclosed in {} text - set to yellow or defined color { std::smatch match; std::regex expr("^(.*?)\\|"); @@ -249,7 +261,7 @@ void CTextContainer::blitLine(Canvas & to, Rect destRect, std::string what) where.x += (int)f->getStringWidth(toPrint); } - currDelimeter++; + currDelimiter++; } while(begin++ != std::string::npos); } @@ -264,7 +276,7 @@ void CMultiLineLabel::showAll(Canvas & to) { CIntObject::showAll(to); - const auto f = graphics->fonts[font]; + const auto & fontPtr = GH.renderHandler().loadFont(font); // calculate which lines should be visible int totalLines = static_cast(lines.size()); @@ -274,17 +286,17 @@ void CMultiLineLabel::showAll(Canvas & to) if(beginLine < 0) beginLine = 0; else - beginLine /= (int)f->getLineHeight(); + beginLine /= fontPtr->getLineHeight(); if(endLine < 0) endLine = 0; else - endLine /= (int)f->getLineHeight(); + endLine /= fontPtr->getLineHeight(); endLine++; // and where they should be displayed - Point lineStart = getTextLocation().topLeft() - visibleSize + Point(0, beginLine * (int)f->getLineHeight()); - Point lineSize = Point(getTextLocation().w, (int)f->getLineHeight()); + Point lineStart = getTextLocation().topLeft() - visibleSize + Point(0, beginLine * fontPtr->getLineHeight()); + Point lineSize = Point(getTextLocation().w, fontPtr->getLineHeight()); CSDL_Ext::CClipRectGuard guard(to.getInternalSurface(), getTextLocation()); // to properly trim text that is too big to fit @@ -293,7 +305,7 @@ void CMultiLineLabel::showAll(Canvas & to) if(!lines[i].empty()) //non-empty line blitLine(to, Rect(lineStart, lineSize), lines[i]); - lineStart.y += (int)f->getLineHeight(); + lineStart.y += fontPtr->getLineHeight(); } } @@ -301,15 +313,15 @@ void CMultiLineLabel::splitText(const std::string & Txt, bool redrawAfter) { lines.clear(); - const auto f = graphics->fonts[font]; - int lineHeight = static_cast(f->getLineHeight()); + const auto & fontPtr = GH.renderHandler().loadFont(font); + int lineHeight = fontPtr->getLineHeight(); lines = CMessage::breakText(Txt, pos.w, font); textSize.y = lineHeight * (int)lines.size(); textSize.x = 0; for(const std::string & line : lines) - vstd::amax(textSize.x, f->getStringWidth(line.c_str())); + vstd::amax(textSize.x, fontPtr->getStringWidth(line.c_str())); if(redrawAfter) redraw(); } @@ -322,15 +334,17 @@ Rect CMultiLineLabel::getTextLocation() if(pos.h <= textSize.y) return pos; - Point textSize(pos.w, (int)graphics->fonts[font]->getLineHeight() * (int)lines.size()); - Point textOffset(pos.w - textSize.x, pos.h - textSize.y); + const auto & fontPtr = GH.renderHandler().loadFont(font); + 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::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(); @@ -339,12 +353,11 @@ Rect CMultiLineLabel::getTextLocation() CLabelGroup::CLabelGroup(EFonts Font, ETextAlignment Align, const ColorRGBA & Color) : font(Font), align(Align), color(Color) { - defActions = 255 - DISPOSE; } void CLabelGroup::add(int x, int y, const std::string & text) { - OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255 - DISPOSE); + OBJECT_CONSTRUCTION; labels.push_back(std::make_shared(x, y, font, align, color, text)); } @@ -357,7 +370,7 @@ CTextBox::CTextBox(std::string Text, const Rect & rect, int SliderStyle, EFonts sliderStyle(SliderStyle), slider(nullptr) { - OBJECT_CONSTRUCTION_CAPTURING(255 - DISPOSE); + OBJECT_CONSTRUCTION; label = std::make_shared(rect, Font, Align, Color); setRedrawParent(true); @@ -421,11 +434,12 @@ void CTextBox::setText(const std::string & text) label->pos.w = pos.w - 16; assert(label->pos.w > 0); label->setText(text); + const auto & fontPtr = GH.renderHandler().loadFont(label->font); - OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255 - DISPOSE); + OBJECT_CONSTRUCTION; slider = std::make_shared(Point(pos.w - 16, 0), pos.h, std::bind(&CTextBox::sliderMoved, this, _1), label->pos.h, label->textSize.y, 0, Orientation::VERTICAL, CSlider::EStyle(sliderStyle)); - slider->setScrollStep((int)graphics->fonts[label->font]->getLineHeight()); + slider->setScrollStep(fontPtr->getLineHeight()); slider->setPanningStep(1); slider->setScrollBounds(pos - slider->pos.topLeft()); } @@ -505,7 +519,7 @@ CGStatusBar::CGStatusBar(int x, int y, const ImagePath & name, int maxw) { addUsedEvents(LCLICK); - OBJECT_CONSTRUCTION_CAPTURING(255 - DISPOSE); + OBJECT_CONSTRUCTION; auto backgroundImage = std::make_shared(name); background = backgroundImage; @@ -544,7 +558,6 @@ void CGStatusBar::activate() void CGStatusBar::deactivate() { - assert(GH.statusbar().get() == this); GH.setStatusbar(nullptr); if (enteringText) 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 new file mode 100644 index 000000000..f8b69fa2c --- /dev/null +++ b/client/widgets/VideoWidget.cpp @@ -0,0 +1,221 @@ +/* + * TextControls.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 "VideoWidget.h" +#include "TextControls.h" +#include "IVideoHolder.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) +{ +} + +VideoWidgetBase::VideoWidgetBase(const Point & position, const VideoPath & video, bool playAudio, float scaleFactor) + : playAudio(playAudio), scaleFactor(scaleFactor) +{ + addUsedEvents(TIME); + pos += position; + playVideo(video); +} + +VideoWidgetBase::~VideoWidgetBase() = default; + +void VideoWidgetBase::playVideo(const VideoPath & fileToPlay) +{ + 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) + { + loadAudio(fileToPlay); + if (isActive()) + startAudio(); + } +} + +void VideoWidgetBase::show(Canvas & to) +{ + if(videoInstance) + videoInstance->show(pos.topLeft(), to); + if(subTitle) + subTitle->showAll(to); +} + +void VideoWidgetBase::loadAudio(const VideoPath & fileToPlay) +{ + if (!playAudio) + return; + + audioData = CCS->videoh->getAudio(fileToPlay); +} + +void VideoWidgetBase::startAudio() +{ + if(audioData.first == nullptr) + return; + + audioHandle = CCS->soundh->playSound(audioData); + + if(audioHandle != -1) + { + CCS->soundh->setCallback( + audioHandle, + [this]() + { + this->audioHandle = -1; + } + ); + } +} + +void VideoWidgetBase::stopAudio() +{ + if(audioHandle != -1) + { + CCS->soundh->resetCallback(audioHandle); + CCS->soundh->stopSound(audioHandle); + audioHandle = -1; + } +} + +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(); + if(audioHandle != -1) + CCS->soundh->resumeSound(audioHandle); + else + startAudio(); + if(videoInstance) + videoInstance->activate(); +} + +void VideoWidgetBase::deactivate() +{ + CIntObject::deactivate(); + 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) +{ + if(videoInstance) + { + videoInstance->tick(msPassed); + + if(!videoInstance->videoEnded() && subTitle) + subTitle->setText(getSubTitleLine(videoInstance->timeStamp())); + + if(videoInstance->videoEnded()) + { + videoInstance.reset(); + stopAudio(); + onPlaybackFinished(); + // WARNING: onPlaybackFinished call may destoy `this`. Make sure that this is the very last operation in this method! + } + } +} + +VideoWidget::VideoWidget(const Point & position, const VideoPath & prologue, const VideoPath & looped, bool playAudio) + : VideoWidgetBase(position, prologue, playAudio) + , loopedVideo(looped) +{ +} + +VideoWidget::VideoWidget(const Point & position, const VideoPath & looped, bool playAudio) + : VideoWidgetBase(position, looped, playAudio) + , loopedVideo(looped) +{ +} + +void VideoWidget::onPlaybackFinished() +{ + playVideo(loopedVideo); +} + +VideoWidgetOnce::VideoWidgetOnce(const Point & position, const VideoPath & video, bool playAudio, IVideoHolder * owner) + : VideoWidgetBase(position, video, playAudio) + , owner(owner) +{ +} + +VideoWidgetOnce::VideoWidgetOnce(const Point & position, const VideoPath & video, bool playAudio, float scaleFactor, IVideoHolder * owner) + : VideoWidgetBase(position, video, playAudio, scaleFactor) + , owner(owner) +{ +} + +void VideoWidgetOnce::onPlaybackFinished() +{ + owner->onVideoPlaybackFinished(); +} diff --git a/client/widgets/VideoWidget.h b/client/widgets/VideoWidget.h new file mode 100644 index 000000000..edccf3650 --- /dev/null +++ b/client/widgets/VideoWidget.h @@ -0,0 +1,74 @@ +/* + * VideoWidget.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 "../gui/CIntObject.h" + +#include "../lib/filesystem/ResourcePath.h" +#include "../lib/json/JsonNode.h" + +class IVideoHolder; +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); + VideoWidgetBase(const Point & position, const VideoPath & video, bool playAudio, float scaleFactor); + + virtual void onPlaybackFinished() = 0; + void playVideo(const VideoPath & video); + +public: + ~VideoWidgetBase(); + + void activate() override; + void deactivate() override; + void show(Canvas & to) override; + void showAll(Canvas & to) override; + void tick(uint32_t msPassed) override; + + void setPlaybackFinishedCallback(std::function); +}; + +class VideoWidget final: public VideoWidgetBase +{ + VideoPath loopedVideo; + + void onPlaybackFinished() final; +public: + VideoWidget(const Point & position, const VideoPath & prologue, const VideoPath & looped, bool playAudio); + VideoWidget(const Point & position, const VideoPath & looped, bool playAudio); +}; + +class VideoWidgetOnce final: public VideoWidgetBase +{ + IVideoHolder * owner; + + void onPlaybackFinished() final; +public: + VideoWidgetOnce(const Point & position, const VideoPath & video, bool playAudio, IVideoHolder * owner); + VideoWidgetOnce(const Point & position, const VideoPath & video, bool playAudio, float scaleFactor, IVideoHolder * owner); +}; diff --git a/client/widgets/markets/CAltarArtifacts.cpp b/client/widgets/markets/CAltarArtifacts.cpp index 997bf9801..81ab7146f 100644 --- a/client/widgets/markets/CAltarArtifacts.cpp +++ b/client/widgets/markets/CAltarArtifacts.cpp @@ -22,19 +22,17 @@ #include "../../../CCallback.h" #include "../../../lib/networkPacks/ArtifactLocation.h" -#include "../../../lib/CGeneralTextHandler.h" +#include "../../../lib/texts/CGeneralTextHandler.h" #include "../../../lib/mapObjects/CGHeroInstance.h" -#include "../../../lib/mapObjects/CGMarket.h" +#include "../../../lib/mapObjects/IMarket.h" CAltarArtifacts::CAltarArtifacts(const IMarket * market, const CGHeroInstance * hero) : CMarketBase(market, hero) { - OBJECT_CONSTRUCTION_CAPTURING(255 - DISPOSE); + OBJECT_CONSTRUCTION; - assert(dynamic_cast(market)); - auto altarObj = dynamic_cast(market); - altarId = altarObj->id; - altarArtifacts = altarObj; + assert(market->getArtifactsStorage()); + altarArtifactsStorage = market->getArtifactsStorage(); deal = std::make_shared(Point(269, 520), AnimationPath::builtin("ALTSACR.DEF"), CGI->generaltexth->zelp[585], [this]() {CAltarArtifacts::makeDeal(); }, EShortcut::MARKET_DEAL); @@ -52,6 +50,7 @@ CAltarArtifacts::CAltarArtifacts(const IMarket * market, const CGHeroInstance * // Hero's artifacts heroArts = std::make_shared(Point(-365, -11)); heroArts->setHero(hero); + heroArts->altarId = market->getObjInstanceID(); // Altar offerTradePanel = std::make_shared([this](const std::shared_ptr & altarSlot) @@ -59,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(); @@ -104,18 +103,18 @@ void CAltarArtifacts::makeDeal() { positions.push_back(artInst->getId()); } - LOCPLINT->cb->trade(market, EMarketMode::ARTIFACT_EXP, positions, std::vector(), std::vector(), hero); + LOCPLINT->cb->trade(market->getObjInstanceID(), EMarketMode::ARTIFACT_EXP, positions, std::vector(), std::vector(), hero); deselect(); } void CAltarArtifacts::sacrificeAll() { - LOCPLINT->cb->bulkMoveArtifacts(heroArts->getHero()->id, altarId, false, true, true); + LOCPLINT->cb->bulkMoveArtifacts(heroArts->getHero()->id, heroArts->altarId, false, true, true); } void CAltarArtifacts::sacrificeBackpack() { - LOCPLINT->cb->bulkMoveArtifacts(heroArts->getHero()->id, altarId, false, false, true); + LOCPLINT->cb->bulkMoveArtifacts(heroArts->getHero()->id, heroArts->altarId, false, false, true); } std::shared_ptr CAltarArtifacts::getAOHset() const @@ -125,7 +124,7 @@ std::shared_ptr CAltarArtifacts::getAOHset() const void CAltarArtifacts::updateAltarSlots() { - assert(altarArtifacts->artifactsInBackpack.size() <= GameConstants::ALTAR_ARTIFACTS_SLOTS); + assert(altarArtifactsStorage->artifactsInBackpack.size() <= GameConstants::ALTAR_ARTIFACTS_SLOTS); assert(tradeSlotsMap.size() <= GameConstants::ALTAR_ARTIFACTS_SLOTS); auto tradeSlotsMapNewArts = tradeSlotsMap; @@ -146,12 +145,12 @@ void CAltarArtifacts::updateAltarSlots() for(auto & tradeSlot : tradeSlotsMapNewArts) { assert(tradeSlot.first->id == -1); - assert(altarArtifacts->getSlotByInstance(tradeSlot.second) != ArtifactPosition::PRE_FIRST); + assert(altarArtifactsStorage->getArtPos(tradeSlot.second) != ArtifactPosition::PRE_FIRST); tradeSlot.first->setID(tradeSlot.second->getTypeId().num); tradeSlot.first->subtitle->setText(std::to_string(calcExpCost(tradeSlot.second->getTypeId()))); } - auto newArtsFromBulkMove = altarArtifacts->artifactsInBackpack; + auto newArtsFromBulkMove = altarArtifactsStorage->artifactsInBackpack; for(const auto & [altarSlot, art] : tradeSlotsMap) { newArtsFromBulkMove.erase(std::remove_if(newArtsFromBulkMove.begin(), newArtsFromBulkMove.end(), [artForRemove = art](auto & slotInfo) @@ -179,8 +178,8 @@ void CAltarArtifacts::putBackArtifacts() { // TODO: If the backpack capacity limit is enabled, artifacts may remain on the altar. // Perhaps should be erased in CGameHandler::objectVisitEnded if id of visited object will be available - if(!altarArtifacts->artifactsInBackpack.empty()) - LOCPLINT->cb->bulkMoveArtifacts(altarId, heroArts->getHero()->id, false, true, true); + if(!altarArtifactsStorage->artifactsInBackpack.empty()) + LOCPLINT->cb->bulkMoveArtifacts(heroArts->altarId, heroArts->getHero()->id, false, true, true); } CMarketBase::MarketShowcasesParams CAltarArtifacts::getShowcasesParams() const @@ -200,16 +199,16 @@ void CAltarArtifacts::onSlotClickPressed(const std::shared_ptr & if(const auto pickedArtInst = heroArts->getPickedArtifact()) { - if(pickedArtInst->canBePutAt(altarArtifacts)) + if(pickedArtInst->canBePutAt(altarArtifactsStorage)) { - if(pickedArtInst->artType->isTradable()) + if(pickedArtInst->getType()->isTradable()) { if(altarSlot->id == -1) tradeSlotsMap.try_emplace(altarSlot, pickedArtInst); deal->block(!LOCPLINT->makingTurn); LOCPLINT->cb->swapArtifacts(ArtifactLocation(heroArts->getHero()->id, ArtifactPosition::TRANSITION_POS), - ArtifactLocation(altarId, ArtifactPosition::ALTAR)); + ArtifactLocation(heroArts->altarId, ArtifactPosition::ALTAR)); } else { @@ -221,9 +220,10 @@ void CAltarArtifacts::onSlotClickPressed(const std::shared_ptr & else if(altarSlot->id != -1) { assert(tradeSlotsMap.at(altarSlot)); - const auto slot = altarArtifacts->getSlotByInstance(tradeSlotsMap.at(altarSlot)); + const auto slot = altarArtifactsStorage->getArtPos(tradeSlotsMap.at(altarSlot)); assert(slot != ArtifactPosition::PRE_FIRST); - LOCPLINT->cb->swapArtifacts(ArtifactLocation(altarId, slot), ArtifactLocation(hero->id, ArtifactPosition::TRANSITION_POS)); + LOCPLINT->cb->swapArtifacts(ArtifactLocation(heroArts->altarId, slot), + ArtifactLocation(hero->id, GH.isKeyboardCtrlDown() ? ArtifactPosition::FIRST_AVAILABLE : ArtifactPosition::TRANSITION_POS)); tradeSlotsMap.erase(altarSlot); } } diff --git a/client/widgets/markets/CAltarArtifacts.h b/client/widgets/markets/CAltarArtifacts.h index 9c3ea8ed8..78d328faf 100644 --- a/client/widgets/markets/CAltarArtifacts.h +++ b/client/widgets/markets/CAltarArtifacts.h @@ -26,8 +26,7 @@ public: void putBackArtifacts(); private: - ObjectInstanceID altarId; - const CArtifactSet * altarArtifacts; + const CArtifactSet * altarArtifactsStorage; std::shared_ptr sacrificeBackpackButton; std::shared_ptr heroArts; std::map, const CArtifactInstance*> tradeSlotsMap; diff --git a/client/widgets/markets/CAltarCreatures.cpp b/client/widgets/markets/CAltarCreatures.cpp index 033f53a3f..c61906f18 100644 --- a/client/widgets/markets/CAltarCreatures.cpp +++ b/client/widgets/markets/CAltarCreatures.cpp @@ -21,17 +21,16 @@ #include "../../../CCallback.h" -#include "../../../lib/CGeneralTextHandler.h" +#include "../../../lib/texts/CGeneralTextHandler.h" #include "../../../lib/mapObjects/CGHeroInstance.h" -#include "../../../lib/mapObjects/CGMarket.h" -#include "../../../lib/MetaString.h" +#include "../../../lib/mapObjects/IMarket.h" CAltarCreatures::CAltarCreatures(const IMarket * market, const CGHeroInstance * hero) : CMarketBase(market, hero) , CMarketSlider(std::bind(&CAltarCreatures::onOfferSliderMoved, this, _1)) , CMarketTraderText(Point(28, 31), FONT_MEDIUM, Colors::YELLOW) { - OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255 - DISPOSE); + OBJECT_CONSTRUCTION; deal = std::make_shared(dealButtonPosWithSlider, AnimationPath::builtin("ALTSACR.DEF"), CGI->generaltexth->zelp[584], [this]() {CAltarCreatures::makeDeal();}, EShortcut::MARKET_DEAL); @@ -158,14 +157,14 @@ void CAltarCreatures::makeDeal() } } - LOCPLINT->cb->trade(market, EMarketMode::CREATURE_EXP, ids, {}, toSacrifice, hero); + LOCPLINT->cb->trade(market->getObjInstanceID(), EMarketMode::CREATURE_EXP, ids, {}, toSacrifice, hero); 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(); @@ -176,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]) @@ -212,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]))) : ""); } @@ -235,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(); } @@ -259,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 @@ -267,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 4bb558ff5..0ae8a8312 100644 --- a/client/widgets/markets/CArtifactsBuying.cpp +++ b/client/widgets/markets/CArtifactsBuying.cpp @@ -11,7 +11,6 @@ #include "StdInc.h" #include "CArtifactsBuying.h" -#include "../../gui/CGuiHandler.h" #include "../../gui/Shortcut.h" #include "../../widgets/Buttons.h" #include "../../widgets/TextControls.h" @@ -21,22 +20,16 @@ #include "../../../CCallback.h" -#include "../../../lib/CGeneralTextHandler.h" #include "../../../lib/mapObjects/CGHeroInstance.h" -#include "../../../lib/mapObjects/CGMarket.h" -#include "../../../lib/mapObjects/CGTownInstance.h" +#include "../../../lib/mapObjects/IMarket.h" +#include "../../../lib/texts/CGeneralTextHandler.h" -CArtifactsBuying::CArtifactsBuying(const IMarket * market, const CGHeroInstance * hero) +CArtifactsBuying::CArtifactsBuying(const IMarket * market, const CGHeroInstance * hero, const std::string & title) : CMarketBase(market, hero) , CResourcesSelling([this](const std::shared_ptr & heroSlot){CArtifactsBuying::onSlotClickPressed(heroSlot, bidTradePanel);}) { - OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255 - DISPOSE); + OBJECT_CONSTRUCTION; - std::string title; - if(auto townMarket = dynamic_cast(market)) - title = (*CGI->townh)[townMarket->getFaction()]->town->buildings[BuildingID::ARTIFACT_MERCHANT]->getNameTranslated(); - else - title = CGI->generaltexth->allTexts[349]; labels.emplace_back(std::make_shared(titlePos.x, titlePos.y, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW, title)); deal = std::make_shared(dealButtonPos, AnimationPath::builtin("TPMRKB.DEF"), CGI->generaltexth->zelp[595], [this](){CArtifactsBuying::makeDeal();}, EShortcut::MARKET_DEAL); @@ -53,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) { @@ -73,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, 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(); } @@ -91,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}; @@ -102,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(); @@ -114,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/CArtifactsBuying.h b/client/widgets/markets/CArtifactsBuying.h index 8393e8c07..11e114387 100644 --- a/client/widgets/markets/CArtifactsBuying.h +++ b/client/widgets/markets/CArtifactsBuying.h @@ -14,7 +14,7 @@ class CArtifactsBuying : public CResourcesSelling, public CMarketTraderText { public: - CArtifactsBuying(const IMarket * market, const CGHeroInstance * hero); + CArtifactsBuying(const IMarket * market, const CGHeroInstance * hero, const std::string & title); void deselect() override; void makeDeal() override; diff --git a/client/widgets/markets/CArtifactsSelling.cpp b/client/widgets/markets/CArtifactsSelling.cpp index fbf0952ab..b31de19e7 100644 --- a/client/widgets/markets/CArtifactsSelling.cpp +++ b/client/widgets/markets/CArtifactsSelling.cpp @@ -22,24 +22,17 @@ #include "../../../CCallback.h" #include "../../../lib/CArtifactInstance.h" -#include "../../../lib/CGeneralTextHandler.h" #include "../../../lib/mapObjects/CGHeroInstance.h" -#include "../../../lib/mapObjects/CGMarket.h" -#include "../../../lib/mapObjects/CGTownInstance.h" +#include "../../../lib/mapObjects/IMarket.h" +#include "../../../lib/texts/CGeneralTextHandler.h" -CArtifactsSelling::CArtifactsSelling(const IMarket * market, const CGHeroInstance * hero) +CArtifactsSelling::CArtifactsSelling(const IMarket * market, const CGHeroInstance * hero, const std::string & title) : CMarketBase(market, hero) , CResourcesBuying( [this](const std::shared_ptr & resSlot){CArtifactsSelling::onSlotClickPressed(resSlot, offerTradePanel);}, [this](){CArtifactsSelling::updateSubtitles();}) { - OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255 - DISPOSE); - - std::string title; - if(const auto townMarket = dynamic_cast(market)) - title = (*CGI->townh)[townMarket->getFaction()]->town->buildings[BuildingID::ARTIFACT_MERCHANT]->getNameTranslated(); - else if(const auto mapMarket = dynamic_cast(market)) - title = mapMarket->title; + OBJECT_CONSTRUCTION; labels.emplace_back(std::make_shared(titlePos.x, titlePos.y, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW, title)); labels.push_back(std::make_shared(155, 56, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, boost::str(boost::format(CGI->generaltexth->allTexts[271]) % hero->getNameTranslated()))); @@ -56,14 +49,18 @@ CArtifactsSelling::CArtifactsSelling(const IMarket * market, const CGHeroInstanc // Hero's artifacts heroArts = std::make_shared(Point(-361, 46), offerTradePanel->selectionWidth); heroArts->setHero(hero); - heroArts->selectArtCallback = [this](const CArtPlace * artPlace) + heroArts->onSelectArtCallback = [this](const CArtPlace * artPlace) { assert(artPlace); selectedHeroSlot = artPlace->slot; CArtifactsSelling::highlightingChanged(); CIntObject::redraw(); }; - + heroArts->onClickNotTradableCallback = []() + { + // This item can't be traded + LOCPLINT->showInfoDialog(CGI->generaltexth->allTexts[21]); + }; CArtifactsSelling::updateShowcases(); CArtifactsSelling::deselect(); } @@ -81,7 +78,8 @@ void CArtifactsSelling::makeDeal() { const auto art = hero->getArt(selectedHeroSlot); assert(art); - LOCPLINT->cb->trade(market, EMarketMode::ARTIFACT_RESOURCE, art->getId(), GameResID(offerTradePanel->getSelectedItemId()), offerQty, hero); + LOCPLINT->cb->trade(market->getObjInstanceID(), EMarketMode::ARTIFACT_RESOURCE, art->getId(), + GameResID(offerTradePanel->getHighlightedItemId()), offerQty, hero); CMarketTraderText::makeDeal(); } @@ -131,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}; @@ -149,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(); @@ -164,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/CArtifactsSelling.h b/client/widgets/markets/CArtifactsSelling.h index 581209a30..74a67e1d8 100644 --- a/client/widgets/markets/CArtifactsSelling.h +++ b/client/widgets/markets/CArtifactsSelling.h @@ -15,7 +15,7 @@ class CArtifactsSelling : public CResourcesBuying, public CMarketTraderText { public: - CArtifactsSelling(const IMarket * market, const CGHeroInstance * hero); + CArtifactsSelling(const IMarket * market, const CGHeroInstance * hero, const std::string & title); void deselect() override; void makeDeal() override; void updateShowcases() override; diff --git a/client/widgets/markets/CFreelancerGuild.cpp b/client/widgets/markets/CFreelancerGuild.cpp index 095a75dcd..cf2081ea2 100644 --- a/client/widgets/markets/CFreelancerGuild.cpp +++ b/client/widgets/markets/CFreelancerGuild.cpp @@ -21,19 +21,18 @@ #include "../../../CCallback.h" -#include "../../../lib/CGeneralTextHandler.h" -#include "../../../lib/MetaString.h" +#include "../../../lib/texts/CGeneralTextHandler.h" #include "../../../lib/mapObjects/CGHeroInstance.h" -#include "../../../lib/mapObjects/CGMarket.h" +#include "../../../lib/mapObjects/IMarket.h" CFreelancerGuild::CFreelancerGuild(const IMarket * market, const CGHeroInstance * hero) : 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_CAPTURING(255 - DISPOSE); + OBJECT_CONSTRUCTION; labels.emplace_back(std::make_shared(titlePos.x, titlePos.y, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW, VLC->generaltexth->translate("object.core.freelancersGuild.name"))); @@ -70,7 +69,7 @@ void CFreelancerGuild::makeDeal() { if(auto toTrade = offerSlider->getValue(); toTrade != 0) { - LOCPLINT->cb->trade(market, 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(); } @@ -81,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}; @@ -92,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); @@ -110,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 888c42aac..d7b75f1a6 100644 --- a/client/widgets/markets/CMarketBase.cpp +++ b/client/widgets/markets/CMarketBase.cpp @@ -23,9 +23,9 @@ #include "../../../CCallback.h" -#include "../../../lib/CGeneralTextHandler.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() @@ -122,7 +122,7 @@ void CMarketBase::highlightingChanged() CExperienceAltar::CExperienceAltar() { - OBJECT_CONSTRUCTION_CAPTURING(255 - DISPOSE); + OBJECT_CONSTRUCTION; // Experience needed to reach next level texts.emplace_back(std::make_shared(CGI->generaltexth->allTexts[475], Rect(15, 415, 125, 50), 0, FONT_SMALL, ETextAlignment::CENTER, Colors::YELLOW)); @@ -144,7 +144,7 @@ void CExperienceAltar::update() CCreaturesSelling::CCreaturesSelling() { - OBJECT_CONSTRUCTION_CAPTURING(255 - DISPOSE); + OBJECT_CONSTRUCTION; assert(hero); CreaturesPanel::slotsData slots; @@ -171,7 +171,7 @@ void CCreaturesSelling::updateSubtitles() const CResourcesBuying::CResourcesBuying(const CTradeableItem::ClickPressedFunctor & clickPressedCallback, const TradePanelBase::UpdateSlotsFunctor & updSlotsCallback) { - OBJECT_CONSTRUCTION_CAPTURING(255 - DISPOSE); + OBJECT_CONSTRUCTION; offerTradePanel = std::make_shared(clickPressedCallback, updSlotsCallback); offerTradePanel->moveTo(pos.topLeft() + Point(327, 182)); @@ -180,7 +180,7 @@ CResourcesBuying::CResourcesBuying(const CTradeableItem::ClickPressedFunctor & c CResourcesSelling::CResourcesSelling(const CTradeableItem::ClickPressedFunctor & clickPressedCallback) { - OBJECT_CONSTRUCTION_CAPTURING(255 - DISPOSE); + OBJECT_CONSTRUCTION; bidTradePanel = std::make_shared(clickPressedCallback, std::bind(&CResourcesSelling::updateSubtitles, this)); labels.emplace_back(std::make_shared(156, 148, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->allTexts[270])); @@ -194,7 +194,7 @@ void CResourcesSelling::updateSubtitles() const CMarketSlider::CMarketSlider(const CSlider::SliderMovingFunctor & movingCallback) { - OBJECT_CONSTRUCTION_CAPTURING(255 - DISPOSE); + OBJECT_CONSTRUCTION; offerSlider = std::make_shared(Point(230, 489), 137, movingCallback, 0, 0, 0, Orientation::HORIZONTAL); maxAmount = std::make_shared(Point(228, 520), AnimationPath::builtin("IRCBTNS.DEF"), CGI->generaltexth->zelp[596], @@ -224,7 +224,7 @@ void CMarketSlider::onOfferSliderMoved(int newVal) CMarketTraderText::CMarketTraderText(const Point & pos, const EFonts & font, const ColorRGBA & color) : madeTransaction(false) { - OBJECT_CONSTRUCTION_CAPTURING(255 - DISPOSE); + OBJECT_CONSTRUCTION; traderText = std::make_shared("", Rect(pos, traderTextDimensions), 0, font, ETextAlignment::CENTER, color); } diff --git a/client/widgets/markets/CMarketResources.cpp b/client/widgets/markets/CMarketResources.cpp index b7b0a4eac..0ddca24ab 100644 --- a/client/widgets/markets/CMarketResources.cpp +++ b/client/widgets/markets/CMarketResources.cpp @@ -21,9 +21,8 @@ #include "../../../CCallback.h" -#include "../../../lib/CGeneralTextHandler.h" -#include "../../../lib/MetaString.h" -#include "../../../lib/mapObjects/CGMarket.h" +#include "../../../lib/texts/CGeneralTextHandler.h" +#include "../../../lib/mapObjects/IMarket.h" CMarketResources::CMarketResources(const IMarket * market, const CGHeroInstance * hero) : CMarketBase(market, hero) @@ -33,7 +32,7 @@ CMarketResources::CMarketResources(const IMarket * market, const CGHeroInstance [this](){CMarketResources::updateSubtitles();}) , CMarketSlider([this](int newVal){CMarketSlider::onOfferSliderMoved(newVal);}) { - OBJECT_CONSTRUCTION_CAPTURING(255 - DISPOSE); + OBJECT_CONSTRUCTION; labels.emplace_back(std::make_shared(titlePos.x, titlePos.y, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->allTexts[158])); deal = std::make_shared(dealButtonPosWithSlider, AnimationPath::builtin("TPMRKB.DEF"), @@ -61,7 +60,7 @@ void CMarketResources::makeDeal() { if(auto toTrade = offerSlider->getValue(); toTrade != 0) { - LOCPLINT->cb->trade(market, 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(); @@ -70,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}; @@ -84,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); @@ -98,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 } @@ -106,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 fc7b5f360..3e58c816c 100644 --- a/client/widgets/markets/CTransferResources.cpp +++ b/client/widgets/markets/CTransferResources.cpp @@ -21,8 +21,9 @@ #include "../../../CCallback.h" -#include "../../../lib/CGeneralTextHandler.h" -#include "../../../lib/MetaString.h" +#include "../../../lib/texts/CGeneralTextHandler.h" +#include "../../../lib/mapObjects/IMarket.h" +#include "../../../lib/texts/MetaString.h" CTransferResources::CTransferResources(const IMarket * market, const CGHeroInstance * hero) : CMarketBase(market, hero) @@ -30,7 +31,7 @@ CTransferResources::CTransferResources(const IMarket * market, const CGHeroInsta , CMarketSlider([this](int newVal){CMarketSlider::onOfferSliderMoved(newVal);}) , CMarketTraderText(Point(28, 48)) { - OBJECT_CONSTRUCTION_CAPTURING(255 - DISPOSE); + OBJECT_CONSTRUCTION; labels.emplace_back(std::make_shared(titlePos.x, titlePos.y, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->allTexts[158])); labels.emplace_back(std::make_shared(445, 56, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->allTexts[169])); @@ -63,8 +64,8 @@ void CTransferResources::makeDeal() { if(auto toTrade = offerSlider->getValue(); toTrade != 0) { - LOCPLINT->cb->trade(market, 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(); } @@ -75,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}; @@ -86,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); @@ -101,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 26c43941d..119f6995a 100644 --- a/client/widgets/markets/TradePanels.cpp +++ b/client/widgets/markets/TradePanels.cpp @@ -20,16 +20,16 @@ #include "../../../CCallback.h" -#include "../../../lib/CGeneralTextHandler.h" +#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_CAPTURING(255 - DISPOSE); + OBJECT_CONSTRUCTION; addUsedEvents(LCLICK); addUsedEvents(HOVER); @@ -46,7 +46,7 @@ void CTradeableItem::setType(EType newType) { if(type != newType) { - OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255 - DISPOSE); + OBJECT_CONSTRUCTION; type = newType; if(getIndex() < 0) @@ -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,14 +255,14 @@ void TradePanelBase::onSlotClickPressed(const std::shared_ptr & bool TradePanelBase::isHighlighted() const { - return getSelectedItemId() != -1; + return highlightedSlot != nullptr; } ResourcesPanel::ResourcesPanel(const CTradeableItem::ClickPressedFunctor & clickPressedCallback, const UpdateSlotsFunctor & updateSubtitles) { assert(resourcesForTrade.size() == slotsPos.size()); - OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255 - DISPOSE); + OBJECT_CONSTRUCTION; for(const auto & res : resourcesForTrade) { @@ -287,7 +279,7 @@ ArtifactsPanel::ArtifactsPanel(const CTradeableItem::ClickPressedFunctor & click { assert(slotsForTrade == slotsPos.size()); assert(slotsForTrade == arts.size()); - OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255 - DISPOSE); + OBJECT_CONSTRUCTION; for(auto slotIdx = 0; slotIdx < slotsForTrade; slotIdx++) { @@ -308,7 +300,7 @@ ArtifactsPanel::ArtifactsPanel(const CTradeableItem::ClickPressedFunctor & click PlayersPanel::PlayersPanel(const CTradeableItem::ClickPressedFunctor & clickPressedCallback) { assert(PlayerColor::PLAYER_LIMIT_I <= slotsPos.size() + 1); - OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255 - DISPOSE); + OBJECT_CONSTRUCTION; std::vector players; for(auto player = PlayerColor(0); player < PlayerColor::PLAYER_LIMIT_I; player++) @@ -334,12 +326,12 @@ CreaturesPanel::CreaturesPanel(const CTradeableItem::ClickPressedFunctor & click { assert(initialSlots.size() <= GameConstants::ARMY_SIZE); assert(slotsPos.size() <= GameConstants::ARMY_SIZE); - OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255 - DISPOSE); + OBJECT_CONSTRUCTION; 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)); @@ -352,12 +344,12 @@ CreaturesPanel::CreaturesPanel(const CTradeableItem::ClickPressedFunctor & click const std::vector> & srcSlots, bool emptySlots) { assert(slots.size() <= GameConstants::ARMY_SIZE); - OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255 - DISPOSE); + OBJECT_CONSTRUCTION; 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); @@ -367,12 +359,12 @@ CreaturesPanel::CreaturesPanel(const CTradeableItem::ClickPressedFunctor & click ArtifactsAltarPanel::ArtifactsAltarPanel(const CTradeableItem::ClickPressedFunctor & clickPressedCallback) { - OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255 - DISPOSE); + OBJECT_CONSTRUCTION; 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 c8b36a4cd..3d917712e 100644 --- a/client/windows/CCastleInterface.cpp +++ b/client/windows/CCastleInterface.cpp @@ -18,12 +18,12 @@ #include "CCreatureWindow.h" #include "../CGameInfo.h" -#include "../CMusicHandler.h" #include "../CPlayerInterface.h" #include "../PlayerLocalState.h" #include "../gui/CGuiHandler.h" #include "../gui/Shortcut.h" #include "../gui/WindowHandler.h" +#include "../media/IMusicPlayer.h" #include "../widgets/MiscWidgets.h" #include "../widgets/CComponent.h" #include "../widgets/CGarrisonInt.h" @@ -42,17 +42,19 @@ #include "../../CCallback.h" #include "../../lib/CArtHandler.h" -#include "../../lib/CBuildingHandler.h" +#include "../../lib/CConfigHandler.h" +#include "../../lib/CSoundBase.h" #include "../../lib/CCreatureHandler.h" -#include "../../lib/CGeneralTextHandler.h" -#include "../../lib/GameSettings.h" +#include "../../lib/texts/CGeneralTextHandler.h" +#include "../../lib/IGameSettings.h" #include "../../lib/spells/CSpellHandler.h" -#include "../../lib/CTownHandler.h" #include "../../lib/GameConstants.h" #include "../../lib/StartInfo.h" #include "../../lib/campaign/CampaignState.h" +#include "../../lib/entities/building/CBuilding.h" #include "../../lib/mapObjects/CGHeroInstance.h" #include "../../lib/mapObjects/CGTownInstance.h" +#include "../../lib/mapObjects/TownBuildingInstance.h" static bool useCompactCreatureBox() @@ -80,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)) @@ -93,10 +95,10 @@ CBuildingRect::CBuildingRect(CCastleBuildings * Par, const CGTownInstance * Town } if(!str->borderName.empty()) - border = GH.renderHandler().loadImage(str->borderName, EImageBlitMode::ALPHA); + 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() @@ -105,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; } @@ -144,7 +146,7 @@ void CBuildingRect::clickPressed(const Point & cursorPosition) if(getBuilding() && area && (parent->selectedBuilding==this)) { auto building = getBuilding(); - parent->buildingClicked(building->bid, building->subId, building->upgrade); + parent->buildingClicked(building->bid); } } @@ -154,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()), @@ -162,7 +164,7 @@ void CBuildingRect::showPopupWindow(const Point & cursorPosition) } else { - int level = ( bid - BuildingID::DWELL_FIRST ) % GameConstants::CREATURES_PER_TOWN; + int level = BuildingID::getLevelFromDwelling(bid); GH.windows().createAndPushWindow(parent->pos.x+parent->pos.w / 2, parent->pos.y+parent->pos.h /2, town, level); } } @@ -232,11 +234,12 @@ std::string CBuildingRect::getSubtitle()//hover text for building int bid = getBuilding()->bid; - if (bid<30)//non-dwellings - only buiding name - return town->town->buildings.at(getBuilding()->bid)->getNameTranslated(); + if (bid<30)//non-dwellings - only building name + return town->getTown()->buildings.at(getBuilding()->bid)->getNameTranslated(); else//dwellings - recruit %creature% { - auto & availableCreatures = town->creatures[(bid-30)%GameConstants::CREATURES_PER_TOWN].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 @@ -269,10 +272,10 @@ bool CBuildingRect::receiveEvent(const Point & position, int eventType) const CDwellingInfoBox::CDwellingInfoBox(int centerX, int centerY, const CGTownInstance * Town, int level) : CWindowObject(RCLICK_POPUP, ImagePath::builtin("CRTOINFO"), Point(centerX, centerY)) { - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); - background->colorize(Town->tempOwner); + OBJECT_CONSTRUCTION; + background->setPlayerColor(Town->tempOwner); - const CCreature * creature = CGI->creh->objects.at(Town->creatures.at(level).second.back()); + const CCreature * creature = Town->creatures.at(level).second.back().toCreature(); title = std::make_shared(80, 30, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, creature->getNamePluralTranslated()); animation = std::make_shared(30, 44, creature, true, true); @@ -305,7 +308,7 @@ CDwellingInfoBox::~CDwellingInfoBox() = default; CHeroGSlot::CHeroGSlot(int x, int y, int updown, const CGHeroInstance * h, HeroSlots * Owner) { - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + OBJECT_CONSTRUCTION; owner = Owner; pos.x += x; @@ -453,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); } } @@ -474,14 +477,14 @@ void CHeroGSlot::setHighlight(bool on) if(owner->garrisonedHero->hero && owner->visitingHero->hero) //two heroes in town { - for(auto & elem : owner->garr->splitButtons) //splitting enabled when slot higlighted + for(auto & elem : owner->garr->splitButtons) //splitting enabled when slot highlighted elem->block(!on); } } void CHeroGSlot::set(const CGHeroInstance * newHero) { - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + OBJECT_CONSTRUCTION; hero = newHero; @@ -506,7 +509,7 @@ HeroSlots::HeroSlots(const CGTownInstance * Town, Point garrPos, Point visitPos, town(Town), garr(Garrison) { - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + OBJECT_CONSTRUCTION; garrisonedHero = std::make_shared(garrPos.x, garrPos.y, 0, town->garrisonHero, this); visitingHero = std::make_shared(visitPos.x, visitPos.y, 1, town->visitingHero, this); } @@ -526,7 +529,7 @@ void HeroSlots::swapArmies() //moving hero out of town - check if it is allowed if (town->garrisonHero) { - if (!town->visitingHero && LOCPLINT->cb->howManyHeroes(false) >= CGI->settings()->getInteger(EGameSettings::HEROES_PER_PLAYER_ON_MAP_CAP)) + if (!town->visitingHero && LOCPLINT->cb->howManyHeroes(false) >= LOCPLINT->cb->getSettings().getInteger(EGameSettings::HEROES_PER_PLAYER_ON_MAP_CAP)) { std::string text = CGI->generaltexth->translate("core.genrltxt.18"); //You already have %d adventuring heroes under your command. boost::algorithm::replace_first(text,"%d",std::to_string(LOCPLINT->cb->howManyHeroes(false))); @@ -562,9 +565,9 @@ CCastleBuildings::CCastleBuildings(const CGTownInstance* Town): town(Town), selectedBuilding(nullptr) { - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + 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; @@ -578,16 +581,16 @@ CCastleBuildings::~CCastleBuildings() = default; void CCastleBuildings::recreate() { selectedBuilding = nullptr; - OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255-DISPOSE); + OBJECT_CONSTRUCTION; buildings.clear(); groups.clear(); //Generate buildings list - auto buildingsCopy = town->builtBuildings;// a bit modified copy of built buildings + auto buildingsCopy = town->getBuildings();// a bit modified copy of built buildings - if(vstd::contains(town->builtBuildings, BuildingID::SHIPYARD)) + if(town->hasBuilt(BuildingID::SHIPYARD)) { auto bayPos = town->bestLocation(); if(!bayPos.valid()) @@ -600,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) { @@ -615,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) { @@ -646,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(); @@ -680,18 +683,82 @@ const CGHeroInstance * CCastleBuildings::getHero() return town->garrisonHero; } -void CCastleBuildings::buildingClicked(BuildingID building, BuildingSubID::EBuildingSubID subID, BuildingID upgrades) +void CCastleBuildings::buildingClicked(BuildingID building) { - logGlobal->trace("You've clicked on %d", (int)building.toEnum()); - const CBuilding *b = town->town->buildings.find(building)->second; - - if (building >= BuildingID::DWELL_FIRST) + BuildingID buildingToEnter = building; + for(;;) { - enterDwelling((building-BuildingID::DWELL_FIRST)%GameConstants::CREATURES_PER_TOWN); + const CBuilding *b = town->getTown()->buildings.find(buildingToEnter)->second; + + if (buildingTryActivateCustomUI(buildingToEnter, building)) + return; + + if (!b->upgrade.hasValue()) + { + enterBuilding(building); + return; + } + + buildingToEnter = b->upgrade; + } +} + +bool CCastleBuildings::buildingTryActivateCustomUI(BuildingID buildingToTest, BuildingID buildingTarget) +{ + logGlobal->trace("You've clicked on %d", (int)buildingToTest.toEnum()); + const CBuilding *b = town->getTown()->buildings.at(buildingToTest); + + if (town->getWarMachineInBuilding(buildingToTest).hasValue()) + { + enterBlacksmith(buildingTarget, town->getWarMachineInBuilding(buildingToTest)); + return true; + } + + if (!b->marketModes.empty()) + { + switch (*b->marketModes.begin()) + { + case EMarketMode::CREATURE_UNDEAD: + GH.windows().createAndPushWindow(town, getHero(), nullptr); + return true; + + case EMarketMode::RESOURCE_SKILL: + if (getHero()) + GH.windows().createAndPushWindow(getHero(), buildingTarget, town, nullptr); + return true; + + case EMarketMode::RESOURCE_RESOURCE: + // can't use allied marketplace + if (town->getOwner() == LOCPLINT->playerID) + { + GH.windows().createAndPushWindow(town, getHero(), nullptr, *b->marketModes.begin()); + return true; + } + else + return false; + default: + if(getHero()) + GH.windows().createAndPushWindow(town, getHero(), nullptr, *b->marketModes.begin()); + else + LOCPLINT->showInfoDialog(boost::str(boost::format(CGI->generaltexth->allTexts[273]) % b->getNameTranslated())); //Only visiting heroes may use the %s. + return true; + } + } + + if (town->rewardableBuildings.count(buildingToTest) && town->getTown()->buildings.at(buildingToTest)->manualHeroVisit) + { + enterRewardable(buildingToTest); + return true; + } + + if (buildingToTest >= BuildingID::DWELL_FIRST) + { + enterDwelling((BuildingID::getLevelFromDwelling(buildingToTest))); + return true; } else { - switch(building) + switch(buildingToTest) { case BuildingID::MAGES_GUILD_1: case BuildingID::MAGES_GUILD_2: @@ -699,139 +766,110 @@ void CCastleBuildings::buildingClicked(BuildingID building, BuildingSubID::EBuil case BuildingID::MAGES_GUILD_4: case BuildingID::MAGES_GUILD_5: enterMagesGuild(); - break; + return true; case BuildingID::TAVERN: LOCPLINT->showTavernWindow(town, nullptr, QueryID::NONE); - break; + return true; case BuildingID::SHIPYARD: if(town->shipyardStatus() == IBoatGenerator::GOOD) + { LOCPLINT->showShipyardDialog(town); + return true; + } else if(town->shipyardStatus() == IBoatGenerator::BOAT_ALREADY_BUILT) + { LOCPLINT->showInfoDialog(CGI->generaltexth->allTexts[51]); - break; + return true; + } + return false; case BuildingID::FORT: case BuildingID::CITADEL: case BuildingID::CASTLE: GH.windows().createAndPushWindow(town); - break; + return true; case BuildingID::VILLAGE_HALL: case BuildingID::CITY_HALL: case BuildingID::TOWN_HALL: case BuildingID::CAPITOL: enterTownHall(); - break; - - case BuildingID::MARKETPLACE: - // can't use allied marketplace - if (town->getOwner() == LOCPLINT->playerID) - GH.windows().createAndPushWindow(town, town->visitingHero, nullptr, EMarketMode::RESOURCE_RESOURCE); - else - enterBuilding(building); - break; - - case BuildingID::BLACKSMITH: - enterBlacksmith(town->town->warMachine); - break; + return true; case BuildingID::SHIP: LOCPLINT->showInfoDialog(CGI->generaltexth->allTexts[51]); //Cannot build another boat - break; + return true; case BuildingID::SPECIAL_1: case BuildingID::SPECIAL_2: case BuildingID::SPECIAL_3: case BuildingID::SPECIAL_4: - switch (subID) + switch (b->subId) { - case BuildingSubID::NONE: - enterBuilding(building); - break; - case BuildingSubID::MYSTIC_POND: - enterFountain(building, subID, upgrades); - break; - - case BuildingSubID::ARTIFACT_MERCHANT: - if(town->visitingHero) - GH.windows().createAndPushWindow(town, town->visitingHero, nullptr, EMarketMode::RESOURCE_ARTIFACT); - else - LOCPLINT->showInfoDialog(boost::str(boost::format(CGI->generaltexth->allTexts[273]) % b->getNameTranslated())); //Only visiting heroes may use the %s. - break; - - case BuildingSubID::FOUNTAIN_OF_FORTUNE: - enterFountain(building, subID, upgrades); - break; - - case BuildingSubID::FREELANCERS_GUILD: - if(getHero()) - GH.windows().createAndPushWindow(town, getHero(), nullptr, EMarketMode::CREATURE_RESOURCE); - else - LOCPLINT->showInfoDialog(boost::str(boost::format(CGI->generaltexth->allTexts[273]) % b->getNameTranslated())); //Only visiting heroes may use the %s. - break; - - case BuildingSubID::MAGIC_UNIVERSITY: - if (getHero()) - GH.windows().createAndPushWindow(getHero(), town, nullptr); - else - enterBuilding(building); - break; - - case BuildingSubID::BROTHERHOOD_OF_SWORD: - if(upgrades == BuildingID::TAVERN) - LOCPLINT->showTavernWindow(town, nullptr, QueryID::NONE); - else - enterBuilding(building); - break; + enterFountain(buildingToTest, b->subId, buildingTarget); + return true; case BuildingSubID::CASTLE_GATE: if (LOCPLINT->makingTurn) + { enterCastleGate(); - else - enterBuilding(building); - break; - - case BuildingSubID::CREATURE_TRANSFORMER: //Skeleton Transformer - GH.windows().createAndPushWindow(town, getHero(), nullptr); - break; + return true; + } + return false; case BuildingSubID::PORTAL_OF_SUMMONING: - if (town->creatures[GameConstants::CREATURES_PER_TOWN].second.empty())//No creatures + if (town->creatures[town->getTown()->creatures.size()].second.empty())//No creatures LOCPLINT->showInfoDialog(CGI->generaltexth->tcommands[30]); else - enterDwelling(GameConstants::CREATURES_PER_TOWN); - break; + enterDwelling(town->getTown()->creatures.size()); + return true; - case BuildingSubID::BALLISTA_YARD: - enterBlacksmith(ArtifactID::BALLISTA); - break; - - case BuildingSubID::THIEVES_GUILD: - enterAnyThievesGuild(); - break; - - default: - enterBuilding(building); - break; + case BuildingSubID::BANK: + enterBank(buildingTarget); + return true; } - break; - - default: - enterBuilding(building); - break; } } + + for (auto const & bonus : b->buildingBonuses) + { + if (bonus->type == BonusType::THIEVES_GUILD_ACCESS) + { + enterAnyThievesGuild(); + return true; + } + } + return false; +} + +void CCastleBuildings::enterRewardable(BuildingID building) +{ + if (town->visitingHero == nullptr) + { + MetaString message; + message.appendTextID("core.genrltxt.273"); // only visiting heroes may visit %s + message.replaceTextID(town->getTown()->buildings.at(building)->getNameTextID()); + + LOCPLINT->showInfoDialog(message.toString()); + } + else + { + if (town->rewardableBuildings.at(building)->wasVisited(town->visitingHero)) + enterBuilding(building); + else + LOCPLINT->cb->visitTownBuilding(town, building); + } } -void CCastleBuildings::enterBlacksmith(ArtifactID artifactID) +void CCastleBuildings::enterBlacksmith(BuildingID building, ArtifactID artifactID) { const CGHeroInstance *hero = town->visitingHero; if(!hero) { - LOCPLINT->showInfoDialog(boost::str(boost::format(CGI->generaltexth->allTexts[273]) % town->town->buildings.find(BuildingID::BLACKSMITH)->second->getNameTranslated())); + LOCPLINT->showInfoDialog(boost::str(boost::format(CGI->generaltexth->allTexts[273]) % town->getTown()->buildings.find(building)->second->getNameTranslated())); return; } auto art = artifactID.toArtifact(); @@ -842,7 +880,7 @@ void CCastleBuildings::enterBlacksmith(ArtifactID artifactID) { for(auto slot : art->getPossibleSlots().at(ArtBearer::HERO)) { - if(hero->getArt(slot) == nullptr) + if(hero->getArt(slot) == nullptr || hero->getArt(slot)->getTypeId() != artifactID) { possible = true; break; @@ -853,14 +891,15 @@ void CCastleBuildings::enterBlacksmith(ArtifactID artifactID) } } } - CreatureID cre = art->getWarMachine(); - GH.windows().createAndPushWindow(possible, cre, artifactID, hero->id); + + CreatureID creatureID = artifactID.toArtifact()->getWarMachine(); + GH.windows().createAndPushWindow(possible, creatureID, artifactID, hero->id); } 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() @@ -877,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()) { - std::shared_ptr a = GH.renderHandler().loadAnimation(AnimationPath::builtin("ITPA")); - a->preload(); - images.push_back(a->getImage(t->town->clientInfo.icons[t->hasFort()][false] + 2)->scaleFast(Point(35, 23))); + 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()); }; @@ -902,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; } @@ -916,8 +955,8 @@ void CCastleBuildings::enterDwelling(int level) void CCastleBuildings::enterToTheQuickRecruitmentWindow() { const auto beginIt = town->creatures.cbegin(); - const auto afterLastIt = town->creatures.size() > GameConstants::CREATURES_PER_TOWN - ? std::next(beginIt, GameConstants::CREATURES_PER_TOWN) + 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; }); @@ -929,32 +968,20 @@ 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; - if(this->town->town->faction->getIndex() == ETownType::RAMPART) - { - hasNotProduced = CGI->generaltexth->allTexts[677]; - hasProduced = CGI->generaltexth->allTexts[678]; - } - else - { - auto buildingName = town->town->getSpecialBuilding(subID)->getNameTranslated(); - - hasNotProduced = std::string(CGI->generaltexth->translate("vcmi.townHall.hasNotProduced")); - hasProduced = std::string(CGI->generaltexth->translate("vcmi.townHall.hasProduced")); - boost::algorithm::replace_first(hasNotProduced, "%s", buildingName); - boost::algorithm::replace_first(hasProduced, "%s", buildingName); - } + hasNotProduced = CGI->generaltexth->allTexts[677]; + hasProduced = CGI->generaltexth->allTexts[678]; 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 { @@ -1007,7 +1034,7 @@ void CCastleBuildings::enterMagesGuild() void CCastleBuildings::enterTownHall() { if(town->visitingHero && town->visitingHero->hasArt(ArtifactID::GRAIL) && - !vstd::contains(town->builtBuildings, BuildingID::GRAIL)) //hero has grail, but town does not have it + !town->hasBuilt(BuildingID::GRAIL)) //hero has grail, but town does not have it { if(!vstd::contains(town->forbiddenBuildings, BuildingID::GRAIL)) { @@ -1030,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); } @@ -1042,20 +1069,35 @@ void CCastleBuildings::openTownHall() void CCastleBuildings::enterAnyThievesGuild() { std::vector towns = LOCPLINT->cb->getTownsInfo(true); - for(auto & town : towns) + for(auto & ownedTown : towns) { - if(town->builtBuildings.count(BuildingID::TAVERN)) + if(ownedTown->hasBuilt(BuildingID::TAVERN)) { - LOCPLINT->showThievesGuildWindow(town); + LOCPLINT->showThievesGuildWindow(ownedTown); return; } } LOCPLINT->showInfoDialog(CGI->generaltexth->translate("vcmi.adventureMap.noTownWithTavern")); } +void CCastleBuildings::enterBank(BuildingID building) +{ + std::vector> components; + if(town->bonusValue.second > 0) + { + components.push_back(std::make_shared(ComponentType::RESOURCE_PER_DAY, GameResID(GameResID::GOLD), -500)); + LOCPLINT->showInfoDialog(CGI->generaltexth->translate("vcmi.townStructure.bank.payBack"), components); + } + else{ + + components.push_back(std::make_shared(ComponentType::RESOURCE, GameResID(GameResID::GOLD), 2500)); + LOCPLINT->showYesNoDialog(CGI->generaltexth->translate("vcmi.townStructure.bank.borrow"), [this, building](){ LOCPLINT->cb->visitTownBuilding(town, building); }, nullptr, components); + } +} + void CCastleBuildings::enterAnyMarket() { - if(town->builtBuildings.count(BuildingID::MARKETPLACE)) + if(town->hasBuilt(BuildingID::MARKETPLACE)) { GH.windows().createAndPushWindow(town, nullptr, nullptr, EMarketMode::RESOURCE_RESOURCE); return; @@ -1064,7 +1106,7 @@ void CCastleBuildings::enterAnyMarket() std::vector towns = LOCPLINT->cb->getTownsInfo(true); for(auto & town : towns) { - if(town->builtBuildings.count(BuildingID::MARKETPLACE)) + if(town->hasBuilt(BuildingID::MARKETPLACE)) { GH.windows().createAndPushWindow(town, nullptr, nullptr, EMarketMode::RESOURCE_RESOURCE); return; @@ -1078,7 +1120,7 @@ CCreaInfo::CCreaInfo(Point position, const CGTownInstance * Town, int Level, boo level(Level), showAvailable(_showAvailable) { - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + OBJECT_CONSTRUCTION; pos += position; if(town->creatures.size() <= level || town->creatures[level].second.empty()) @@ -1088,10 +1130,9 @@ CCreaInfo::CCreaInfo(Point position, const CGTownInstance * Town, int Level, boo } addUsedEvents(LCLICK | SHOW_POPUP | HOVER); - ui32 creatureID = town->creatures[level].second.back(); - creature = CGI->creh->objects[creatureID]; + creature = town->creatures[level].second.back(); - picture = std::make_shared(AnimationPath::builtin("CPRSMALL"), creature->getIconIndex(), 0, 8, 0); + picture = std::make_shared(AnimationPath::builtin("CPRSMALL"), creature.toEntity(VLC)->getIconIndex(), 0, 8, 0); std::string value; if(showAvailable) @@ -1131,16 +1172,17 @@ void CCreaInfo::update() void CCreaInfo::hover(bool on) { - std::string message = CGI->generaltexth->allTexts[588]; - boost::algorithm::replace_first(message, "%s", creature->getNamePluralTranslated()); + MetaString message; + message.appendTextID("core.genrltxt.588"); + message.replaceNameSingular(creature); if(on) { - GH.statusbar()->write(message); + GH.statusbar()->write(message.toString()); } else { - GH.statusbar()->clearIfMatching(message); + GH.statusbar()->clearIfMatching(message.toString()); } } @@ -1157,12 +1199,18 @@ void CCreaInfo::clickPressed(const Point & cursorPosition) std::string CCreaInfo::genGrowthText() { GrowthInfo gi = town->getGrowthInfo(level); - std::string descr = boost::str(boost::format(CGI->generaltexth->allTexts[589]) % creature->getNameSingularTranslated() % gi.totalGrowth()); + MetaString descr; + descr.appendTextID("core.genrltxt.589"); + descr.replaceNameSingular(creature); + descr.replaceNumber(gi.totalGrowth()); for(const GrowthInfo::Entry & entry : gi.entries) - descr +="\n" + entry.description; + { + descr.appendEOL(); + descr.appendRawString(entry.description); + } - return descr; + return descr.toString(); } void CCreaInfo::showPopupWindow(const Point & cursorPosition) @@ -1170,7 +1218,7 @@ void CCreaInfo::showPopupWindow(const Point & cursorPosition) if (showAvailable) GH.windows().createAndPushWindow(GH.screenDimensions().x / 2, GH.screenDimensions().y / 2, town, level); else - CRClickPopup::createAndPush(genGrowthText(), std::make_shared(ComponentType::CREATURE, creature->getId())); + CRClickPopup::createAndPush(genGrowthText(), std::make_shared(ComponentType::CREATURE, creature)); } bool CCreaInfo::getShowAvailable() @@ -1182,7 +1230,7 @@ CTownInfo::CTownInfo(int posX, int posY, const CGTownInstance * Town, bool townH : town(Town), building(nullptr) { - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + OBJECT_CONSTRUCTION; addUsedEvents(SHOW_POPUP | HOVER); pos.x += posX; pos.y += posY; @@ -1200,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; } @@ -1230,14 +1278,14 @@ CCastleInterface::CCastleInterface(const CGTownInstance * Town, const CGTownInst CWindowObject(PLAYER_COLORED | BORDERED), town(Town) { - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + OBJECT_CONSTRUCTION; LOCPLINT->castleInt = this; addUsedEvents(KEYBOARD); builds = std::make_shared(town); panel = std::make_shared(ImagePath::builtin("TOWNSCRN"), 0, builds->pos.h); - panel->colorize(LOCPLINT->playerID); + panel->setPlayerColor(LOCPLINT->playerID); pos.w = panel->pos.w; pos.h = builds->pos.h + panel->pos.h; center(); @@ -1275,7 +1323,7 @@ CCastleInterface::CCastleInterface(const CGTownInstance * Town, const CGTownInst recreateIcons(); if (!from) adventureInt->onAudioPaused(); - CCS->musich->playMusic(town->town->clientInfo.musicTheme, true, false); + CCS->musich->playMusicFromSet("faction", town->getFaction()->getJsonKey(), true, false); } CCastleInterface::~CCastleInterface() @@ -1355,8 +1403,8 @@ void CCastleInterface::removeBuilding(BuildingID bid) void CCastleInterface::recreateIcons() { - OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255-DISPOSE); - size_t iconIndex = town->town->clientInfo.icons[town->hasFort()][town->builded >= CGI->settings()->getInteger(EGameSettings::TOWNS_BUILDINGS_PER_TURN_CAP)]; + OBJECT_CONSTRUCTION; + 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(); @@ -1375,11 +1423,11 @@ void CCastleInterface::recreateIcons() fastMarket = std::make_shared(Rect(163, 410, 64, 42), [this]() { builds->enterAnyMarket(); }); fastTavern = std::make_shared(Rect(15, 387, 58, 64), [&]() { - if(town->builtBuildings.count(BuildingID::TAVERN)) + 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(); @@ -1462,7 +1510,7 @@ CHallInterface::CBuildingBox::CBuildingBox(int x, int y, const CGTownInstance * town(Town), building(Building) { - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + OBJECT_CONSTRUCTION; addUsedEvents(LCLICK | SHOW_POPUP | HOVER); pos.x += x; pos.y += y; @@ -1480,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); @@ -1522,10 +1570,10 @@ 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_CAPTURING(255-DISPOSE); + OBJECT_CONSTRUCTION; resdatabar = std::make_shared(); resdatabar->moveBy(pos.topLeft(), true); @@ -1534,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); - if(vstd::contains(town->builtBuildings, buildingID)) + const CBuilding * current = town->getTown()->buildings.at(buildingID); + if(town->hasBuilt(buildingID)) { building = current; } @@ -1580,9 +1628,9 @@ CBuildWindow::CBuildWindow(const CGTownInstance *Town, const CBuilding * Buildin town(Town), building(Building) { - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + 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); @@ -1664,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]; @@ -1674,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; } } @@ -1683,7 +1731,7 @@ std::string CBuildWindow::getTextForState(EBuildingState state) LabeledValue::LabeledValue(Rect size, std::string name, std::string descr, int min, int max) { - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + OBJECT_CONSTRUCTION; pos.x+=size.x; pos.y+=size.y; pos.w = size.w; @@ -1693,7 +1741,7 @@ LabeledValue::LabeledValue(Rect size, std::string name, std::string descr, int m LabeledValue::LabeledValue(Rect size, std::string name, std::string descr, int val) { - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + OBJECT_CONSTRUCTION; pos.x+=size.x; pos.y+=size.y; pos.w = size.w; @@ -1731,12 +1779,13 @@ void LabeledValue::hover(bool on) CFortScreen::CFortScreen(const CGTownInstance * town): CWindowObject(PLAYER_COLORED | BORDERED, getBgName(town)) { - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + OBJECT_CONSTRUCTION; ui32 fortSize = static_cast(town->creatures.size()); - if(fortSize > GameConstants::CREATURES_PER_TOWN && 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()); @@ -1751,25 +1800,25 @@ CFortScreen::CFortScreen(const CGTownInstance * town): if(fortSize == GameConstants::CREATURES_PER_TOWN) { - positions.push_back(Point(206,421)); + positions.push_back(Point(10, 421)); + positions.push_back(Point(404,421)); } else { - positions.push_back(Point(10, 421)); - positions.push_back(Point(404,421)); + positions.push_back(Point(206,421)); } for(ui32 i=0; igetTown()->creatures.size()) { - BuildingID dwelling = BuildingID::DWELL_UP_FIRST+i; + BuildingID dwelling = BuildingID::getDwellingFromLevel(i, 1); - if(vstd::contains(town->builtBuildings, dwelling)) - buildingID = BuildingID(BuildingID::DWELL_UP_FIRST+i); + if(town->hasBuilt(dwelling)) + buildingID = BuildingID(BuildingID::getDwellingFromLevel(i, 1)); else - buildingID = BuildingID(BuildingID::DWELL_FIRST+i); + buildingID = BuildingID(BuildingID::getDwellingFromLevel(i, 0)); } else { @@ -1791,13 +1840,14 @@ CFortScreen::CFortScreen(const CGTownInstance * town): ImagePath CFortScreen::getBgName(const CGTownInstance * town) { ui32 fortSize = static_cast(town->creatures.size()); - if(fortSize > GameConstants::CREATURES_PER_TOWN && 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("TPCASTL7"); - else return ImagePath::builtin("TPCASTL8"); + else + return ImagePath::builtin("TPCASTL7"); } void CFortScreen::creaturesChangedEventHandler() @@ -1813,7 +1863,7 @@ CFortScreen::RecruitArea::RecruitArea(int posX, int posY, const CGTownInstance * level(Level), availableCount(nullptr) { - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + OBJECT_CONSTRUCTION; pos.x +=posX; pos.y +=posY; pos.w = 386; @@ -1828,10 +1878,10 @@ 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(vstd::contains(town->builtBuildings, getMyBuilding()->bid)) + if(town->hasBuilt(getMyBuilding()->bid)) { ui32 available = town->creatures[level].first; std::string availableText = CGI->generaltexth->allTexts[217]+ std::to_string(available); @@ -1863,29 +1913,30 @@ CFortScreen::RecruitArea::RecruitArea(int posX, int posY, const CGTownInstance * const CCreature * CFortScreen::RecruitArea::getMyCreature() { if(!town->creatures.at(level).second.empty()) // built - return VLC->creh->objects[town->creatures.at(level).second.back()]; - if(!town->town->creatures.at(level).empty()) // there are creatures on this level - return VLC->creh->objects[town->town->creatures.at(level).front()]; + return town->creatures.at(level).second.back().toCreature(); + if(!town->getTown()->creatures.at(level).empty()) // there are creatures on this level + return town->getTown()->creatures.at(level).front().toCreature(); return nullptr; } const CBuilding * CFortScreen::RecruitArea::getMyBuilding() { - BuildingID myID = BuildingID(BuildingID::DWELL_FIRST + level); + BuildingID myID = BuildingID(BuildingID::getDwellingFromLevel(level, 0)); - if (level == GameConstants::CREATURES_PER_TOWN) - 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); - myID.advance(GameConstants::CREATURES_PER_TOWN); + build = town->getTown()->buildings.at(myID); + BuildingID::advanceDwelling(myID); } + return build; } @@ -1918,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_CAPTURING(255-DISPOSE); + 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); @@ -1934,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)}, @@ -1943,23 +2003,30 @@ 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], CGI->spellh->objects[owner->town->spells[i][j]])); + 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_CAPTURING(255-DISPOSE); + OBJECT_CONSTRUCTION; addUsedEvents(LCLICK | SHOW_POPUP | HOVER); pos += position; @@ -1969,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) @@ -1989,7 +2113,7 @@ void CMageGuildScreen::Scroll::hover(bool on) CBlacksmithDialog::CBlacksmithDialog(bool possible, CreatureID creMachineID, ArtifactID aid, ObjectInstanceID hid): CWindowObject(PLAYER_COLORED, ImagePath::builtin("TPSMITH")) { - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + OBJECT_CONSTRUCTION; Rect barRect(8, pos.h - 26, pos.w - 16, 19); @@ -1999,7 +2123,7 @@ CBlacksmithDialog::CBlacksmithDialog(bool possible, CreatureID creMachineID, Art animBG = std::make_shared(ImagePath::builtin("TPSMITBK"), 64, 50); animBG->needRefresh = true; - const CCreature * creature = CGI->creh->objects[creMachineID]; + const CCreature * creature = creMachineID.toCreature(); anim = std::make_shared(64, 50, creature->animDefName); anim->clipRect(113,125,200,150); diff --git a/client/windows/CCastleInterface.h b/client/windows/CCastleInterface.h index 7c3b62add..717c1a748 100644 --- a/client/windows/CCastleInterface.h +++ b/client/windows/CCastleInterface.h @@ -150,7 +150,7 @@ class CCastleBuildings : public CIntObject const CGHeroInstance* getHero();//Select hero for buildings usage - void enterBlacksmith(ArtifactID artifactID);//support for blacksmith + ballista yard + void enterBlacksmith(BuildingID building, ArtifactID artifactID);//support for blacksmith + ballista yard void enterBuilding(BuildingID building);//for buildings with simple description + pic left-click messages void enterCastleGate(); void enterFountain(const BuildingID & building, BuildingSubID::EBuildingSubID subID, BuildingID upgrades);//Rampart's fountains @@ -167,12 +167,15 @@ public: void enterDwelling(int level); void enterTownHall(); + void enterRewardable(BuildingID building); void enterMagesGuild(); void enterAnyMarket(); void enterAnyThievesGuild(); + void enterBank(BuildingID building); void enterToTheQuickRecruitmentWindow(); - void buildingClicked(BuildingID building, BuildingSubID::EBuildingSubID subID = BuildingSubID::NONE, BuildingID upgrades = BuildingID::NONE); + bool buildingTryActivateCustomUI(BuildingID buildingToTest, BuildingID buildingTarget); + void buildingClicked(BuildingID building); void addBuilding(BuildingID building); void removeBuilding(BuildingID building);//FIXME: not tested!!! }; @@ -181,7 +184,7 @@ public: class CCreaInfo : public CIntObject { const CGTownInstance * town; - const CCreature * creature; + CreatureID creature; int level; bool showAvailable; @@ -376,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; @@ -390,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 2394dc400..1ef1d08bb 100644 --- a/client/windows/CCreatureWindow.cpp +++ b/client/windows/CCreatureWindow.cpp @@ -17,25 +17,27 @@ #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/CGeneralTextHandler.h" -#include "../../lib/GameSettings.h" -#include "../../lib/CHeroHandler.h" +#include "../../lib/IGameSettings.h" +#include "../../lib/entities/hero/CHeroHandler.h" #include "../../lib/gameState/CGameState.h" #include "../../lib/networkPacks/ArtifactLocation.h" -#include "../../lib/TextOperations.h" +#include "../../lib/texts/CGeneralTextHandler.h" +#include "../../lib/texts/TextOperations.h" class CCreatureArtifactInstance; class CSelectableSkill; @@ -59,7 +61,7 @@ public: std::function callback; }; - // pointers to permament objects in game state + // pointers to permanent objects in game state const CCreature * creature; const CCommanderInstance * commander; const CStackInstance * stackNode; @@ -89,7 +91,7 @@ public: std::string getName() const { if(commander) - return commander->type->getNameSingularTranslated(); + return commander->getType()->getNameSingularTranslated(); else return creature->getNamePluralTranslated(); } @@ -144,10 +146,10 @@ void CCommanderSkillIcon::show(Canvas &to) static ImagePath skillToFile(int skill, int level, bool selected) { - // FIXME: is this a correct hadling? + // FIXME: is this a correct handling? // level 0 = skill not present, use image with "no" suffix // level 1-5 = skill available, mapped to images indexed as 0-4 - // selecting skill means that it will appear one level higher (as if alredy upgraded) + // selecting skill means that it will appear one level higher (as if already upgraded) std::string file = "zvs/Lib1.res/_"; switch (skill) { @@ -170,24 +172,24 @@ static ImagePath skillToFile(int skill, int level, bool selected) file += "MP"; break; } - std::string sufix; + std::string suffix; if (selected) level++; // UI will display resulting level if (level == 0) - sufix = "no"; //not avaliable - no number + suffix = "no"; //not available - no number else - sufix = std::to_string(level-1); + suffix = std::to_string(level-1); if (selected) - sufix += "="; //level-up highlight + suffix += "="; //level-up highlight - return ImagePath::builtin(file + sufix + ".bmp"); + return ImagePath::builtin(file + suffix + ".bmp"); } CStackWindow::CWindowSection::CWindowSection(CStackWindow * parent, const ImagePath & backgroundPath, int yOffset) : parent(parent) { pos.y += yOffset; - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + OBJECT_CONSTRUCTION; if(!backgroundPath.empty()) { background = std::make_shared(backgroundPath); @@ -202,7 +204,7 @@ CStackWindow::ActiveSpellsSection::ActiveSpellsSection(CStackWindow * owner, int static const Point firstPos(6, 2); // position of 1st spell box static const Point offset(54, 0); // offset of each spell box from previous - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + OBJECT_CONSTRUCTION; const CStack * battleStack = parent->info->stack; @@ -241,7 +243,7 @@ CStackWindow::ActiveSpellsSection::ActiveSpellsSection(CStackWindow * owner, int CStackWindow::BonusLineSection::BonusLineSection(CStackWindow * owner, size_t lineIndex) : CWindowSection(owner, ImagePath::builtin("stackWindow/bonus-effects"), 0) { - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + OBJECT_CONSTRUCTION; static const std::array offset = { @@ -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); } } } @@ -267,7 +311,7 @@ CStackWindow::BonusLineSection::BonusLineSection(CStackWindow * owner, size_t li CStackWindow::BonusesSection::BonusesSection(CStackWindow * owner, int yOffset, std::optional preferredSize): CWindowSection(owner, {}, yOffset) { - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + OBJECT_CONSTRUCTION; // size of single image for an item static const int itemHeight = 59; @@ -283,13 +327,13 @@ 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) : CWindowSection(owner, ImagePath::builtin("stackWindow/button-panel"), yOffset) { - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + OBJECT_CONSTRUCTION; if(parent->info->dismissInfo && parent->info->dismissInfo->callback) { @@ -343,7 +387,7 @@ CStackWindow::ButtonsSection::ButtonsSection(CStackWindow * owner, int yOffset) upgradeBtn->setOverlay(std::make_shared(AnimationPath::builtin("CPRSMALL"), VLC->creh->objects[upgradeInfo.info.newID[buttonIndex]]->getIconIndex())); - if(buttonsToCreate == 1) // single upgrade avaialbe + if(buttonsToCreate == 1) // single upgrade available upgradeBtn->assignedKey = EShortcut::RECRUITMENT_UPGRADE; upgrade[buttonIndex] = upgradeBtn; @@ -378,7 +422,7 @@ CStackWindow::ButtonsSection::ButtonsSection(CStackWindow * owner, int yOffset) CStackWindow::CommanderMainSection::CommanderMainSection(CStackWindow * owner, int yOffset) : CWindowSection(owner, ImagePath::builtin("stackWindow/commander-bg"), yOffset) { - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + OBJECT_CONSTRUCTION; auto getSkillPos = [](int index) { @@ -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); } @@ -489,7 +535,7 @@ CStackWindow::CommanderMainSection::CommanderMainSection(CStackWindow * owner, i CStackWindow::MainSection::MainSection(CStackWindow * owner, int yOffset, bool showExp, bool showArt) : CWindowSection(owner, getBackgroundName(showExp, showArt), yOffset) { - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + OBJECT_CONSTRUCTION; statNames = { @@ -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,10 +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->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( @@ -692,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); @@ -704,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()); @@ -721,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; @@ -734,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()); @@ -752,7 +808,7 @@ CStackWindow::~CStackWindow() void CStackWindow::init() { - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + OBJECT_CONSTRUCTION; if(!info->stackNode) info->stackNode = new CStackInstance(info->creature, 1, true);// FIXME: free data @@ -773,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()) { @@ -788,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 @@ -798,10 +861,10 @@ void CStackWindow::initBonusesList() void CStackWindow::initSections() { - OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255-DISPOSE); + OBJECT_CONSTRUCTION; - bool showArt = CGI->settings()->getBoolean(EGameSettings::MODULE_STACK_ARTIFACT) && info->commander == nullptr && info->stackNode; - bool showExp = (CGI->settings()->getBoolean(EGameSettings::MODULE_STACK_EXPERIENCE) || info->commander != nullptr) && info->stackNode; + bool showArt = LOCPLINT->cb->getSettings().getBoolean(EGameSettings::MODULE_STACK_ARTIFACT) && info->commander == nullptr && info->stackNode; + bool showExp = (LOCPLINT->cb->getSettings().getBoolean(EGameSettings::MODULE_STACK_EXPERIENCE) || info->commander != nullptr) && info->stackNode; mainSection = std::make_shared(this, pos.h, showExp, showArt); @@ -866,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; @@ -904,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) @@ -920,7 +999,7 @@ void CStackWindow::setSelection(si32 newSkill, std::shared_ptrcommander->secondarySkills[skillIndex], selected); }; - OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255-DISPOSE); + OBJECT_CONSTRUCTION; int oldSelection = selectedSkill; // update selection selectedSkill = newSkill; @@ -986,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 d4360462f..acf43029a 100644 --- a/client/windows/CCreatureWindow.h +++ b/client/windows/CCreatureWindow.h @@ -28,14 +28,16 @@ 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? { std::shared_ptr object; // passive object that will be used to determine clickable area bool isMasterAbility; // refers to WoG abilities obtainable via combining master skills (for example attack + speed unlocks shoot) - bool isSelected; // used only for programatically created border around selected "master abilities" + bool isSelected; // used only for programmatically created border around selected "master abilities" public: CCommanderSkillIcon(std::shared_ptr object_, bool isMasterAbility_, std::function callback); @@ -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 e2b3e847b..e97f50412 100644 --- a/client/windows/CExchangeWindow.cpp +++ b/client/windows/CExchangeWindow.cpp @@ -26,16 +26,15 @@ #include "../widgets/TextControls.h" #include "../render/IRenderHandler.h" -#include "../render/CAnimation.h" #include "../../CCallback.h" -#include "../lib/mapObjects/CGHeroInstance.h" -#include "../lib/CGeneralTextHandler.h" -#include "../lib/CHeroHandler.h" -#include "../lib/filesystem/Filesystem.h" #include "../lib/CSkillHandler.h" -#include "../lib/TextOperations.h" +#include "../lib/entities/hero/CHeroHandler.h" +#include "../lib/filesystem/Filesystem.h" +#include "../lib/mapObjects/CGHeroInstance.h" +#include "../lib/texts/CGeneralTextHandler.h" +#include "../lib/texts/TextOperations.h" static const std::string QUICK_EXCHANGE_BG = "quick-exchange/TRADEQE"; @@ -50,7 +49,7 @@ CExchangeWindow::CExchangeWindow(ObjectInstanceID hero1, ObjectInstanceID hero2, { const bool qeLayout = isQuickExchangeLayoutAvailable(); - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + OBJECT_CONSTRUCTION; addUsedEvents(KEYBOARD); heroInst[0] = LOCPLINT->cb->getHero(hero1); @@ -66,17 +65,12 @@ CExchangeWindow::CExchangeWindow(ObjectInstanceID hero1, ObjectInstanceID hero2, titles[0] = std::make_shared(147, 25, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, genTitle(heroInst[0])); titles[1] = std::make_shared(653, 25, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, genTitle(heroInst[1])); - auto PSKIL32 = GH.renderHandler().loadAnimation(AnimationPath::builtin("PSKIL32")); - PSKIL32->preload(); - - auto SECSK32 = GH.renderHandler().loadAnimation(AnimationPath::builtin("SECSK32")); - for(int g = 0; g < 4; ++g) { if (qeLayout) - primSkillImages.push_back(std::make_shared(PSKIL32, g, Rect(389, 12 + 26 * g, 22, 22))); + primSkillImages.push_back(std::make_shared(AnimationPath::builtin("PSKIL32"), g, Rect(389, 12 + 26 * g, 22, 22))); else - primSkillImages.push_back(std::make_shared(PSKIL32, g, 0, 385, 19 + 36 * g)); + primSkillImages.push_back(std::make_shared(AnimationPath::builtin("PSKIL32"), g, 0, 385, 19 + 36 * g)); } for(int leftRight : {0, 1}) @@ -88,24 +82,32 @@ CExchangeWindow::CExchangeWindow(ObjectInstanceID hero1, ObjectInstanceID hero2, for(int m=0; m < hero->secSkills.size(); ++m) - secSkillIcons[leftRight].push_back(std::make_shared(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(PSKIL32, 4, 0, 103 + 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); - manaImages[leftRight] = std::make_shared(PSKIL32, 5, 0, 139 + 490 * leftRight, qeLayout ? 41 : 45); + manaImages[leftRight] = std::make_shared(AnimationPath::builtin("PSKIL32"), 5, 0, 139 + 490 * leftRight, qeLayout ? 41 : 45); manaValues[leftRight] = std::make_shared(155 + 490 * leftRight, qeLayout ? 66 : 71, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE); } 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, 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, 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]); - addSetAndCallbacks(artifs[0]); - addSetAndCallbacks(artifs[1]); + + addSet(artifs[0]); + addSet(artifs[1]); for(int g=0; g<4; ++g) { @@ -125,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 { @@ -150,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)); @@ -191,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"))); @@ -226,21 +247,29 @@ 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]), - std::bind(&CExchangeController::moveStack, &controller, false, SlotID(i)))); + CButton::tooltip(CGI->generaltexth->translate("vcmi.quickExchange.moveUnit")), + [this, i]() { creatureArrowButtonCallback(false, SlotID(i)); })); moveUnitFromRightToLeftButtons.back()->block(leftHeroBlock); moveUnitFromLeftToRightButtons.push_back( std::make_shared( Point(66 + 35 * i, 154), AnimationPath::builtin("quick-exchange/unitRight.DEF"), - CButton::tooltip(CGI->generaltexth->qeModCommands[1]), - std::bind(&CExchangeController::moveStack, &controller, true, SlotID(i)))); + CButton::tooltip(CGI->generaltexth->translate("vcmi.quickExchange.moveUnit")), + [this, i]() { creatureArrowButtonCallback(true, SlotID(i)); })); moveUnitFromLeftToRightButtons.back()->block(rightHeroBlock); } } - updateWidgets(); + CExchangeWindow::update(); +} + +void CExchangeWindow::creatureArrowButtonCallback(bool leftToRight, SlotID slotId) +{ + if (GH.isKeyboardAltDown()) + controller.moveArmy(leftToRight, slotId); + else + controller.moveStack(leftToRight, slotId); } void CExchangeWindow::moveArtifactsCallback(bool leftToRight) @@ -328,7 +357,7 @@ void CExchangeWindow::updateGarrisons() { garr->recreateSlots(); - updateWidgets(); + update(); } bool CExchangeWindow::holdsGarrison(const CArmedInstance * army) @@ -342,8 +371,10 @@ void CExchangeWindow::questLogShortcut() LOCPLINT->showQuestLog(); } -void CExchangeWindow::updateWidgets() +void CExchangeWindow::update() { + CWindowWithArtifacts::update(); + for(size_t leftRight : {0, 1}) { const CGHeroInstance * hero = heroInst.at(leftRight); @@ -359,7 +390,7 @@ void CExchangeWindow::updateWidgets() 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 6f300b8fa..e1c31d3e2 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; @@ -55,6 +54,7 @@ class CExchangeWindow : public CStatusbarWindow, public IGarrisonHolder, public std::shared_ptr backpackButtonRight; CExchangeController controller; + void creatureArrowButtonCallback(bool leftToRight, SlotID slotID); void moveArtifactsCallback(bool leftToRight); void swapArtifactsCallback(); void moveUnitsShortcut(bool leftToRight); @@ -71,7 +71,7 @@ public: void keyPressed(EShortcut key) override; - void updateWidgets(); + void update() override; // IGarrisonHolder impl void updateGarrisons() override; diff --git a/client/windows/CHeroBackpackWindow.cpp b/client/windows/CHeroBackpackWindow.cpp index 369cc73af..94b7a622f 100644 --- a/client/windows/CHeroBackpackWindow.cpp +++ b/client/windows/CHeroBackpackWindow.cpp @@ -20,28 +20,61 @@ #include "render/Canvas.h" #include "CPlayerInterface.h" +#include "../../CCallback.h" + +#include "../../lib/mapObjects/CGHeroInstance.h" +#include "../../lib/networkPacks/ArtifactLocation.h" + CHeroBackpackWindow::CHeroBackpackWindow(const CGHeroInstance * hero, const std::vector & artsSets) : CWindowWithArtifacts(&artsSets) { - OBJECT_CONSTRUCTION_CAPTURING(255 - DISPOSE); + OBJECT_CONSTRUCTION; stretchedBackground = std::make_shared(ImagePath::builtin("DIBOXBCK"), Rect(0, 0, 0, 0)); arts = std::make_shared(); arts->moveBy(Point(windowMargin, windowMargin)); - addSetAndCallbacks(arts); + arts->clickPressedCallback = [this](const CArtPlace & artPlace, const Point & cursorPosition) + { + clickPressedOnArtPlace(arts->getHero(), artPlace.slot, true, false, true, cursorPosition); + }; + arts->showPopupCallback = [this](CArtPlace & artPlace, const Point & cursorPosition) + { + showArtifactAssembling(*arts, artPlace, cursorPosition); + }; + addSet(arts); arts->setHero(hero); - addCloseCallback(std::bind(&CHeroBackpackWindow::close, this)); - 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); @@ -50,15 +83,20 @@ void CHeroBackpackWindow::showAll(Canvas & to) CHeroQuickBackpackWindow::CHeroQuickBackpackWindow(const CGHeroInstance * hero, ArtifactPosition targetSlot) { - OBJECT_CONSTRUCTION_CAPTURING(255 - DISPOSE); + OBJECT_CONSTRUCTION; stretchedBackground = std::make_shared(ImagePath::builtin("DIBOXBCK"), Rect(0, 0, 0, 0)); arts = std::make_shared(targetSlot); arts->moveBy(Point(windowMargin, windowMargin)); - addSetAndCallbacks(static_cast>(arts)); + arts->clickPressedCallback = [this](const CArtPlace & artPlace, const Point & cursorPosition) + { + if(const auto curHero = arts->getHero()) + swapArtifactAndClose(*arts, artPlace.slot, ArtifactLocation(curHero->id, arts->getFilterSlot())); + }; + addSet(arts); arts->setHero(hero); - addCloseCallback(std::bind(&CHeroQuickBackpackWindow::close, this)); addUsedEvents(GESTURE); + addUsedEvents(LCLICK); pos.w = stretchedBackground->pos.w = arts->pos.w + 2 * windowMargin; pos.h = stretchedBackground->pos.h = arts->pos.h + windowMargin; } @@ -78,6 +116,11 @@ void CHeroQuickBackpackWindow::gesturePanning(const Point & initialPosition, con redraw(); } +void CHeroQuickBackpackWindow::notFocusedClick() +{ + close(); +} + void CHeroQuickBackpackWindow::showAll(Canvas & to) { if(arts->getSlotsNum() == 0) diff --git a/client/windows/CHeroBackpackWindow.h b/client/windows/CHeroBackpackWindow.h index aff8cbca9..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; @@ -33,6 +34,7 @@ public: CHeroQuickBackpackWindow(const CGHeroInstance * hero, ArtifactPosition targetSlot); void gesture(bool on, const Point & initialPosition, const Point & finalPosition) override; void gesturePanning(const Point & initialPosition, const Point & currentPosition, const Point & lastUpdateDistance) override; + void notFocusedClick() override; private: std::shared_ptr arts; diff --git a/client/windows/CHeroOverview.cpp b/client/windows/CHeroOverview.cpp index 2a5412dc1..1c1f0bacf 100644 --- a/client/windows/CHeroOverview.cpp +++ b/client/windows/CHeroOverview.cpp @@ -10,29 +10,31 @@ #include "StdInc.h" #include "CHeroOverview.h" +#include "../CCallback.h" #include "../CGameInfo.h" +#include "../CPlayerInterface.h" #include "../gui/CGuiHandler.h" #include "../render/Canvas.h" #include "../render/Colors.h" -#include "../render/Graphics.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/GameSettings.h" -#include "../../lib/CGeneralTextHandler.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" CHeroOverview::CHeroOverview(const HeroTypeID & h) : CWindowObject(BORDERED | RCLICK_POPUP), hero { h } { - OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + OBJECT_CONSTRUCTION; heroIdx = hero.getNum(); @@ -124,7 +126,7 @@ void CHeroOverview::genControls() r = Rect(302, 3 * borderOffset + yOffset + 62, 292, 32); backgroundRectangles.push_back(std::make_shared(r.resize(1), rectangleColor, borderColor)); - auto stacksCountChances = VLC->settings()->getVector(EGameSettings::HEROES_STARTING_STACKS_CHANCES); + auto stacksCountChances = CGI->engineSettings()->getVector(EGameSettings::HEROES_STARTING_STACKS_CHANCES); // army int space = (260 - 7 * 32) / 6; @@ -204,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++; @@ -225,12 +228,12 @@ void CHeroOverview::genControls() { if((*CGI->heroh)[heroIdx]->haveSpellBook) { - imageSpells.push_back(std::make_shared(GH.renderHandler().loadAnimation(AnimationPath::builtin("ARTIFACT")), 0, Rect(302 + (292 / 2) + 2 * borderOffset, 7 * borderOffset + yOffset + 186 + i * (32 + borderOffset), 32, 32), 0)); + imageSpells.push_back(std::make_shared(AnimationPath::builtin("ARTIFACT"), 0, Rect(302 + (292 / 2) + 2 * borderOffset, 7 * borderOffset + yOffset + 186 + i * (32 + borderOffset), 32, 32), 0)); } i++; } - imageSpells.push_back(std::make_shared(GH.renderHandler().loadAnimation(AnimationPath::builtin("SPELLBON")), (*CGI->spellh)[spell]->getIconIndex(), Rect(302 + (292 / 2) + 2 * borderOffset, 7 * borderOffset + yOffset + 186 + i * (32 + borderOffset), 32, 32), 0)); + imageSpells.push_back(std::make_shared(AnimationPath::builtin("SPELLBON"), (*CGI->spellh)[spell]->getIconIndex(), Rect(302 + (292 / 2) + 2 * borderOffset, 7 * borderOffset + yOffset + 186 + i * (32 + borderOffset), 32, 32), 0)); labelSpellsNames.push_back(std::make_shared(302 + (292 / 2) + 3 * borderOffset + 32 + borderOffset, 8 * borderOffset + yOffset + 186 + i * (32 + borderOffset) + 3, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE, (*CGI->spellh)[spell]->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 2824fa2fb..ad73c3960 100644 --- a/client/windows/CHeroWindow.cpp +++ b/client/windows/CHeroWindow.cpp @@ -28,7 +28,6 @@ #include "../widgets/CGarrisonInt.h" #include "../widgets/TextControls.h" #include "../widgets/Buttons.h" -#include "../render/CAnimation.h" #include "../render/IRenderHandler.h" #include "../../CCallback.h" @@ -36,18 +35,18 @@ #include "../lib/ArtifactUtils.h" #include "../lib/CArtHandler.h" #include "../lib/CConfigHandler.h" -#include "../lib/CGeneralTextHandler.h" -#include "../lib/CHeroHandler.h" +#include "../lib/entities/hero/CHeroHandler.h" +#include "../lib/texts/CGeneralTextHandler.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) { //TODO: do not recreate window if (false) { - owner->update(hero, true); + owner->update(); } else { @@ -62,7 +61,7 @@ CHeroSwitcher::CHeroSwitcher(CHeroWindow * owner_, Point pos_, const CGHeroInsta owner(owner_), hero(hero_) { - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + OBJECT_CONSTRUCTION; pos += pos_; image = std::make_shared(AnimationPath::builtin("PortraitsSmall"), hero->getIconIndex()); @@ -75,7 +74,7 @@ CHeroWindow::CHeroWindow(const CGHeroInstance * hero) { auto & heroscrn = CGI->generaltexth->heroscrn; - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + OBJECT_CONSTRUCTION; curHero = hero; banner = std::make_shared(AnimationPath::builtin("CREST58"), LOCPLINT->playerID.getNum(), 0, 606, 8); @@ -131,14 +130,12 @@ CHeroWindow::CHeroWindow(const CGHeroInstance * hero) primSkillValues.push_back(value); } - auto primSkills = GH.renderHandler().loadAnimation(AnimationPath::builtin("PSKIL42")); - primSkills->preload(); - primSkillImages.push_back(std::make_shared(primSkills, 0, 0, 32, 111)); - primSkillImages.push_back(std::make_shared(primSkills, 1, 0, 102, 111)); - primSkillImages.push_back(std::make_shared(primSkills, 2, 0, 172, 111)); - primSkillImages.push_back(std::make_shared(primSkills, 3, 0, 162, 230)); - primSkillImages.push_back(std::make_shared(primSkills, 4, 0, 20, 230)); - primSkillImages.push_back(std::make_shared(primSkills, 5, 0, 242, 111)); + primSkillImages.push_back(std::make_shared(AnimationPath::builtin("PSKIL42"), 0, 0, 32, 111)); + primSkillImages.push_back(std::make_shared(AnimationPath::builtin("PSKIL42"), 1, 0, 102, 111)); + primSkillImages.push_back(std::make_shared(AnimationPath::builtin("PSKIL42"), 2, 0, 172, 111)); + primSkillImages.push_back(std::make_shared(AnimationPath::builtin("PSKIL42"), 3, 0, 162, 230)); + primSkillImages.push_back(std::make_shared(AnimationPath::builtin("PSKIL42"), 4, 0, 20, 230)); + primSkillImages.push_back(std::make_shared(AnimationPath::builtin("PSKIL42"), 5, 0, 242, 111)); specImage = std::make_shared(AnimationPath::builtin("UN44"), 0, 0, 18, 180); specArea = std::make_shared(Rect(18, 180, 136, 42), CGI->generaltexth->heroscrn[27]); @@ -152,12 +149,10 @@ CHeroWindow::CHeroWindow(const CGHeroInstance * hero) expValue = std::make_shared(68, 252); manaValue = std::make_shared(211, 252); - auto secSkills = GH.renderHandler().loadAnimation(AnimationPath::builtin("SECSKILL")); 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(secSkills, 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); @@ -176,27 +171,21 @@ CHeroWindow::CHeroWindow(const CGHeroInstance * hero) labels.push_back(std::make_shared(69, 232, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::YELLOW, CGI->generaltexth->jktexts[6])); labels.push_back(std::make_shared(213, 232, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::YELLOW, CGI->generaltexth->jktexts[7])); - update(hero); + CHeroWindow::update(); } -void CHeroWindow::update(const CGHeroInstance * hero, bool redrawNeeded) +void CHeroWindow::update() { + CWindowWithArtifacts::update(); auto & heroscrn = CGI->generaltexth->heroscrn; - - if(!hero) //something strange... no hero? it shouldn't happen - { - logGlobal->error("Set nullptr hero? no way..."); - return; - } - - assert(hero == curHero); + assert(curHero); 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]); @@ -207,22 +196,26 @@ void CHeroWindow::update(const CGHeroInstance * hero, bool redrawNeeded) portraitImage->setFrame(curHero->getIconIndex()); { - OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255-DISPOSE); + 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); - auto split = std::make_shared(Point(539, 519), AnimationPath::builtin("hsbtns9.def"), CButton::tooltip(CGI->generaltexth->allTexts[256], helpBox), [&](){ garr->splitClick(); }, EShortcut::HERO_ARMY_SPLIT); + 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, 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); - addSetAndCallbacks(arts); - enableArtifactsCostumeSwitcher(); + addSet(arts); + enableKeyboardShortcuts(); } int serial = LOCPLINT->cb->getHeroSerial(curHero, false); @@ -241,20 +234,16 @@ void CHeroWindow::update(const CGHeroInstance * hero, bool redrawNeeded) } //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; @@ -313,8 +302,7 @@ void CHeroWindow::update(const CGHeroInstance * hero, bool redrawNeeded) morale->set(curHero); luck->set(curHero); - if(redrawNeeded) - redraw(); + redraw(); } void CHeroWindow::dismissCurrent() @@ -324,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 dfebf45d2..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; @@ -100,9 +99,9 @@ public: CHeroWindow(const CGHeroInstance * hero); - void update(const CGHeroInstance * hero, bool redrawNeeded = false); //sets main displayed hero + void update() override; - void dismissCurrent(); //dissmissed currently displayed hero (curHero) + void dismissCurrent(); //dismissed currently displayed hero (curHero) void commanderWindow(); void switchHero(); //changes displayed hero void updateGarrisons() override; diff --git a/client/windows/CKingdomInterface.cpp b/client/windows/CKingdomInterface.cpp index 48e1a784c..61254a60a 100644 --- a/client/windows/CKingdomInterface.cpp +++ b/client/windows/CKingdomInterface.cpp @@ -33,11 +33,11 @@ #include "../../lib/CConfigHandler.h" #include "../../lib/CCreatureHandler.h" -#include "../../lib/CGeneralTextHandler.h" -#include "../../lib/CHeroHandler.h" -#include "../../lib/GameSettings.h" +#include "../../lib/entities/hero/CHeroHandler.h" +#include "../../lib/texts/CGeneralTextHandler.h" +#include "../../lib/IGameSettings.h" #include "../../lib/CSkillHandler.h" -#include "../../lib/CTownHandler.h" +#include "../../lib/StartInfo.h" #include "../../lib/mapObjects/CGHeroInstance.h" #include "../../lib/mapObjects/CGTownInstance.h" #include "../../lib/mapObjects/MiscObjects.h" @@ -56,7 +56,7 @@ InfoBox::InfoBox(Point position, InfoPos Pos, InfoSize Size, std::shared_ptr(data->getImageName(size), data->getImageIndex()); @@ -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; @@ -462,7 +462,7 @@ void InfoBoxCustom::prepareMessage(std::string & text, std::shared_ptr(std::bind(&CKingdomInterface::createMainTab, this, _1), Point(4,4)); @@ -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()); } @@ -550,7 +550,19 @@ std::shared_ptr CKingdomInterface::createMainTab(size_t index) case 0: return std::make_shared(size, [this](const CWindowWithArtifacts::CArtifactsOfHeroPtr & newHeroSet) { - addSetAndCallbacks(newHeroSet); + newHeroSet->clickPressedCallback = [this, newHeroSet](const CArtPlace & artPlace, const Point & cursorPosition) + { + clickPressedOnArtPlace(newHeroSet->getHero(), artPlace.slot, false, false, false, cursorPosition); + }; + newHeroSet->showPopupCallback = [this, newHeroSet](CArtPlace & artPlace, const Point & cursorPosition) + { + showArtifactAssembling(*newHeroSet, artPlace, cursorPosition); + }; + newHeroSet->gestureCallback = [this, newHeroSet](const CArtPlace & artPlace, const Point & cursorPosition) + { + showQuickBackpackWindow(newHeroSet->getHero(), artPlace.slot, cursorPosition); + }; + addSet(newHeroSet); }); case 1: return std::make_shared(size); @@ -571,31 +583,18 @@ void CKingdomInterface::generateMinesList(const std::vectorID == Obj::MINE || object->ID == Obj::ABANDONED_MINE) { const CGMine * mine = dynamic_cast(object); - assert(mine); minesCount[mine->producedResource]++; - - if (mine->producedResource == EGameResID::GOLD) - totalIncome += mine->producedQuantity; } } - //Heroes can produce gold as well - skill, specialty or arts - std::vector heroes = LOCPLINT->cb->getHeroesInfo(true); - for(auto & hero : heroes) - { - totalIncome += hero->valOfBonuses(Selector::typeSubtype(BonusType::GENERATE_RESOURCE, BonusSubtypeID(GameResID(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 - totalIncome += LOCPLINT->cb->getPlayerState(LOCPLINT->playerID)->valOfBonuses(BonusType::RESOURCES_CONSTANT_BOOST, BonusSubtypeID(GameResID(EGameResID::GOLD))); - totalIncome += LOCPLINT->cb->getPlayerState(LOCPLINT->playerID)->valOfBonuses(BonusType::RESOURCES_TOWN_MULTIPLYING_BOOST, BonusSubtypeID(GameResID(EGameResID::GOLD))) * towns.size(); + 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; for(int i=0; i<7; i++) { @@ -678,9 +677,9 @@ bool CKingdomInterface::holdsGarrison(const CArmedInstance * army) CKingdHeroList::CKingdHeroList(size_t maxSize, const CreateHeroItemFunctor & onCreateHeroItemCallback) { - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + OBJECT_CONSTRUCTION; title = std::make_shared(ImagePath::builtin("OVTITLE"),16,0); - title->colorize(LOCPLINT->playerID); + title->setPlayerColor(LOCPLINT->playerID); heroLabel = std::make_shared(150, 10, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->overview[0]); skillsLabel = std::make_shared(500, 10, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->overview[1]); @@ -722,9 +721,9 @@ bool CKingdHeroList::holdsGarrison(const CArmedInstance * army) CKingdTownList::CKingdTownList(size_t maxSize) { - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + OBJECT_CONSTRUCTION; title = std::make_shared(ImagePath::builtin("OVTITLE"), 16, 0); - title->colorize(LOCPLINT->playerID); + title->setPlayerColor(LOCPLINT->playerID); townLabel = std::make_shared(146, 10,FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->overview[3]); garrHeroLabel = std::make_shared(375, 10, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->overview[4]); visitHeroLabel = std::make_shared(608, 10, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->overview[5]); @@ -778,7 +777,7 @@ std::shared_ptr CKingdTownList::createTownItem(size_t index) CTownItem::CTownItem(const CGTownInstance * Town) : town(Town) { - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + OBJECT_CONSTRUCTION; background = std::make_shared(AnimationPath::builtin("OVSLOT"), 6); name = std::make_shared(74, 8, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE, town->getNameTranslated()); @@ -789,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->builded >= CGI->settings()->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)); @@ -809,18 +808,18 @@ CTownItem::CTownItem(const CGTownInstance * Town) fastTavern = std::make_shared(Rect(5, 6, 58, 64), [&]() { - if(town->builtBuildings.count(BuildingID::TAVERN)) + 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), []() { std::vector towns = LOCPLINT->cb->getTownsInfo(true); for(auto & town : towns) { - if(town->builtBuildings.count(BuildingID::MARKETPLACE)) + if(town->hasBuilt(BuildingID::MARKETPLACE)) { GH.windows().createAndPushWindow(town, nullptr, nullptr, EMarketMode::RESOURCE_RESOURCE); return; @@ -855,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(); @@ -868,9 +867,9 @@ public: std::shared_ptr background; std::vector> arts; - ArtSlotsTab() + ArtSlotsTab(CIntObject * parent) { - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + OBJECT_CONSTRUCTION_TARGETED(parent); background = std::make_shared(AnimationPath::builtin("OVSLOT"), 4); pos = background->pos; for(int i=0; i<9; i++) @@ -886,9 +885,9 @@ public: std::shared_ptr btnLeft; std::shared_ptr btnRight; - BackpackTab() + BackpackTab(CIntObject * parent) { - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + OBJECT_CONSTRUCTION_TARGETED(parent); background = std::make_shared(AnimationPath::builtin("OVSLOT"), 5); pos = background->pos; btnLeft = std::make_shared(Point(269, 66), AnimationPath::builtin("HSBTNS3"), CButton::tooltip(), 0); @@ -901,12 +900,12 @@ public: CHeroItem::CHeroItem(const CGHeroInstance * Hero) : hero(Hero) { - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + OBJECT_CONSTRUCTION; artTabs.resize(3); - auto arts1 = std::make_shared(); - auto arts2 = std::make_shared(); - auto backpack = std::make_shared(); + auto arts1 = std::make_shared(this); + auto arts2 = std::make_shared(this); + auto backpack = std::make_shared(this); artTabs[0] = arts1; artTabs[1] = arts2; artTabs[2] = backpack; diff --git a/client/windows/CMapOverview.cpp b/client/windows/CMapOverview.cpp index b60973945..c0845133d 100644 --- a/client/windows/CMapOverview.cpp +++ b/client/windows/CMapOverview.cpp @@ -20,14 +20,11 @@ #include "../widgets/TextControls.h" #include "../windows/GUIClasses.h" #include "../windows/InfoWindows.h" -#include "../render/CAnimation.h" #include "../render/Canvas.h" #include "../render/IImage.h" #include "../render/IRenderHandler.h" #include "../render/Graphics.h" -#include "../../lib/CGeneralTextHandler.h" -#include "../../lib/TextOperations.h" #include "../../lib/CConfigHandler.h" #include "../../lib/campaign/CampaignState.h" #include "../../lib/mapping/CMap.h" @@ -38,16 +35,17 @@ #include "../../lib/TerrainHandler.h" #include "../../lib/filesystem/Filesystem.h" -#include "../../lib/serializer/CLoadFile.h" #include "../../lib/StartInfo.h" #include "../../lib/rmg/CMapGenOptions.h" -#include "../../lib/Languages.h" +#include "../../lib/serializer/CLoadFile.h" +#include "../../lib/texts/CGeneralTextHandler.h" +#include "../../lib/texts/TextOperations.h" -CMapOverview::CMapOverview(std::string mapName, std::string fileName, std::string date, ResourcePath resource, ESelectionScreen tabType) - : CWindowObject(BORDERED | RCLICK_POPUP), resource(resource), mapName(mapName), fileName(fileName), date(date), tabType(tabType) +CMapOverview::CMapOverview(const std::string & mapName, const std::string & fileName, const std::string & date, const std::string & author, const std::string & version, const ResourcePath & resource, ESelectionScreen tabType) + : CWindowObject(BORDERED | RCLICK_POPUP), resource(resource), mapName(mapName), fileName(fileName), date(date), author(author), version(version), tabType(tabType) { - OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + OBJECT_CONSTRUCTION; widget = std::make_shared(*this); @@ -62,16 +60,16 @@ CMapOverview::CMapOverview(std::string mapName, std::string fileName, std::strin Canvas CMapOverviewWidget::createMinimapForLayer(std::unique_ptr & map, int layer) const { - Canvas canvas = Canvas(Point(map->width, map->height)); + Canvas canvas = Canvas(Point(map->width, map->height), CanvasScalingPolicy::IGNORE); for (int y = 0; y < map->height; ++y) for (int x = 0; x < map->width; ++x) { 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 @@ -136,12 +134,12 @@ std::shared_ptr CMapOverviewWidget::buildDrawMinimap(const JsonNode & return nullptr; Rect minimapRect = minimaps[id].getRenderArea(); - double maxSideLenghtSrc = std::max(minimapRect.w, minimapRect.h); - double maxSideLenghtDst = std::max(rect.w, rect.h); - double resize = maxSideLenghtSrc / maxSideLenghtDst; + double maxSideLengthSrc = std::max(minimapRect.w, minimapRect.h); + double maxSideLengthDst = std::max(rect.w, rect.h); + double resize = maxSideLengthSrc / maxSideLengthDst; Point newMinimapSize = Point(minimapRect.w / resize, minimapRect.h / resize); - Canvas canvasScaled = Canvas(Point(rect.w, rect.h)); + Canvas canvasScaled = Canvas(Point(rect.w, rect.h), CanvasScalingPolicy::AUTO); canvasScaled.drawScaled(minimaps[id], Point((rect.w - newMinimapSize.x) / 2, (rect.h - newMinimapSize.y) / 2), newMinimapSize); std::shared_ptr img = GH.renderHandler().createImage(canvasScaled.getInternalSurface()); @@ -204,6 +202,14 @@ CMapOverviewWidget::CMapOverviewWidget(CMapOverview& parent): else w->setText(p.date); } + if(auto w = widget("author")) + { + w->setText(p.author.empty() ? "-" : p.author); + } + if(auto w = widget("version")) + { + w->setText(p.version); + } if(auto w = widget("noUnderground")) { if(minimaps.size() == 0) diff --git a/client/windows/CMapOverview.h b/client/windows/CMapOverview.h index bbd0b0e79..f1b7caf25 100644 --- a/client/windows/CMapOverview.h +++ b/client/windows/CMapOverview.h @@ -53,7 +53,9 @@ public: const std::string mapName; const std::string fileName; const std::string date; + const std::string author; + const std::string version; const ESelectionScreen tabType; - CMapOverview(std::string mapName, std::string fileName, std::string date, ResourcePath resource, ESelectionScreen tabType); + CMapOverview(const std::string & mapName, const std::string & fileName, const std::string & date, const std::string & author, const std::string & version, const ResourcePath & resource, ESelectionScreen tabType); }; diff --git a/client/windows/CMarketWindow.cpp b/client/windows/CMarketWindow.cpp index a4a71c1ae..1fbd750fd 100644 --- a/client/windows/CMarketWindow.cpp +++ b/client/windows/CMarketWindow.cpp @@ -27,11 +27,14 @@ #include "../CGameInfo.h" #include "../CPlayerInterface.h" -#include "../../lib/CGeneralTextHandler.h" +#include "../../lib/entities/building/CBuilding.h" +#include "../../lib/texts/CGeneralTextHandler.h" #include "../../lib/mapObjects/CGTownInstance.h" #include "../../lib/mapObjects/CGMarket.h" #include "../../lib/mapObjects/CGHeroInstance.h" +#include "../../CCallback.h" + CMarketWindow::CMarketWindow(const IMarket * market, const CGHeroInstance * hero, const std::function & onWindowClosed, EMarketMode mode) : CWindowObject(PLAYER_COLORED) , windowClosedCallback(onWindowClosed) @@ -40,7 +43,7 @@ CMarketWindow::CMarketWindow(const IMarket * market, const CGHeroInstance * hero mode == EMarketMode::RESOURCE_ARTIFACT || mode == EMarketMode::ARTIFACT_RESOURCE || mode == EMarketMode::ARTIFACT_EXP || mode == EMarketMode::CREATURE_EXP); - OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255 - DISPOSE); + OBJECT_CONSTRUCTION; if(mode == EMarketMode::RESOURCE_RESOURCE) createMarketResources(market, hero); @@ -62,24 +65,27 @@ CMarketWindow::CMarketWindow(const IMarket * market, const CGHeroInstance * hero void CMarketWindow::updateArtifacts() { - assert(marketWidget); - marketWidget->update(); + update(); } void CMarketWindow::updateGarrisons() { - assert(marketWidget); - marketWidget->update(); + update(); } -void CMarketWindow::updateResource() +void CMarketWindow::updateResources() { - assert(marketWidget); - marketWidget->update(); + update(); } -void CMarketWindow::updateHero() +void CMarketWindow::updateExperience() { + update(); +} + +void CMarketWindow::update() +{ + CWindowWithArtifacts::update(); assert(marketWidget); marketWidget->update(); } @@ -98,21 +104,6 @@ bool CMarketWindow::holdsGarrison(const CArmedInstance * army) return marketWidget->hero == army; } -void CMarketWindow::artifactRemoved(const ArtifactLocation & artLoc) -{ - marketWidget->update(); - CWindowWithArtifacts::artifactRemoved(artLoc); -} - -void CMarketWindow::artifactMoved(const ArtifactLocation & srcLoc, const ArtifactLocation & destLoc, bool withRedraw) -{ - if(!getState().has_value()) - return; - CWindowWithArtifacts::artifactMoved(srcLoc, destLoc, withRedraw); - assert(marketWidget); - marketWidget->update(); -} - void CMarketWindow::createChangeModeButtons(EMarketMode currentMode, const IMarket * market, const CGHeroInstance * hero) { auto isButtonVisible = [currentMode, market, hero](EMarketMode modeButton) -> bool @@ -123,6 +114,11 @@ void CMarketWindow::createChangeModeButtons(EMarketMode currentMode, const IMark if(!market->allowsTrade(modeButton)) return false; + if(currentMode == EMarketMode::ARTIFACT_EXP && modeButton != EMarketMode::CREATURE_EXP) + return false; + if(currentMode == EMarketMode::CREATURE_EXP && modeButton != EMarketMode::ARTIFACT_EXP) + return false; + if(modeButton == EMarketMode::RESOURCE_RESOURCE || modeButton == EMarketMode::RESOURCE_PLAYER) { if(const auto town = dynamic_cast(market)) @@ -187,33 +183,50 @@ void CMarketWindow::initWidgetInternals(const EMarketMode mode, const std::pair< redraw(); } +std::string CMarketWindow::getMarketTitle(const ObjectInstanceID marketId, const EMarketMode mode) const +{ + assert(LOCPLINT->cb->getMarket(marketId)); + assert(vstd::contains(LOCPLINT->cb->getMarket(marketId)->availableModes(), mode)); + + if(const auto town = LOCPLINT->cb->getTown(marketId)) + { + for(const auto & buildingId : town->getBuildings()) + { + if(const auto building = town->getTown()->buildings.at(buildingId); vstd::contains(building->marketModes, mode)) + return building->getNameTranslated(); + } + } + return LOCPLINT->cb->getObj(marketId)->getObjectName(); +} + void CMarketWindow::createArtifactsBuying(const IMarket * market, const CGHeroInstance * hero) { - OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255 - DISPOSE); + OBJECT_CONSTRUCTION; background = createBg(ImagePath::builtin("TPMRKABS.bmp"), PLAYER_COLORED); - marketWidget = std::make_shared(market, hero); + marketWidget = std::make_shared(market, hero, getMarketTitle(market->getObjInstanceID(), EMarketMode::RESOURCE_ARTIFACT)); initWidgetInternals(EMarketMode::RESOURCE_ARTIFACT, CGI->generaltexth->zelp[600]); } void CMarketWindow::createArtifactsSelling(const IMarket * market, const CGHeroInstance * hero) { - OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255 - DISPOSE); + OBJECT_CONSTRUCTION; background = createBg(ImagePath::builtin("TPMRKASS.bmp"), PLAYER_COLORED); // Create image that copies part of background containing slot MISC_1 into position of slot MISC_5 artSlotBack = std::make_shared(background->getSurface(), Rect(20, 187, 47, 47), 0, 0); artSlotBack->moveTo(pos.topLeft() + Point(18, 339)); - auto artsSellingMarket = std::make_shared(market, hero); + auto artsSellingMarket = std::make_shared(market, hero, getMarketTitle(market->getObjInstanceID(), EMarketMode::ARTIFACT_RESOURCE)); artSets.clear(); - addSetAndCallbacks(artsSellingMarket->getAOHset()); + const auto heroArts = artsSellingMarket->getAOHset(); + addSet(heroArts); marketWidget = artsSellingMarket; - initWidgetInternals(EMarketMode::ARTIFACT_RESOURCE, CGI->generaltexth->zelp[600]); + initWidgetInternals(EMarketMode::ARTIFACT_RESOURCE, CGI->generaltexth->zelp[600]); } void CMarketWindow::createMarketResources(const IMarket * market, const CGHeroInstance * hero) { - OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255 - DISPOSE); + OBJECT_CONSTRUCTION; background = createBg(ImagePath::builtin("TPMRKRES.bmp"), PLAYER_COLORED); marketWidget = std::make_shared(market, hero); @@ -222,7 +235,7 @@ void CMarketWindow::createMarketResources(const IMarket * market, const CGHeroIn void CMarketWindow::createFreelancersGuild(const IMarket * market, const CGHeroInstance * hero) { - OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255 - DISPOSE); + OBJECT_CONSTRUCTION; background = createBg(ImagePath::builtin("TPMRKCRS.bmp"), PLAYER_COLORED); marketWidget = std::make_shared(market, hero); @@ -231,7 +244,7 @@ void CMarketWindow::createFreelancersGuild(const IMarket * market, const CGHeroI void CMarketWindow::createTransferResources(const IMarket * market, const CGHeroInstance * hero) { - OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255 - DISPOSE); + OBJECT_CONSTRUCTION; background = createBg(ImagePath::builtin("TPMRKPTS.bmp"), PLAYER_COLORED); marketWidget = std::make_shared(market, hero); @@ -240,24 +253,37 @@ void CMarketWindow::createTransferResources(const IMarket * market, const CGHero void CMarketWindow::createAltarArtifacts(const IMarket * market, const CGHeroInstance * hero) { - OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255 - DISPOSE); + OBJECT_CONSTRUCTION; background = createBg(ImagePath::builtin("ALTRART2.bmp"), PLAYER_COLORED); - auto altarArtifacts = std::make_shared(market, hero); - marketWidget = altarArtifacts; + auto altarArtifactsStorage = std::make_shared(market, hero); + marketWidget = altarArtifactsStorage; artSets.clear(); - addSetAndCallbacks(altarArtifacts->getAOHset()); + const auto heroArts = altarArtifactsStorage->getAOHset(); + heroArts->clickPressedCallback = [this, heroArts](const CArtPlace & artPlace, const Point & cursorPosition) + { + clickPressedOnArtPlace(heroArts->getHero(), artPlace.slot, true, true, false, cursorPosition); + }; + heroArts->showPopupCallback = [this, heroArts](CArtPlace & artPlace, const Point & cursorPosition) + { + showArtifactAssembling(*heroArts, artPlace, cursorPosition); + }; + heroArts->gestureCallback = [this, heroArts](const CArtPlace & artPlace, const Point & cursorPosition) + { + showQuickBackpackWindow(heroArts->getHero(), artPlace.slot, cursorPosition); + }; + addSet(heroArts); initWidgetInternals(EMarketMode::ARTIFACT_EXP, CGI->generaltexth->zelp[568]); - updateHero(); - quitButton->addCallback([altarArtifacts](){altarArtifacts->putBackArtifacts();}); + updateExperience(); + quitButton->addCallback([altarArtifactsStorage](){altarArtifactsStorage->putBackArtifacts();}); } void CMarketWindow::createAltarCreatures(const IMarket * market, const CGHeroInstance * hero) { - OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255 - DISPOSE); + OBJECT_CONSTRUCTION; background = createBg(ImagePath::builtin("ALTARMON.bmp"), PLAYER_COLORED); marketWidget = std::make_shared(market, hero); initWidgetInternals(EMarketMode::CREATURE_EXP, CGI->generaltexth->zelp[568]); - updateHero(); + updateExperience(); } diff --git a/client/windows/CMarketWindow.h b/client/windows/CMarketWindow.h index e6b57f2a7..27b1026bc 100644 --- a/client/windows/CMarketWindow.h +++ b/client/windows/CMarketWindow.h @@ -12,22 +12,22 @@ #include "../widgets/markets/CMarketBase.h" #include "CWindowWithArtifacts.h" -class CMarketWindow : public CStatusbarWindow, public CWindowWithArtifacts, public IGarrisonHolder +class CMarketWindow final : public CStatusbarWindow, public CWindowWithArtifacts, public IGarrisonHolder, public IMarketHolder { public: CMarketWindow(const IMarket * market, const CGHeroInstance * hero, const std::function & onWindowClosed, EMarketMode mode); - void updateResource(); - void updateArtifacts(); + void updateResources() override; + void updateArtifacts() override; void updateGarrisons() override; - void updateHero(); + void updateExperience() override; + void update() override; void close() override; bool holdsGarrison(const CArmedInstance * army) override; - void artifactRemoved(const ArtifactLocation & artLoc) override; - void artifactMoved(const ArtifactLocation & srcLoc, const ArtifactLocation & destLoc, bool withRedraw) override; private: void createChangeModeButtons(EMarketMode currentMode, const IMarket * market, const CGHeroInstance * hero); void initWidgetInternals(const EMarketMode mode, const std::pair & quitButtonHelpContainer); + std::string getMarketTitle(const ObjectInstanceID marketId, const EMarketMode mode) const; void createArtifactsBuying(const IMarket * market, const CGHeroInstance * hero); void createArtifactsSelling(const IMarket * market, const CGHeroInstance * hero); diff --git a/client/windows/CMessage.cpp b/client/windows/CMessage.cpp index fa575c7e6..ed7f09fe3 100644 --- a/client/windows/CMessage.cpp +++ b/client/windows/CMessage.cpp @@ -11,8 +11,6 @@ #include "StdInc.h" #include "CMessage.h" -#include "../../lib/TextOperations.h" - #include "../gui/CGuiHandler.h" #include "../render/CAnimation.h" #include "../render/Canvas.h" @@ -27,6 +25,8 @@ #include "../widgets/TextControls.h" #include "../windows/InfoWindows.h" +#include "../../lib/texts/TextOperations.h" + constexpr int RIGHT_CLICK_POPUP_MIN_SIZE = 100; constexpr int SIDE_MARGIN = 11; constexpr int TOP_MARGIN = 20; @@ -41,8 +41,7 @@ void CMessage::init() { for(int i = 0; i < PlayerColor::PLAYER_LIMIT_I; i++) { - dialogBorders[i] = GH.renderHandler().loadAnimation(AnimationPath::builtin("DIALGBOX")); - dialogBorders[i]->preload(); + dialogBorders[i] = GH.renderHandler().loadAnimation(AnimationPath::builtin("DIALGBOX"), EImageBlitMode::COLORKEY); for(int j = 0; j < dialogBorders[i]->size(0); j++) { @@ -71,23 +70,24 @@ std::vector CMessage::breakText(std::string text, size_t maxLineWid boost::algorithm::trim_right_if(text, boost::algorithm::is_any_of(std::string(" "))); + const auto & fontPtr = GH.renderHandler().loadFont(font); + // each iteration generates one output line while(text.length()) { - ui32 lineWidth = 0; //in characters or given char metric ui32 wordBreak = -1; //last position for line break (last space character) ui32 currPos = 0; //current position in text bool opened = false; //set to true when opening brace is found std::string color; //color found size_t symbolSize = 0; // width of character, in bytes - size_t glyphWidth = 0; // width of printable glyph, pixels + + std::string printableString; // loops till line is full or end of text reached - while(currPos < text.length() && text[currPos] != 0x0a && lineWidth < maxLineWidth) + while(currPos < text.length() && text[currPos] != 0x0a && fontPtr->getStringWidth(printableString) <= maxLineWidth) { symbolSize = TextOperations::getUnicodeCharacterSize(text[currPos]); - glyphWidth = graphics->fonts[font]->getGlyphWidth(text.data() + currPos); // candidate for line break if(ui8(text[currPos]) <= ui8(' ')) @@ -117,15 +117,23 @@ std::vector CMessage::breakText(std::string text, size_t maxLineWid color = ""; } else - lineWidth += glyphWidth; + printableString.append(text.data() + currPos, symbolSize); currPos += symbolSize; } - // long line, create line break + // not all line has been processed - it turned out to be too long, so erase everything after last word break + // if string consists from a single word (or this is Chinese/Korean) - erase only last symbol to bring line back to allowed length if(currPos < text.length() && (text[currPos] != 0x0a)) { if(wordBreak != ui32(-1)) + { currPos = wordBreak; + if(boost::count(text.substr(0, currPos), '{') == boost::count(text.substr(0, currPos), '}')) + { + opened = false; + color = ""; + } + } else currPos -= symbolSize; } @@ -176,9 +184,9 @@ std::vector CMessage::breakText(std::string text, size_t maxLineWid std::string CMessage::guessHeader(const std::string & msg) { size_t begin = 0; - std::string delimeters = "{}"; - size_t start = msg.find_first_of(delimeters[0], begin); - size_t end = msg.find_first_of(delimeters[1], start); + std::string delimiters = "{}"; + size_t start = msg.find_first_of(delimiters[0], begin); + size_t end = msg.find_first_of(delimiters[1], start); if(start > msg.size() || end > msg.size()) return ""; return msg.substr(begin, end); @@ -186,9 +194,9 @@ std::string CMessage::guessHeader(const std::string & msg) int CMessage::guessHeight(const std::string & txt, int width, EFonts font) { - const auto f = graphics->fonts[font]; + const auto & fontPtr = GH.renderHandler().loadFont(font); const auto lines = CMessage::breakText(txt, width, font); - size_t lineHeight = f->getLineHeight(); + size_t lineHeight = fontPtr->getLineHeight(); return lineHeight * lines.size(); } diff --git a/client/windows/CPuzzleWindow.cpp b/client/windows/CPuzzleWindow.cpp index 8b5f70edb..35beae3b0 100644 --- a/client/windows/CPuzzleWindow.cpp +++ b/client/windows/CPuzzleWindow.cpp @@ -11,20 +11,21 @@ #include "CPuzzleWindow.h" #include "../CGameInfo.h" -#include "../CMusicHandler.h" #include "../CPlayerInterface.h" #include "../adventureMap/CResDataBar.h" #include "../gui/CGuiHandler.h" #include "../gui/TextAlignment.h" #include "../gui/Shortcut.h" #include "../mapView/MapView.h" +#include "../media/ISoundPlayer.h" #include "../widgets/Buttons.h" #include "../widgets/Images.h" #include "../widgets/TextControls.h" #include "../../CCallback.h" -#include "../../lib/CGeneralTextHandler.h" -#include "../../lib/CTownHandler.h" +#include "../../lib/entities/faction/CFaction.h" +#include "../../lib/entities/faction/CTownHandler.h" +#include "../../lib/texts/CGeneralTextHandler.h" #include "../../lib/StartInfo.h" CPuzzleWindow::CPuzzleWindow(const int3 & GrailPos, double discoveredRatio) @@ -32,7 +33,7 @@ CPuzzleWindow::CPuzzleWindow(const int3 & GrailPos, double discoveredRatio) grailPos(GrailPos), currentAlpha(ColorRGBA::ALPHA_OPAQUE) { - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + OBJECT_CONSTRUCTION; CCS->soundh->playSound(soundBase::OBELISK); @@ -53,7 +54,7 @@ CPuzzleWindow::CPuzzleWindow(const int3 & GrailPos, double discoveredRatio) { const SPuzzleInfo & info = elem; - auto piece = std::make_shared(info.filename, info.x, info.y); + auto piece = std::make_shared(info.filename, info.position.x, info.position.y); //piece that will slowly disappear if(info.whenUncovered <= GameConstants::PUZZLE_MAP_PIECES * discoveredRatio) diff --git a/client/windows/CQuestLog.cpp b/client/windows/CQuestLog.cpp index bb7b37c04..dbc39d439 100644 --- a/client/windows/CQuestLog.cpp +++ b/client/windows/CQuestLog.cpp @@ -27,8 +27,7 @@ #include "../../lib/CArtHandler.h" #include "../../lib/CConfigHandler.h" #include "../../lib/gameState/QuestInfo.h" -#include "../../lib/CGeneralTextHandler.h" -#include "../../lib/MetaString.h" +#include "../../lib/texts/CGeneralTextHandler.h" #include "../../lib/mapObjects/CQuest.h" VCMI_LIB_NAMESPACE_BEGIN @@ -74,12 +73,12 @@ CQuestMinimap::CQuestMinimap(const Rect & position) void CQuestMinimap::addQuestMarks (const QuestInfo * q) { - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + OBJECT_CONSTRUCTION; icons.clear(); int3 tile; if (q->obj) - tile = q->obj->pos; + tile = q->obj->visitablePos(); else tile = q->tile; @@ -105,7 +104,7 @@ void CQuestMinimap::update() void CQuestMinimap::iconClicked() { if(currentQuest->obj) - adventureInt->centerOnTile(currentQuest->obj->pos); + adventureInt->centerOnTile(currentQuest->obj->visitablePos()); //moveAdvMapSelection(); } @@ -123,13 +122,13 @@ CQuestLog::CQuestLog (const std::vector & Quests) hideComplete(false), quests(Quests) { - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + OBJECT_CONSTRUCTION; minimap = std::make_shared(Rect(12, 12, 169, 169)); // TextBox have it's own 4 pixel padding from top at least for English. To achieve 10px from both left and top only add 6px margin description = std::make_shared("", Rect(205, 18, 385, DESCRIPTION_HEIGHT_MAX), CSlider::BROWN, FONT_MEDIUM, ETextAlignment::TOPLEFT, Colors::WHITE); ok = std::make_shared(Point(539, 398), AnimationPath::builtin("IOKAY.DEF"), CGI->generaltexth->zelp[445], std::bind(&CQuestLog::close, this), EShortcut::GLOBAL_RETURN); - // Both button and lable are shifted to -2px by x and y to not make them actually look like they're on same line with quests list and ok button + // Both button and label are shifted to -2px by x and y to not make them actually look like they're on same line with quests list and ok button hideCompleteButton = std::make_shared(Point(10, 396), AnimationPath::builtin("sysopchk.def"), CButton::tooltipLocalized("vcmi.questLog.hideComplete"), std::bind(&CQuestLog::toggleComplete, this, _1)); hideCompleteLabel = std::make_shared(46, 398, FONT_MEDIUM, ETextAlignment::TOPLEFT, Colors::WHITE, CGI->generaltexth->translate("vcmi.questLog.hideComplete.hover")); slider = std::make_shared(Point(166, 195), 191, std::bind(&CQuestLog::sliderMoved, this, _1), QUEST_COUNT, 0, 0, Orientation::VERTICAL, CSlider::BROWN); @@ -141,7 +140,7 @@ CQuestLog::CQuestLog (const std::vector & Quests) void CQuestLog::recreateLabelList() { - OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255-DISPOSE); + OBJECT_CONSTRUCTION; labels.clear(); bool completeMissing = true; @@ -293,7 +292,7 @@ void CQuestLog::selectQuest(int which, int labelId) break; }*/ - OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255-DISPOSE); + OBJECT_CONSTRUCTION; std::vector> comps; for(auto & component : components) diff --git a/client/windows/CSpellWindow.cpp b/client/windows/CSpellWindow.cpp index d752c24af..c65171a4d 100644 --- a/client/windows/CSpellWindow.cpp +++ b/client/windows/CSpellWindow.cpp @@ -19,27 +19,25 @@ #include "../CGameInfo.h" #include "../CPlayerInterface.h" #include "../PlayerLocalState.h" -#include "../CVideoHandler.h" #include "../battle/BattleInterface.h" #include "../gui/CGuiHandler.h" #include "../gui/Shortcut.h" #include "../gui/WindowHandler.h" +#include "../media/IVideoPlayer.h" #include "../widgets/GraphicalPrimitiveCanvas.h" #include "../widgets/CComponent.h" #include "../widgets/CTextInput.h" #include "../widgets/TextControls.h" +#include "../widgets/Buttons.h" +#include "../widgets/VideoWidget.h" #include "../adventureMap/AdventureMapInterface.h" -#include "../render/CAnimation.h" -#include "../render/IRenderHandler.h" -#include "../render/IImage.h" -#include "../render/IImageLoader.h" -#include "../render/Canvas.h" +#include "../render/AssetGenerator.h" #include "../../CCallback.h" #include "../../lib/CConfigHandler.h" -#include "../../lib/CGeneralTextHandler.h" +#include "../../lib/texts/CGeneralTextHandler.h" #include "../../lib/spells/CSpellHandler.h" #include "../../lib/spells/ISpellMechanics.h" #include "../../lib/spells/Problem.h" @@ -98,13 +96,15 @@ public: } }; -CSpellWindow::CSpellWindow(const CGHeroInstance * _myHero, CPlayerInterface * _myInt, bool openOnBattleSpells): +CSpellWindow::CSpellWindow(const CGHeroInstance * _myHero, CPlayerInterface * _myInt, bool openOnBattleSpells, const std::function & onSpellSelect): CWindowObject(PLAYER_COLORED | (settings["gameTweaks"]["enableLargeSpellbook"].Bool() ? BORDERED : 0)), battleSpellsOnly(openOnBattleSpells), selectedTab(4), currentPage(0), myHero(_myHero), myInt(_myInt), + openOnBattleSpells(openOnBattleSpells), + onSpellSelect(onSpellSelect), isBigSpellbook(settings["gameTweaks"]["enableLargeSpellbook"].Bool()), spellsPerPage(24), offL(-11), @@ -113,11 +113,12 @@ CSpellWindow::CSpellWindow(const CGHeroInstance * _myHero, CPlayerInterface * _m offT(-37), offB(56) { - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + OBJECT_CONSTRUCTION; if(isBigSpellbook) { - background = std::make_shared(createBigSpellBook(), Point(0, 0)); + AssetGenerator::createBigSpellBook(); + background = std::make_shared(ImagePath::builtin("SpellBookLarge"), 0, 0); updateShadow(); } else @@ -129,9 +130,9 @@ CSpellWindow::CSpellWindow(const CGHeroInstance * _myHero, CPlayerInterface * _m pos = background->center(Point(pos.w/2 + pos.x, pos.h/2 + pos.y)); + Rect r(90, isBigSpellbook ? 480 : 420, isBigSpellbook ? 160 : 110, 16); if(settings["general"]["enableUiEnhancements"].Bool()) { - Rect r(90, isBigSpellbook ? 480 : 420, isBigSpellbook ? 160 : 110, 16); const ColorRGBA rectangleColor = ColorRGBA(0, 0, 0, 75); const ColorRGBA borderColor = ColorRGBA(128, 100, 75); const ColorRGBA grayedColor = ColorRGBA(158, 130, 105); @@ -142,6 +143,13 @@ CSpellWindow::CSpellWindow(const CGHeroInstance * _myHero, CPlayerInterface * _m searchBox->setCallback(std::bind(&CSpellWindow::searchInput, this)); } + if(onSpellSelect) + { + Point boxPos = r.bottomLeft() + Point(-2, 5); + showAllSpells = std::make_shared(boxPos, AnimationPath::builtin("sysopchk.def"), CButton::tooltip(CGI->generaltexth->translate("core.help.458.hover"), CGI->generaltexth->translate("core.help.458.hover")), [this](bool state){ searchInput(); }); + showAllSpellsDescription = std::make_shared(boxPos.x + 40, boxPos.y + 12, FONT_SMALL, ETextAlignment::CENTERLEFT, Colors::WHITE, CGI->generaltexth->translate("core.help.458.hover")); + } + processSpells(); //numbers of spell pages computed @@ -149,18 +157,9 @@ CSpellWindow::CSpellWindow(const CGHeroInstance * _myHero, CPlayerInterface * _m leftCorner = std::make_shared(ImagePath::builtin("SpelTrnL.bmp"), 97 + offL, 77 + offT); rightCorner = std::make_shared(ImagePath::builtin("SpelTrnR.bmp"), 487 + offR, 72 + offT); - spellIcons = GH.renderHandler().loadAnimation(AnimationPath::builtin("Spells")); - schoolTab = std::make_shared(AnimationPath::builtin("SpelTab"), selectedTab, 0, 524 + offR, 88); schoolPicture = std::make_shared(AnimationPath::builtin("Schools"), 0, 0, 117 + offL, 74 + offT); - schoolBorders[0] = GH.renderHandler().loadAnimation(AnimationPath::builtin("SplevA.def")); - schoolBorders[1] = GH.renderHandler().loadAnimation(AnimationPath::builtin("SplevF.def")); - schoolBorders[2] = GH.renderHandler().loadAnimation(AnimationPath::builtin("SplevW.def")); - schoolBorders[3] = GH.renderHandler().loadAnimation(AnimationPath::builtin("SplevE.def")); - - for(auto item : schoolBorders) - item->preload(); mana = std::make_shared(435 + (isBigSpellbook ? 159 : 0), 426 + offB, FONT_SMALL, ETextAlignment::CENTER, Colors::YELLOW, std::to_string(myHero->mana)); if(isBigSpellbook) @@ -207,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); @@ -221,55 +220,6 @@ CSpellWindow::~CSpellWindow() { } -std::shared_ptr CSpellWindow::createBigSpellBook() -{ - std::shared_ptr img = GH.renderHandler().loadImage(ImagePath::builtin("SpelBack")); - Canvas canvas = Canvas(Point(800, 600)); - // edges - canvas.draw(img, Point(0, 0), Rect(15, 38, 90, 45)); - canvas.draw(img, Point(0, 460), Rect(15, 400, 90, 141)); - canvas.draw(img, Point(705, 0), Rect(509, 38, 95, 45)); - canvas.draw(img, Point(705, 460), Rect(509, 400, 95, 141)); - // left / right - Canvas tmp1 = Canvas(Point(90, 355 - 45)); - tmp1.draw(img, Point(0, 0), Rect(15, 38 + 45, 90, 355 - 45)); - canvas.drawScaled(tmp1, Point(0, 45), Point(90, 415)); - Canvas tmp2 = Canvas(Point(95, 355 - 45)); - tmp2.draw(img, Point(0, 0), Rect(509, 38 + 45, 95, 355 - 45)); - canvas.drawScaled(tmp2, Point(705, 45), Point(95, 415)); - // top / bottom - Canvas tmp3 = Canvas(Point(409, 45)); - tmp3.draw(img, Point(0, 0), Rect(100, 38, 409, 45)); - canvas.drawScaled(tmp3, Point(90, 0), Point(615, 45)); - Canvas tmp4 = Canvas(Point(409, 141)); - tmp4.draw(img, Point(0, 0), Rect(100, 400, 409, 141)); - canvas.drawScaled(tmp4, Point(90, 460), Point(615, 141)); - // middle - Canvas tmp5 = Canvas(Point(409, 141)); - tmp5.draw(img, Point(0, 0), Rect(100, 38 + 45, 509 - 15, 400 - 38)); - canvas.drawScaled(tmp5, Point(90, 45), Point(615, 415)); - // carpet - Canvas tmp6 = Canvas(Point(590, 59)); - tmp6.draw(img, Point(0, 0), Rect(15, 484, 590, 59)); - canvas.drawScaled(tmp6, Point(0, 545), Point(800, 59)); - // remove bookmarks - for (int i = 0; i < 56; i++) - canvas.draw(Canvas(canvas, Rect(i < 30 ? 268 : 327, 464, 1, 46)), Point(269 + i, 464)); - for (int i = 0; i < 56; i++) - canvas.draw(Canvas(canvas, Rect(469, 464, 1, 42)), Point(470 + i, 464)); - for (int i = 0; i < 57; i++) - canvas.draw(Canvas(canvas, Rect(i < 30 ? 564 : 630, 464, 1, 44)), Point(565 + i, 464)); - for (int i = 0; i < 56; i++) - canvas.draw(Canvas(canvas, Rect(656, 464, 1, 47)), Point(657 + i, 464)); - // draw bookmarks - canvas.draw(img, Point(278, 464), Rect(220, 405, 37, 47)); - canvas.draw(img, Point(481, 465), Rect(354, 406, 37, 41)); - canvas.draw(img, Point(575, 465), Rect(417, 406, 37, 45)); - canvas.draw(img, Point(667, 465), Rect(478, 406, 37, 47)); - - return GH.renderHandler().createImage(canvas.getInternalSurface()); -} - void CSpellWindow::searchInput() { if(searchBox) @@ -290,11 +240,19 @@ void CSpellWindow::processSpells() //initializing castable spells mySpells.reserve(CGI->spellh->objects.size()); - for(const CSpell * spell : CGI->spellh->objects) + for(auto const & spell : CGI->spellh->objects) { bool searchTextFound = !searchBox || boost::algorithm::contains(boost::algorithm::to_lower_copy(spell->getNameTranslated()), boost::algorithm::to_lower_copy(searchBox->getText())); - if(!spell->isCreatureAbility() && myHero->canCastThisSpell(spell) && searchTextFound) - mySpells.push_back(spell); + + if(onSpellSelect) + { + if(spell->isCombat() == openOnBattleSpells && !spell->isSpecial() && !spell->isCreatureAbility() && searchTextFound && (showAllSpells->isSelected() || myHero->canCastThisSpell(spell.get()))) + mySpells.push_back(spell.get()); + continue; + } + + if(!spell->isCreatureAbility() && myHero->canCastThisSpell(spell.get()) && searchTextFound) + mySpells.push_back(spell.get()); } SpellbookSpellSorter spellsorter; @@ -356,8 +314,21 @@ 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); close(); } @@ -425,6 +396,8 @@ void CSpellWindow::fRcornerb() void CSpellWindow::show(Canvas & to) { + if(video) + video->show(to); statusBar->show(to); } @@ -523,14 +496,22 @@ void CSpellWindow::setCurrentPage(int value) void CSpellWindow::turnPageLeft() { + OBJECT_CONSTRUCTION; if(settings["video"]["spellbookAnimation"].Bool() && !isBigSpellbook) - CCS->videoh->openAndPlayVideo(VideoPath::builtin("PGTRNLFT.SMK"), pos.x+13, pos.y+15, EVideoType::SPELLBOOK); + video = std::make_shared(Point(13, 14), VideoPath::builtin("PGTRNLFT.SMK"), false, this); } void CSpellWindow::turnPageRight() { + OBJECT_CONSTRUCTION; if(settings["video"]["spellbookAnimation"].Bool() && !isBigSpellbook) - CCS->videoh->openAndPlayVideo(VideoPath::builtin("PGTRNRGH.SMK"), pos.x+13, pos.y+15, EVideoType::SPELLBOOK); + video = std::make_shared(Point(13, 14), VideoPath::builtin("PGTRNRGH.SMK"), false, this); +} + +void CSpellWindow::onVideoPlaybackFinished() +{ + video.reset(); + redraw(); } void CSpellWindow::keyPressed(EShortcut key) @@ -583,9 +564,9 @@ CSpellWindow::SpellArea::SpellArea(Rect pos, CSpellWindow * owner) schoolLevel = -1; mySpell = nullptr; - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + OBJECT_CONSTRUCTION; - image = std::make_shared(owner->spellIcons, 0, 0); + image = std::make_shared(AnimationPath::builtin("Spells"), 0, 0); image->visible = false; name = std::make_shared(39, 70, FONT_TINY, ETextAlignment::CENTER); @@ -602,6 +583,13 @@ void CSpellWindow::SpellArea::clickPressed(const Point & cursorPosition) { if(mySpell) { + if(owner->onSpellSelect) + { + owner->onSpellSelect(mySpell->id); + owner->close(); + return; + } + auto spellCost = owner->myInt->cb->getSpellCost(mySpell, owner->myHero); if(spellCost > owner->myHero->mana) //insufficient mana { @@ -652,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; @@ -726,19 +716,27 @@ void CSpellWindow::SpellArea::setSpell(const CSpell * spell) image->visible = true; { - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + OBJECT_CONSTRUCTION; + + static const std::array schoolBorders = { + AnimationPath::builtin("SplevA.def"), + AnimationPath::builtin("SplevF.def"), + AnimationPath::builtin("SplevW.def"), + AnimationPath::builtin("SplevE.def") + }; + schoolBorder.reset(); if (owner->selectedTab >= 4) { if (whichSchool.getNum() != SpellSchool()) - schoolBorder = std::make_shared(owner->schoolBorders.at(whichSchool.getNum()), schoolLevel); + schoolBorder = std::make_shared(schoolBorders.at(whichSchool.getNum()), schoolLevel); } else - schoolBorder = std::make_shared(owner->schoolBorders.at(owner->selectedTab), schoolLevel); + schoolBorder = std::make_shared(schoolBorders.at(owner->selectedTab), schoolLevel); } ColorRGBA firstLineColor, secondLineColor; - if(spellCost > owner->myHero->mana) //hero cannot cast this spell + if(spellCost > owner->myHero->mana && !owner->onSpellSelect) //hero cannot cast this spell { firstLineColor = Colors::WHITE; secondLineColor = Colors::ORANGE; diff --git a/client/windows/CSpellWindow.h b/client/windows/CSpellWindow.h index 16401a752..6f5eeb719 100644 --- a/client/windows/CSpellWindow.h +++ b/client/windows/CSpellWindow.h @@ -10,6 +10,7 @@ #pragma once #include "CWindowObject.h" +#include "../widgets/IVideoHolder.h" VCMI_LIB_NAMESPACE_BEGIN @@ -19,7 +20,6 @@ class CSpell; VCMI_LIB_NAMESPACE_END class IImage; -class CAnimation; class CAnimImage; class CPicture; class CLabel; @@ -28,9 +28,11 @@ class CPlayerInterface; class CSpellWindow; class CTextInput; class TransparentFilledRectangle; +class CToggleButton; +class VideoWidgetOnce; /// The spell window -class CSpellWindow : public CWindowObject +class CSpellWindow : public CWindowObject, public IVideoHolder { class SpellArea : public CIntObject { @@ -67,9 +69,6 @@ class CSpellWindow : public CWindowObject InteractiveArea(const Rect &myRect, std::function funcL, int helpTextId, CSpellWindow * _owner); }; - std::shared_ptr spellIcons; - std::array, 4> schoolBorders; //[0]: air, [1]: fire, [2]: water, [3]: earth - std::shared_ptr leftCorner; std::shared_ptr rightCorner; @@ -86,6 +85,11 @@ class CSpellWindow : public CWindowObject std::shared_ptr searchBoxRectangle; std::shared_ptr searchBoxDescription; + std::shared_ptr showAllSpells; + std::shared_ptr showAllSpellsDescription; + + std::shared_ptr video; + bool isBigSpellbook; int spellsPerPage; int offL; @@ -113,10 +117,13 @@ class CSpellWindow : public CWindowObject void turnPageLeft(); void turnPageRight(); - std::shared_ptr createBigSpellBook(); + void onVideoPlaybackFinished() override; + + bool openOnBattleSpells; + std::function onSpellSelect; //external processing of selected spell public: - CSpellWindow(const CGHeroInstance * _myHero, CPlayerInterface * _myInt, bool openOnBattleSpells = true); + CSpellWindow(const CGHeroInstance * _myHero, CPlayerInterface * _myInt, bool openOnBattleSpells = true, const std::function & onSpellSelect = nullptr); ~CSpellWindow(); void fexitb(); diff --git a/client/windows/CTutorialWindow.cpp b/client/windows/CTutorialWindow.cpp index 884cf6960..59d1ca451 100644 --- a/client/windows/CTutorialWindow.cpp +++ b/client/windows/CTutorialWindow.cpp @@ -13,10 +13,9 @@ #include "../eventsSDL/InputHandler.h" #include "../../lib/CConfigHandler.h" #include "../ConditionalWait.h" -#include "../../lib/CGeneralTextHandler.h" +#include "../../lib/texts/CGeneralTextHandler.h" #include "../CPlayerInterface.h" #include "../CGameInfo.h" -#include "../CVideoHandler.h" #include "../gui/CGuiHandler.h" #include "../gui/Shortcut.h" @@ -24,12 +23,13 @@ #include "../widgets/Images.h" #include "../widgets/Buttons.h" #include "../widgets/TextControls.h" +#include "../widgets/VideoWidget.h" #include "../render/Canvas.h" CTutorialWindow::CTutorialWindow(const TutorialMode & m) : CWindowObject(BORDERED, ImagePath::builtin("DIBOXBCK")), mode { m }, page { 0 } { - OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + OBJECT_CONSTRUCTION; pos = Rect(pos.x, pos.y, 380, 400); //video: 320x240 background = std::make_shared(ImagePath::builtin("DIBOXBCK"), Rect(0, 0, pos.w, pos.h)); @@ -54,7 +54,10 @@ CTutorialWindow::CTutorialWindow(const TutorialMode & m) void CTutorialWindow::setContent() { - video = "tutorial/" + videos[page]; + OBJECT_CONSTRUCTION; + auto video = VideoPath::builtin("tutorial/" + videos[page]); + + videoPlayer = std::make_shared(Point(30, 120), video, false); buttonLeft->block(page<1); buttonRight->block(page>videos.size() - 2); @@ -64,7 +67,7 @@ void CTutorialWindow::setContent() void CTutorialWindow::openWindowFirstTime(const TutorialMode & m) { - if(GH.input().hasTouchInputDevice() && !persistentStorage["gui"]["tutorialCompleted" + std::to_string(m)].Bool()) + if(GH.input().getCurrentInputMode() == InputMode::TOUCH && !persistentStorage["gui"]["tutorialCompleted" + std::to_string(m)].Bool()) { if(LOCPLINT) LOCPLINT->showingDialog->setBusy(); @@ -98,26 +101,3 @@ void CTutorialWindow::previous() deactivate(); activate(); } - -void CTutorialWindow::show(Canvas & to) -{ - CCS->videoh->update(pos.x + 30, pos.y + 120, to.getInternalSurface(), true, false, - [&]() - { - CCS->videoh->close(); - CCS->videoh->open(VideoPath::builtin(video)); - }); - - CIntObject::show(to); -} - -void CTutorialWindow::activate() -{ - CCS->videoh->open(VideoPath::builtin(video)); - CIntObject::activate(); -} - -void CTutorialWindow::deactivate() -{ - CCS->videoh->close(); -} diff --git a/client/windows/CTutorialWindow.h b/client/windows/CTutorialWindow.h index d002f9aed..c4fcf31f9 100644 --- a/client/windows/CTutorialWindow.h +++ b/client/windows/CTutorialWindow.h @@ -15,6 +15,7 @@ class CFilledTexture; class CButton; class CLabel; class CMultiLineLabel; +class VideoWidget; enum TutorialMode { @@ -33,8 +34,8 @@ class CTutorialWindow : public CWindowObject std::shared_ptr labelTitle; std::shared_ptr labelInformation; + std::shared_ptr videoPlayer; - std::string video; std::vector videos; int page; @@ -47,8 +48,4 @@ class CTutorialWindow : public CWindowObject public: CTutorialWindow(const TutorialMode & m); static void openWindowFirstTime(const TutorialMode & m); - - void show(Canvas & to) override; - void activate() override; - void deactivate() override; }; diff --git a/client/windows/CWindowObject.cpp b/client/windows/CWindowObject.cpp index 0403c8c91..d60bf3a9d 100644 --- a/client/windows/CWindowObject.cpp +++ b/client/windows/CWindowObject.cpp @@ -20,17 +20,17 @@ #include "../windows/CMessage.h" #include "../renderSDL/SDL_PixelAccess.h" #include "../render/IImage.h" +#include "../render/IScreenHandler.h" #include "../render/IRenderHandler.h" #include "../render/Canvas.h" #include "../CGameInfo.h" #include "../CPlayerInterface.h" -#include "../CMusicHandler.h" #include "../../CCallback.h" #include "../../lib/CConfigHandler.h" -#include "../../lib/CGeneralTextHandler.h" //for Unicode related stuff +#include "../../lib/texts/CGeneralTextHandler.h" //for Unicode related stuff #include @@ -42,8 +42,6 @@ CWindowObject::CWindowObject(int options_, const ImagePath & imageName, Point ce if(!(options & NEEDS_ANIMATED_BACKGROUND)) //currently workaround for highscores (currently uses window as normal control, because otherwise videos are not played in background yet) assert(parent == nullptr); //Safe to remove, but windows should not have parent - defActions = 255-DISPOSE; - if (options & RCLICK_POPUP) CCS->curh->hide(); @@ -64,8 +62,6 @@ CWindowObject::CWindowObject(int options_, const ImagePath & imageName): if(!(options & NEEDS_ANIMATED_BACKGROUND)) //currently workaround for highscores (currently uses window as normal control, because otherwise videos are not played in background yet) assert(parent == nullptr); //Safe to remove, but windows should not have parent - defActions = 255-DISPOSE; - if(options & RCLICK_POPUP) CCS->curh->hide(); @@ -86,7 +82,7 @@ CWindowObject::~CWindowObject() std::shared_ptr CWindowObject::createBg(const ImagePath & imageName, bool playerColored) { - OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255-DISPOSE); + OBJECT_CONSTRUCTION; if(imageName.empty()) return nullptr; @@ -94,13 +90,13 @@ std::shared_ptr CWindowObject::createBg(const ImagePath & imageName, b auto image = std::make_shared(imageName); image->getSurface()->setBlitMode(EImageBlitMode::OPAQUE); if(playerColored) - image->colorize(LOCPLINT->playerID); + image->setPlayerColor(LOCPLINT->playerID); return image; } void CWindowObject::setBackground(const ImagePath & filename) { - OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255-DISPOSE); + OBJECT_CONSTRUCTION; background = createBg(filename, options & PLAYER_COLORED); @@ -120,7 +116,8 @@ void CWindowObject::updateShadow() void CWindowObject::setShadow(bool on) { //size of shadow - static const int size = 8; + int sizeOriginal = 8; + int size = sizeOriginal * GH.screenHandler().getScalingFactor(); if(on == !shadowParts.empty()) return; @@ -185,9 +182,9 @@ void CWindowObject::setShadow(bool on) //FIXME: do something with this points Point shadowStart; if (options & BORDERED) - shadowStart = Point(size - 14, size - 14); + shadowStart = Point(sizeOriginal - 14, sizeOriginal - 14); else - shadowStart = Point(size, size); + shadowStart = Point(sizeOriginal, sizeOriginal); Point shadowPos; if (options & BORDERED) @@ -203,15 +200,15 @@ void CWindowObject::setShadow(bool on) //create base 8x8 piece of shadow SDL_Surface * shadowCorner = CSDL_Ext::copySurface(shadowCornerTempl); - SDL_Surface * shadowBottom = CSDL_Ext::scaleSurfaceFast(shadowBottomTempl, fullsize.x - size, size); - SDL_Surface * shadowRight = CSDL_Ext::scaleSurfaceFast(shadowRightTempl, size, fullsize.y - size); + SDL_Surface * shadowBottom = CSDL_Ext::scaleSurface(shadowBottomTempl, (fullsize.x - sizeOriginal) * GH.screenHandler().getScalingFactor(), size); + SDL_Surface * shadowRight = CSDL_Ext::scaleSurface(shadowRightTempl, size, (fullsize.y - sizeOriginal) * GH.screenHandler().getScalingFactor()); blitAlphaCol(shadowBottom, 0); blitAlphaRow(shadowRight, 0); //generate "shadow" object with these 3 pieces in it { - OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255-DISPOSE); + OBJECT_CONSTRUCTION; shadowParts.push_back(std::make_shared( GH.renderHandler().createImage(shadowCorner), Point(shadowPos.x, shadowPos.y))); shadowParts.push_back(std::make_shared( GH.renderHandler().createImage(shadowRight ), Point(shadowPos.x, shadowStart.y))); diff --git a/client/windows/CWindowObject.h b/client/windows/CWindowObject.h index feecc1243..807c20272 100644 --- a/client/windows/CWindowObject.h +++ b/client/windows/CWindowObject.h @@ -42,7 +42,7 @@ public: }; /* - * options - EOpions enum + * options - EOptions enum * imageName - name for background image, can be empty * centerAt - position of window center. Default - center of the screen */ diff --git a/client/windows/CWindowWithArtifacts.cpp b/client/windows/CWindowWithArtifacts.cpp index 5efc8b61c..67afb0c1c 100644 --- a/client/windows/CWindowWithArtifacts.cpp +++ b/client/windows/CWindowWithArtifacts.cpp @@ -29,7 +29,7 @@ #include "../CGameInfo.h" #include "../../lib/ArtifactUtils.h" -#include "../../lib/CGeneralTextHandler.h" +#include "../../lib/texts/CGeneralTextHandler.h" #include "../../lib/mapObjects/CGHeroInstance.h" #include "../../lib/networkPacks/ArtifactLocation.h" #include "../../lib/CConfigHandler.h" @@ -42,302 +42,113 @@ CWindowWithArtifacts::CWindowWithArtifacts(const std::vectorartSets.insert(this->artSets.end(), artSets->begin(), artSets->end()); } -void CWindowWithArtifacts::addSet(CArtifactsOfHeroPtr newArtSet) +void CWindowWithArtifacts::addSet(const std::shared_ptr & newArtSet) { artSets.emplace_back(newArtSet); } -void CWindowWithArtifacts::addSetAndCallbacks(CArtifactsOfHeroPtr newArtSet) +const CGHeroInstance * CWindowWithArtifacts::getHeroPickedArtifact() const { - addSet(newArtSet); - std::visit([this](auto artSetWeak) + const CGHeroInstance * hero = nullptr; + + for(const auto & artSet : artSets) + if(const auto pickedArt = artSet->getHero()->getArt(ArtifactPosition::TRANSITION_POS)) { - auto artSet = artSetWeak.lock(); - artSet->clickPressedCallback = std::bind(&CWindowWithArtifacts::clickPressedArtPlaceHero, this, _1, _2, _3); - artSet->showPopupCallback = std::bind(&CWindowWithArtifacts::showPopupArtPlaceHero, this, _1, _2, _3); - artSet->gestureCallback = std::bind(&CWindowWithArtifacts::gestureArtPlaceHero, this, _1, _2, _3); - }, newArtSet); + hero = artSet->getHero(); + break; + } + return hero; } -void CWindowWithArtifacts::addCloseCallback(const CloseCallback & callback) +const CArtifactInstance * CWindowWithArtifacts::getPickedArtifact() const { - closeCallback = callback; + for(const auto & artSet : artSets) + if(const auto pickedArt = artSet->getHero()->getArt(ArtifactPosition::TRANSITION_POS)) + { + return pickedArt; + } + return nullptr; } -const CGHeroInstance * CWindowWithArtifacts::getHeroPickedArtifact() +void CWindowWithArtifacts::clickPressedOnArtPlace(const CGHeroInstance * hero, const ArtifactPosition & slot, + bool allowExchange, bool altarTrading, bool closeWindow, const Point & cursorPosition) { - auto res = getState(); - if(res.has_value()) - return std::get(res.value()); - else - return nullptr; -} - -const CArtifactInstance * CWindowWithArtifacts::getPickedArtifact() -{ - auto res = getState(); - if(res.has_value()) - return std::get(res.value()); - else - return nullptr; -} - -void CWindowWithArtifacts::clickPressedArtPlaceHero(const CArtifactsOfHeroBase & artsInst, CArtPlace & artPlace, const Point & cursorPosition) -{ - const auto currentArtSet = findAOHbyRef(artsInst); - assert(currentArtSet.has_value()); - - if(artPlace.isLocked()) + if(!LOCPLINT->makingTurn) + return; + if(hero == nullptr) return; - if (!LOCPLINT->makingTurn) - return; - - std::visit( - [this, &artPlace](auto artSetWeak) -> void + if(const auto heroArtOwner = getHeroPickedArtifact()) + { + if(allowExchange || hero->id == heroArtOwner->id) + putPickedArtifact(*hero, slot); + } + else if(GH.isKeyboardShiftDown()) + { + showQuickBackpackWindow(hero, slot, cursorPosition); + } + else if(auto art = hero->getArt(slot)) + { + if(hero->getOwner() == LOCPLINT->playerID) { - const auto artSetPtr = artSetWeak.lock(); - - // Hero(Main, Exchange) window, Kingdom window, Altar window, Backpack window left click handler - if constexpr( - std::is_same_v> || - std::is_same_v> || - std::is_same_v> || - std::is_same_v>) - { - const auto pickedArtInst = getPickedArtifact(); - const auto heroPickedArt = getHeroPickedArtifact(); - const auto hero = artSetPtr->getHero(); - auto isTransferAllowed = false; - std::string msg; - - if(pickedArtInst) + if(checkSpecialArts(*art, *hero, altarTrading)) + onClickPressedCommonArtifact(*hero, slot, closeWindow); + } + else + { + for(const auto & artSlot : ArtifactUtils::unmovableSlots()) + if(slot == artSlot) { - auto srcLoc = ArtifactLocation(heroPickedArt->id, ArtifactPosition::TRANSITION_POS); - auto dstLoc = ArtifactLocation(hero->id, artPlace.slot); - - if(ArtifactUtils::isSlotBackpack(artPlace.slot)) - { - if(pickedArtInst->artType->isBig()) - { - // War machines cannot go to backpack - msg = boost::str(boost::format(CGI->generaltexth->allTexts[153]) % pickedArtInst->artType->getNameTranslated()); - } - else - { - if(ArtifactUtils::isBackpackFreeSlots(heroPickedArt)) - isTransferAllowed = true; - else - msg = CGI->generaltexth->translate("core.genrltxt.152"); - } - } - // Check if artifact transfer is possible - else if(pickedArtInst->canBePutAt(hero, artPlace.slot, true) && (!artPlace.getArt() || hero->tempOwner == LOCPLINT->playerID)) - { - isTransferAllowed = true; - } - if constexpr(std::is_same_v>) - { - if(hero != heroPickedArt) - isTransferAllowed = false; - } - if(isTransferAllowed) - LOCPLINT->cb->swapArtifacts(srcLoc, dstLoc); + LOCPLINT->showInfoDialog(CGI->generaltexth->allTexts[21]); + break; } - else if(auto art = artPlace.getArt()) - { - if(artSetPtr->getHero()->getOwner() == LOCPLINT->playerID) - { - if(checkSpecialArts(*art, hero, std::is_same_v> ? true : false)) - { - assert(artSetPtr->getHero()->getSlotByInstance(art) != ArtifactPosition::PRE_FIRST); - - if(GH.isKeyboardCmdDown()) - { - std::shared_ptr anotherHeroEquipmentPointer = nullptr; - - for(auto set : artSets) - { - if(std::holds_alternative>(set)) - { - std::shared_ptr heroEquipmentPointer = std::get>(set).lock(); - if(heroEquipmentPointer->getHero()->id != artSetPtr->getHero()->id) - { - anotherHeroEquipmentPointer = heroEquipmentPointer; - break; - } - } - } - - if(anotherHeroEquipmentPointer != nullptr) - { - ArtifactPosition availablePosition = ArtifactUtils::getArtAnyPosition(anotherHeroEquipmentPointer->getHero(), art->getTypeId()); - if(availablePosition != ArtifactPosition::PRE_FIRST) - { - LOCPLINT->cb->swapArtifacts(ArtifactLocation(artSetPtr->getHero()->id, artSetPtr->getHero()->getSlotByInstance(art)), - ArtifactLocation(anotherHeroEquipmentPointer->getHero()->id, availablePosition)); - } - } - } - else if(GH.isKeyboardAltDown()) - { - ArtifactPosition destinationPosition = ArtifactPosition::PRE_FIRST; - - if(ArtifactUtils::isSlotEquipment(artPlace.slot)) - { - ArtifactPosition availablePosition = ArtifactUtils::getArtBackpackPosition(artSetPtr->getHero(), art->getTypeId()); - if(availablePosition != ArtifactPosition::PRE_FIRST) - { - destinationPosition = availablePosition; - } - } - else if(ArtifactUtils::isSlotBackpack(artPlace.slot)) - { - ArtifactPosition availablePosition = ArtifactUtils::getArtAnyPosition(artSetPtr->getHero(), art->getTypeId()); - if(availablePosition != ArtifactPosition::PRE_FIRST && availablePosition != ArtifactPosition::BACKPACK_START) - { - destinationPosition = availablePosition; - } - } - - if(destinationPosition != ArtifactPosition::PRE_FIRST) - { - LOCPLINT->cb->swapArtifacts(ArtifactLocation(artSetPtr->getHero()->id, artPlace.slot), - ArtifactLocation(artSetPtr->getHero()->id, destinationPosition)); - } - } - else - { - LOCPLINT->cb->swapArtifacts(ArtifactLocation(artSetPtr->getHero()->id, artPlace.slot), - ArtifactLocation(artSetPtr->getHero()->id, ArtifactPosition::TRANSITION_POS)); - } - } - } - else - { - for(const auto artSlot : ArtifactUtils::unmovableSlots()) - if(artPlace.slot == artSlot) - { - msg = CGI->generaltexth->allTexts[21]; - break; - } - } - } - - if constexpr(std::is_same_v>) - { - if(!isTransferAllowed && artPlace.getArt() && closeCallback) - closeCallback(); - } - else - { - if(!msg.empty()) - LOCPLINT->showInfoDialog(msg); - } - } - // Market window left click handler - else if constexpr(std::is_same_v>) - { - if(artSetPtr->selectArtCallback && artPlace.getArt()) - { - if(artPlace.getArt()->artType->isTradable()) - { - artSetPtr->unmarkSlots(); - artPlace.selectSlot(true); - artSetPtr->selectArtCallback(&artPlace); - } - else - { - // This item can't be traded - LOCPLINT->showInfoDialog(CGI->generaltexth->allTexts[21]); - } - } - } - else if constexpr(std::is_same_v>) - { - const auto hero = artSetPtr->getHero(); - LOCPLINT->cb->swapArtifacts(ArtifactLocation(hero->id, artPlace.slot), ArtifactLocation(hero->id, artSetPtr->getFilterSlot())); - if(closeCallback) - closeCallback(); - } - }, currentArtSet.value()); + } + } } -void CWindowWithArtifacts::showPopupArtPlaceHero(const CArtifactsOfHeroBase & artsInst, CArtPlace & artPlace, const Point & cursorPosition) +void CWindowWithArtifacts::swapArtifactAndClose(const CArtifactsOfHeroBase & artsInst, const ArtifactPosition & slot, + const ArtifactLocation & dstLoc) { - const auto currentArtSet = findAOHbyRef(artsInst); - assert(currentArtSet.has_value()); - - if(artPlace.isLocked()) - return; - - std::visit( - [&artPlace, &cursorPosition](auto artSetWeak) -> void - { - const auto artSetPtr = artSetWeak.lock(); - - // Hero (Main, Exchange) window, Kingdom window, Backpack window right click handler - if constexpr( - std::is_same_v> || - std::is_same_v> || - std::is_same_v> || - std::is_same_v>) - { - if(artPlace.getArt()) - { - if(ArtifactUtilsClient::askToDisassemble(artSetPtr->getHero(), artPlace.slot)) - { - return; - } - if(ArtifactUtilsClient::askToAssemble(artSetPtr->getHero(), artPlace.slot)) - { - return; - } - if(artPlace.text.size()) - artPlace.LRClickableAreaWTextComp::showPopupWindow(cursorPosition); - } - } - // Altar window, Market window right click handler - else if constexpr( - std::is_same_v> || - std::is_same_v>) - { - if(artPlace.getArt() && artPlace.text.size()) - artPlace.LRClickableAreaWTextComp::showPopupWindow(cursorPosition); - } - }, currentArtSet.value()); + LOCPLINT->cb->swapArtifacts(ArtifactLocation(artsInst.getHero()->id, slot), dstLoc); + close(); } -void CWindowWithArtifacts::gestureArtPlaceHero(const CArtifactsOfHeroBase & artsInst, CArtPlace & artPlace, const Point & cursorPosition) +void CWindowWithArtifacts::showArtifactAssembling(const CArtifactsOfHeroBase & artsInst, CArtPlace & artPlace, + const Point & cursorPosition) const { - const auto currentArtSet = findAOHbyRef(artsInst); - assert(currentArtSet.has_value()); - if(artPlace.isLocked()) + if(artsInst.getArt(artPlace.slot)) + { + if(LOCPLINT->artifactController->askToDisassemble(artsInst.getHero(), artPlace.slot)) + return; + if(LOCPLINT->artifactController->askToAssemble(artsInst.getHero(), artPlace.slot)) + return; + if(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; - std::visit( - [&artPlace, cursorPosition](auto artSetWeak) -> void - { - const auto artSetPtr = artSetWeak.lock(); - if constexpr( - std::is_same_v> || - std::is_same_v>) - { - if(!settings["general"]["enableUiEnhancements"].Bool()) - return; + if(!ArtifactUtils::isSlotEquipment(slot)) + return; - GH.windows().createAndPushWindow(artSetPtr->getHero(), artPlace.slot); - auto backpackWindow = GH.windows().topWindow(); - backpackWindow->moveTo(cursorPosition - Point(1, 1)); - backpackWindow->fitToScreen(15); - } - }, currentArtSet.value()); + GH.windows().createAndPushWindow(hero, slot); + auto backpackWindow = GH.windows().topWindow(); + backpackWindow->moveTo(cursorPosition - Point(1, 1)); + backpackWindow->fitToScreen(15); } void CWindowWithArtifacts::activate() { if(const auto art = getPickedArtifact()) + { + markPossibleSlots(); setCursorAnimation(*art); + } CWindowObject::activate(); } @@ -347,191 +158,60 @@ void CWindowWithArtifacts::deactivate() CWindowObject::deactivate(); } -void CWindowWithArtifacts::enableArtifactsCostumeSwitcher() const +void CWindowWithArtifacts::enableKeyboardShortcuts() const { - for(auto artSet : artSets) - std::visit( - [](auto artSetWeak) - { - if constexpr(std::is_same_v>) - { - const auto artSetPtr = artSetWeak.lock(); - artSetPtr->enableArtifactsCostumeSwitcher(); - } - }, artSet); + for(auto & artSet : artSets) + artSet->enableKeyboardShortcuts(); } -void CWindowWithArtifacts::artifactRemoved(const ArtifactLocation & artLoc) +void CWindowWithArtifacts::update() { - update(); -} - -void CWindowWithArtifacts::artifactMoved(const ArtifactLocation & srcLoc, const ArtifactLocation & destLoc, bool withRedraw) -{ - auto curState = getState(); - if(!curState.has_value()) - // Transition state. Nothing to do here. Just skip. Need to wait for final state. - return; - - auto pickedArtInst = std::get(curState.value()); - auto artifactMovedBody = [this, withRedraw, &destLoc, &pickedArtInst](auto artSetWeak) -> void + for(const auto & artSet : artSets) { - auto artSetPtr = artSetWeak.lock(); - if(artSetPtr) + artSet->updateWornSlots(); + artSet->updateBackpackSlots(); + + if(const auto pickedArtInst = getPickedArtifact()) { - const auto hero = artSetPtr->getHero(); - if(pickedArtInst) - { - setCursorAnimation(*pickedArtInst); - } - else - { - artSetPtr->unmarkSlots(); - CCS->curh->dragAndDropCursor(nullptr); - } - if(withRedraw) - { - artSetPtr->updateWornSlots(); - artSetPtr->updateBackpackSlots(); - - // Update arts bonuses on window. - // TODO rework this part when CHeroWindow and CExchangeWindow are reworked - if(auto * chw = dynamic_cast(this)) - { - chw->update(hero, true); - } - else if(auto * cew = dynamic_cast(this)) - { - cew->updateWidgets(); - } - artSetPtr->redraw(); - } - - // Make sure the status bar is updated so it does not display old text - if(destLoc.artHolder == hero->id) - { - if(auto artPlace = artSetPtr->getArtPlace(destLoc.slot)) - artPlace->hover(true); - } + markPossibleSlots(); + setCursorAnimation(*pickedArtInst); } - }; - - for(auto artSetWeak : artSets) - std::visit(artifactMovedBody, artSetWeak); -} - -void CWindowWithArtifacts::artifactDisassembled(const ArtifactLocation & artLoc) -{ - update(); -} - -void CWindowWithArtifacts::artifactAssembled(const ArtifactLocation & artLoc) -{ - markPossibleSlots(); - update(); -} - -void CWindowWithArtifacts::update() const -{ - auto updateSlotBody = [](auto artSetWeak) -> void - { - if(const auto artSetPtr = artSetWeak.lock()) + else { - artSetPtr->updateWornSlots(); - artSetPtr->updateBackpackSlots(); - artSetPtr->redraw(); + artSet->unmarkSlots(); + CCS->curh->dragAndDropCursor(nullptr); } - }; - for(auto artSetWeak : artSets) - std::visit(updateSlotBody, artSetWeak); -} - -std::optional> CWindowWithArtifacts::getState() -{ - const CArtifactInstance * artInst = nullptr; - std::map pickedCnt; - - auto getHeroArtBody = [&artInst, &pickedCnt](auto artSetWeak) -> void - { - auto artSetPtr = artSetWeak.lock(); - if(artSetPtr) - { - if(const auto art = artSetPtr->getPickedArtifact()) - { - const auto hero = artSetPtr->getHero(); - if(pickedCnt.count(hero) == 0) - { - pickedCnt.insert({ hero, hero->artifactsTransitionPos.size() }); - artInst = art; - } - } - } - }; - for(auto artSetWeak : artSets) - std::visit(getHeroArtBody, artSetWeak); - - // The state is possible when the hero has placed an artifact in the ArtifactPosition::TRANSITION_POS, - // and the previous artifact has not yet removed from the ArtifactPosition::TRANSITION_POS. - // This is a transitional state. Then return nullopt. - if(std::accumulate(std::begin(pickedCnt), std::end(pickedCnt), 0, [](size_t accum, const auto & value) - { - return accum + value.second; - }) > 1) - return std::nullopt; - else - return std::make_tuple(pickedCnt.begin()->first, artInst); -} - -std::optional CWindowWithArtifacts::findAOHbyRef(const CArtifactsOfHeroBase & artsInst) -{ - std::optional res; - - auto findAOHBody = [&res, &artsInst](auto & artSetWeak) -> void - { - if(&artsInst == artSetWeak.lock().get()) - res = artSetWeak; - }; - - for(auto artSetWeak : artSets) - { - std::visit(findAOHBody, artSetWeak); - if(res.has_value()) - return res; + // Make sure the status bar is updated so it does not display old text + if(auto artPlace = artSet->getArtPlace(GH.getCursorPosition())) + artPlace->hover(true); } - return res; + redraw(); } -void CWindowWithArtifacts::markPossibleSlots() +void CWindowWithArtifacts::markPossibleSlots() const { if(const auto pickedArtInst = getPickedArtifact()) { - const auto heroArtOwner = getHeroPickedArtifact(); - auto artifactAssembledBody = [&pickedArtInst, &heroArtOwner](auto artSetWeak) -> void + for(const auto & artSet : artSets) { - if(auto artSetPtr = artSetWeak.lock()) - { - if(artSetPtr->isActive()) - { - const auto hero = artSetPtr->getHero(); - if(heroArtOwner == hero || !std::is_same_v>) - artSetPtr->markPossibleSlots(pickedArtInst, hero->tempOwner == LOCPLINT->playerID); - } - } - }; + const auto hero = artSet->getHero(); + if(hero == nullptr || !artSet->isActive()) + continue; - for(auto artSetWeak : artSets) - std::visit(artifactAssembledBody, artSetWeak); + if(getHeroPickedArtifact() == hero || !std::dynamic_pointer_cast(artSet)) + artSet->markPossibleSlots(pickedArtInst->getType(), hero->tempOwner == LOCPLINT->playerID); + } } } -bool CWindowWithArtifacts::checkSpecialArts(const CArtifactInstance & artInst, const CGHeroInstance * hero, bool isTrade) const +bool CWindowWithArtifacts::checkSpecialArts(const CArtifactInstance & artInst, const CGHeroInstance & hero, bool isTrade) const { const auto artId = artInst.getTypeId(); if(artId == ArtifactID::SPELLBOOK) { - GH.windows().createAndPushWindow(hero, LOCPLINT, LOCPLINT->battleInt.get()); + GH.windows().createAndPushWindow(&hero, LOCPLINT, LOCPLINT->battleInt.get()); return false; } if(artId == ArtifactID::CATAPULT) @@ -541,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))); @@ -550,18 +230,87 @@ bool CWindowWithArtifacts::checkSpecialArts(const CArtifactInstance & artInst, c return true; } -void CWindowWithArtifacts::setCursorAnimation(const CArtifactInstance & artInst) +void CWindowWithArtifacts::setCursorAnimation(const CArtifactInstance & artInst) const { - markPossibleSlots(); if(artInst.isScroll() && settings["general"]["enableUiEnhancements"].Bool()) { assert(artInst.getScrollSpellID().num >= 0); - const auto animation = GH.renderHandler().loadAnimation(AnimationPath::builtin("spellscr")); - animation->load(artInst.getScrollSpellID().num); - CCS->curh->dragAndDropCursor(animation->getImage(artInst.getScrollSpellID().num)->scaleFast(Point(44, 34))); + auto image = GH.renderHandler().loadImage(AnimationPath::builtin("spellscr"), artInst.getScrollSpellID().num, 0, EImageBlitMode::COLORKEY); + image->scaleTo(Point(44,34)); + + CCS->curh->dragAndDropCursor(image); } else { - CCS->curh->dragAndDropCursor(AnimationPath::builtin("artifact"), artInst.artType->getIconIndex()); + CCS->curh->dragAndDropCursor(AnimationPath::builtin("artifact"), artInst.getType()->getIconIndex()); } } + +void CWindowWithArtifacts::putPickedArtifact(const CGHeroInstance & curHero, const ArtifactPosition & targetSlot) const +{ + const auto heroArtOwner = getHeroPickedArtifact(); + const auto pickedArt = getPickedArtifact(); + auto srcLoc = ArtifactLocation(heroArtOwner->id, ArtifactPosition::TRANSITION_POS); + auto dstLoc = ArtifactLocation(curHero.id, targetSlot); + + if(ArtifactUtils::isSlotBackpack(dstLoc.slot)) + { + if(pickedArt->getType()->isBig()) + { + // War machines cannot go to backpack + LOCPLINT->showInfoDialog(boost::str(boost::format(CGI->generaltexth->allTexts[153]) % pickedArt->getType()->getNameTranslated())); + } + else + { + if(ArtifactUtils::isBackpackFreeSlots(heroArtOwner)) + LOCPLINT->cb->swapArtifacts(srcLoc, dstLoc); + else + LOCPLINT->showInfoDialog(CGI->generaltexth->translate("core.genrltxt.152")); + } + } + // Check if artifact transfer is possible + else if(pickedArt->canBePutAt(&curHero, dstLoc.slot, true) && (!curHero.getArt(targetSlot) || curHero.tempOwner == LOCPLINT->playerID)) + { + LOCPLINT->cb->swapArtifacts(srcLoc, dstLoc); + } +} + +void CWindowWithArtifacts::onClickPressedCommonArtifact(const CGHeroInstance & curHero, const ArtifactPosition & slot, bool closeWindow) +{ + assert(curHero.getArt(slot)); + auto srcLoc = ArtifactLocation(curHero.id, slot); + auto dstLoc = ArtifactLocation(curHero.id, ArtifactPosition::TRANSITION_POS); + + if(GH.isKeyboardCmdDown()) + { + for(const auto & anotherSet : artSets) + { + if(std::dynamic_pointer_cast(anotherSet) && curHero.id != anotherSet->getHero()->id) + { + dstLoc.slot = ArtifactPosition::FIRST_AVAILABLE; + dstLoc.artHolder = anotherSet->getHero()->id; + break; + } + if(const auto heroSetAltar = std::dynamic_pointer_cast(anotherSet)) + { + dstLoc.slot = ArtifactPosition::FIRST_AVAILABLE; + dstLoc.artHolder = heroSetAltar->altarId; + break; + } + } + } + else if(GH.isKeyboardAltDown()) + { + const auto artId = curHero.getArt(slot)->getTypeId(); + if(ArtifactUtils::isSlotEquipment(slot)) + dstLoc.slot = ArtifactUtils::getArtBackpackPosition(&curHero, artId); + else if(ArtifactUtils::isSlotBackpack(slot)) + dstLoc.slot = ArtifactUtils::getArtEquippedPosition(&curHero, artId); + } + else if(closeWindow) + { + close(); + } + if(dstLoc.slot != ArtifactPosition::PRE_FIRST) + LOCPLINT->cb->swapArtifacts(srcLoc, dstLoc); +} diff --git a/client/windows/CWindowWithArtifacts.h b/client/windows/CWindowWithArtifacts.h index 41d960de3..cd6a42484 100644 --- a/client/windows/CWindowWithArtifacts.h +++ b/client/windows/CWindowWithArtifacts.h @@ -19,41 +19,29 @@ class CWindowWithArtifacts : virtual public CWindowObject { public: - using CArtifactsOfHeroPtr = std::variant< - std::weak_ptr, - std::weak_ptr, - std::weak_ptr, - std::weak_ptr, - std::weak_ptr, - std::weak_ptr>; - using CloseCallback = std::function; + using CArtifactsOfHeroPtr = std::shared_ptr; std::vector artSets; - CloseCallback closeCallback; explicit CWindowWithArtifacts(const std::vector * artSets = nullptr); - void addSet(CArtifactsOfHeroPtr newArtSet); - void addSetAndCallbacks(CArtifactsOfHeroPtr newArtSet); - void addCloseCallback(const CloseCallback & callback); - const CGHeroInstance * getHeroPickedArtifact(); - const CArtifactInstance * getPickedArtifact(); - void clickPressedArtPlaceHero(const CArtifactsOfHeroBase & artsInst, CArtPlace & artPlace, const Point & cursorPosition); - void showPopupArtPlaceHero(const CArtifactsOfHeroBase & artsInst, CArtPlace & artPlace, const Point & cursorPosition); - void gestureArtPlaceHero(const CArtifactsOfHeroBase & artsInst, CArtPlace & artPlace, const Point & cursorPosition); + void addSet(const std::shared_ptr & newArtSet); + const CGHeroInstance * getHeroPickedArtifact() const; + const CArtifactInstance * getPickedArtifact() const; + void clickPressedOnArtPlace(const CGHeroInstance * hero, const ArtifactPosition & slot, + 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 showQuickBackpackWindow(const CGHeroInstance * hero, const ArtifactPosition & slot, const Point & cursorPosition) const; void activate() override; void deactivate() override; - void enableArtifactsCostumeSwitcher() const; + void enableKeyboardShortcuts() const; - virtual void artifactRemoved(const ArtifactLocation & artLoc); - virtual void artifactMoved(const ArtifactLocation & srcLoc, const ArtifactLocation & destLoc, bool withRedraw); - virtual void artifactDisassembled(const ArtifactLocation & artLoc); - virtual void artifactAssembled(const ArtifactLocation & artLoc); + virtual void update(); protected: - void update() const; - std::optional> getState(); - std::optional findAOHbyRef(const CArtifactsOfHeroBase & artsInst); - void markPossibleSlots(); - bool checkSpecialArts(const CArtifactInstance & artInst, const CGHeroInstance * hero, bool isTrade) const; - void setCursorAnimation(const CArtifactInstance & artInst); + void markPossibleSlots() const; + bool checkSpecialArts(const CArtifactInstance & artInst, const CGHeroInstance & hero, bool isTrade) const; + void setCursorAnimation(const CArtifactInstance & artInst) const; + void putPickedArtifact(const CGHeroInstance & curHero, const ArtifactPosition & targetSlot) const; + void onClickPressedCommonArtifact(const CGHeroInstance & curHero, const ArtifactPosition & slot, bool closeWindow); }; diff --git a/client/windows/CreaturePurchaseCard.cpp b/client/windows/CreaturePurchaseCard.cpp index 5592f5e44..50ddc9175 100644 --- a/client/windows/CreaturePurchaseCard.cpp +++ b/client/windows/CreaturePurchaseCard.cpp @@ -50,12 +50,12 @@ void CreaturePurchaseCard::initCreatureSwitcherButton() void CreaturePurchaseCard::switchCreatureLevel() { - OBJECT_CONSTRUCTION_CAPTURING(ACTIVATE + DEACTIVATE + UPDATE + SHOWALL + SHARE_POS); + OBJECT_CONSTRUCTION; 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/CreaturePurchaseCard.h b/client/windows/CreaturePurchaseCard.h index e5a3388c2..176b95576 100644 --- a/client/windows/CreaturePurchaseCard.h +++ b/client/windows/CreaturePurchaseCard.h @@ -44,7 +44,7 @@ private: void initCostBox(); // This just wraps a clickeable area. There's a weird layout scheme in the file and - // it's easier to just add a separate invisble box on top + // it's easier to just add a separate invisible box on top class CCreatureClickArea : public CIntObject { public: diff --git a/client/windows/GUIClasses.cpp b/client/windows/GUIClasses.cpp index 6747df0ad..bd9cec14f 100644 --- a/client/windows/GUIClasses.cpp +++ b/client/windows/GUIClasses.cpp @@ -18,9 +18,7 @@ #include "../CGameInfo.h" #include "../CServerHandler.h" #include "../Client.h" -#include "../CMusicHandler.h" #include "../CPlayerInterface.h" -#include "../CVideoHandler.h" #include "../gui/CGuiHandler.h" #include "../gui/CursorHandler.h" @@ -35,14 +33,18 @@ #include "../widgets/Slider.h" #include "../widgets/TextControls.h" #include "../widgets/ObjectLists.h" +#include "../widgets/VideoWidget.h" +#include "../widgets/GraphicalPrimitiveCanvas.h" #include "../render/Canvas.h" -#include "../render/CAnimation.h" #include "../render/IRenderHandler.h" #include "../render/IImage.h" #include "../../CCallback.h" +#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" @@ -51,11 +53,12 @@ #include "../lib/gameState/CGameState.h" #include "../lib/gameState/SThievesGuildInfo.h" #include "../lib/gameState/TavernHeroesPool.h" -#include "../lib/CGeneralTextHandler.h" -#include "../lib/CHeroHandler.h" -#include "../lib/GameSettings.h" +#include "../lib/texts/CGeneralTextHandler.h" +#include "../lib/IGameSettings.h" #include "ConditionalWait.h" +#include "../lib/CRandomGenerator.h" #include "../lib/CSkillHandler.h" +#include "../lib/CSoundBase.h" CRecruitmentWindow::CCreatureCard::CCreatureCard(CRecruitmentWindow * window, const CCreature * crea, int totalAmount) : CIntObject(LCLICK | SHOW_POPUP), @@ -64,7 +67,7 @@ CRecruitmentWindow::CCreatureCard::CCreatureCard(CRecruitmentWindow * window, co creature(crea), amount(totalAmount) { - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + OBJECT_CONSTRUCTION; animation = std::make_shared(1, 1, creature, true, true); // 1 + 1 px for borders pos.w = animation->pos.w + 2; @@ -151,9 +154,9 @@ void CRecruitmentWindow::buy() if(!dstslot.validSlot() && (selected->creature->warMachine == ArtifactID::NONE)) //no available slot { std::pair toMerge; - bool allowMerge = CGI->settings()->getBoolean(EGameSettings::DWELLINGS_ACCUMULATE_WHEN_OWNED); + bool allowMerge = LOCPLINT->cb->getSettings().getBoolean(EGameSettings::DWELLINGS_ACCUMULATE_WHEN_OWNED); - if (allowMerge && dst->mergableStacks(toMerge)) + if (allowMerge && dst->mergeableStacks(toMerge)) { LOCPLINT->cb->mergeStacks( dst, dst, toMerge.first, toMerge.second); } @@ -211,7 +214,7 @@ CRecruitmentWindow::CRecruitmentWindow(const CGDwelling * Dwelling, int Level, c { moveBy(Point(0, y_offset)); - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + OBJECT_CONSTRUCTION; statusbar = CGStatusBar::create(std::make_shared(background->getSurface(), Rect(8, pos.h - 26, pos.w - 16, 19), 8, pos.h - 26)); @@ -236,7 +239,7 @@ CRecruitmentWindow::CRecruitmentWindow(const CGDwelling * Dwelling, int Level, c void CRecruitmentWindow::availableCreaturesChanged() { - OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255-DISPOSE); + OBJECT_CONSTRUCTION; size_t selectedIndex = 0; @@ -257,7 +260,7 @@ void CRecruitmentWindow::availableCreaturesChanged() //create new cards for(auto & creature : boost::adaptors::reverse(dwelling->creatures[i].second)) - cards.push_back(std::make_shared(this, CGI->creh->objects[creature], amount)); + cards.push_back(std::make_shared(this, creature.toCreature(), amount)); } const int creatureWidth = 102; @@ -314,7 +317,7 @@ CSplitWindow::CSplitWindow(const CCreature * creature, std::functionshowingDialog->setBusy(); @@ -451,7 +454,7 @@ CTavernWindow::CTavernWindow(const CGObjectInstance * TavernObj, const std::func tavernObj(TavernObj), heroToInvite(nullptr) { - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + OBJECT_CONSTRUCTION; std::vector h = LOCPLINT->cb->getAvailableHeroes(TavernObj); if(h.size() < 2) @@ -489,7 +492,7 @@ CTavernWindow::CTavernWindow(const CGObjectInstance * TavernObj, const std::func recruit->addHoverText(EButtonState::NORMAL, CGI->generaltexth->tavernInfo[0]); //Cannot afford a Hero recruit->block(true); } - else if(LOCPLINT->cb->howManyHeroes(true) >= CGI->settings()->getInteger(EGameSettings::HEROES_PER_PLAYER_TOTAL_CAP)) + else if(LOCPLINT->cb->howManyHeroes(true) >= LOCPLINT->cb->getSettings().getInteger(EGameSettings::HEROES_PER_PLAYER_TOTAL_CAP)) { MetaString message; message.appendTextID("core.tvrninfo.1"); @@ -499,7 +502,7 @@ CTavernWindow::CTavernWindow(const CGObjectInstance * TavernObj, const std::func recruit->addHoverText(EButtonState::NORMAL, message.toString()); recruit->block(true); } - else if(LOCPLINT->cb->howManyHeroes(false) >= CGI->settings()->getInteger(EGameSettings::HEROES_PER_PLAYER_ON_MAP_CAP)) + else if(LOCPLINT->cb->howManyHeroes(false) >= LOCPLINT->cb->getSettings().getInteger(EGameSettings::HEROES_PER_PLAYER_ON_MAP_CAP)) { MetaString message; message.appendTextID("core.tvrninfo.1"); @@ -519,20 +522,20 @@ CTavernWindow::CTavernWindow(const CGObjectInstance * TavernObj, const std::func recruit->block(true); } if(LOCPLINT->castleInt) - CCS->videoh->open(LOCPLINT->castleInt->town->town->clientInfo.tavernVideo); + videoPlayer = std::make_shared(Point(70, 56), LOCPLINT->castleInt->town->getTown()->clientInfo.tavernVideo, false); else if(const auto * townObj = dynamic_cast(TavernObj)) - CCS->videoh->open(townObj->town->clientInfo.tavernVideo); + videoPlayer = std::make_shared(Point(70, 56), townObj->getTown()->clientInfo.tavernVideo, false); else - CCS->videoh->open(VideoPath::builtin("TAVERN.BIK")); + videoPlayer = std::make_shared(Point(70, 56), VideoPath::builtin("TAVERN.BIK"), false); addInvite(); } void CTavernWindow::addInvite() { - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + OBJECT_CONSTRUCTION; - if(!VLC->settings()->getBoolean(EGameSettings::HEROES_TAVERN_INVITE)) + if(!LOCPLINT->cb->getSettings().getBoolean(EGameSettings::HEROES_TAVERN_INVITE)) return; const auto & heroesPool = CSH->client->gameState()->heroesPool; @@ -545,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)); }); } } @@ -559,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(); } @@ -576,11 +580,6 @@ void CTavernWindow::close() CStatusbarWindow::close(); } -CTavernWindow::~CTavernWindow() -{ - CCS->videoh->close(); -} - void CTavernWindow::show(Canvas & to) { CWindowObject::show(to); @@ -604,8 +603,6 @@ void CTavernWindow::show(Canvas & to) to.drawBorder(Rect::createAround(sel->pos, 2), Colors::BRIGHT_YELLOW, 2); } - - CCS->videoh->update(pos.x+70, pos.y+56, to.getInternalSurface(), true, false); } void CTavernWindow::HeroPortrait::clickPressed(const Point & cursorPosition) @@ -635,7 +632,7 @@ CTavernWindow::HeroPortrait::HeroPortrait(int & sel, int id, int x, int y, const : CIntObject(LCLICK | DOUBLECLICK | SHOW_POPUP | HOVER), h(H), _sel(&sel), _id(id), onChoose(OnChoose) { - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + OBJECT_CONSTRUCTION; h = H; pos.x += x; pos.y += y; @@ -674,17 +671,52 @@ void CTavernWindow::HeroPortrait::hover(bool on) CTavernWindow::HeroSelector::HeroSelector(std::map InviteableHeroes, std::function OnChoose) : CWindowObject(BORDERED), inviteableHeroes(InviteableHeroes), onChoose(OnChoose) { - OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + OBJECT_CONSTRUCTION; - pos = Rect(0, 0, 16 * 48, (inviteableHeroes.size() / 16 + (inviteableHeroes.size() % 16 != 0)) * 32); + pos = Rect( + pos.x, + pos.y, + ELEM_PER_LINES * 48, + std::min((int)(inviteableHeroes.size() / ELEM_PER_LINES + (inviteableHeroes.size() % ELEM_PER_LINES != 0)), MAX_LINES) * 32 + ); background = std::make_shared(ImagePath::builtin("DIBOXBCK"), Rect(0, 0, pos.w, pos.h)); + if(inviteableHeroes.size() / ELEM_PER_LINES > MAX_LINES) + { + pos.w += 16; + slider = std::make_shared(Point(pos.w - 16, 0), pos.h, std::bind(&CTavernWindow::HeroSelector::sliderMove, this, _1), MAX_LINES, std::ceil((double)inviteableHeroes.size() / ELEM_PER_LINES), 0, Orientation::VERTICAL, CSlider::BROWN); + slider->setPanningStep(32); + slider->setScrollBounds(Rect(-pos.w + slider->pos.w, 0, pos.w, pos.h)); + } + + recreate(); + center(); +} + +void CTavernWindow::HeroSelector::sliderMove(int slidPos) +{ + if(!slider) + return; // ignore spurious call when slider is being created + recreate(); + redraw(); +} + +void CTavernWindow::HeroSelector::recreate() +{ + OBJECT_CONSTRUCTION; + + int sliderLine = slider ? slider->getValue() : 0; int x = 0; - int y = 0; + int y = -sliderLine; + portraits.clear(); + portraitAreas.clear(); for(auto & h : inviteableHeroes) { - portraits.push_back(std::make_shared(AnimationPath::builtin("PortraitsSmall"), (*CGI->heroh)[h.first]->imageIndex, 0, x * 48, y * 32)); - portraitAreas.push_back(std::make_shared(Rect(x * 48, y * 32, 48, 32), [this, h](){ close(); onChoose(inviteableHeroes[h.first]); }, [this, h](){ GH.windows().createAndPushWindow(std::make_shared(inviteableHeroes[h.first])); })); + if(y >= 0 && y <= MAX_LINES - 1) + { + portraits.push_back(std::make_shared(AnimationPath::builtin("PortraitsSmall"), (*CGI->heroh)[h.first]->imageIndex, 0, x * 48, y * 32)); + portraitAreas.push_back(std::make_shared(Rect(x * 48, y * 32, 48, 32), [this, h](){ close(); onChoose(inviteableHeroes[h.first]); }, [this, h](){ GH.windows().createAndPushWindow(std::make_shared(inviteableHeroes[h.first])); })); + } if(x > 0 && x % 15 == 0) { @@ -694,14 +726,12 @@ CTavernWindow::HeroSelector::HeroSelector(std::map else x++; } - - center(); } CShipyardWindow::CShipyardWindow(const TResources & cost, int state, BoatId boatType, const std::function & onBuy) : CWindowObject(PLAYER_COLORED, ImagePath::builtin("TPSHIP")) { - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + OBJECT_CONSTRUCTION; bgWater = std::make_shared(ImagePath::builtin("TPSHIPBK"), 100, 69); @@ -716,8 +746,9 @@ CShipyardWindow::CShipyardWindow(const TResources & cost, int state, BoatId boat AnimationPath boatFilename = boatConstructor->getBoatAnimationName(); Point waterCenter = Point(bgWater->pos.x+bgWater->pos.w/2, bgWater->pos.y+bgWater->pos.h/2); - bgShip = std::make_shared(boatFilename, 0, 7, 120, 96, 0); + bgShip = std::make_shared(120, 96, boatFilename, CShowableAnim::CREATURE_MODE, 100, 7); bgShip->center(waterCenter); + bgWater->needRefresh = true; } // Create resource icons and costs. @@ -775,7 +806,7 @@ CTransformerWindow::CItem::CItem(CTransformerWindow * parent_, int size_, int id size(size_), parent(parent_) { - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + OBJECT_CONSTRUCTION; left = true; pos.w = 58; pos.h = 64; @@ -791,7 +822,7 @@ void CTransformerWindow::makeDeal() for(auto & elem : items) { if(!elem->left) - LOCPLINT->cb->trade(market, EMarketMode::CREATURE_UNDEAD, SlotID(elem->id), {}, {}, hero); + LOCPLINT->cb->trade(market->getObjInstanceID(), EMarketMode::CREATURE_UNDEAD, SlotID(elem->id), {}, {}, hero); } } @@ -822,7 +853,7 @@ CTransformerWindow::CTransformerWindow(const IMarket * _market, const CGHeroInst onWindowClosed(onWindowClosed), market(_market) { - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + OBJECT_CONSTRUCTION; if(hero) army = hero; else @@ -861,88 +892,67 @@ CUniversityWindow::CItem::CItem(CUniversityWindow * _parent, int _ID, int X, int ID(_ID), parent(_parent) { - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + OBJECT_CONSTRUCTION; pos.x += X; pos.y += Y; - topBar = std::make_shared(parent->bars, 0, 0, -28, -22); - bottomBar = std::make_shared(parent->bars, 0, 0, -28, 48); + 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); - icon = std::make_shared(AnimationPath::builtin("SECSKILL"), _ID * 3 + 3, 0); - - name = std::make_shared(22, -13, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, CGI->skillh->getByIndex(ID)->getNameTranslated()); - level = std::make_shared(22, 57, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->levels[0]); - - pos.h = icon->pos.h; - pos.w = icon->pos.w; + if(!skillKnown && canLearn) + GH.windows().createAndPushWindow(parent, ID, LOCPLINT->cb->getResourceAmount(EGameResID::GOLD) >= 2000); + }); + update(); } -void CUniversityWindow::CItem::clickPressed(const Point & cursorPosition) +void CUniversityWindow::CItem::update() { - if(state() == 2) - GH.windows().createAndPushWindow(parent, ID, LOCPLINT->cb->getResourceAmount(EGameResID::GOLD) >= 2000); -} + bool skillKnown = parent->hero->getSecSkillLevel(ID); + bool canLearn = parent->hero->canLearnSkill(ID); -void CUniversityWindow::CItem::showPopupWindow(const Point & cursorPosition) -{ - CRClickPopup::createAndPush(CGI->skillh->getByIndex(ID)->getDescriptionTranslated(1), std::make_shared(ComponentType::SEC_SKILL, ID, 1)); -} + ImagePath image; -void CUniversityWindow::CItem::hover(bool on) -{ - if(on) - GH.statusbar()->write(CGI->skillh->getByIndex(ID)->getNameTranslated()); + if (skillKnown) + image = ImagePath::builtin("UNIVGOLD"); + else if (canLearn) + image = ImagePath::builtin("UNIVGREN"); else - GH.statusbar()->clear(); + image = ImagePath::builtin("UNIVRED"); + + OBJECT_CONSTRUCTION; + topBar = std::make_shared(image, Point(-28, -22)); + bottomBar = std::make_shared(image, Point(-28, 48)); + + // needs to be on top of background bars + name = std::make_shared(22, -13, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, ID.toEntity(VLC)->getNameTranslated()); + level = std::make_shared(22, 57, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->levels[0]); } -int CUniversityWindow::CItem::state() -{ - if(parent->hero->getSecSkillLevel(SecondarySkill(ID)))//hero know this skill - return 1; - if(!parent->hero->canLearnSkill(SecondarySkill(ID)))//can't learn more skills - return 0; - return 2; -} - -void CUniversityWindow::CItem::showAll(Canvas & to) -{ - //TODO: update when state actually changes - auto stateIndex = state(); - topBar->setFrame(stateIndex); - bottomBar->setFrame(stateIndex); - - CIntObject::showAll(to); -} - -CUniversityWindow::CUniversityWindow(const CGHeroInstance * _hero, const IMarket * _market, const std::function & onWindowClosed) +CUniversityWindow::CUniversityWindow(const CGHeroInstance * _hero, BuildingID building, const IMarket * _market, const std::function & onWindowClosed) : CWindowObject(PLAYER_COLORED, ImagePath::builtin("UNIVERS1")), hero(_hero), onWindowClosed(onWindowClosed), market(_market) { - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + OBJECT_CONSTRUCTION; + - bars = GH.renderHandler().createAnimation(); - bars->setCustom("UNIVRED", 0, 0); - bars->setCustom("UNIVGOLD", 1, 0); - bars->setCustom("UNIVGREN", 2, 0); - bars->preload(); - std::string titleStr = CGI->generaltexth->allTexts[602]; std::string speechStr = CGI->generaltexth->allTexts[603]; if(auto town = dynamic_cast(_market)) { - auto faction = town->town->faction->getId(); - auto bid = town->town->getSpecialBuilding(BuildingSubID::MAGIC_UNIVERSITY)->bid; - titlePic = std::make_shared((*CGI->townh)[faction]->town->clientInfo.buildingsIcons, bid); + 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 { @@ -971,17 +981,22 @@ void CUniversityWindow::close() CStatusbarWindow::close(); } -void CUniversityWindow::makeDeal(SecondarySkill skill) +void CUniversityWindow::updateSecondarySkills() { - LOCPLINT->cb->trade(market, EMarketMode::RESOURCE_SKILL, GameResID(GameResID::GOLD), skill, 1, hero); + for (auto const & item : items) + item->update(); } +void CUniversityWindow::makeDeal(SecondarySkill skill) +{ + LOCPLINT->cb->trade(market->getObjInstanceID(), EMarketMode::RESOURCE_SKILL, GameResID(GameResID::GOLD), skill, 1, hero); +} CUnivConfirmWindow::CUnivConfirmWindow(CUniversityWindow * owner_, SecondarySkill SKILL, bool available) : CWindowObject(PLAYER_COLORED, ImagePath::builtin("UNIVERS2.PCX")), owner(owner_) { - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + OBJECT_CONSTRUCTION; std::string text = CGI->generaltexth->allTexts[608]; boost::replace_first(text, "%s", CGI->generaltexth->levels[0]); @@ -1021,14 +1036,14 @@ void CUnivConfirmWindow::makeDeal(SecondarySkill skill) CGarrisonWindow::CGarrisonWindow(const CArmedInstance * up, const CGHeroInstance * down, bool removableUnits) : CWindowObject(PLAYER_COLORED, ImagePath::builtin("GARRISON")) { - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + OBJECT_CONSTRUCTION; garr = std::make_shared(Point(92, 127), 4, Point(0,96), up, down, removableUnits); { - auto split = std::make_shared(Point(88, 314), AnimationPath::builtin("IDV6432.DEF"), CButton::tooltip(CGI->generaltexth->tcommands[3], ""), [&](){ garr->splitClick(); }, EShortcut::HERO_ARMY_SPLIT ); + auto split = std::make_shared(Point(88, 314), AnimationPath::builtin("IDV6432.DEF"), CButton::tooltip(CGI->generaltexth->tcommands[3], ""), [this](){ garr->splitClick(); }, EShortcut::HERO_ARMY_SPLIT ); garr->addSplitBtn(split); } - quit = std::make_shared(Point(399, 314), AnimationPath::builtin("IOK6432.DEF"), CButton::tooltip(CGI->generaltexth->tcommands[8], ""), [&](){ close(); }, EShortcut::GLOBAL_ACCEPT); + quit = std::make_shared(Point(399, 314), AnimationPath::builtin("IOK6432.DEF"), CButton::tooltip(CGI->generaltexth->tcommands[8], ""), [this](){ close(); }, EShortcut::GLOBAL_ACCEPT); std::string titleText; if(down->tempOwner == up->tempOwner) @@ -1041,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 { @@ -1069,7 +1084,7 @@ CHillFortWindow::CHillFortWindow(const CGHeroInstance * visitor, const CGObjectI fort(object), hero(visitor) { - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + OBJECT_CONSTRUCTION; title = std::make_shared(325, 32, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW, fort->getObjectName()); @@ -1098,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(); } @@ -1115,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(); @@ -1166,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()) { @@ -1211,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; @@ -1250,29 +1288,35 @@ 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): CWindowObject(PLAYER_COLORED | BORDERED, ImagePath::builtin("TpRank")), owner(_owner) { - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + OBJECT_CONSTRUCTION; SThievesGuildInfo tgi; //info to be displayed LOCPLINT->cb->getThievesGuildInfo(tgi, owner); @@ -1304,18 +1348,12 @@ CThievesGuildWindow::CThievesGuildWindow(const CGObjectInstance * _owner): rowHeaders.push_back(std::make_shared(135, y, FONT_MEDIUM, ETextAlignment::CENTER, Colors::YELLOW, text)); } - auto PRSTRIPS = GH.renderHandler().loadAnimation(AnimationPath::builtin("PRSTRIPS")); - PRSTRIPS->preload(); - for(int g=1; g(PRSTRIPS, g-1, 0, 250 + 66*g, 7)); + columnBackgrounds.push_back(std::make_shared(AnimationPath::builtin("PRSTRIPS"), g-1, 0, 250 + 66*g, 7)); for(int g=0; g(283 + 66*g, 24, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->jktexts[16+g])); - auto itgflags = GH.renderHandler().loadAnimation(AnimationPath::builtin("itgflags")); - itgflags->preload(); - //printing flags for(int g = 0; g < std::size(fields); ++g) //by lines { @@ -1339,7 +1377,7 @@ CThievesGuildWindow::CThievesGuildWindow(const CGObjectInstance * _owner): int rowStartY = ypos + (j ? 4 : 0); for(size_t i=0; i < rowLength[j]; i++) - cells.push_back(std::make_shared(itgflags, players[i + j*4].getNum(), 0, rowStartX + (int)i*12, rowStartY)); + cells.push_back(std::make_shared(AnimationPath::builtin("itgflags"), players[i + j*4].getNum(), 0, rowStartX + (int)i*12, rowStartY)); } } } @@ -1406,7 +1444,7 @@ CObjectListWindow::CItem::CItem(CObjectListWindow * _parent, size_t _id, std::st parent(_parent), index(_id) { - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + OBJECT_CONSTRUCTION; if(parent->images.size() > index) icon = std::make_shared(parent->images[index], Point(1, 1)); border = std::make_shared(ImagePath::builtin("TPGATES")); @@ -1453,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_CAPTURING(255-DISPOSE); + 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_CAPTURING(255-DISPOSE); + 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_; @@ -1496,29 +1542,55 @@ void CObjectListWindow::init(std::shared_ptr titleWidget_, std::stri if(titleWidget) { addChild(titleWidget.get()); - titleWidget->recActions = 255-DISPOSE; titleWidget->pos.x = pos.w/2 + pos.x - titleWidget->pos.w/2; 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 } @@ -1574,13 +1646,69 @@ 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(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; + + addUsedEvents(LCLICK | KEYBOARD); + + if(showBackground) + backgroundAroundWindow = std::make_shared(ImagePath::builtin("DIBOXBCK"), Rect(0, 0, GH.screenDimensions().x, GH.screenDimensions().y)); + + if(!rim.empty()) + { + setBackground(rim); + videoPlayer = std::make_shared(Point(80, 186), video, true, this); + pos = center(Rect(0, 0, 800, 600)); + } + else + { + blackBackground = std::make_shared(Rect(0, 0, GH.screenDimensions().x, GH.screenDimensions().y)); + videoPlayer = std::make_shared(Point(0, 0), video, true, scaleFactor, this); + pos = center(Rect(0, 0, videoPlayer->pos.w, videoPlayer->pos.h)); + blackBackground->addBox(Point(0, 0), Point(pos.x, pos.y), Colors::BLACK); + } + + if(backgroundAroundWindow) + backgroundAroundWindow->pos.moveTo(Point(0, 0)); +} + +void VideoWindow::onVideoPlaybackFinished() +{ + exit(false); +} + + +void VideoWindow::exit(bool skipped) +{ + close(); + if(closeCb) + closeCb(skipped); +} + +void VideoWindow::clickPressed(const Point & cursorPosition) +{ + exit(true); +} + +void VideoWindow::keyPressed(EShortcut key) +{ + exit(true); +} + +void VideoWindow::notFocusedClick() +{ + exit(true); +} diff --git a/client/windows/GUIClasses.h b/client/windows/GUIClasses.h index bcf8b6794..74ee6d916 100644 --- a/client/windows/GUIClasses.h +++ b/client/windows/GUIClasses.h @@ -12,6 +12,7 @@ #include "CWindowObject.h" #include "../lib/ResourceSet.h" #include "../widgets/Images.h" +#include "../widgets/IVideoHolder.h" VCMI_LIB_NAMESPACE_BEGIN @@ -42,6 +43,11 @@ class CHeroArea; class CAnimImage; class CFilledTexture; class IImage; +class VideoWidget; +class VideoWidgetOnce; +class GraphicalPrimitiveCanvas; +class TransparentFilledRectangle; +class CSecSkillPlace; enum class EUserEvent; @@ -183,9 +189,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 @@ -197,8 +208,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 @@ -237,6 +248,10 @@ public: { public: std::shared_ptr background; + std::shared_ptr slider; + + const int MAX_LINES = 18; + const int ELEM_PER_LINES = 16; HeroSelector(std::map InviteableHeroes, std::function OnChoose); @@ -246,6 +261,9 @@ public: std::vector> portraits; std::vector> portraitAreas; + + void recreate(); + void sliderMove(int slidPos); }; //recruitable heroes @@ -265,6 +283,7 @@ public: std::shared_ptr cost; std::shared_ptr heroesForHire; std::shared_ptr heroDescription; + std::shared_ptr videoPlayer; std::shared_ptr rumor; @@ -276,7 +295,6 @@ public: void addInvite(); CTavernWindow(const CGObjectInstance * TavernObj, const std::function & onWindowClosed); - ~CTavernWindow(); void close() override; void recruitb(); @@ -288,7 +306,7 @@ public: class CShipyardWindow : public CStatusbarWindow { std::shared_ptr bgWater; - std::shared_ptr bgShip; + std::shared_ptr bgShip; std::shared_ptr title; std::shared_ptr costLabel; @@ -350,32 +368,26 @@ public: CTransformerWindow(const IMarket * _market, const CGHeroInstance * _hero, const std::function & onWindowClosed); }; -class CUniversityWindow : public CStatusbarWindow +class CUniversityWindow final : public CStatusbarWindow, public IMarketHolder { - class CItem : public CIntObject + class CItem final : public CIntObject { - std::shared_ptr icon; - std::shared_ptr topBar; - std::shared_ptr bottomBar; + std::shared_ptr skill; + std::shared_ptr topBar; + std::shared_ptr bottomBar; std::shared_ptr name; std::shared_ptr level; public: SecondarySkill ID;//id of selected skill CUniversityWindow * parent; - void showAll(Canvas & to) override; - void clickPressed(const Point & cursorPosition) override; - void showPopupWindow(const Point & cursorPosition) override; - void hover(bool on) override; - int state();//0=can't learn, 1=learned, 2=can learn + void update(); CItem(CUniversityWindow * _parent, int _ID, int X, int Y); }; const CGHeroInstance * hero; const IMarket * market; - std::shared_ptr bars; - std::vector> items; std::shared_ptr cancel; @@ -386,14 +398,17 @@ class CUniversityWindow : public CStatusbarWindow std::function onWindowClosed; public: - CUniversityWindow(const CGHeroInstance * _hero, const IMarket * _market, const std::function & onWindowClosed); + CUniversityWindow(const CGHeroInstance * _hero, BuildingID building, const IMarket * _market, const std::function & onWindowClosed); void makeDeal(SecondarySkill skill); - void close(); + void close() override; + + // IMarketHolder impl + void updateSecondarySkills() override; }; /// Confirmation window for University -class CUnivConfirmWindow : public CStatusbarWindow +class CUnivConfirmWindow final : public CStatusbarWindow { std::shared_ptr clerkSpeech; std::shared_ptr name; @@ -435,9 +450,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; @@ -449,7 +466,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; @@ -464,7 +481,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 @@ -493,3 +510,20 @@ public: CThievesGuildWindow(const CGObjectInstance * _owner); }; +class VideoWindow : public CWindowObject, public IVideoHolder +{ + std::shared_ptr videoPlayer; + std::shared_ptr backgroundAroundWindow; + std::shared_ptr blackBackground; + + std::function closeCb; + + void onVideoPlaybackFinished() override; + void exit(bool skipped); +public: + 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; + void notFocusedClick() override; +}; diff --git a/client/windows/InfoWindows.cpp b/client/windows/InfoWindows.cpp index b124c547b..494192e4c 100644 --- a/client/windows/InfoWindows.cpp +++ b/client/windows/InfoWindows.cpp @@ -38,7 +38,7 @@ CSelWindow::CSelWindow( const std::string & Text, PlayerColor player, int charperline, const std::vector> & comps, const std::vector>> & Buttons, QueryID askID) { - OBJECT_CONSTRUCTION_CAPTURING(255 - DISPOSE); + OBJECT_CONSTRUCTION; backgroundTexture = std::make_shared(ImagePath::builtin("DiBoxBck"), pos); @@ -94,7 +94,7 @@ void CSelWindow::madeChoiceAndClose() CInfoWindow::CInfoWindow(const std::string & Text, PlayerColor player, const TCompsInfo & comps, const TButtonsInfo & Buttons) { - OBJECT_CONSTRUCTION_CAPTURING(255 - DISPOSE); + OBJECT_CONSTRUCTION; backgroundTexture = std::make_shared(ImagePath::builtin("DiBoxBck"), pos); @@ -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 @@ -245,11 +240,12 @@ void CRClickPopup::createAndPush(const CGObjectInstance * obj, const Point & p, } } -CRClickPopupInt::CRClickPopupInt(const std::shared_ptr & our) +CRClickPopupInt::CRClickPopupInt(const std::shared_ptr & our) : + dragDistance(Point(0, 0)) { + addUsedEvents(DRAG_POPUP); + CCS->curh->hide(); - defActions = SHOWALL | UPDATE; - our->recActions = defActions; inner = our; addChild(our.get(), false); } @@ -259,51 +255,80 @@ CRClickPopupInt::~CRClickPopupInt() CCS->curh->show(); } -Point CInfoBoxPopup::toScreen(Point p) +void CRClickPopupInt::mouseDraggedPopup(const Point & cursorPosition, const Point & lastUpdateDistance) { - 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; + if(!settings["adventure"]["rightButtonDrag"].Bool()) + return; + + dragDistance += lastUpdateDistance; + + if(dragDistance.length() > 16) + close(); } +void CInfoBoxPopup::mouseDraggedPopup(const Point & cursorPosition, const Point & lastUpdateDistance) +{ + if(!settings["adventure"]["rightButtonDrag"].Bool()) + return; + + dragDistance += lastUpdateDistance; + + if(dragDistance.length() > 16) + close(); +} + + 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->getCurrentTown()); //todo: should this be nearest hero? + LOCPLINT->cb->getTownInfo(town, iah, LOCPLINT->localState->getCurrentArmy()); //todo: should this be nearest hero? - OBJECT_CONSTRUCTION_CAPTURING(255 - DISPOSE); + OBJECT_CONSTRUCTION; 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->getCurrentHero()); //todo: should this be nearest hero? + LOCPLINT->cb->getHeroInfo(hero, iah, LOCPLINT->localState->getCurrentArmy()); //todo: should this be nearest hero? - OBJECT_CONSTRUCTION_CAPTURING(255 - DISPOSE); + OBJECT_CONSTRUCTION; 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); - OBJECT_CONSTRUCTION_CAPTURING(255 - DISPOSE); + OBJECT_CONSTRUCTION; 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_CAPTURING(255 - DISPOSE); + 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 5da10014b..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); @@ -78,22 +77,29 @@ class CRClickPopupInt : public CRClickPopup { std::shared_ptr inner; + Point dragDistance; + public: CRClickPopupInt(const std::shared_ptr & our); ~CRClickPopupInt(); + + void mouseDraggedPopup(const Point & cursorPosition, const Point & lastUpdateDistance) override; }; /// popup on adventure map for town\hero and other objects with customized popup content class CInfoBoxPopup : public CWindowObject { std::shared_ptr tooltip; - Point toScreen(Point pos); + + Point dragDistance; public: CInfoBoxPopup(Point position, const CGTownInstance * town); CInfoBoxPopup(Point position, const CGHeroInstance * hero); CInfoBoxPopup(Point position, const CGGarrison * garr); CInfoBoxPopup(Point position, const CGCreature * creature); + + void mouseDraggedPopup(const Point & cursorPosition, const Point & lastUpdateDistance) override; }; /// component selection window diff --git a/client/windows/QuickRecruitmentWindow.cpp b/client/windows/QuickRecruitmentWindow.cpp index ae976282b..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 < GameConstants::CREATURES_PER_TOWN; 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; @@ -106,7 +106,16 @@ void QuickRecruitmentWindow::purchaseUnits() { if(selected->slider->getValue()) { - auto onRecruit = [=](CreatureID id, int count){ LOCPLINT->cb->recruitCreatures(town, town->getUpperArmy(), id, count, selected->creatureOnTheCard->getLevel()-1); }; + int level = 0; + int i = 0; + for(auto c : town->getTown()->creatures) + { + for(auto c2 : c) + if(c2 == selected->creatureOnTheCard->getId()) + level = i; + i++; + } + auto onRecruit = [=](CreatureID id, int count){ LOCPLINT->cb->recruitCreatures(town, town->getUpperArmy(), id, count, level); }; CreatureID crid = selected->creatureOnTheCard->getId(); SlotID dstslot = town -> getSlotFor(crid); if(!dstslot.validSlot()) @@ -120,8 +129,8 @@ void QuickRecruitmentWindow::purchaseUnits() int QuickRecruitmentWindow::getAvailableCreatures() { int creaturesAmount = 0; - for (int i=0; i< GameConstants::CREATURES_PER_TOWN; 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; } @@ -151,10 +160,12 @@ QuickRecruitmentWindow::QuickRecruitmentWindow(const CGTownInstance * townd, Rec : CWindowObject(PLAYER_COLORED | BORDERED), town(townd) { - OBJECT_CONSTRUCTION_CAPTURING(ACTIVATE + DEACTIVATE + UPDATE + SHOWALL); + OBJECT_CONSTRUCTION; initWindow(startupPosition); setButtons(); setCreaturePurchaseCards(); maxAllCards(cards); + + center(); } diff --git a/client/windows/settings/AdventureOptionsTab.cpp b/client/windows/settings/AdventureOptionsTab.cpp index f514f33f1..0fa168a7b 100644 --- a/client/windows/settings/AdventureOptionsTab.cpp +++ b/client/windows/settings/AdventureOptionsTab.cpp @@ -11,6 +11,7 @@ #include "AdventureOptionsTab.h" +#include "../../eventsSDL/InputHandler.h" #include "../../../lib/filesystem/ResourcePath.h" #include "../../gui/CGuiHandler.h" #include "../../widgets/Buttons.h" @@ -33,9 +34,12 @@ static void setIntSetting(std::string group, std::string field, int value) AdventureOptionsTab::AdventureOptionsTab() : InterfaceObjectConfigurable() { - OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + OBJECT_CONSTRUCTION; setRedrawParent(true); + addConditional("touchscreen", GH.input().getCurrentInputMode() == InputMode::TOUCH); + addConditional("keyboardMouse", GH.input().getCurrentInputMode() == InputMode::KEYBOARD_AND_MOUSE); + addConditional("controller", GH.input().getCurrentInputMode() == InputMode::CONTROLLER); #ifdef VCMI_MOBILE addConditional("mobile", true); addConditional("desktop", false); @@ -126,6 +130,10 @@ AdventureOptionsTab::AdventureOptionsTab() { return setBoolSetting("adventure", "leftButtonDrag", value); }); + addCallback("rightButtonDragChanged", [](bool value) + { + return setBoolSetting("adventure", "rightButtonDrag", value); + }); addCallback("smoothDraggingChanged", [](bool value) { return setBoolSetting("adventure", "smoothDragging", value); @@ -177,6 +185,10 @@ AdventureOptionsTab::AdventureOptionsTab() if (leftButtonDragCheckbox) leftButtonDragCheckbox->setSelected(settings["adventure"]["leftButtonDrag"].Bool()); + std::shared_ptr rightButtonDragCheckbox = widget("rightButtonDragCheckbox"); + if (rightButtonDragCheckbox) + rightButtonDragCheckbox->setSelected(settings["adventure"]["rightButtonDrag"].Bool()); + std::shared_ptr smoothDraggingCheckbox = widget("smoothDraggingCheckbox"); if (smoothDraggingCheckbox) smoothDraggingCheckbox->setSelected(settings["adventure"]["smoothDragging"].Bool()); diff --git a/client/windows/settings/BattleOptionsTab.cpp b/client/windows/settings/BattleOptionsTab.cpp index 7c803614e..adefb75c3 100644 --- a/client/windows/settings/BattleOptionsTab.cpp +++ b/client/windows/settings/BattleOptionsTab.cpp @@ -14,13 +14,13 @@ #include "../../gui/CGuiHandler.h" #include "../../../lib/CConfigHandler.h" #include "../../../lib/filesystem/ResourcePath.h" -#include "../../../lib/CGeneralTextHandler.h" +#include "../../../lib/texts/CGeneralTextHandler.h" #include "../../widgets/Buttons.h" #include "../../widgets/TextControls.h" BattleOptionsTab::BattleOptionsTab(BattleInterface * owner) { - OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + OBJECT_CONSTRUCTION; setRedrawParent(true); const JsonNode config(JsonPath::builtin("config/widgets/settings/battleOptionsTab.json")); @@ -64,6 +64,10 @@ BattleOptionsTab::BattleOptionsTab(BattleInterface * owner) { showStickyHeroWindowsChangedCallback(value, owner); }); + addCallback("showQuickSpellChanged", [this, owner](bool value) + { + showQuickSpellChangedCallback(value, owner); + }); addCallback("enableAutocombatSpellsChanged", [this](bool value) { enableAutocombatSpellsChangedCallback(value); @@ -95,6 +99,9 @@ BattleOptionsTab::BattleOptionsTab(BattleInterface * owner) std::shared_ptr showStickyHeroInfoWindowsCheckbox = widget("showStickyHeroInfoWindowsCheckbox"); showStickyHeroInfoWindowsCheckbox->setSelected(settings["battle"]["stickyHeroInfoWindows"].Bool()); + std::shared_ptr showQuickSpellCheckbox = widget("showQuickSpellCheckbox"); + showQuickSpellCheckbox->setSelected(settings["battle"]["enableQuickSpellPanel"].Bool()); + std::shared_ptr mouseShadowCheckbox = widget("mouseShadowCheckbox"); mouseShadowCheckbox->setSelected(settings["battle"]["mouseShadow"].Bool()); @@ -228,6 +235,19 @@ void BattleOptionsTab::showStickyHeroWindowsChangedCallback(bool value, BattleIn } } +void BattleOptionsTab::showQuickSpellChangedCallback(bool value, BattleInterface * parentBattleInterface) +{ + if(!parentBattleInterface) + { + Settings showQuickSpell = settings.write["battle"]["enableQuickSpellPanel"]; + showQuickSpell->Bool() = value; + } + else + { + parentBattleInterface->setStickyQuickSpellWindowVisibility(value); + } +} + void BattleOptionsTab::queueSizeChangedCallback(int value, BattleInterface * parentBattleInterface) { if (value == -1) diff --git a/client/windows/settings/BattleOptionsTab.h b/client/windows/settings/BattleOptionsTab.h index 633726b2c..59e7e3c88 100644 --- a/client/windows/settings/BattleOptionsTab.h +++ b/client/windows/settings/BattleOptionsTab.h @@ -32,6 +32,7 @@ private: void queueSizeChangedCallback(int value, BattleInterface * parentBattleInterface); void skipBattleIntroMusicChangedCallback(bool value); void showStickyHeroWindowsChangedCallback(bool value, BattleInterface * parentBattleInterface); + void showQuickSpellChangedCallback(bool value, BattleInterface * parentBattleInterface); void enableAutocombatSpellsChangedCallback(bool value); void endWithAutocombatChangedCallback(bool value); public: diff --git a/client/windows/settings/GeneralOptionsTab.cpp b/client/windows/settings/GeneralOptionsTab.cpp index 2058b664c..6571a3422 100644 --- a/client/windows/settings/GeneralOptionsTab.cpp +++ b/client/windows/settings/GeneralOptionsTab.cpp @@ -11,9 +11,10 @@ #include "GeneralOptionsTab.h" #include "CGameInfo.h" -#include "CMusicHandler.h" #include "CPlayerInterface.h" #include "CServerHandler.h" +#include "media/IMusicPlayer.h" +#include "media/ISoundPlayer.h" #include "render/IScreenHandler.h" #include "windows/GUIClasses.h" @@ -25,7 +26,7 @@ #include "../../widgets/Slider.h" #include "../../widgets/TextControls.h" -#include "../../../lib/CGeneralTextHandler.h" +#include "../../../lib/texts/CGeneralTextHandler.h" #include "../../../lib/filesystem/ResourcePath.h" static void setIntSetting(std::string group, std::string field, int value) @@ -93,10 +94,12 @@ GeneralOptionsTab::GeneralOptionsTab() : InterfaceObjectConfigurable(), onFullscreenChanged(settings.listen["video"]["fullscreen"]) { - OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + OBJECT_CONSTRUCTION; setRedrawParent(true); - addConditional("touchscreen", GH.input().hasTouchInputDevice()); + addConditional("touchscreen", GH.input().getCurrentInputMode() == InputMode::TOUCH); + addConditional("keyboardMouse", GH.input().getCurrentInputMode() == InputMode::KEYBOARD_AND_MOUSE); + addConditional("controller", GH.input().getCurrentInputMode() == InputMode::CONTROLLER); #ifdef VCMI_MOBILE addConditional("mobile", true); addConditional("desktop", false); @@ -191,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/client/windows/settings/OtherOptionsTab.cpp b/client/windows/settings/OtherOptionsTab.cpp index c2709b6a6..bf555ff90 100644 --- a/client/windows/settings/OtherOptionsTab.cpp +++ b/client/windows/settings/OtherOptionsTab.cpp @@ -24,7 +24,7 @@ static void setBoolSetting(std::string group, std::string field, bool value) OtherOptionsTab::OtherOptionsTab() : InterfaceObjectConfigurable() { - OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + OBJECT_CONSTRUCTION; const JsonNode config(JsonPath::builtin("config/widgets/settings/otherOptionsTab.json")); addCallback("availableCreaturesAsDwellingLabelChanged", [](bool value) diff --git a/client/windows/settings/SettingsMainWindow.cpp b/client/windows/settings/SettingsMainWindow.cpp index f5c3ab7ac..d5bd2831f 100644 --- a/client/windows/settings/SettingsMainWindow.cpp +++ b/client/windows/settings/SettingsMainWindow.cpp @@ -18,7 +18,7 @@ #include "CMT.h" #include "CGameInfo.h" -#include "CGeneralTextHandler.h" +#include "texts/CGeneralTextHandler.h" #include "CPlayerInterface.h" #include "CServerHandler.h" #include "filesystem/ResourcePath.h" @@ -33,7 +33,7 @@ SettingsMainWindow::SettingsMainWindow(BattleInterface * parentBattleUi) : InterfaceObjectConfigurable() { - OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + OBJECT_CONSTRUCTION; const JsonNode config(JsonPath::builtin("config/widgets/settings/settingsMainContainer.json")); addCallback("activateSettingsTab", [this](int tabId) { openTab(tabId); }); @@ -45,6 +45,8 @@ SettingsMainWindow::SettingsMainWindow(BattleInterface * parentBattleUi) : Inter addCallback("closeWindow", [this](int) { backButtonCallback(); }); build(config); + addUsedEvents(INPUT_MODE_CHANGE); + std::shared_ptr background = widget("background"); pos.w = background->pos.w; pos.h = background->pos.h; @@ -144,7 +146,6 @@ void SettingsMainWindow::mainMenuButtonCallback() { close(); CSH->endGameplay(); - GH.defActionsDef = 63; CMM->menu->switchToTab("main"); }, 0 diff --git a/client/xBRZ/Changelog.txt b/client/xBRZ/Changelog.txt new file mode 100644 index 000000000..f7cef74d8 --- /dev/null +++ b/client/xBRZ/Changelog.txt @@ -0,0 +1,66 @@ +xBRZ 1.8 [2019-11-28] +--------------------- +Consider ARGB outside area as transparent +Fixed ARGB scaling issue on image borders + + +xBRZ 1.7 [2019-07-04] +--------------------- +Fixed asymmetric color distance +New parameter: "Center direction bias" + + +xBRZ 1.6 [2018-02-27] +--------------------- +Added bilinear scaling +Option to skip color buffer creation +Updated license info + + +xBRZ 1.5 [2017-08-07] +--------------------- +Added RGB conversion routines + + +xBRZ 1.4 [2015-07-25] +--------------------- +Added 6xBRZ scaler +Create color distance buffer lazily + + +xBRZ 1.3 [2015-04-03] +--------------------- +Improved ARGB performance by 15% +Fixed alpha channel gradient bug + + +xBRZ 1.2 [2014-11-21] +--------------------- +Further improved performance by over 30% + + +xBRZ 1.1 [2014-11-02] +--------------------- +Support images with alpha channel +Improved color analysis + + +xBRZ 1.0 [2013-02-11] +--------------------- +Fixed xBRZ scaler compiler issues for GCC + + +xBRZ 0.2 [2012-12-11] +--------------------- +Added 5xBRZ scaler +Optimized xBRZ scaler performance by factor 3 +Further improved image quality of xBRZ scaler + + +xBRZ 0.1 [2012-09-26] +--------------------- +Initial release: +- scale while preserving small image features +- support multithreading +- support 64-bit architectures +- support processing image slices diff --git a/client/xBRZ/License.txt b/client/xBRZ/License.txt new file mode 100644 index 000000000..94a045322 --- /dev/null +++ b/client/xBRZ/License.txt @@ -0,0 +1,621 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS diff --git a/client/xBRZ/xbrz.cpp b/client/xBRZ/xbrz.cpp new file mode 100644 index 000000000..1eda5a46b --- /dev/null +++ b/client/xBRZ/xbrz.cpp @@ -0,0 +1,1384 @@ +// **************************************************************************** +// * This file is part of the xBRZ project. It is distributed under * +// * GNU General Public License: https://www.gnu.org/licenses/gpl-3.0 * +// * Copyright (C) Zenju (zenju AT gmx DOT de) - All Rights Reserved * +// * * +// * Additionally and as a special exception, the author gives permission * +// * to link the code of this program with the following libraries * +// * (or with modified versions that use the same licenses), and distribute * +// * linked combinations including the two: MAME, FreeFileSync, Snes9x, ePSXe * +// * You must obey the GNU General Public License in all respects for all of * +// * the code used other than MAME, FreeFileSync, Snes9x, ePSXe. * +// * If you modify this file, you may extend this exception to your version * +// * of the file, but you are not obligated to do so. If you do not wish to * +// * do so, delete this exception statement from your version. * +// **************************************************************************** + +#include "xbrz.h" +#include +#include +#include +#include //std::sqrt +#include "xbrz_tools.h" + +#if defined _MSC_VER +#pragma warning(disable:5051) +#endif + +using namespace xbrz; + + +namespace +{ +template inline +uint32_t gradientRGB(uint32_t pixFront, uint32_t pixBack) //blend front color with opacity M / N over opaque background: https://en.wikipedia.org/wiki/Alpha_compositing#Alpha_blending +{ + static_assert(0 < M && M < N && N <= 1000); + + auto calcColor = [](unsigned char colFront, unsigned char colBack) -> unsigned char { return (colFront * M + colBack * (N - M)) / N; }; + + return makePixel(calcColor(getRed (pixFront), getRed (pixBack)), + calcColor(getGreen(pixFront), getGreen(pixBack)), + calcColor(getBlue (pixFront), getBlue (pixBack))); +} + + +template inline +uint32_t gradientARGB(uint32_t pixFront, uint32_t pixBack) //find intermediate color between two colors with alpha channels (=> NO alpha blending!!!) +{ + static_assert(0 < M && M < N && N <= 1000); + + const unsigned int weightFront = getAlpha(pixFront) * M; + const unsigned int weightBack = getAlpha(pixBack) * (N - M); + const unsigned int weightSum = weightFront + weightBack; + if (weightSum == 0) + return 0; + + auto calcColor = [=](unsigned char colFront, unsigned char colBack) + { + return static_cast((colFront * weightFront + colBack * weightBack) / weightSum); + }; + + return makePixel(static_cast(weightSum / N), + calcColor(getRed (pixFront), getRed (pixBack)), + calcColor(getGreen(pixFront), getGreen(pixBack)), + calcColor(getBlue (pixFront), getBlue (pixBack))); +} + + +//inline +//double fastSqrt(double n) +//{ +// __asm //speeds up xBRZ by about 9% compared to std::sqrt which internally uses the same assembler instructions but adds some "fluff" +// { +// fld n +// fsqrt +// } +//} +// + + +#ifdef _MSC_VER + #define FORCE_INLINE __forceinline +#elif defined __GNUC__ + #define FORCE_INLINE __attribute__((always_inline)) inline +#else + #define FORCE_INLINE inline +#endif + + +enum RotationDegree //clock-wise +{ + ROT_0, + ROT_90, + ROT_180, + ROT_270 +}; + +//calculate input matrix coordinates after rotation at compile time +template +struct MatrixRotation; + +template +struct MatrixRotation +{ + static const size_t I_old = I; + static const size_t J_old = J; +}; + +template //(i, j) = (row, col) indices, N = size of (square) matrix +struct MatrixRotation +{ + static const size_t I_old = N - 1 - MatrixRotation(rotDeg - 1), I, J, N>::J_old; //old coordinates before rotation! + static const size_t J_old = MatrixRotation(rotDeg - 1), I, J, N>::I_old; // +}; + + +template +class OutputMatrix +{ +public: + OutputMatrix(uint32_t* out, int outWidth) : //access matrix area, top-left at position "out" for image with given width + out_(out), + outWidth_(outWidth) {} + + template + uint32_t& ref() const + { + static const size_t I_old = MatrixRotation::I_old; + static const size_t J_old = MatrixRotation::J_old; + return *(out_ + J_old + I_old * outWidth_); + } + +private: + uint32_t* out_; + const int outWidth_; +}; + + +template inline +T square(T value) { return value * value; } + + +#if 0 +inline +double distRGB(uint32_t pix1, uint32_t pix2) +{ + const double r_diff = static_cast(getRed (pix1)) - getRed (pix2); + const double g_diff = static_cast(getGreen(pix1)) - getGreen(pix2); + const double b_diff = static_cast(getBlue (pix1)) - getBlue (pix2); + + //euklidean RGB distance + return std::sqrt(square(r_diff) + square(g_diff) + square(b_diff)); +} +#endif + + +inline +double distYCbCr(uint32_t pix1, uint32_t pix2, double lumaWeight) +{ + //https://en.wikipedia.org/wiki/YCbCr#ITU-R_BT.601_conversion + //YCbCr conversion is a matrix multiplication => take advantage of linearity by subtracting first! + const int r_diff = static_cast(getRed (pix1)) - getRed (pix2); //we may delay division by 255 to after matrix multiplication + const int g_diff = static_cast(getGreen(pix1)) - getGreen(pix2); // + const int b_diff = static_cast(getBlue (pix1)) - getBlue (pix2); //substraction for int is noticeable faster than for double! + + //const double k_b = 0.0722; //ITU-R BT.709 conversion + //const double k_r = 0.2126; // + const double k_b = 0.0593; //ITU-R BT.2020 conversion + const double k_r = 0.2627; // + const double k_g = 1 - k_b - k_r; + + const double scale_b = 0.5 / (1 - k_b); + const double scale_r = 0.5 / (1 - k_r); + + const double y = k_r * r_diff + k_g * g_diff + k_b * b_diff; //[!], analog YCbCr! + const double c_b = scale_b * (b_diff - y); + const double c_r = scale_r * (r_diff - y); + + //we skip division by 255 to have similar range like other distance functions + return std::sqrt(square(lumaWeight * y) + square(c_b) + square(c_r)); +} + + +inline +double distYCbCrBuffered(uint32_t pix1, uint32_t pix2) +{ + //30% perf boost compared to plain distYCbCr()! + //consumes 64 MB memory; using double is only 2% faster, but takes 128 MB + static const std::vector diffToDist = [] + { + std::vector tmp; + + for (uint32_t i = 0; i < 256 * 256 * 256; ++i) //startup time: 114 ms on Intel Core i5 (four cores) + { + const int r_diff = static_cast(getByte<2>(i)) * 2; + const int g_diff = static_cast(getByte<1>(i)) * 2; + const int b_diff = static_cast(getByte<0>(i)) * 2; + + const double k_b = 0.0593; //ITU-R BT.2020 conversion + const double k_r = 0.2627; // + const double k_g = 1 - k_b - k_r; + + const double scale_b = 0.5 / (1 - k_b); + const double scale_r = 0.5 / (1 - k_r); + + const double y = k_r * r_diff + k_g * g_diff + k_b * b_diff; //[!], analog YCbCr! + const double c_b = scale_b * (b_diff - y); + const double c_r = scale_r * (r_diff - y); + + tmp.push_back(static_cast(std::sqrt(square(y) + square(c_b) + square(c_r)))); + } + return tmp; + }(); + + //if (pix1 == pix2) -> 8% perf degradation! + // return 0; + //if (pix1 < pix2) + // std::swap(pix1, pix2); -> 30% perf degradation!!! + + const int r_diff = static_cast(getRed (pix1)) - getRed (pix2); + const int g_diff = static_cast(getGreen(pix1)) - getGreen(pix2); + const int b_diff = static_cast(getBlue (pix1)) - getBlue (pix2); + + const size_t index = (static_cast(r_diff / 2) << 16) | //slightly reduce precision (division by 2) to squeeze value into single byte + (static_cast(g_diff / 2) << 8) | + (static_cast(b_diff / 2)); + +#if 0 //attention: the following calculation creates an asymmetric color distance!!! (e.g. r_diff=46 will be unpacked as 45, but r_diff=-46 unpacks to -47 + const size_t index = (((r_diff + 0xFF) / 2) << 16) | //slightly reduce precision (division by 2) to squeeze value into single byte + (((g_diff + 0xFF) / 2) << 8) | + (( b_diff + 0xFF) / 2); +#endif + return diffToDist[index]; +} + + +#if defined _MSC_VER && !defined NDEBUG + const int debugPixelX = -1; + const int debugPixelY = 58; + + thread_local bool breakIntoDebugger = false; +#endif + + +enum BlendType +{ + BLEND_NONE = 0, + BLEND_NORMAL, //a normal indication to blend + BLEND_DOMINANT, //a strong indication to blend + //attention: BlendType must fit into the value range of 2 bit!!! +}; + +struct BlendResult +{ + BlendType + /**/blend_f, blend_g, + /**/blend_j, blend_k; +}; + + +struct Kernel_3x3 +{ + uint32_t + a, b, c, + d, e, f, + g, h, i; +}; + +struct Kernel_4x4 //kernel for preprocessing step +{ + uint32_t + a, b, c, // + e, f, g, // support reinterpret_cast from Kernel_4x4 => Kernel_3x3 + i, j, k, // + m, n, o, + d, h, l, p; +}; + +/* input kernel area naming convention: +----------------- +| A | B | C | D | +|---|---|---|---| +| E | F | G | H | evaluate the four corners between F, G, J, K +|---|---|---|---| input pixel is at position F +| I | J | K | L | +|---|---|---|---| +| M | N | O | P | +----------------- +*/ +template +FORCE_INLINE //detect blend direction +BlendResult preProcessCorners(const Kernel_4x4& ker, const xbrz::ScalerCfg& cfg) //result: F, G, J, K corners of "GradientType" +{ +#if defined _MSC_VER && !defined NDEBUG + if (breakIntoDebugger) + __debugbreak(); //__asm int 3; +#endif + + BlendResult result = {}; + + if ((ker.f == ker.g && + ker.j == ker.k) || + (ker.f == ker.j && + ker.g == ker.k)) + return result; + + auto dist = [&](uint32_t pix1, uint32_t pix2) { return ColorDistance::dist(pix1, pix2, cfg.luminanceWeight); }; + + double jg = dist(ker.i, ker.f) + dist(ker.f, ker.c) + dist(ker.n, ker.k) + dist(ker.k, ker.h) + cfg.centerDirectionBias * dist(ker.j, ker.g); + double fk = dist(ker.e, ker.j) + dist(ker.j, ker.o) + dist(ker.b, ker.g) + dist(ker.g, ker.l) + cfg.centerDirectionBias * dist(ker.f, ker.k); + + if (jg < fk) //test sample: 70% of values max(jg, fk) / min(jg, fk) are between 1.1 and 3.7 with median being 1.8 + { + const bool dominantGradient = cfg.dominantDirectionThreshold * jg < fk; + if (ker.f != ker.g && ker.f != ker.j) + result.blend_f = dominantGradient ? BLEND_DOMINANT : BLEND_NORMAL; + + if (ker.k != ker.j && ker.k != ker.g) + result.blend_k = dominantGradient ? BLEND_DOMINANT : BLEND_NORMAL; + } + else if (fk < jg) + { + const bool dominantGradient = cfg.dominantDirectionThreshold * fk < jg; + if (ker.j != ker.f && ker.j != ker.k) + result.blend_j = dominantGradient ? BLEND_DOMINANT : BLEND_NORMAL; + + if (ker.g != ker.f && ker.g != ker.k) + result.blend_g = dominantGradient ? BLEND_DOMINANT : BLEND_NORMAL; + } + return result; +} + +#define DEF_GETTER(x) template uint32_t inline get_##x(const Kernel_3x3& ker) { return ker.x; } +//we cannot and NEED NOT write "ker.##x" since ## concatenates preprocessor tokens but "." is not a token +DEF_GETTER(a) DEF_GETTER(b) DEF_GETTER(c) +DEF_GETTER(d) DEF_GETTER(e) DEF_GETTER(f) +DEF_GETTER(g) DEF_GETTER(h) DEF_GETTER(i) +#undef DEF_GETTER + +#define DEF_GETTER(x, y) template <> [[maybe_unused]] inline uint32_t get_##x(const Kernel_3x3& ker) { return ker.y; } +DEF_GETTER(a, g) DEF_GETTER(b, d) DEF_GETTER(c, a) +DEF_GETTER(d, h) DEF_GETTER(e, e) DEF_GETTER(f, b) +DEF_GETTER(g, i) DEF_GETTER(h, f) DEF_GETTER(i, c) +#undef DEF_GETTER + +#define DEF_GETTER(x, y) template <> [[maybe_unused]] inline uint32_t get_##x(const Kernel_3x3& ker) { return ker.y; } +DEF_GETTER(a, i) DEF_GETTER(b, h) DEF_GETTER(c, g) +DEF_GETTER(d, f) DEF_GETTER(e, e) DEF_GETTER(f, d) +DEF_GETTER(g, c) DEF_GETTER(h, b) DEF_GETTER(i, a) +#undef DEF_GETTER + +#define DEF_GETTER(x, y) template <> [[maybe_unused]] inline uint32_t get_##x(const Kernel_3x3& ker) { return ker.y; } +DEF_GETTER(a, c) DEF_GETTER(b, f) DEF_GETTER(c, i) +DEF_GETTER(d, b) DEF_GETTER(e, e) DEF_GETTER(f, h) +DEF_GETTER(g, a) DEF_GETTER(h, d) DEF_GETTER(i, g) +#undef DEF_GETTER + + +//compress four blend types into a single byte +//inline BlendType getTopL (unsigned char b) { return static_cast(0x3 & b); } +inline BlendType getTopR (unsigned char b) { return static_cast(0x3 & (b >> 2)); } +inline BlendType getBottomR(unsigned char b) { return static_cast(0x3 & (b >> 4)); } +inline BlendType getBottomL(unsigned char b) { return static_cast(0x3 & (b >> 6)); } + +inline void clearAddTopL(unsigned char& b, BlendType bt) { b = static_cast(bt); } +inline void addTopR (unsigned char& b, BlendType bt) { b |= (bt << 2); } //buffer is assumed to be initialized before preprocessing! +inline void addBottomR (unsigned char& b, BlendType bt) { b |= (bt << 4); } //e.g. via clearAddTopL() +inline void addBottomL (unsigned char& b, BlendType bt) { b |= (bt << 6); } // + +inline bool blendingNeeded(unsigned char b) +{ + static_assert(BLEND_NONE == 0); + return b != 0; +} + +template inline +unsigned char rotateBlendInfo(unsigned char b) { return b; } +template <> inline unsigned char rotateBlendInfo(unsigned char b) { return ((b << 2) | (b >> 6)) & 0xff; } +template <> inline unsigned char rotateBlendInfo(unsigned char b) { return ((b << 4) | (b >> 4)) & 0xff; } +template <> inline unsigned char rotateBlendInfo(unsigned char b) { return ((b << 6) | (b >> 2)) & 0xff; } + + +/* input kernel area naming convention: +------------- +| A | B | C | +|---|---|---| +| D | E | F | input pixel is at position E +|---|---|---| +| G | H | I | +------------- +*/ +template +FORCE_INLINE //perf: quite worth it! +void blendPixel(const Kernel_3x3& ker, + uint32_t* target, int trgWidth, + unsigned char blendInfo, //result of preprocessing all four corners of pixel "e" + const xbrz::ScalerCfg& cfg) +{ + //#define a get_a(ker) +#define b get_b(ker) +#define c get_c(ker) +#define d get_d(ker) +#define e get_e(ker) +#define f get_f(ker) +#define g get_g(ker) +#define h get_h(ker) +#define i get_i(ker) + +#if defined _MSC_VER && !defined NDEBUG + if (breakIntoDebugger) + __debugbreak(); //__asm int 3; +#endif + + const unsigned char blend = rotateBlendInfo(blendInfo); + + if (getBottomR(blend) >= BLEND_NORMAL) + { + auto eq = [&](uint32_t pix1, uint32_t pix2) { return ColorDistance::dist(pix1, pix2, cfg.luminanceWeight) < cfg.equalColorTolerance; }; + auto dist = [&](uint32_t pix1, uint32_t pix2) { return ColorDistance::dist(pix1, pix2, cfg.luminanceWeight); }; + + const bool doLineBlend = [&]() -> bool + { + if (getBottomR(blend) >= BLEND_DOMINANT) + return true; + + //make sure there is no second blending in an adjacent rotation for this pixel: handles insular pixels, mario eyes + if (getTopR(blend) != BLEND_NONE && !eq(e, g)) //but support double-blending for 90° corners + return false; + if (getBottomL(blend) != BLEND_NONE && !eq(e, c)) + return false; + + //no full blending for L-shapes; blend corner only (handles "mario mushroom eyes") + if (!eq(e, i) && eq(g, h) && eq(h, i) && eq(i, f) && eq(f, c)) + return false; + + return true; + }(); + + const uint32_t px = dist(e, f) <= dist(e, h) ? f : h; //choose most similar color + + OutputMatrix out(target, trgWidth); + + if (doLineBlend) + { + const double fg = dist(f, g); //test sample: 70% of values max(fg, hc) / min(fg, hc) are between 1.1 and 3.7 with median being 1.9 + const double hc = dist(h, c); // + + const bool haveShallowLine = cfg.steepDirectionThreshold * fg <= hc && e != g && d != g; + const bool haveSteepLine = cfg.steepDirectionThreshold * hc <= fg && e != c && b != c; + + if (haveShallowLine) + { + if (haveSteepLine) + Scaler::blendLineSteepAndShallow(px, out); + else + Scaler::blendLineShallow(px, out); + } + else + { + if (haveSteepLine) + Scaler::blendLineSteep(px, out); + else + Scaler::blendLineDiagonal(px, out); + } + } + else + Scaler::blendCorner(px, out); + } + + //#undef a +#undef b +#undef c +#undef d +#undef e +#undef f +#undef g +#undef h +#undef i +} + + +class OobReaderTransparent +{ +public: + OobReaderTransparent(const uint32_t* src, int srcWidth, int srcHeight, int y) : + s_m1(0 <= y - 1 && y - 1 < srcHeight ? src + srcWidth * (y - 1) : nullptr), + s_0 (0 <= y && y < srcHeight ? src + srcWidth * y : nullptr), + s_p1(0 <= y + 1 && y + 1 < srcHeight ? src + srcWidth * (y + 1) : nullptr), + s_p2(0 <= y + 2 && y + 2 < srcHeight ? src + srcWidth * (y + 2) : nullptr), + srcWidth_(srcWidth) {} + + void readDhlp(Kernel_4x4& ker, int x) const //(x, y) is at kernel position F + { + [[likely]] if (const int x_p2 = x + 2; 0 <= x_p2 && x_p2 < srcWidth_) + { + ker.d = s_m1 ? s_m1[x_p2] : 0; + ker.h = s_0 ? s_0 [x_p2] : 0; + ker.l = s_p1 ? s_p1[x_p2] : 0; + ker.p = s_p2 ? s_p2[x_p2] : 0; + } + else + { + ker.d = 0; + ker.h = 0; + ker.l = 0; + ker.p = 0; + } + } + +private: + const uint32_t* const s_m1; + const uint32_t* const s_0; + const uint32_t* const s_p1; + const uint32_t* const s_p2; + const int srcWidth_; +}; + + +class OobReaderDuplicate +{ +public: + OobReaderDuplicate(const uint32_t* src, int srcWidth, int srcHeight, int y) : + s_m1(src + srcWidth * std::clamp(y - 1, 0, srcHeight - 1)), + s_0 (src + srcWidth * std::clamp(y, 0, srcHeight - 1)), + s_p1(src + srcWidth * std::clamp(y + 1, 0, srcHeight - 1)), + s_p2(src + srcWidth * std::clamp(y + 2, 0, srcHeight - 1)), + srcWidth_(srcWidth) {} + + void readDhlp(Kernel_4x4& ker, int x) const //(x, y) is at kernel position F + { + const int x_p2 = std::clamp(x + 2, 0, srcWidth_ - 1); + ker.d = s_m1[x_p2]; + ker.h = s_0 [x_p2]; + ker.l = s_p1[x_p2]; + ker.p = s_p2[x_p2]; + } + +private: + const uint32_t* const s_m1; + const uint32_t* const s_0; + const uint32_t* const s_p1; + const uint32_t* const s_p2; + const int srcWidth_; +}; + + +template //scaler policy: see "Scaler2x" reference implementation +void scaleImage(const uint32_t* src, uint32_t* trg, int srcWidth, int srcHeight, const xbrz::ScalerCfg& cfg, int yFirst, int yLast) +{ + yFirst = std::max(yFirst, 0); + yLast = std::min(yLast, srcHeight); + if (yFirst >= yLast || srcWidth <= 0) + return; + + const int trgWidth = srcWidth * Scaler::scale; + + //(ab)use space of "sizeof(uint32_t) * srcWidth * Scaler::scale" at the end of the image as temporary + //buffer for "on the fly preprocessing" without risk of accidental overwriting before accessing + unsigned char* const preProcBuf = reinterpret_cast(trg + yLast * Scaler::scale * trgWidth) - srcWidth; + + //initialize preprocessing buffer for first row of current stripe: detect upper left and right corner blending + //this cannot be optimized for adjacent processing stripes; we must not allow for a memory race condition! + { + const OobReader oobReader(src, srcWidth, srcHeight, yFirst - 1); + + //initialize at position x = -1 + Kernel_4x4 ker4 = {}; + oobReader.readDhlp(ker4, -4); //hack: read a, e, i, m at x = -1 + ker4.a = ker4.d; + ker4.e = ker4.h; + ker4.i = ker4.l; + ker4.m = ker4.p; + + oobReader.readDhlp(ker4, -3); + ker4.b = ker4.d; + ker4.f = ker4.h; + ker4.j = ker4.l; + ker4.n = ker4.p; + + oobReader.readDhlp(ker4, -2); + ker4.c = ker4.d; + ker4.g = ker4.h; + ker4.k = ker4.l; + ker4.o = ker4.p; + + oobReader.readDhlp(ker4, -1); + + { + const BlendResult res = preProcessCorners(ker4, cfg); + clearAddTopL(preProcBuf[0], res.blend_k); //set 1st known corner for (0, yFirst) + } + + for (int x = 0; x < srcWidth; ++x) + { + ker4.a = ker4.b; //shift previous kernel to the left + ker4.e = ker4.f; // ----------------- + ker4.i = ker4.j; // | A | B | C | D | + ker4.m = ker4.n; // |---|---|---|---| + /**/ // | E | F | G | H | (x, yFirst - 1) is at position F + ker4.b = ker4.c; // |---|---|---|---| + ker4.f = ker4.g; // | I | J | K | L | + ker4.j = ker4.k; // |---|---|---|---| + ker4.n = ker4.o; // | M | N | O | P | + /**/ // ----------------- + ker4.c = ker4.d; + ker4.g = ker4.h; + ker4.k = ker4.l; + ker4.o = ker4.p; + + oobReader.readDhlp(ker4, x); + + /* preprocessing blend result: + --------- + | F | G | evaluate corner between F, G, J, K + |---+---| current input pixel is at position F + | J | K | + --------- */ + const BlendResult res = preProcessCorners(ker4, cfg); + addTopR(preProcBuf[x], res.blend_j); //set 2nd known corner for (x, yFirst) + + if (x + 1 < srcWidth) + clearAddTopL(preProcBuf[x + 1], res.blend_k); //set 1st known corner for (x + 1, yFirst) + } + } + //------------------------------------------------------------------------------------ + + for (int y = yFirst; y < yLast; ++y) + { + uint32_t* out = trg + Scaler::scale * y * trgWidth; //consider MT "striped" access + + const OobReader oobReader(src, srcWidth, srcHeight, y); + + //initialize at position x = -1 + Kernel_4x4 ker4 = {}; + oobReader.readDhlp(ker4, -4); //hack: read a, e, i, m at x = -1 + ker4.a = ker4.d; + ker4.e = ker4.h; + ker4.i = ker4.l; + ker4.m = ker4.p; + + oobReader.readDhlp(ker4, -3); + ker4.b = ker4.d; + ker4.f = ker4.h; + ker4.j = ker4.l; + ker4.n = ker4.p; + + oobReader.readDhlp(ker4, -2); + ker4.c = ker4.d; + ker4.g = ker4.h; + ker4.k = ker4.l; + ker4.o = ker4.p; + + oobReader.readDhlp(ker4, -1); + + unsigned char blend_xy1 = 0; //corner blending for current (x, y + 1) position + { + const BlendResult res = preProcessCorners(ker4, cfg); + clearAddTopL(blend_xy1, res.blend_k); //set 1st known corner for (0, y + 1) and buffer for use on next column + + addBottomL(preProcBuf[0], res.blend_g); //set 3rd known corner for (0, y) + } + + for (int x = 0; x < srcWidth; ++x, out += Scaler::scale) + { +#if defined _MSC_VER && !defined NDEBUG + breakIntoDebugger = debugPixelX == x && debugPixelY == y; +#endif + ker4.a = ker4.b; //shift previous kernel to the left + ker4.e = ker4.f; // ----------------- + ker4.i = ker4.j; // | A | B | C | D | + ker4.m = ker4.n; // |---|---|---|---| + /**/ // | E | F | G | H | (x, y) is at position F + ker4.b = ker4.c; // |---|---|---|---| + ker4.f = ker4.g; // | I | J | K | L | + ker4.j = ker4.k; // |---|---|---|---| + ker4.n = ker4.o; // | M | N | O | P | + /**/ // ----------------- + ker4.c = ker4.d; + ker4.g = ker4.h; + ker4.k = ker4.l; + ker4.o = ker4.p; + + oobReader.readDhlp(ker4, x); + + //evaluate the four corners on bottom-right of current pixel + unsigned char blend_xy = preProcBuf[x]; //for current (x, y) position + { + /* preprocessing blend result: + --------- + | F | G | evaluate corner between F, G, J, K + |---+---| current input pixel is at position F + | J | K | + --------- */ + const BlendResult res = preProcessCorners(ker4, cfg); + addBottomR(blend_xy, res.blend_f); //all four corners of (x, y) have been determined at this point due to processing sequence! + + addTopR(blend_xy1, res.blend_j); //set 2nd known corner for (x, y + 1) + preProcBuf[x] = blend_xy1; //store on current buffer position for use on next row + + [[likely]] if (x + 1 < srcWidth) + { + //blend_xy1 -> blend_x1y1 + clearAddTopL(blend_xy1, res.blend_k); //set 1st known corner for (x + 1, y + 1) and buffer for use on next column + + addBottomL(preProcBuf[x + 1], res.blend_g); //set 3rd known corner for (x + 1, y) + } + } + + //fill block of size scale * scale with the given color + fillBlock(out, trgWidth * sizeof(uint32_t), ker4.f, Scaler::scale, Scaler::scale); + //place *after* preprocessing step, to not overwrite the results while processing the last pixel! + + //blend all four corners of current pixel + if (blendingNeeded(blend_xy)) + { +#ifndef _MSC_VER +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wstrict-aliasing" +#endif + const auto& ker3 = reinterpret_cast(ker4); //"The Things We Do for Perf" + blendPixel(ker3, out, trgWidth, blend_xy, cfg); + blendPixel(ker3, out, trgWidth, blend_xy, cfg); + blendPixel(ker3, out, trgWidth, blend_xy, cfg); + blendPixel(ker3, out, trgWidth, blend_xy, cfg); +#ifndef _MSC_VER +#pragma GCC diagnostic pop +#endif + } + } + } +} + +//------------------------------------------------------------------------------------ + +template +struct Scaler2x : public ColorGradient +{ + static const int scale = 2; + + template //bring template function into scope for GCC + static void alphaGrad(uint32_t& pixBack, uint32_t pixFront) { ColorGradient::template alphaGrad(pixBack, pixFront); } + + + template + static void blendLineShallow(uint32_t col, OutputMatrix& out) + { + alphaGrad<1, 4>(out.template ref(), col); + alphaGrad<3, 4>(out.template ref(), col); + } + + template + static void blendLineSteep(uint32_t col, OutputMatrix& out) + { + alphaGrad<1, 4>(out.template ref<0, scale - 1>(), col); + alphaGrad<3, 4>(out.template ref<1, scale - 1>(), col); + } + + template + static void blendLineSteepAndShallow(uint32_t col, OutputMatrix& out) + { + alphaGrad<1, 4>(out.template ref<1, 0>(), col); + alphaGrad<1, 4>(out.template ref<0, 1>(), col); + alphaGrad<5, 6>(out.template ref<1, 1>(), col); //[!] fixes 7/8 used in xBR + } + + template + static void blendLineDiagonal(uint32_t col, OutputMatrix& out) + { + alphaGrad<1, 2>(out.template ref<1, 1>(), col); + } + + template + static void blendCorner(uint32_t col, OutputMatrix& out) + { + //model a round corner + alphaGrad<21, 100>(out.template ref<1, 1>(), col); //exact: 1 - pi/4 = 0.2146018366 + } +}; + + +template +struct Scaler3x : public ColorGradient +{ + static const int scale = 3; + + template //bring template function into scope for GCC + static void alphaGrad(uint32_t& pixBack, uint32_t pixFront) { ColorGradient::template alphaGrad(pixBack, pixFront); } + + + template + static void blendLineShallow(uint32_t col, OutputMatrix& out) + { + alphaGrad<1, 4>(out.template ref(), col); + alphaGrad<1, 4>(out.template ref(), col); + + alphaGrad<3, 4>(out.template ref(), col); + out.template ref() = col; + } + + template + static void blendLineSteep(uint32_t col, OutputMatrix& out) + { + alphaGrad<1, 4>(out.template ref<0, scale - 1>(), col); + alphaGrad<1, 4>(out.template ref<2, scale - 2>(), col); + + alphaGrad<3, 4>(out.template ref<1, scale - 1>(), col); + out.template ref<2, scale - 1>() = col; + } + + template + static void blendLineSteepAndShallow(uint32_t col, OutputMatrix& out) + { + alphaGrad<1, 4>(out.template ref<2, 0>(), col); + alphaGrad<1, 4>(out.template ref<0, 2>(), col); + alphaGrad<3, 4>(out.template ref<2, 1>(), col); + alphaGrad<3, 4>(out.template ref<1, 2>(), col); + out.template ref<2, 2>() = col; + } + + template + static void blendLineDiagonal(uint32_t col, OutputMatrix& out) + { + alphaGrad<1, 8>(out.template ref<1, 2>(), col); //conflict with other rotations for this odd scale + alphaGrad<1, 8>(out.template ref<2, 1>(), col); + alphaGrad<7, 8>(out.template ref<2, 2>(), col); // + } + + template + static void blendCorner(uint32_t col, OutputMatrix& out) + { + //model a round corner + alphaGrad<45, 100>(out.template ref<2, 2>(), col); //exact: 0.4545939598 + //alphaGrad<7, 256>(out.template ref<2, 1>(), col); //0.02826017254 -> negligible + avoid conflicts with other rotations for this odd scale + //alphaGrad<7, 256>(out.template ref<1, 2>(), col); //0.02826017254 + } +}; + + +template +struct Scaler4x : public ColorGradient +{ + static const int scale = 4; + + template //bring template function into scope for GCC + static void alphaGrad(uint32_t& pixBack, uint32_t pixFront) { ColorGradient::template alphaGrad(pixBack, pixFront); } + + + template + static void blendLineShallow(uint32_t col, OutputMatrix& out) + { + alphaGrad<1, 4>(out.template ref(), col); + alphaGrad<1, 4>(out.template ref(), col); + + alphaGrad<3, 4>(out.template ref(), col); + alphaGrad<3, 4>(out.template ref(), col); + + out.template ref() = col; + out.template ref() = col; + } + + template + static void blendLineSteep(uint32_t col, OutputMatrix& out) + { + alphaGrad<1, 4>(out.template ref<0, scale - 1>(), col); + alphaGrad<1, 4>(out.template ref<2, scale - 2>(), col); + + alphaGrad<3, 4>(out.template ref<1, scale - 1>(), col); + alphaGrad<3, 4>(out.template ref<3, scale - 2>(), col); + + out.template ref<2, scale - 1>() = col; + out.template ref<3, scale - 1>() = col; + } + + template + static void blendLineSteepAndShallow(uint32_t col, OutputMatrix& out) + { + alphaGrad<3, 4>(out.template ref<3, 1>(), col); + alphaGrad<3, 4>(out.template ref<1, 3>(), col); + alphaGrad<1, 4>(out.template ref<3, 0>(), col); + alphaGrad<1, 4>(out.template ref<0, 3>(), col); + + alphaGrad<1, 3>(out.template ref<2, 2>(), col); //[!] fixes 1/4 used in xBR + + out.template ref<3, 3>() = col; + out.template ref<3, 2>() = col; + out.template ref<2, 3>() = col; + } + + template + static void blendLineDiagonal(uint32_t col, OutputMatrix& out) + { + alphaGrad<1, 2>(out.template ref(), col); + alphaGrad<1, 2>(out.template ref(), col); + out.template ref() = col; + } + + template + static void blendCorner(uint32_t col, OutputMatrix& out) + { + //model a round corner + alphaGrad<68, 100>(out.template ref<3, 3>(), col); //exact: 0.6848532563 + alphaGrad< 9, 100>(out.template ref<3, 2>(), col); //0.08677704501 + alphaGrad< 9, 100>(out.template ref<2, 3>(), col); //0.08677704501 + } +}; + + +template +struct Scaler5x : public ColorGradient +{ + static const int scale = 5; + + template //bring template function into scope for GCC + static void alphaGrad(uint32_t& pixBack, uint32_t pixFront) { ColorGradient::template alphaGrad(pixBack, pixFront); } + + + template + static void blendLineShallow(uint32_t col, OutputMatrix& out) + { + alphaGrad<1, 4>(out.template ref(), col); + alphaGrad<1, 4>(out.template ref(), col); + alphaGrad<1, 4>(out.template ref(), col); + + alphaGrad<3, 4>(out.template ref(), col); + alphaGrad<3, 4>(out.template ref(), col); + + out.template ref() = col; + out.template ref() = col; + out.template ref() = col; + out.template ref() = col; + } + + template + static void blendLineSteep(uint32_t col, OutputMatrix& out) + { + alphaGrad<1, 4>(out.template ref<0, scale - 1>(), col); + alphaGrad<1, 4>(out.template ref<2, scale - 2>(), col); + alphaGrad<1, 4>(out.template ref<4, scale - 3>(), col); + + alphaGrad<3, 4>(out.template ref<1, scale - 1>(), col); + alphaGrad<3, 4>(out.template ref<3, scale - 2>(), col); + + out.template ref<2, scale - 1>() = col; + out.template ref<3, scale - 1>() = col; + out.template ref<4, scale - 1>() = col; + out.template ref<4, scale - 2>() = col; + } + + template + static void blendLineSteepAndShallow(uint32_t col, OutputMatrix& out) + { + alphaGrad<1, 4>(out.template ref<0, scale - 1>(), col); + alphaGrad<1, 4>(out.template ref<2, scale - 2>(), col); + alphaGrad<3, 4>(out.template ref<1, scale - 1>(), col); + + alphaGrad<1, 4>(out.template ref(), col); + alphaGrad<1, 4>(out.template ref(), col); + alphaGrad<3, 4>(out.template ref(), col); + + alphaGrad<2, 3>(out.template ref<3, 3>(), col); + + out.template ref<2, scale - 1>() = col; + out.template ref<3, scale - 1>() = col; + out.template ref<4, scale - 1>() = col; + + out.template ref() = col; + out.template ref() = col; + } + + template + static void blendLineDiagonal(uint32_t col, OutputMatrix& out) + { + alphaGrad<1, 8>(out.template ref(), col); //conflict with other rotations for this odd scale + alphaGrad<1, 8>(out.template ref(), col); + alphaGrad<1, 8>(out.template ref(), col); // + + alphaGrad<7, 8>(out.template ref<4, 3>(), col); + alphaGrad<7, 8>(out.template ref<3, 4>(), col); + + out.template ref<4, 4>() = col; + } + + template + static void blendCorner(uint32_t col, OutputMatrix& out) + { + //model a round corner + alphaGrad<86, 100>(out.template ref<4, 4>(), col); //exact: 0.8631434088 + alphaGrad<23, 100>(out.template ref<4, 3>(), col); //0.2306749731 + alphaGrad<23, 100>(out.template ref<3, 4>(), col); //0.2306749731 + //alphaGrad<1, 64>(out.template ref<4, 2>(), col); //0.01676812367 -> negligible + avoid conflicts with other rotations for this odd scale + //alphaGrad<1, 64>(out.template ref<2, 4>(), col); //0.01676812367 + } +}; + + +template +struct Scaler6x : public ColorGradient +{ + static const int scale = 6; + + template //bring template function into scope for GCC + static void alphaGrad(uint32_t& pixBack, uint32_t pixFront) { ColorGradient::template alphaGrad(pixBack, pixFront); } + + + template + static void blendLineShallow(uint32_t col, OutputMatrix& out) + { + alphaGrad<1, 4>(out.template ref(), col); + alphaGrad<1, 4>(out.template ref(), col); + alphaGrad<1, 4>(out.template ref(), col); + + alphaGrad<3, 4>(out.template ref(), col); + alphaGrad<3, 4>(out.template ref(), col); + alphaGrad<3, 4>(out.template ref(), col); + + out.template ref() = col; + out.template ref() = col; + out.template ref() = col; + out.template ref() = col; + + out.template ref() = col; + out.template ref() = col; + } + + template + static void blendLineSteep(uint32_t col, OutputMatrix& out) + { + alphaGrad<1, 4>(out.template ref<0, scale - 1>(), col); + alphaGrad<1, 4>(out.template ref<2, scale - 2>(), col); + alphaGrad<1, 4>(out.template ref<4, scale - 3>(), col); + + alphaGrad<3, 4>(out.template ref<1, scale - 1>(), col); + alphaGrad<3, 4>(out.template ref<3, scale - 2>(), col); + alphaGrad<3, 4>(out.template ref<5, scale - 3>(), col); + + out.template ref<2, scale - 1>() = col; + out.template ref<3, scale - 1>() = col; + out.template ref<4, scale - 1>() = col; + out.template ref<5, scale - 1>() = col; + + out.template ref<4, scale - 2>() = col; + out.template ref<5, scale - 2>() = col; + } + + template + static void blendLineSteepAndShallow(uint32_t col, OutputMatrix& out) + { + alphaGrad<1, 4>(out.template ref<0, scale - 1>(), col); + alphaGrad<1, 4>(out.template ref<2, scale - 2>(), col); + alphaGrad<3, 4>(out.template ref<1, scale - 1>(), col); + alphaGrad<3, 4>(out.template ref<3, scale - 2>(), col); + + alphaGrad<1, 4>(out.template ref(), col); + alphaGrad<1, 4>(out.template ref(), col); + alphaGrad<3, 4>(out.template ref(), col); + alphaGrad<3, 4>(out.template ref(), col); + + out.template ref<2, scale - 1>() = col; + out.template ref<3, scale - 1>() = col; + out.template ref<4, scale - 1>() = col; + out.template ref<5, scale - 1>() = col; + + out.template ref<4, scale - 2>() = col; + out.template ref<5, scale - 2>() = col; + + out.template ref() = col; + out.template ref() = col; + } + + template + static void blendLineDiagonal(uint32_t col, OutputMatrix& out) + { + alphaGrad<1, 2>(out.template ref(), col); + alphaGrad<1, 2>(out.template ref(), col); + alphaGrad<1, 2>(out.template ref(), col); + + out.template ref() = col; + out.template ref() = col; + out.template ref() = col; + } + + template + static void blendCorner(uint32_t col, OutputMatrix& out) + { + //model a round corner + alphaGrad<97, 100>(out.template ref<5, 5>(), col); //exact: 0.9711013910 + alphaGrad<42, 100>(out.template ref<4, 5>(), col); //0.4236372243 + alphaGrad<42, 100>(out.template ref<5, 4>(), col); //0.4236372243 + alphaGrad< 6, 100>(out.template ref<5, 3>(), col); //0.05652034508 + alphaGrad< 6, 100>(out.template ref<3, 5>(), col); //0.05652034508 + } +}; + +//------------------------------------------------------------------------------------ + +struct ColorDistanceRGB +{ + static double dist(uint32_t pix1, uint32_t pix2, double luminanceWeight) + { + return distYCbCrBuffered(pix1, pix2); + + //if (pix1 == pix2) //about 4% perf boost + // return 0; + //return distYCbCr(pix1, pix2, luminanceWeight); + } +}; + +struct ColorDistanceARGB +{ + static double dist(uint32_t pix1, uint32_t pix2, double luminanceWeight) + { + const double a1 = getAlpha(pix1) / 255.0 ; + const double a2 = getAlpha(pix2) / 255.0 ; + /* + Requirements for a color distance handling alpha channel: with a1, a2 in [0, 1] + + 1. if a1 = a2, distance should be: a1 * distYCbCr() + 2. if a1 = 0, distance should be: a2 * distYCbCr(black, white) = a2 * 255 + 3. if a1 = 1, ??? maybe: 255 * (1 - a2) + a2 * distYCbCr() + */ + + //return std::min(a1, a2) * distYCbCrBuffered(pix1, pix2) + 255 * abs(a1 - a2); + //=> following code is 15% faster: + const double d = distYCbCrBuffered(pix1, pix2); + if (a1 < a2) + return a1 * d + 255 * (a2 - a1); + else + return a2 * d + 255 * (a1 - a2); + + //alternative? return std::sqrt(a1 * a2 * square(distYCbCrBuffered(pix1, pix2)) + square(255 * (a1 - a2))); + } +}; + + +struct ColorDistanceUnbufferedARGB +{ + static double dist(uint32_t pix1, uint32_t pix2, double luminanceWeight) + { + const double a1 = getAlpha(pix1) / 255.0 ; + const double a2 = getAlpha(pix2) / 255.0 ; + + const double d = distYCbCr(pix1, pix2, luminanceWeight); + if (a1 < a2) + return a1 * d + 255 * (a2 - a1); + else + return a2 * d + 255 * (a1 - a2); + } +}; + + +struct ColorGradientRGB +{ + template + static void alphaGrad(uint32_t& pixBack, uint32_t pixFront) + { + pixBack = gradientRGB(pixFront, pixBack); + } +}; + +struct ColorGradientARGB +{ + template + static void alphaGrad(uint32_t& pixBack, uint32_t pixFront) + { + pixBack = gradientARGB(pixFront, pixBack); + } +}; +} + + +void xbrz::scale(size_t factor, const uint32_t* src, uint32_t* trg, int srcWidth, int srcHeight, ColorFormat colFmt, const xbrz::ScalerCfg& cfg, int yFirst, int yLast) +{ + if (factor == 1) + { + std::copy(src + yFirst * srcWidth, src + yLast * srcWidth, trg); + return; + } + + static_assert(SCALE_FACTOR_MAX == 6); + switch (colFmt) + { + case ColorFormat::RGB: + switch (factor) + { + case 2: + return scaleImage, ColorDistanceRGB, OobReaderDuplicate>(src, trg, srcWidth, srcHeight, cfg, yFirst, yLast); + case 3: + return scaleImage, ColorDistanceRGB, OobReaderDuplicate>(src, trg, srcWidth, srcHeight, cfg, yFirst, yLast); + case 4: + return scaleImage, ColorDistanceRGB, OobReaderDuplicate>(src, trg, srcWidth, srcHeight, cfg, yFirst, yLast); + case 5: + return scaleImage, ColorDistanceRGB, OobReaderDuplicate>(src, trg, srcWidth, srcHeight, cfg, yFirst, yLast); + case 6: + return scaleImage, ColorDistanceRGB, OobReaderDuplicate>(src, trg, srcWidth, srcHeight, cfg, yFirst, yLast); + } + break; + + case ColorFormat::ARGB_CLAMPED: + switch (factor) + { + case 2: + return scaleImage, ColorDistanceARGB, OobReaderDuplicate>(src, trg, srcWidth, srcHeight, cfg, yFirst, yLast); + case 3: + return scaleImage, ColorDistanceARGB, OobReaderDuplicate>(src, trg, srcWidth, srcHeight, cfg, yFirst, yLast); + case 4: + return scaleImage, ColorDistanceARGB, OobReaderDuplicate>(src, trg, srcWidth, srcHeight, cfg, yFirst, yLast); + case 5: + return scaleImage, ColorDistanceARGB, OobReaderDuplicate>(src, trg, srcWidth, srcHeight, cfg, yFirst, yLast); + case 6: + return scaleImage, ColorDistanceARGB, OobReaderDuplicate>(src, trg, srcWidth, srcHeight, cfg, yFirst, yLast); + } + break; + + case ColorFormat::ARGB: + switch (factor) + { + case 2: + return scaleImage, ColorDistanceARGB, OobReaderTransparent>(src, trg, srcWidth, srcHeight, cfg, yFirst, yLast); + case 3: + return scaleImage, ColorDistanceARGB, OobReaderTransparent>(src, trg, srcWidth, srcHeight, cfg, yFirst, yLast); + case 4: + return scaleImage, ColorDistanceARGB, OobReaderTransparent>(src, trg, srcWidth, srcHeight, cfg, yFirst, yLast); + case 5: + return scaleImage, ColorDistanceARGB, OobReaderTransparent>(src, trg, srcWidth, srcHeight, cfg, yFirst, yLast); + case 6: + return scaleImage, ColorDistanceARGB, OobReaderTransparent>(src, trg, srcWidth, srcHeight, cfg, yFirst, yLast); + } + break; + + case ColorFormat::ARGB_UNBUFFERED: + switch (factor) + { + case 2: + return scaleImage, ColorDistanceUnbufferedARGB, OobReaderTransparent>(src, trg, srcWidth, srcHeight, cfg, yFirst, yLast); + case 3: + return scaleImage, ColorDistanceUnbufferedARGB, OobReaderTransparent>(src, trg, srcWidth, srcHeight, cfg, yFirst, yLast); + case 4: + return scaleImage, ColorDistanceUnbufferedARGB, OobReaderTransparent>(src, trg, srcWidth, srcHeight, cfg, yFirst, yLast); + case 5: + return scaleImage, ColorDistanceUnbufferedARGB, OobReaderTransparent>(src, trg, srcWidth, srcHeight, cfg, yFirst, yLast); + case 6: + return scaleImage, ColorDistanceUnbufferedARGB, OobReaderTransparent>(src, trg, srcWidth, srcHeight, cfg, yFirst, yLast); + } + break; + } + assert(false); +} + + +bool xbrz::equalColorTest(uint32_t col1, uint32_t col2, ColorFormat colFmt, double luminanceWeight, double equalColorTolerance) +{ + switch (colFmt) + { + case ColorFormat::RGB: + return ColorDistanceRGB::dist(col1, col2, luminanceWeight) < equalColorTolerance; + case ColorFormat::ARGB: + case ColorFormat::ARGB_CLAMPED: + return ColorDistanceARGB::dist(col1, col2, luminanceWeight) < equalColorTolerance; + case ColorFormat::ARGB_UNBUFFERED: + return ColorDistanceUnbufferedARGB::dist(col1, col2, luminanceWeight) < equalColorTolerance; + } + assert(false); + return false; +} + + +void xbrz::bilinearScale(const uint32_t* src, int srcWidth, int srcHeight, + /**/ uint32_t* trg, int trgWidth, int trgHeight) +{ + bilinearScale(src, srcWidth, srcHeight, srcWidth * sizeof(uint32_t), + trg, trgWidth, trgHeight, trgWidth * sizeof(uint32_t), + 0, trgHeight, [](uint32_t pix) { return pix; }); +} + + +void xbrz::nearestNeighborScale(const uint32_t* src, int srcWidth, int srcHeight, + /**/ uint32_t* trg, int trgWidth, int trgHeight) +{ + nearestNeighborScale(src, srcWidth, srcHeight, srcWidth * sizeof(uint32_t), + trg, trgWidth, trgHeight, trgWidth * sizeof(uint32_t), + 0, trgHeight, [](uint32_t pix) { return pix; }); +} + + +#if 0 +//#include +void bilinearScaleCpu(const uint32_t* src, int srcWidth, int srcHeight, + /**/ uint32_t* trg, int trgWidth, int trgHeight) +{ + const int TASK_GRANULARITY = 16; + + concurrency::task_group tg; + + for (int i = 0; i < trgHeight; i += TASK_GRANULARITY) + tg.run([=] + { + const int iLast = std::min(i + TASK_GRANULARITY, trgHeight); + xbrz::bilinearScale(src, srcWidth, srcHeight, srcWidth * sizeof(uint32_t), + trg, trgWidth, trgHeight, trgWidth * sizeof(uint32_t), + i, iLast, [](uint32_t pix) { return pix; }); + }); + tg.wait(); +} + + +//Perf: AMP vs CPU: merely ~10% shorter runtime (scaling 1280x800 -> 1920x1080) +//#include +void bilinearScaleAmp(const uint32_t* src, int srcWidth, int srcHeight, //throw concurrency::runtime_exception + /**/ uint32_t* trg, int trgWidth, int trgHeight) +{ + //C++ AMP reference: https://msdn.microsoft.com/en-us/library/hh289390.aspx + //introduction to C++ AMP: https://msdn.microsoft.com/en-us/magazine/hh882446.aspx + using namespace concurrency; + //TODO: pitch + + if (srcHeight <= 0 || srcWidth <= 0) return; + + const float scaleX = static_cast(trgWidth ) / srcWidth; + const float scaleY = static_cast(trgHeight) / srcHeight; + + array_view srcView(srcHeight, srcWidth, src); + array_view< uint32_t, 2> trgView(trgHeight, trgWidth, trg); + trgView.discard_data(); + + parallel_for_each(trgView.extent, [=](index<2> idx) restrict(amp) //throw ? + { + const int y = idx[0]; + const int x = idx[1]; + //Perf notes: + // -> float-based calculation is (almost) 2x as fas as double! + // -> no noticeable improvement via tiling: https://msdn.microsoft.com/en-us/magazine/hh882447.aspx + // -> no noticeable improvement with restrict(amp,cpu) + // -> iterating over y-axis only is significantly slower! + // -> pre-calculating x,y-dependent variables in a buffer + array_view<> is ~ 20 % slower! + const int y1 = srcHeight * y / trgHeight; + int y2 = y1 + 1; + if (y2 == srcHeight) --y2; + + const float yy1 = y / scaleY - y1; + const float y2y = 1 - yy1; + //------------------------------------- + const int x1 = srcWidth * x / trgWidth; + int x2 = x1 + 1; + if (x2 == srcWidth) --x2; + + const float xx1 = x / scaleX - x1; + const float x2x = 1 - xx1; + //------------------------------------- + const float x2xy2y = x2x * y2y; + const float xx1y2y = xx1 * y2y; + const float x2xyy1 = x2x * yy1; + const float xx1yy1 = xx1 * yy1; + + auto interpolate = [=](int offset) + { + /* + https://en.wikipedia.org/wiki/Bilinear_interpolation + (c11(x2 - x) + c21(x - x1)) * (y2 - y ) + + (c12(x2 - x) + c22(x - x1)) * (y - y1) + */ + const auto c11 = (srcView(y1, x1) >> (8 * offset)) & 0xff; + const auto c21 = (srcView(y1, x2) >> (8 * offset)) & 0xff; + const auto c12 = (srcView(y2, x1) >> (8 * offset)) & 0xff; + const auto c22 = (srcView(y2, x2) >> (8 * offset)) & 0xff; + + return c11 * x2xy2y + c21 * xx1y2y + + c12 * x2xyy1 + c22 * xx1yy1; + }; + + const float bi = interpolate(0); + const float gi = interpolate(1); + const float ri = interpolate(2); + const float ai = interpolate(3); + + const auto b = static_cast(bi + 0.5f); + const auto g = static_cast(gi + 0.5f); + const auto r = static_cast(ri + 0.5f); + const auto a = static_cast(ai + 0.5f); + + trgView(y, x) = (a << 24) | (r << 16) | (g << 8) | b; + }); + trgView.synchronize(); //throw ? +} +#endif diff --git a/client/xBRZ/xbrz.h b/client/xBRZ/xbrz.h new file mode 100644 index 000000000..4e646dba6 --- /dev/null +++ b/client/xBRZ/xbrz.h @@ -0,0 +1,80 @@ +// **************************************************************************** +// * This file is part of the xBRZ project. It is distributed under * +// * GNU General Public License: https://www.gnu.org/licenses/gpl-3.0 * +// * Copyright (C) Zenju (zenju AT gmx DOT de) - All Rights Reserved * +// * * +// * Additionally and as a special exception, the author gives permission * +// * to link the code of this program with the following libraries * +// * (or with modified versions that use the same licenses), and distribute * +// * linked combinations including the two: MAME, FreeFileSync, Snes9x, ePSXe * +// * You must obey the GNU General Public License in all respects for all of * +// * the code used other than MAME, FreeFileSync, Snes9x, ePSXe. * +// * If you modify this file, you may extend this exception to your version * +// * of the file, but you are not obligated to do so. If you do not wish to * +// * do so, delete this exception statement from your version. * +// **************************************************************************** + +#ifndef XBRZ_HEADER_3847894708239054 +#define XBRZ_HEADER_3847894708239054 + +#include //size_t +#include //uint32_t +#include +#include "xbrz_config.h" + + +namespace xbrz +{ +/* +------------------------------------------------------------------------- +| xBRZ: "Scale by rules" - high quality image upscaling filter by Zenju | +------------------------------------------------------------------------- +using a modified approach of xBR: +http://board.byuu.org/viewtopic.php?f=10&t=2248 +- new rule set preserving small image features +- highly optimized for performance +- support alpha channel +- support multithreading +- support 64-bit architectures +- support processing image slices +- support scaling up to 6xBRZ +*/ + +enum class ColorFormat //from high bits -> low bits, 8 bit per channel +{ + RGB, //8 bit for each red, green, blue, upper 8 bits unused + ARGB, //including alpha channel, BGRA byte order on little-endian machines + ARGB_CLAMPED, // like ARGB, but edges are treated as opaque, with same color as edge + ARGB_UNBUFFERED, //like ARGB, but without the one-time buffer creation overhead (ca. 100 - 300 ms) at the expense of a slightly slower scaling time +}; + +const int SCALE_FACTOR_MAX = 6; + +/* +-> map source (srcWidth * srcHeight) to target (scale * width x scale * height) image, optionally processing a half-open slice of rows [yFirst, yLast) only +-> if your emulator changes only a few image slices during each cycle (e.g. DOSBox) then there's no need to run xBRZ on the complete image: + Just make sure you enlarge the source image slice by 2 rows on top and 2 on bottom (this is the additional range the xBRZ algorithm is using during analysis) + CAVEAT: If there are multiple changed slices, make sure they do not overlap after adding these additional rows in order to avoid a memory race condition + in the target image data if you are using multiple threads for processing each enlarged slice! + +THREAD-SAFETY: - parts of the same image may be scaled by multiple threads as long as the [yFirst, yLast) ranges do not overlap! + - there is a minor inefficiency for the first row of a slice, so avoid processing single rows only; suggestion: process at least 8-16 rows +*/ +void scale(size_t factor, //valid range: 2 - SCALE_FACTOR_MAX + const uint32_t* src, uint32_t* trg, int srcWidth, int srcHeight, + ColorFormat colFmt, + const ScalerCfg& cfg = ScalerCfg(), + int yFirst = 0, int yLast = std::numeric_limits::max()); //slice of source image + +void bilinearScale(const uint32_t* src, int srcWidth, int srcHeight, + /**/ uint32_t* trg, int trgWidth, int trgHeight); + +void nearestNeighborScale(const uint32_t* src, int srcWidth, int srcHeight, + /**/ uint32_t* trg, int trgWidth, int trgHeight); + + +//parameter tuning +bool equalColorTest(uint32_t col1, uint32_t col2, ColorFormat colFmt, double luminanceWeight, double equalColorTolerance); +} + +#endif diff --git a/client/xBRZ/xbrz_config.h b/client/xBRZ/xbrz_config.h new file mode 100644 index 000000000..fcfda99ab --- /dev/null +++ b/client/xBRZ/xbrz_config.h @@ -0,0 +1,35 @@ +// **************************************************************************** +// * This file is part of the xBRZ project. It is distributed under * +// * GNU General Public License: https://www.gnu.org/licenses/gpl-3.0 * +// * Copyright (C) Zenju (zenju AT gmx DOT de) - All Rights Reserved * +// * * +// * Additionally and as a special exception, the author gives permission * +// * to link the code of this program with the following libraries * +// * (or with modified versions that use the same licenses), and distribute * +// * linked combinations including the two: MAME, FreeFileSync, Snes9x, ePSXe * +// * You must obey the GNU General Public License in all respects for all of * +// * the code used other than MAME, FreeFileSync, Snes9x, ePSXe. * +// * If you modify this file, you may extend this exception to your version * +// * of the file, but you are not obligated to do so. If you do not wish to * +// * do so, delete this exception statement from your version. * +// **************************************************************************** + +#ifndef XBRZ_CONFIG_HEADER_284578425345 +#define XBRZ_CONFIG_HEADER_284578425345 + +//do NOT include any headers here! used by xBRZ_dll!!! + +namespace xbrz +{ +struct ScalerCfg +{ + double luminanceWeight = 1; + double equalColorTolerance = 30; + double centerDirectionBias = 4; + double dominantDirectionThreshold = 3.6; + double steepDirectionThreshold = 2.2; + double newTestAttribute = 0; //unused; test new parameters +}; +} + +#endif diff --git a/client/xBRZ/xbrz_tools.h b/client/xBRZ/xbrz_tools.h new file mode 100644 index 000000000..b8bb8aa0c --- /dev/null +++ b/client/xBRZ/xbrz_tools.h @@ -0,0 +1,266 @@ +// **************************************************************************** +// * This file is part of the xBRZ project. It is distributed under * +// * GNU General Public License: https://www.gnu.org/licenses/gpl-3.0 * +// * Copyright (C) Zenju (zenju AT gmx DOT de) - All Rights Reserved * +// * * +// * Additionally and as a special exception, the author gives permission * +// * to link the code of this program with the following libraries * +// * (or with modified versions that use the same licenses), and distribute * +// * linked combinations including the two: MAME, FreeFileSync, Snes9x, ePSXe * +// * You must obey the GNU General Public License in all respects for all of * +// * the code used other than MAME, FreeFileSync, Snes9x, ePSXe. * +// * If you modify this file, you may extend this exception to your version * +// * of the file, but you are not obligated to do so. If you do not wish to * +// * do so, delete this exception statement from your version. * +// **************************************************************************** + +#ifndef XBRZ_TOOLS_H_825480175091875 +#define XBRZ_TOOLS_H_825480175091875 + +#include +#include +#include + + +namespace xbrz +{ +template inline +unsigned char getByte(uint32_t val) { return static_cast((val >> (8 * N)) & 0xff); } + +inline unsigned char getAlpha(uint32_t pix) { return getByte<3>(pix); } +inline unsigned char getRed (uint32_t pix) { return getByte<2>(pix); } +inline unsigned char getGreen(uint32_t pix) { return getByte<1>(pix); } +inline unsigned char getBlue (uint32_t pix) { return getByte<0>(pix); } + +inline uint32_t makePixel(unsigned char a, unsigned char r, unsigned char g, unsigned char b) { return (a << 24) | (r << 16) | (g << 8) | b; } +inline uint32_t makePixel( unsigned char r, unsigned char g, unsigned char b) { return (r << 16) | (g << 8) | b; } + +inline uint32_t rgb555to888(uint16_t pix) { return ((pix & 0x7C00) << 9) | ((pix & 0x03E0) << 6) | ((pix & 0x001F) << 3); } +inline uint32_t rgb565to888(uint16_t pix) { return ((pix & 0xF800) << 8) | ((pix & 0x07E0) << 5) | ((pix & 0x001F) << 3); } + +inline uint16_t rgb888to555(uint32_t pix) { return static_cast(((pix & 0xF80000) >> 9) | ((pix & 0x00F800) >> 6) | ((pix & 0x0000F8) >> 3)); } +inline uint16_t rgb888to565(uint32_t pix) { return static_cast(((pix & 0xF80000) >> 8) | ((pix & 0x00FC00) >> 5) | ((pix & 0x0000F8) >> 3)); } + + +template inline +Pix* byteAdvance(Pix* ptr, int bytes) +{ + using PixNonConst = typename std::remove_cv::type; + using PixByte = typename std::conditional::value, char, const char>::type; + + static_assert(std::is_integral::value, "Pix* is expected to be cast-able to char*"); + + return reinterpret_cast(reinterpret_cast(ptr) + bytes); +} + + +//fill block with the given color +template inline +void fillBlock(Pix* trg, int pitch /*[bytes]*/, Pix col, int blockWidth, int blockHeight) +{ + //for (int y = 0; y < blockHeight; ++y, trg = byteAdvance(trg, pitch)) + // std::fill(trg, trg + blockWidth, col); + + for (int y = 0; y < blockHeight; ++y, trg = byteAdvance(trg, pitch)) + for (int x = 0; x < blockWidth; ++x) + trg[x] = col; +} + + +//nearest-neighbor (going over target image - slow for upscaling, since source is read multiple times missing out on cache! Fast for similar image sizes!) +template +void nearestNeighborScale(const PixSrc* src, int srcWidth, int srcHeight, int srcPitch /*[bytes]*/, + /**/ PixTrg* trg, int trgWidth, int trgHeight, int trgPitch /*[bytes]*/, + int yFirst, int yLast, PixConverter pixCvrt /*convert PixSrc to PixTrg*/) +{ + static_assert(std::is_integral::value, "PixSrc* is expected to be cast-able to char*"); + static_assert(std::is_integral::value, "PixTrg* is expected to be cast-able to char*"); + + static_assert(std::is_same::value, "PixConverter returning wrong pixel format"); + + if (srcPitch < srcWidth * static_cast(sizeof(PixSrc)) || + trgPitch < trgWidth * static_cast(sizeof(PixTrg))) + { + assert(false); + return; + } + + yFirst = std::max(yFirst, 0); + yLast = std::min(yLast, trgHeight); + if (yFirst >= yLast || srcHeight <= 0 || srcWidth <= 0) return; + + for (int y = yFirst; y < yLast; ++y) + { + const int ySrc = srcHeight * y / trgHeight; + const PixSrc* const srcLine = byteAdvance(src, ySrc * srcPitch); + PixTrg* const trgLine = byteAdvance(trg, y * trgPitch); + + for (int x = 0; x < trgWidth; ++x) + { + const int xSrc = srcWidth * x / trgWidth; + trgLine[x] = pixCvrt(srcLine[xSrc]); + } + } +} + + +//nearest-neighbor (going over source image - fast for upscaling, since source is read only once +template +void nearestNeighborScaleOverSource(const PixSrc* src, int srcWidth, int srcHeight, int srcPitch /*[bytes]*/, + /**/ PixTrg* trg, int trgWidth, int trgHeight, int trgPitch /*[bytes]*/, + int yFirst, int yLast, PixConverter pixCvrt /*convert PixSrc to PixTrg*/) +{ + static_assert(std::is_integral::value, "PixSrc* is expected to be cast-able to char*"); + static_assert(std::is_integral::value, "PixTrg* is expected to be cast-able to char*"); + + static_assert(std::is_same::value, "PixConverter returning wrong pixel format"); + + if (srcPitch < srcWidth * static_cast(sizeof(PixSrc)) || + trgPitch < trgWidth * static_cast(sizeof(PixTrg))) + { + assert(false); + return; + } + + yFirst = std::max(yFirst, 0); + yLast = std::min(yLast, srcHeight); + if (yFirst >= yLast || trgWidth <= 0 || trgHeight <= 0) return; + + for (int y = yFirst; y < yLast; ++y) + { + //mathematically: ySrc = floor(srcHeight * yTrg / trgHeight) + // => search for integers in: [ySrc, ySrc + 1) * trgHeight / srcHeight + + //keep within for loop to support MT input slices! + const int yTrgFirst = ( y * trgHeight + srcHeight - 1) / srcHeight; //=ceil(y * trgHeight / srcHeight) + const int yTrgLast = ((y + 1) * trgHeight + srcHeight - 1) / srcHeight; //=ceil(((y + 1) * trgHeight) / srcHeight) + const int blockHeight = yTrgLast - yTrgFirst; + + if (blockHeight > 0) + { + const PixSrc* srcLine = byteAdvance(src, y * srcPitch); + /**/ PixTrg* trgLine = byteAdvance(trg, yTrgFirst * trgPitch); + int xTrgFirst = 0; + + for (int x = 0; x < srcWidth; ++x) + { + const int xTrgLast = ((x + 1) * trgWidth + srcWidth - 1) / srcWidth; + const int blockWidth = xTrgLast - xTrgFirst; + if (blockWidth > 0) + { + xTrgFirst = xTrgLast; + + const auto trgPix = pixCvrt(srcLine[x]); + fillBlock(trgLine, trgPitch, trgPix, blockWidth, blockHeight); + trgLine += blockWidth; + } + } + } + } +} + + +template +void bilinearScale(const uint32_t* src, int srcWidth, int srcHeight, int srcPitch, + /**/ PixTrg* trg, int trgWidth, int trgHeight, int trgPitch, + int yFirst, int yLast, PixConverter pixCvrt /*convert uint32_t to PixTrg*/) +{ + static_assert(std::is_integral::value, "PixTrg* is expected to be cast-able to char*"); + static_assert(std::is_same::value, "PixConverter returning wrong pixel format"); + + if (srcPitch < srcWidth * static_cast(sizeof(uint32_t)) || + trgPitch < trgWidth * static_cast(sizeof(PixTrg))) + { + assert(false); + return; + } + + yFirst = std::max(yFirst, 0); + yLast = std::min(yLast, trgHeight); + if (yFirst >= yLast || srcHeight <= 0 || srcWidth <= 0) return; + + const double scaleX = static_cast(trgWidth ) / srcWidth; + const double scaleY = static_cast(trgHeight) / srcHeight; + + //perf notes: + // -> double-based calculation is (slightly) faster than float + // -> pre-calculation gives significant boost; std::vector<> memory allocation is negligible! + struct CoeffsX + { + int x1 = 0; + int x2 = 0; + double xx1 = 0; + double x2x = 0; + }; + std::vector buf(trgWidth); + for (int x = 0; x < trgWidth; ++x) + { + const int x1 = srcWidth * x / trgWidth; + int x2 = x1 + 1; + if (x2 == srcWidth) --x2; + + const double xx1 = x / scaleX - x1; + const double x2x = 1 - xx1; + + buf[x] = { x1, x2, xx1, x2x }; + } + + for (int y = yFirst; y < yLast; ++y) + { + const int y1 = srcHeight * y / trgHeight; + int y2 = y1 + 1; + if (y2 == srcHeight) --y2; + + const double yy1 = y / scaleY - y1; + const double y2y = 1 - yy1; + + const uint32_t* const srcLine = byteAdvance(src, y1 * srcPitch); + const uint32_t* const srcLineNext = byteAdvance(src, y2 * srcPitch); + PixTrg* const trgLine = byteAdvance(trg, y * trgPitch); + + for (int x = 0; x < trgWidth; ++x) + { + //perf: do NOT "simplify" the variable layout without measurement! + const int x1 = buf[x].x1; + const int x2 = buf[x].x2; + const double xx1 = buf[x].xx1; + const double x2x = buf[x].x2x; + + const double x2xy2y = x2x * y2y; + const double xx1y2y = xx1 * y2y; + const double x2xyy1 = x2x * yy1; + const double xx1yy1 = xx1 * yy1; + + auto interpolate = [=](int offset) + { + /* https://en.wikipedia.org/wiki/Bilinear_interpolation + (c11(x2 - x) + c21(x - x1)) * (y2 - y ) + + (c12(x2 - x) + c22(x - x1)) * (y - y1) */ + const auto c11 = (srcLine [x1] >> (8 * offset)) & 0xff; + const auto c21 = (srcLine [x2] >> (8 * offset)) & 0xff; + const auto c12 = (srcLineNext[x1] >> (8 * offset)) & 0xff; + const auto c22 = (srcLineNext[x2] >> (8 * offset)) & 0xff; + + return c11 * x2xy2y + c21 * xx1y2y + + c12 * x2xyy1 + c22 * xx1yy1; + }; + + const double bi = interpolate(0); + const double gi = interpolate(1); + const double ri = interpolate(2); + const double ai = interpolate(3); + + const auto b = static_cast(bi + 0.5); + const auto g = static_cast(gi + 0.5); + const auto r = static_cast(ri + 0.5); + const auto a = static_cast(ai + 0.5); + + const uint32_t trgPix = (a << 24) | (r << 16) | (g << 8) | b; + + trgLine[x] = pixCvrt(trgPix); + } + } +} +} + +#endif //XBRZ_TOOLS_H_825480175091875 diff --git a/client/CFocusableHelper.cpp b/clientapp/CFocusableHelper.cpp similarity index 90% rename from client/CFocusableHelper.cpp rename to clientapp/CFocusableHelper.cpp index ea0297eca..3772694d2 100644 --- a/client/CFocusableHelper.cpp +++ b/clientapp/CFocusableHelper.cpp @@ -9,7 +9,7 @@ */ #include "StdInc.h" #include "CFocusableHelper.h" -#include "widgets/CTextInput.h" +#include "../client/widgets/CTextInput.h" void removeFocusFromActiveInput() { diff --git a/client/CFocusableHelper.h b/clientapp/CFocusableHelper.h similarity index 100% rename from client/CFocusableHelper.h rename to clientapp/CFocusableHelper.h diff --git a/clientapp/CMakeLists.txt b/clientapp/CMakeLists.txt new file mode 100644 index 000000000..40061748d --- /dev/null +++ b/clientapp/CMakeLists.txt @@ -0,0 +1,141 @@ +set(clientapp_SRCS + StdInc.cpp + EntryPoint.cpp +) + +set(clientapp_HEADERS + StdInc.h +) + +if(APPLE_IOS) + set(clientapp_SRCS ${clientapp_SRCS} + CFocusableHelper.cpp + ios/GameChatKeyboardHandler.m + ios/main.m + ios/startSDL.mm + ) + set(clientapp_HEADERS ${clientapp_HEADERS} + CFocusableHelper.h + ios/GameChatKeyboardHandler.h + ios/startSDL.h + ) +endif() + +assign_source_group(${clientapp_SRCS} ${clientapp_HEADERS}) + +if(ANDROID) + add_library(vcmiclient SHARED ${clientapp_SRCS} ${clientapp_HEADERS}) + set_target_properties(vcmiclient PROPERTIES + OUTPUT_NAME "vcmiclient_${ANDROID_ABI}" # required by Qt + ) +else() + add_executable(vcmiclient ${clientapp_SRCS} ${clientapp_HEADERS}) +endif() + +target_link_libraries(vcmiclient PRIVATE vcmiclientcommon) + +if(ENABLE_SINGLE_APP_BUILD AND ENABLE_LAUNCHER) + target_link_libraries(vcmiclient PRIVATE vcmilauncher) +endif() + +target_include_directories(vcmiclient + PUBLIC ${CMAKE_CURRENT_SOURCE_DIR} +) + +if(WIN32) + target_sources(vcmiclient PRIVATE "VCMI_client.rc") + set_target_properties(vcmiclient + PROPERTIES + OUTPUT_NAME "VCMI_client" + PROJECT_LABEL "VCMI_client" + ) + set_property(DIRECTORY ${CMAKE_SOURCE_DIR} PROPERTY VS_STARTUP_PROJECT vcmiclient) + if(NOT ENABLE_DEBUG_CONSOLE) + set_target_properties(vcmiclient PROPERTIES WIN32_EXECUTABLE) + target_link_libraries(vcmiclient SDL2::SDL2main) + 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 + WORKING_DIRECTORY "$" + COMMAND ${CMAKE_COMMAND} -E copy AI/fuzzylite.dll fuzzylite.dll + COMMAND ${CMAKE_COMMAND} -E copy AI/tbb12.dll tbb12.dll + ) + endif() +elseif(APPLE_IOS) + set_target_properties(vcmiclient PROPERTIES + MACOSX_BUNDLE_INFO_PLIST "${CMAKE_CURRENT_LIST_DIR}/ios/Info.plist" + XCODE_ATTRIBUTE_LD_RUNPATH_SEARCH_PATHS "@executable_path/Frameworks" + XCODE_ATTRIBUTE_CODE_SIGNING_ALLOWED "$(CODE_SIGNING_ALLOWED_FOR_APPS)" + XCODE_ATTRIBUTE_ASSETCATALOG_COMPILER_APPICON_NAME AppIcon + ) + + foreach(XCODE_RESOURCE LaunchScreen.storyboard Images.xcassets Settings.bundle vcmi_logo.png) + set(XCODE_RESOURCE_PATH ios/${XCODE_RESOURCE}) + target_sources(vcmiclient PRIVATE ${XCODE_RESOURCE_PATH}) + set_source_files_properties(${XCODE_RESOURCE_PATH} PROPERTIES MACOSX_PACKAGE_LOCATION Resources) + + # workaround to prevent CMAKE_SKIP_PRECOMPILE_HEADERS being added as compile flag + if(CMAKE_VERSION VERSION_GREATER_EQUAL "3.22.0" AND CMAKE_VERSION VERSION_LESS "3.25.0") + set_source_files_properties(${XCODE_RESOURCE_PATH} PROPERTIES LANGUAGE CXX) + endif() + endforeach() + + set(CMAKE_EXE_LINKER_FLAGS "-Wl,-e,_client_main") +endif() + +vcmi_set_output_dir(vcmiclient "") +enable_pch(vcmiclient) + +if(APPLE_IOS) + vcmi_install_conan_deps("\${CMAKE_INSTALL_PREFIX}") + add_custom_command(TARGET vcmiclient POST_BUILD + COMMAND ios/set_build_version.sh "$" + COMMAND ${CMAKE_COMMAND} --install "${CMAKE_BINARY_DIR}" --component "${CMAKE_INSTALL_DEFAULT_COMPONENT_NAME}" --config "$" --prefix "$" + COMMAND ios/rpath_remove_symlinks.sh + COMMAND ios/codesign.sh + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} + ) + install(TARGETS vcmiclient DESTINATION Payload COMPONENT app) # for ipa generation with cpack +elseif(ANDROID) + find_program(androidDeployQt androiddeployqt + PATHS "${qtBinDir}" + ) + vcmi_install_conan_deps("\${CMAKE_INSTALL_PREFIX}/${LIB_DIR}") + + add_custom_target(android_deploy ALL + COMMAND ${CMAKE_COMMAND} --install "${CMAKE_BINARY_DIR}" --config "$" --prefix "${androidQtBuildDir}" + COMMAND "${androidDeployQt}" --input "${CMAKE_BINARY_DIR}/androiddeployqt.json" --output "${androidQtBuildDir}" --android-platform "android-${ANDROID_TARGET_SDK_VERSION}" --verbose $<$>:--release> ${ANDROIDDEPLOYQT_OPTIONS} + COMMAND_EXPAND_LISTS + VERBATIM + COMMENT "Create android package" + ) + add_dependencies(android_deploy vcmiclient) +else() + install(TARGETS vcmiclient DESTINATION ${BIN_DIR}) +endif() + +#install icons and desktop file on Linux +if(NOT WIN32 AND NOT APPLE AND NOT ANDROID) + foreach(iconSize 16 22 32 48 64 128 256 512 1024 2048) + install(FILES "icons/vcmiclient.${iconSize}x${iconSize}.png" + DESTINATION "share/icons/hicolor/${iconSize}x${iconSize}/apps" + RENAME vcmiclient.png + ) + endforeach() + + install(FILES icons/vcmiclient.svg + DESTINATION share/icons/hicolor/scalable/apps + RENAME vcmiclient.svg + ) + install(FILES icons/vcmiclient.desktop + DESTINATION share/applications + ) +endif() diff --git a/client/CMT.cpp b/clientapp/EntryPoint.cpp similarity index 88% rename from client/CMT.cpp rename to clientapp/EntryPoint.cpp index c54d27e2d..8d0ffa278 100644 --- a/client/CMT.cpp +++ b/clientapp/EntryPoint.cpp @@ -1,5 +1,5 @@ /* - * CMT.cpp, part of VCMI engine + * EntryPoint.cpp, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * @@ -8,35 +8,39 @@ * */ -// CMT.cpp : Defines the entry point for the console application. +// EntryPoint.cpp : Defines the entry point for the console application. + #include "StdInc.h" -#include "CMT.h" +#include "../Global.h" -#include "CGameInfo.h" -#include "mainmenu/CMainMenu.h" -#include "gui/CursorHandler.h" -#include "eventsSDL/InputHandler.h" -#include "CPlayerInterface.h" -#include "CVideoHandler.h" -#include "CMusicHandler.h" -#include "gui/CGuiHandler.h" -#include "gui/WindowHandler.h" -#include "CServerHandler.h" -#include "ClientCommandManager.h" -#include "windows/CMessage.h" -#include "windows/InfoWindows.h" -#include "render/IScreenHandler.h" -#include "render/Graphics.h" +#include "../client/CGameInfo.h" +#include "../client/ClientCommandManager.h" +#include "../client/CMT.h" +#include "../client/CPlayerInterface.h" +#include "../client/CServerHandler.h" +#include "../client/eventsSDL/InputHandler.h" +#include "../client/gui/CGuiHandler.h" +#include "../client/gui/CursorHandler.h" +#include "../client/gui/WindowHandler.h" +#include "../client/mainmenu/CMainMenu.h" +#include "../client/media/CEmptyVideoPlayer.h" +#include "../client/media/CMusicHandler.h" +#include "../client/media/CSoundHandler.h" +#include "../client/media/CVideoHandler.h" +#include "../client/render/Graphics.h" +#include "../client/render/IRenderHandler.h" +#include "../client/render/IScreenHandler.h" +#include "../client/lobby/CBonusSelection.h" +#include "../client/windows/CMessage.h" +#include "../client/windows/InfoWindows.h" -#include "../lib/CConfigHandler.h" -#include "../lib/CGeneralTextHandler.h" #include "../lib/CThreadHelper.h" #include "../lib/ExceptionsCommon.h" -#include "../lib/VCMIDirs.h" -#include "../lib/VCMI_Lib.h" #include "../lib/filesystem/Filesystem.h" - #include "../lib/logging/CBasicLogConfigurator.h" +#include "../lib/texts/CGeneralTextHandler.h" +#include "../lib/VCMI_Lib.h" +#include "../lib/VCMIDirs.h" #include #include @@ -62,7 +66,6 @@ static std::optional criticalInitializationError; #ifndef VCMI_IOS void processCommand(const std::string &message); #endif -void playIntro(); [[noreturn]] static void quitApplication(); static void mainLoop(); @@ -280,7 +283,7 @@ int main(int argc, char * argv[]) GH.init(); CCS = new CClientState(); - CGI = new CGameInfo(); //contains all global informations about game (texts, lodHandlers, map handler etc.) + CGI = new CGameInfo(); //contains all global information about game (texts, lodHandlers, map handler etc.) CSH = new CServerHandler(); // Initialize video @@ -299,10 +302,8 @@ int main(int argc, char * argv[]) { //initializing audio CCS->soundh = new CSoundHandler(); - CCS->soundh->init(); CCS->soundh->setVolume((ui32)settings["general"]["sound"].Float()); CCS->musich = new CMusicHandler(); - CCS->musich->init(); CCS->musich->setVolume((ui32)settings["general"]["music"].Float()); logGlobal->info("Initializing screen and sound handling: %d ms", pomtime.getDiff()); } @@ -318,13 +319,6 @@ int main(int argc, char * argv[]) init(); #endif - if(!settings["session"]["headless"].Bool()) - { - if(!vm.count("battle") && !vm.count("nointro") && settings["video"]["showIntro"].Bool()) - playIntro(); - GH.screenHandler().clearScreen(); - } - #ifndef VCMI_NO_THREADED_LOAD #ifdef VCMI_ANDROID // android loads the data quite slowly so we display native progressbar to prevent having only black screen for few seconds { @@ -347,6 +341,7 @@ int main(int argc, char * argv[]) { pomtime.getDiff(); graphics = new Graphics(); // should be before curh + GH.renderHandler().onLibraryLoadingFinished(CGI); CCS->curh = new CursorHandler(); logGlobal->info("Screen handler: %d ms", pomtime.getDiff()); @@ -379,6 +374,12 @@ int main(int argc, char * argv[]) { auto mmenu = CMainMenu::create(); GH.curInt = mmenu.get(); + + bool playIntroVideo = !settings["session"]["headless"].Bool() && !vm.count("battle") && !vm.count("nointro") && settings["video"]["showIntro"].Bool(); + if(playIntroVideo) + mmenu->playIntroVideos(); + else + mmenu->playMusic(); } std::vector names; @@ -400,25 +401,6 @@ int main(int argc, char * argv[]) return 0; } -//plays intro, ends when intro is over or button has been pressed (handles events) -void playIntro() -{ - auto audioData = CCS->videoh->getAudio(VideoPath::builtin("3DOLOGO.SMK")); - int sound = CCS->soundh->playSound(audioData); - if(CCS->videoh->openAndPlayVideo(VideoPath::builtin("3DOLOGO.SMK"), 0, 1, EVideoType::INTRO)) - { - audioData = CCS->videoh->getAudio(VideoPath::builtin("NWCLOGO.SMK")); - sound = CCS->soundh->playSound(audioData); - if (CCS->videoh->openAndPlayVideo(VideoPath::builtin("NWCLOGO.SMK"), 0, 1, EVideoType::INTRO)) - { - audioData = CCS->videoh->getAudio(VideoPath::builtin("H3INTRO.SMK")); - sound = CCS->soundh->playSound(audioData); - CCS->videoh->openAndPlayVideo(VideoPath::builtin("H3INTRO.SMK"), 0, 1, EVideoType::INTRO); - } - } - CCS->soundh->stopSound(sound); -} - static void mainLoop() { #ifndef VCMI_UNIX @@ -468,9 +450,6 @@ static void mainLoop() // cleanup, mostly to remove false leaks from analyzer if(CCS) { - CCS->musich->release(); - CCS->soundh->release(); - delete CCS->consoleh; delete CCS->curh; delete CCS->videoh; @@ -533,9 +512,11 @@ void handleQuit(bool ask) CInfoWindow::showYesNoDialog(CGI->generaltexth->allTexts[69], {}, quitApplication, {}, PlayerColor(1)); } +/// Notify user about encountered fatal error and terminate the game +/// TODO: decide on better location for this method void handleFatalError(const std::string & message, bool terminate) { - logGlobal->error("FATAL ERROR ENCOUTERED, VCMI WILL NOW TERMINATE"); + logGlobal->error("FATAL ERROR ENCOUNTERED, VCMI WILL NOW TERMINATE"); logGlobal->error("Reason: %s", message); std::string messageToShow = "Fatal error! " + message; diff --git a/clientapp/StdInc.cpp b/clientapp/StdInc.cpp new file mode 100644 index 000000000..277dd9af0 --- /dev/null +++ b/clientapp/StdInc.cpp @@ -0,0 +1,11 @@ +/* + * 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/clientapp/StdInc.h b/clientapp/StdInc.h new file mode 100644 index 000000000..d03216bdf --- /dev/null +++ b/clientapp/StdInc.h @@ -0,0 +1,14 @@ +/* + * 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" + +VCMI_LIB_USING_NAMESPACE diff --git a/clientapp/VCMI_client.rc b/clientapp/VCMI_client.rc new file mode 100644 index 000000000..44812e214 --- /dev/null +++ b/clientapp/VCMI_client.rc @@ -0,0 +1 @@ +IDI_ICON1 ICON "icons/vcmi.ico" \ No newline at end of file diff --git a/client/icons/generate_icns.py b/clientapp/icons/generate_icns.py old mode 100755 new mode 100644 similarity index 100% rename from client/icons/generate_icns.py rename to clientapp/icons/generate_icns.py diff --git a/client/vcmi.ico b/clientapp/icons/vcmi.ico similarity index 100% rename from client/vcmi.ico rename to clientapp/icons/vcmi.ico diff --git a/client/icons/vcmiclient.1024x1024.png b/clientapp/icons/vcmiclient.1024x1024.png similarity index 100% rename from client/icons/vcmiclient.1024x1024.png rename to clientapp/icons/vcmiclient.1024x1024.png diff --git a/client/icons/vcmiclient.128x128.png b/clientapp/icons/vcmiclient.128x128.png similarity index 100% rename from client/icons/vcmiclient.128x128.png rename to clientapp/icons/vcmiclient.128x128.png diff --git a/client/icons/vcmiclient.16x16.png b/clientapp/icons/vcmiclient.16x16.png similarity index 100% rename from client/icons/vcmiclient.16x16.png rename to clientapp/icons/vcmiclient.16x16.png diff --git a/client/icons/vcmiclient.2048x2048.png b/clientapp/icons/vcmiclient.2048x2048.png similarity index 100% rename from client/icons/vcmiclient.2048x2048.png rename to clientapp/icons/vcmiclient.2048x2048.png diff --git a/client/icons/vcmiclient.22x22.png b/clientapp/icons/vcmiclient.22x22.png similarity index 100% rename from client/icons/vcmiclient.22x22.png rename to clientapp/icons/vcmiclient.22x22.png diff --git a/client/icons/vcmiclient.256x256.png b/clientapp/icons/vcmiclient.256x256.png similarity index 100% rename from client/icons/vcmiclient.256x256.png rename to clientapp/icons/vcmiclient.256x256.png diff --git a/client/icons/vcmiclient.32x32.png b/clientapp/icons/vcmiclient.32x32.png similarity index 100% rename from client/icons/vcmiclient.32x32.png rename to clientapp/icons/vcmiclient.32x32.png diff --git a/client/icons/vcmiclient.48x48.png b/clientapp/icons/vcmiclient.48x48.png similarity index 100% rename from client/icons/vcmiclient.48x48.png rename to clientapp/icons/vcmiclient.48x48.png diff --git a/client/icons/vcmiclient.512x512.png b/clientapp/icons/vcmiclient.512x512.png similarity index 100% rename from client/icons/vcmiclient.512x512.png rename to clientapp/icons/vcmiclient.512x512.png diff --git a/client/icons/vcmiclient.64x64.png b/clientapp/icons/vcmiclient.64x64.png similarity index 100% rename from client/icons/vcmiclient.64x64.png rename to clientapp/icons/vcmiclient.64x64.png diff --git a/client/icons/vcmiclient.desktop b/clientapp/icons/vcmiclient.desktop similarity index 64% rename from client/icons/vcmiclient.desktop rename to clientapp/icons/vcmiclient.desktop index 68d446c47..3a94d8deb 100644 --- a/client/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/client/icons/vcmiclient.svg b/clientapp/icons/vcmiclient.svg similarity index 100% rename from client/icons/vcmiclient.svg rename to clientapp/icons/vcmiclient.svg diff --git a/client/ios/GameChatKeyboardHandler.h b/clientapp/ios/GameChatKeyboardHandler.h similarity index 100% rename from client/ios/GameChatKeyboardHandler.h rename to clientapp/ios/GameChatKeyboardHandler.h diff --git a/client/ios/GameChatKeyboardHandler.m b/clientapp/ios/GameChatKeyboardHandler.m similarity index 100% rename from client/ios/GameChatKeyboardHandler.m rename to clientapp/ios/GameChatKeyboardHandler.m diff --git a/client/ios/Images.xcassets/AppIcon.appiconset/Contents.json b/clientapp/ios/Images.xcassets/AppIcon.appiconset/Contents.json similarity index 100% rename from client/ios/Images.xcassets/AppIcon.appiconset/Contents.json rename to clientapp/ios/Images.xcassets/AppIcon.appiconset/Contents.json diff --git a/client/ios/Images.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/clientapp/ios/Images.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png similarity index 100% rename from client/ios/Images.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png rename to clientapp/ios/Images.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png diff --git a/client/ios/Images.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/clientapp/ios/Images.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png similarity index 100% rename from client/ios/Images.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png rename to clientapp/ios/Images.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png diff --git a/client/ios/Images.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/clientapp/ios/Images.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png similarity index 100% rename from client/ios/Images.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png rename to clientapp/ios/Images.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png diff --git a/client/ios/Images.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/clientapp/ios/Images.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png similarity index 100% rename from client/ios/Images.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png rename to clientapp/ios/Images.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png diff --git a/client/ios/Images.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/clientapp/ios/Images.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png similarity index 100% rename from client/ios/Images.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png rename to clientapp/ios/Images.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png diff --git a/client/ios/Images.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/clientapp/ios/Images.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png similarity index 100% rename from client/ios/Images.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png rename to clientapp/ios/Images.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png diff --git a/client/ios/Images.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/clientapp/ios/Images.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png similarity index 100% rename from client/ios/Images.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png rename to clientapp/ios/Images.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png diff --git a/client/ios/Images.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/clientapp/ios/Images.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png similarity index 100% rename from client/ios/Images.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png rename to clientapp/ios/Images.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png diff --git a/client/ios/Images.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/clientapp/ios/Images.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png similarity index 100% rename from client/ios/Images.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png rename to clientapp/ios/Images.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png diff --git a/client/ios/Images.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/clientapp/ios/Images.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png similarity index 100% rename from client/ios/Images.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png rename to clientapp/ios/Images.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png diff --git a/client/ios/Images.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/clientapp/ios/Images.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png similarity index 100% rename from client/ios/Images.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png rename to clientapp/ios/Images.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png diff --git a/client/ios/Images.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/clientapp/ios/Images.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png similarity index 100% rename from client/ios/Images.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png rename to clientapp/ios/Images.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png diff --git a/client/ios/Images.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/clientapp/ios/Images.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png similarity index 100% rename from client/ios/Images.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png rename to clientapp/ios/Images.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png diff --git a/client/ios/Images.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/clientapp/ios/Images.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png similarity index 100% rename from client/ios/Images.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png rename to clientapp/ios/Images.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png diff --git a/client/ios/Images.xcassets/Contents.json b/clientapp/ios/Images.xcassets/Contents.json similarity index 100% rename from client/ios/Images.xcassets/Contents.json rename to clientapp/ios/Images.xcassets/Contents.json diff --git a/client/ios/Info.plist b/clientapp/ios/Info.plist similarity index 100% rename from client/ios/Info.plist rename to clientapp/ios/Info.plist diff --git a/client/ios/LaunchScreen.storyboard b/clientapp/ios/LaunchScreen.storyboard similarity index 100% rename from client/ios/LaunchScreen.storyboard rename to clientapp/ios/LaunchScreen.storyboard diff --git a/client/ios/Settings.bundle/Root.plist b/clientapp/ios/Settings.bundle/Root.plist similarity index 100% rename from client/ios/Settings.bundle/Root.plist rename to clientapp/ios/Settings.bundle/Root.plist diff --git a/client/ios/Settings.bundle/en.lproj/Root.strings b/clientapp/ios/Settings.bundle/en.lproj/Root.strings similarity index 100% rename from client/ios/Settings.bundle/en.lproj/Root.strings rename to clientapp/ios/Settings.bundle/en.lproj/Root.strings diff --git a/client/ios/Settings.bundle/ru.lproj/Root.strings b/clientapp/ios/Settings.bundle/ru.lproj/Root.strings similarity index 100% rename from client/ios/Settings.bundle/ru.lproj/Root.strings rename to clientapp/ios/Settings.bundle/ru.lproj/Root.strings diff --git a/client/ios/main.m b/clientapp/ios/main.m similarity index 100% rename from client/ios/main.m rename to clientapp/ios/main.m diff --git a/client/ios/startSDL.h b/clientapp/ios/startSDL.h similarity index 100% rename from client/ios/startSDL.h rename to clientapp/ios/startSDL.h diff --git a/client/ios/startSDL.mm b/clientapp/ios/startSDL.mm similarity index 100% rename from client/ios/startSDL.mm rename to clientapp/ios/startSDL.mm diff --git a/client/ios/vcmi_logo.png b/clientapp/ios/vcmi_logo.png similarity index 100% rename from client/ios/vcmi_logo.png rename to clientapp/ios/vcmi_logo.png diff --git a/cmake_modules/FindSDL2.cmake b/cmake_modules/FindSDL2.cmake index 4228a3c0b..5021681dc 100644 --- a/cmake_modules/FindSDL2.cmake +++ b/cmake_modules/FindSDL2.cmake @@ -82,11 +82,11 @@ This module responds to the following cache variables: SDL2_LIBRARY SDL2 Library (.dll, .so, .a, etc) path. - SDL2MAIN_LIBRAY + SDL2MAIN_LIBRARY SDL2main Library (.a) path. SDL2_BUILDING_LIBRARY - This flag is useful only when linking to SDL2_LIBRARIES insead of + This flag is useful only when linking to SDL2_LIBRARIES instead of SDL2::SDL2main. It is required only when building a library that links to SDL2_LIBRARIES, because only applications need main() (No need to also link to SDL2main). diff --git a/cmake_modules/VersionDefinition.cmake b/cmake_modules/VersionDefinition.cmake index 3dd1094ca..b7fbfa5b0 100644 --- a/cmake_modules/VersionDefinition.cmake +++ b/cmake_modules/VersionDefinition.cmake @@ -1,6 +1,6 @@ set(VCMI_VERSION_MAJOR 1) -set(VCMI_VERSION_MINOR 5) -set(VCMI_VERSION_PATCH 7) +set(VCMI_VERSION_MINOR 6) +set(VCMI_VERSION_PATCH 0) add_definitions( -DVCMI_VERSION_MAJOR=${VCMI_VERSION_MAJOR} -DVCMI_VERSION_MINOR=${VCMI_VERSION_MINOR} diff --git a/conanfile.py b/conanfile.py index 220340bd0..302aba724 100644 --- a/conanfile.py +++ b/conanfile.py @@ -15,11 +15,13 @@ 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]", - "onetbb/[^2021.3]", + # 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 ] requires = _libRequires + _clientRequires @@ -38,7 +40,6 @@ class VCMI(ConanFile): "boost/*:shared": True, "minizip/*:shared": True, - "onetbb/*:shared": True, } def configure(self): @@ -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,31 +107,77 @@ 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"].avdevice = False - self.options["ffmpeg"].avfilter = False - self.options["ffmpeg"].postproc = False - self.options["ffmpeg"].swresample = False - self.options["ffmpeg"].with_asm = self.settings.os != "Android" + self.options["ffmpeg"].disable_all_bitstream_filters = True + self.options["ffmpeg"].disable_all_decoders = True + self.options["ffmpeg"].disable_all_demuxers = True + self.options["ffmpeg"].disable_all_encoders = True + self.options["ffmpeg"].disable_all_filters = True + self.options["ffmpeg"].disable_all_hardware_accelerators = True + self.options["ffmpeg"].disable_all_muxers = True + self.options["ffmpeg"].disable_all_parsers = True + self.options["ffmpeg"].disable_all_protocols = True + + self.options["ffmpeg"].with_asm = False + self.options["ffmpeg"].with_bzip2 = False self.options["ffmpeg"].with_freetype = False - self.options["ffmpeg"].with_libfdk_aac = False + self.options["ffmpeg"].with_libaom = False + self.options["ffmpeg"].with_libdav1d = False + self.options["ffmpeg"].with_libiconv = False self.options["ffmpeg"].with_libmp3lame = False + self.options["ffmpeg"].with_libsvtav1 = False self.options["ffmpeg"].with_libvpx = False self.options["ffmpeg"].with_libwebp = False self.options["ffmpeg"].with_libx264 = False self.options["ffmpeg"].with_libx265 = False + self.options["ffmpeg"].with_lzma = True self.options["ffmpeg"].with_openh264 = False self.options["ffmpeg"].with_openjpeg = False self.options["ffmpeg"].with_opus = False self.options["ffmpeg"].with_programs = False + self.options["ffmpeg"].with_sdl = False self.options["ffmpeg"].with_ssl = False self.options["ffmpeg"].with_vorbis = False + self.options["ffmpeg"].with_zlib = False + if self.settings.os != "Android": + self.options["ffmpeg"].with_libfdk_aac = False + + self.options["ffmpeg"].avcodec = True + self.options["ffmpeg"].avdevice = False + self.options["ffmpeg"].avfilter = False + self.options["ffmpeg"].avformat = True + self.options["ffmpeg"].postproc = False + self.options["ffmpeg"].swresample = True # For resampling of audio in 'planar' formats + self.options["ffmpeg"].swscale = True # For video scaling + + # We want following options supported: + # H3:SoD - .bik and .smk + # H3:HD - ogg container / theora video / vorbis sound (not supported by vcmi at the moment, but might be supported in future) + # and for mods - webm container / vp8 or vp9 video / opus sound + # TODO: add av1 support for mods (requires enabling libdav1d which currently fails to build via Conan) + self.options["ffmpeg"].enable_protocols = "file" + self.options["ffmpeg"].enable_demuxers = "bink,binka,ogg,smacker,webm_dash_manifest" + self.options["ffmpeg"].enable_parsers = "opus,vorbis,vp8,vp9,webp" + self.options["ffmpeg"].enable_decoders = "bink,binkaudio_dct,binkaudio_rdft,smackaud,smacker,theora,vorbis,vp8,vp9,opus" + + #optionally, for testing - enable ffplay/ffprobe binaries in conan package: + #if self.settings.os == "Windows": + # self.options["ffmpeg"].with_programs = True + # self.options["ffmpeg"].avfilter = True + # self.options["ffmpeg"].with_sdl = True + # self.options["ffmpeg"].enable_filters = "aresample,scale" 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 @@ -122,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]) @@ -198,7 +269,7 @@ class VCMI(ConanFile): # client if self.options.with_ffmpeg: - self.requires("ffmpeg/[^4.4]") + self.requires("ffmpeg/[>=4.4]") # launcher if self.settings.os == "Android": diff --git a/config/ai/nkai/nkai-settings.json b/config/ai/nkai/nkai-settings.json index f597be497..a4356c34a 100644 --- a/config/ai/nkai/nkai-settings.json +++ b/config/ai/nkai/nkai-settings.json @@ -1,10 +1,129 @@ { - "maxRoamingHeroes" : 8, - "maxpass" : 30, - "mainHeroTurnDistanceLimit" : 10, - "scoutHeroTurnDistanceLimit" : 5, - "maxGoldPressure" : 0.3, - "useTroopsFromGarrisons" : true, - "openMap": true, - "allowObjectGraph": true + // "maxRoamingHeroes" - AI will never recruit new heroes above this value. + // Note that AI might end up with more heroes - due to prisons or if he has large number of heroes on start + // + // "maxpass" - ??? + // + // "mainHeroTurnDistanceLimit" - AI will only run pathfinding for specified number of turns for his main hero. + // "scoutHeroTurnDistanceLimit" - AI will only run pathfinding for specified number of turns for his secondary (scout) heroes + // Limiting this will make AI faster, but may result in AI being unable to discover objects outside of this range + // + // "maxGoldPressure" - ??? + // + // "useTroopsFromGarrisons" - AI can take troops from garrisons on map. + // Note that at the moment AI will not deliberately seek out such garrisons, he can only take troops from them when passing through. + // This option is always disabled on H3 RoE campaign maps to be in line with H3 AI + // + // "openMap" - AI will use map reveal cheat if cheats are enabled and AI is not allied with human player + // This improves AI decision making, but may lead AI to deliberately targeting targets that he should not be able to see at the moment + // + // "allowObjectGraph" - if used, AI will build "cache" for pathfinder on first turn, which should make AI faster. Requires openMap. + // + // "pathfinderBucketsCount" - ??? + // "pathfinderBucketSize" - ??? + // + // "retreatThresholdRelative" - AI will consider retreating from battle only if his troops are less than specified ratio compated to enemy + // "retreatThresholdAbsolute" - AI will consider retreating from battle only if total fight value of his troops are less than specified value + // + // "maxArmyLossTarget" - AI will try keep army loss below specified target + // + // "safeAttackRatio" - TODO: figure out how exactly it affects AI decision making + // + // "useFuzzy" - allow using of fuzzy logic. TODO: better description + + + "pawn" : { + "maxRoamingHeroes" : 4, //H3 value: 3, + "maxpass" : 30, + "mainHeroTurnDistanceLimit" : 10, + "scoutHeroTurnDistanceLimit" : 5, + "maxGoldPressure" : 0.3, + "updateHitmapOnTileReveal" : false, + "useTroopsFromGarrisons" : true, + "openMap": true, + "allowObjectGraph": false, + "pathfinderBucketsCount" : 1, // old value: 3, + "pathfinderBucketSize" : 32, // old value: 7, + "retreatThresholdRelative" : 0, + "retreatThresholdAbsolute" : 0, + "safeAttackRatio" : 1.1, + "maxArmyLossTarget" : 0.5, + "useFuzzy" : false + }, + + "knight" : { + "maxRoamingHeroes" : 6, //H3 value: 3, + "maxpass" : 30, + "mainHeroTurnDistanceLimit" : 10, + "scoutHeroTurnDistanceLimit" : 5, + "maxGoldPressure" : 0.3, + "updateHitmapOnTileReveal" : false, + "useTroopsFromGarrisons" : true, + "openMap": true, + "allowObjectGraph": false, + "pathfinderBucketsCount" : 1, // old value: 3, + "pathfinderBucketSize" : 32, // old value: 7, + "retreatThresholdRelative" : 0.1, + "retreatThresholdAbsolute" : 5000, + "safeAttackRatio" : 1.1, + "maxArmyLossTarget" : 0.35, + "useFuzzy" : false + }, + + "rook" : { + "maxRoamingHeroes" : 8, //H3 value: 4 + "maxpass" : 30, + "mainHeroTurnDistanceLimit" : 10, + "scoutHeroTurnDistanceLimit" : 5, + "maxGoldPressure" : 0.3, + "updateHitmapOnTileReveal" : false, + "useTroopsFromGarrisons" : true, + "openMap": true, + "allowObjectGraph": false, + "pathfinderBucketsCount" : 1, // old value: 3, + "pathfinderBucketSize" : 32, // old value: 7, + "retreatThresholdRelative" : 0.3, + "retreatThresholdAbsolute" : 10000, + "safeAttackRatio" : 1.1, + "maxArmyLossTarget" : 0.25, + "useFuzzy" : false + }, + + "queen" : { + "maxRoamingHeroes" : 8, //H3 value: 5 + "maxpass" : 30, + "mainHeroTurnDistanceLimit" : 10, + "scoutHeroTurnDistanceLimit" : 5, + "maxGoldPressure" : 0.3, + "updateHitmapOnTileReveal" : false, + "useTroopsFromGarrisons" : true, + "openMap": true, + "allowObjectGraph": false, + "pathfinderBucketsCount" : 1, // old value: 3, + "pathfinderBucketSize" : 32, // old value: 7, + "retreatThresholdRelative" : 0.3, + "retreatThresholdAbsolute" : 10000, + "safeAttackRatio" : 1.1, + "maxArmyLossTarget" : 0.25, + "useFuzzy" : false + }, + + "king" : { + "maxRoamingHeroes" : 8, //H3 value: 6 + "maxpass" : 30, + "mainHeroTurnDistanceLimit" : 10, + "scoutHeroTurnDistanceLimit" : 5, + "maxGoldPressure" : 0.3, + "updateHitmapOnTileReveal" : false, + "useTroopsFromGarrisons" : true, + "openMap": true, + "allowObjectGraph": false, + "pathfinderBucketsCount" : 1, // old value: 3, + "pathfinderBucketSize" : 32, // old value: 7, + "retreatThresholdRelative" : 0.3, + "retreatThresholdAbsolute" : 10000, + "safeAttackRatio" : 1.1, + "maxArmyLossTarget" : 0.25, + "useFuzzy" : false + } } \ No newline at end of file diff --git a/config/ai/nkai/object-priorities.txt b/config/ai/nkai/object-priorities.txt index 44c8ef954..3d804f0ef 100644 --- a/config/ai/nkai/object-priorities.txt +++ b/config/ai/nkai/object-priorities.txt @@ -15,9 +15,9 @@ InputVariable: scoutTurnDistance range: 0.000 10.000 lock-range: true term: LOWEST Ramp 0.250 0.000 - term: LOW Discrete 0.000 1.000 0.500 0.800 1.000 0.000 - term: MEDIUM Discrete 0.000 0.000 0.500 0.200 1.000 1.000 2.500 0.300 4.000 0.000 - term: LONG Discrete 1.000 0.000 1.500 0.200 3.000 0.800 10.000 1.000 + term: LOW Discrete 0.000 1.000 1.000 0.800 2.500 0.000 + term: MEDIUM Discrete 0.000 0.000 1.000 0.200 2.500 1.000 3.500 0.300 5.000 0.000 + term: LONG Discrete 2.500 0.000 3.500 0.200 5.000 0.800 10.000 1.000 InputVariable: goldReward description: estimated amount of gold received enabled: true 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/battleStartpos.json b/config/battleStartpos.json deleted file mode 100644 index 5921cc071..000000000 --- a/config/battleStartpos.json +++ /dev/null @@ -1,87 +0,0 @@ -{ - "battle_positions": - [ - { - "name" : "attackerLoose", // loose formation, attacker - "levels": [ - [ 86 ], - [ 35, 137 ], - [ 35, 86, 137 ], - [ 1, 69, 103, 171 ], - [ 1, 35, 86, 137, 171 ], - [ 1, 35, 69, 103, 137, 171 ], - [ 1, 35, 69, 86, 103, 137, 171 ] - ] - }, - - { - "name" : "defenderLoose", // loose formation, defender - "levels": [ - [ 100 ], - [ 49, 151 ], - [ 49, 100, 151 ], - [ 15, 83, 117, 185 ], - [ 15, 49, 100, 151, 185 ], - [ 15, 49, 83, 117, 151, 185 ], - [ 15, 49, 83, 100, 117, 151, 185 ] - ] - }, - - { - "name" : "attackerTight", // tight formation, attacker - "levels": [ - [ 86 ], - [ 69, 103 ], - [ 69, 86, 103 ], - [ 35, 69, 103, 137 ], - [ 35, 69, 86, 103, 137 ], - [ 1, 35, 69, 103, 137, 171 ], - [ 1, 35, 69, 86, 103, 137, 171 ] - ] - }, - - { - "name" : "defenderTight", // tight formation, defender - "levels": [ - [ 100 ], - [ 83, 117 ], - [ 83, 100, 117 ], - [ 49, 83, 117, 151 ], - [ 49, 83, 100, 117, 151 ], - [ 15, 49, 83, 117, 151, 185 ], - [ 15, 49, 83, 100, 117, 151, 185 ] - ] - }, - - { - "name" : "attackerCreBank", // creature bank, attacker - "levels": [ - [ 57 ], - [ 57, 61 ], - [ 57, 61, 90 ], - [ 57, 61, 90, 93 ], - [ 57, 61, 90, 93, 96 ], - [ 57, 61, 90, 93, 96, 125 ], - [ 57, 61, 90, 93, 96, 125, 129 ] - ] - }, - - { - "name" : "defenderCreBank", // creature bank, defender - "levels": [ - [ 15 ], - [ 15, 185 ], - [ 15, 185, 172 ], - [ 15, 185, 172, 2 ], - [ 15, 185, 172, 2, 100 ], - [ 15, 185, 172, 2, 100, 86 ], - [ 15, 185, 172, 2, 100, 86, 8 ] - ] - } - ], - "commanderPositions": - { - "field" : [88, 98], //attacker/defender - "creBank" : [95, 8] //not expecting defendig hero at bank, but hell knows - } -} 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 b6d3ca125..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": @@ -591,6 +607,24 @@ { "icon": "zvs/Lib1.res/MEGABREATH" } + }, + + "DISINTEGRATE": + { + "graphics": + { + "icon": "zvs/Lib1.res/DISINTEGRATE" + } + + }, + + "INVINCIBLE": + { + "graphics": + { + "icon": "zvs/Lib1.res/INVINCIBLE" + } + } } diff --git a/config/buildingsLibrary.json b/config/buildingsLibrary.json new file mode 100644 index 000000000..2bcb3b598 --- /dev/null +++ b/config/buildingsLibrary.json @@ -0,0 +1,266 @@ +{ + "mageGuild1": { "id" : 0 }, + "mageGuild2": { "id" : 1, "upgrades" : "mageGuild1" }, + "mageGuild3": { "id" : 2, "upgrades" : "mageGuild2" }, + "mageGuild4": { "id" : 3, "upgrades" : "mageGuild3" }, + "mageGuild5": { "id" : 4, "upgrades" : "mageGuild4" }, + "tavern": { + "id" : 5, + "bonuses": [ + { + "type": "MORALE", + "val": 1 + }, + { + "propagator": "PLAYER_PROPAGATOR", + "type": "THIEVES_GUILD_ACCESS", + "val": 1 + } + ] + }, + "shipyard": { "id" : 6 }, + "fort": { + "id" : 7, + "fortifications" : { + "wallsHealth" : 2 + } + }, + + "citadel": { + "id" : 8, + "upgrades" : "fort", + "fortifications" : { + "citadelHealth" : 2, + "hasMoat" : true + } + }, + + "castle": { + "id" : 9, + "upgrades" : "citadel", + "fortifications" : { + "wallsHealth" : 3, + "upperTowerHealth" : 2, + "lowerTowerHealth" : 2 + } + }, + + "villageHall": { + "id" : 10, + "mode" : "auto", + "produce": { "gold": 500 } + }, + + "townHall": { + "id" : 11, + "upgrades" : "villageHall", + "requires" : [ "tavern" ], + "produce": { "gold": 1000 } + }, + "cityHall": { + "id" : 12, + "upgrades" : "townHall", + "requires" : [ "allOf", [ "mageGuild1" ], [ "marketplace" ], [ "blacksmith" ] ], + "produce": { "gold": 2000 } + }, + "capitol": { + "id" : 13, + "upgrades" : "cityHall", + "requires" : [ "castle" ], + "produce": { "gold": 4000 } + }, + + "marketplace": { + "id" : 14, + "marketModes" : ["resource-resource", "resource-player"] + }, + "resourceSilo": { "id" : 15, "requires" : [ "marketplace" ] }, + "blacksmith": { "id" : 16 }, + + // Previously hardcoded buildings that might be used by mods + // Section 1 - building with bonuses during sieges + "brotherhoodOfSword" : { + "bonuses": [ + { + "type": "MORALE", + "val": 2 + } + ] + }, + + "fountainOfFortune" : { + "bonuses": [ + { + "type": "LUCK", + "val": 2 + } + ] + }, + + "spellPowerGarrisonBonus" : { + "bonuses": [ + { + "type": "PRIMARY_SKILL", + "subtype": "primarySkill.spellpower", + "val": 2 + } + ] + }, + + "attackGarrisonBonus" : { + "bonuses": [ + { + "type": "PRIMARY_SKILL", + "subtype": "primarySkill.attack", + "val": 2 + } + ] + }, + + "defenseGarrisonBonus" : { + "bonuses": [ + { + "type": "PRIMARY_SKILL", + "subtype": "primarySkill.defence", + "val": 2 + } + ] + }, + + "lighthouse" : { + "bonuses": [ + { + "propagator": "PLAYER_PROPAGATOR", + "type": "MOVEMENT", + "subtype": "heroMovementSea", + "val": 500 + } + ] + }, + + // Section 2 - buildings that are visitable by hero + "stables": { + "configuration" : { + "visitMode" : "bonus", + "rewards" : [ + { + "message" : "@core.genrltxt.580", + "movePoints" : 400, + "bonuses" : [ { "type" : "MOVEMENT", "subtype" : "heroMovementLand", "val" : 400, "valueType" : "ADDITIVE_VALUE", "duration" : "ONE_WEEK"} ] + } + ] + } + }, + "manaVortex": { + "configuration" : { + "resetParameters" : { + "period" : 7, + "visitors" : true + }, + "visitMode" : "hero", // Should be 'once' to match (somewhat buggy) H3 logic + "rewards" : [ + { + "limiter" : { + "noneOf" : [ { "manaPercentage" : 200 } ] + }, + "message" : "@core.genrltxt.579", + "manaPercentage" : 200 + } + ] + } + }, + + "attackVisitingBonus": { + "configuration" : { + "visitMode" : "hero", + "rewards" : [ + { + "message" : "@core.genrltxt.584", + "primary" : { "attack" : 1 } + } + ] + } + }, + + "defenceVisitingBonus": { + "configuration" : { + "visitMode" : "hero", + "rewards" : [ + { + "message" : "@core.genrltxt.585", + "primary" : { "defence" : 1 } + } + ] + } + }, + + "spellPowerVisitingBonus": { + "configuration" : { + "visitMode" : "hero", + "rewards" : [ + { + "message" : "@core.genrltxt.582", + "primary" : { "spellpower" : 1 } + } + ] + } + }, + + "knowledgeVisitingBonus": { + "configuration" : { + "visitMode" : "hero", + "rewards" : [ + { + "message" : "@core.genrltxt.581", + "primary" : { "knowledge" : 1 } + } + ] + } + }, + + "experienceVisitingBonus": { + "configuration" : { + "visitMode" : "hero", + "rewards" : [ + { + "message" : "@core.genrltxt.583", + "heroExperience" : 1000 + } + ] + } + }, + + // Section 3 - markets + "artifactMerchant" : { + "requires" : [ "marketplace" ], + "marketModes" : ["resource-artifact", "artifact-resource"] + }, + + "freelancersGuild" : { + "requires" : [ "marketplace" ], + "marketModes" : ["creature-resource"] + }, + + "magicUniversity" : { + "marketModes" : ["resource-skill"] + }, + + "creatureTransformer" : { + "marketModes" : ["creature-undead"] + }, + + // Section 4 - buildings that now have dedicated mechanics + "ballistaYard": { + "warMachine" : "ballista" + }, + + "thievesGuild" : { + "bonuses": [ + { + "propagator": "PLAYER_PROPAGATOR", + "type": "THIEVES_GUILD_ACCESS", + "val": 2 + } + ] + } +} \ No newline at end of file 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/campaignOverrides.json b/config/campaignOverrides.json new file mode 100644 index 000000000..45b11877c --- /dev/null +++ b/config/campaignOverrides.json @@ -0,0 +1,261 @@ +{ + "DATA/GOOD3" : { // RoE - "Song for the Father" + "outroVideo": "Endgame" + }, + "DATA/AB" : { // AB Intro + "introVideo": "H3X1intr", + "videoRim": "IntroRm2" + }, + "MAPS/CHRONICLES/HC1_MAIN" : { // Heroes Chronicles 1 + "regions": + { + "background": "chronicles_1/CamBkHc", + "prefix": "chronicles_1/HcSc", + "suffix": ["1", "2", "3"], + "color_suffix_length": 0, + "desc": [ + { "infix": "1", "x": 27, "y": 43, "labelPos": { "x": 98, "y": 112 } }, + { "infix": "2", "x": 231, "y": 43, "labelPos": { "x": 98, "y": 112 } }, + { "infix": "3", "x": 27, "y": 178, "labelPos": { "x": 98, "y": 112 } }, + { "infix": "4", "x": 231, "y": 178, "labelPos": { "x": 98, "y": 112 } }, + { "infix": "5", "x": 27, "y": 312, "labelPos": { "x": 98, "y": 112 } }, + { "infix": "6", "x": 231, "y": 312, "labelPos": { "x": 98, "y": 112 } }, + { "infix": "7", "x": 27, "y": 447, "labelPos": { "x": 98, "y": 112 } }, + { "infix": "8", "x": 231, "y": 447, "labelPos": { "x": 98, "y": 112 } } + ] + }, + "scenarioCount": 8, + "scenarios": [ + { "voiceProlog": "chronicles_1/ABVOFL4" }, + { "voiceProlog": "chronicles_1/H3X2UAE" }, + { "voiceProlog": "chronicles_1/H3X2BBA" }, + { "voiceProlog": "chronicles_1/H3X2RND" }, + { "voiceProlog": "chronicles_1/G1C" }, + { "voiceProlog": "chronicles_1/G2C" }, + { "voiceProlog": "chronicles_1/ABVOFL3" }, + { "voiceProlog": "chronicles_1/H3X2BBF", "voiceEpilog": "chronicles_1/N1C_D" } + ], + "loadingBackground": "chronicles_1/LoadBar", + "videoRim": "chronicles_1/INTRORIM", + "introVideo": "chronicles_1/Intro" + }, + "MAPS/CHRONICLES/HC2_MAIN" : { // Heroes Chronicles 2 + "regions": + { + "background": "chronicles_2/CamBkHc", + "prefix": "chronicles_2/HcSc", + "suffix": ["1", "2", "3"], + "color_suffix_length": 0, + "desc": [ + { "infix": "1", "x": 27, "y": 43, "labelPos": { "x": 98, "y": 112 } }, + { "infix": "2", "x": 231, "y": 43, "labelPos": { "x": 98, "y": 112 } }, + { "infix": "3", "x": 27, "y": 178, "labelPos": { "x": 98, "y": 112 } }, + { "infix": "4", "x": 231, "y": 178, "labelPos": { "x": 98, "y": 112 } }, + { "infix": "5", "x": 27, "y": 312, "labelPos": { "x": 98, "y": 112 } }, + { "infix": "6", "x": 231, "y": 312, "labelPos": { "x": 98, "y": 112 } }, + { "infix": "7", "x": 27, "y": 447, "labelPos": { "x": 98, "y": 112 } }, + { "infix": "8", "x": 231, "y": 447, "labelPos": { "x": 98, "y": 112 } } + ] + }, + "scenarioCount": 8, + "scenarios": [ + { "voiceProlog": "chronicles_2/H3X2ELB" }, + { "voiceProlog": "chronicles_2/H3X2NBA" }, + { "voiceProlog": "chronicles_2/H3X2RNA" }, + { "voiceProlog": "chronicles_2/ABVOAB8" }, + { "voiceProlog": "chronicles_2/H3X2UAL" }, + { "voiceProlog": "chronicles_2/E1A" }, + { "voiceProlog": "chronicles_2/ABVOAB2" }, + { "voiceProlog": "chronicles_2/G1A", "voiceEpilog": "chronicles_2/S1C" } + ], + "loadingBackground": "chronicles_2/LoadBar", + "videoRim": "chronicles_2/INTRORIM", + "introVideo": "chronicles_2/Intro" + }, + "MAPS/CHRONICLES/HC3_MAIN" : { // Heroes Chronicles 3 + "regions": + { + "background": "chronicles_3/CamBkHc", + "prefix": "chronicles_3/HcSc", + "suffix": ["1", "2", "3"], + "color_suffix_length": 0, + "desc": [ + { "infix": "1", "x": 27, "y": 43, "labelPos": { "x": 98, "y": 112 } }, + { "infix": "2", "x": 231, "y": 43, "labelPos": { "x": 98, "y": 112 } }, + { "infix": "3", "x": 27, "y": 178, "labelPos": { "x": 98, "y": 112 } }, + { "infix": "4", "x": 231, "y": 178, "labelPos": { "x": 98, "y": 112 } }, + { "infix": "5", "x": 27, "y": 312, "labelPos": { "x": 98, "y": 112 } }, + { "infix": "6", "x": 231, "y": 312, "labelPos": { "x": 98, "y": 112 } }, + { "infix": "7", "x": 27, "y": 447, "labelPos": { "x": 98, "y": 112 } }, + { "infix": "8", "x": 231, "y": 447, "labelPos": { "x": 98, "y": 112 } } + ] + }, + "scenarioCount": 8, + "scenarios": [ + { "voiceProlog": "chronicles_3/G2C" }, + { "voiceProlog": "chronicles_3/ABVOAB1" }, + { "voiceProlog": "chronicles_3/G2D" }, + { "voiceProlog": "chronicles_3/E1B" }, + { "voiceProlog": "chronicles_3/ABVOAB2" }, + { "voiceProlog": "chronicles_3/ABVOAB4" }, + { "voiceProlog": "chronicles_3/ABVOAB6" }, + { "voiceProlog": "chronicles_3/G3B", "voiceEpilog": "chronicles_3/ABVOFL2" } + ], + "loadingBackground": "chronicles_3/LoadBar", + "videoRim": "chronicles_3/INTRORIM", + "introVideo": "chronicles_3/Intro" + }, + "MAPS/CHRONICLES/HC4_MAIN" : { // Heroes Chronicles 4 + "regions": + { + "background": "chronicles_4/CamBkHc", + "prefix": "chronicles_4/HcSc", + "suffix": ["1", "2", "3"], + "color_suffix_length": 0, + "desc": [ + { "infix": "1", "x": 27, "y": 43, "labelPos": { "x": 98, "y": 112 } }, + { "infix": "2", "x": 231, "y": 43, "labelPos": { "x": 98, "y": 112 } }, + { "infix": "3", "x": 27, "y": 178, "labelPos": { "x": 98, "y": 112 } }, + { "infix": "4", "x": 231, "y": 178, "labelPos": { "x": 98, "y": 112 } }, + { "infix": "5", "x": 27, "y": 312, "labelPos": { "x": 98, "y": 112 } }, + { "infix": "6", "x": 231, "y": 312, "labelPos": { "x": 98, "y": 112 } }, + { "infix": "7", "x": 27, "y": 447, "labelPos": { "x": 98, "y": 112 } }, + { "infix": "8", "x": 231, "y": 447, "labelPos": { "x": 98, "y": 112 } } + ] + }, + "scenarioCount": 8, + "scenarios": [ + { "voiceProlog": "chronicles_4/ABVOAB1" }, + { "voiceProlog": "chronicles_4/ABVODB4" }, + { "voiceProlog": "chronicles_4/H3X2ELC" }, + { "voiceProlog": "chronicles_4/ABVODS2" }, + { "voiceProlog": "chronicles_4/ABVODS1" }, + { "voiceProlog": "chronicles_4/ABVODS3" }, + { "voiceProlog": "chronicles_4/ABVODS4" }, + { "voiceProlog": "chronicles_4/H3X2NBD", "voiceEpilog": "chronicles_4/S1C" } + ], + "loadingBackground": "chronicles_4/LoadBar", + "videoRim": "chronicles_4/INTRORIM", + "introVideo": "chronicles_4/Intro" + }, + "MAPS/CHRONICLES/HC5_MAIN" : { // Heroes Chronicles 5 + "regions": + { + "background": "chronicles_5/CamBkHc", + "prefix": "chronicles_5/HcSc", + "suffix": ["1", "2", "3"], + "color_suffix_length": 0, + "desc": [ + { "infix": "1", "x": 34, "y": 184, "labelPos": { "x": 98, "y": 112 } }, + { "infix": "2", "x": 235, "y": 184, "labelPos": { "x": 98, "y": 112 } }, + { "infix": "3", "x": 34, "y": 320, "labelPos": { "x": 98, "y": 112 } }, + { "infix": "4", "x": 235, "y": 320, "labelPos": { "x": 98, "y": 112 } }, + { "infix": "5", "x": 129, "y": 459, "labelPos": { "x": 98, "y": 112 } } + ] + }, + "scenarioCount": 5, + "scenarios": [ + { "voiceProlog": "chronicles_5/ABVOAB1" }, + { "voiceProlog": "chronicles_5/H3X2RNA" }, + { "voiceProlog": "chronicles_5/ABVOFL2" }, + { "voiceProlog": "chronicles_5/ABVOFL4" }, + { "voiceProlog": "chronicles_5/H3X2UAH", "voiceEpilog": "chronicles_5/N1C_D" } + ], + "loadingBackground": "chronicles_5/LoadBar", + "videoRim": "chronicles_5/INTRORIM", + "introVideo": "chronicles_5/Intro" + }, + "MAPS/CHRONICLES/HC6_MAIN" : { // Heroes Chronicles 6 + "regions": + { + "background": "chronicles_6/CamBkHc", + "prefix": "chronicles_6/HcSc", + "suffix": ["1", "2", "3"], + "color_suffix_length": 0, + "desc": [ + { "infix": "1", "x": 34, "y": 184, "labelPos": { "x": 98, "y": 112 } }, + { "infix": "2", "x": 235, "y": 184, "labelPos": { "x": 98, "y": 112 } }, + { "infix": "3", "x": 34, "y": 320, "labelPos": { "x": 98, "y": 112 } }, + { "infix": "4", "x": 235, "y": 320, "labelPos": { "x": 98, "y": 112 } }, + { "infix": "5", "x": 129, "y": 459, "labelPos": { "x": 98, "y": 112 } } + ] + }, + "scenarioCount": 5, + "scenarios": [ + { "voiceProlog": "chronicles_6/H3X2ELB" }, + { "voiceProlog": "chronicles_6/E1A" }, + { "voiceProlog": "chronicles_6/H3X2BBA" }, + { "voiceProlog": "chronicles_6/ABVOAB2" }, + { "voiceProlog": "chronicles_6/ABVOAB5", "voiceEpilog": "chronicles_6/ABVODB2" } + ], + "loadingBackground": "chronicles_6/LoadBar", + "videoRim": "chronicles_6/INTRORIM", + "introVideo": "chronicles_6/Intro" + }, + "MAPS/CHRONICLES/HC7_MAIN" : { // Heroes Chronicles 7 + "regions": + { + "background": "chronicles_7/CamBkHc", + "prefix": "chronicles_7/HcSc", + "suffix": ["1", "2", "3"], + "color_suffix_length": 0, + "desc": [ + { "infix": "1", "x": 27, "y": 43, "labelPos": { "x": 98, "y": 112 } }, + { "infix": "2", "x": 231, "y": 43, "labelPos": { "x": 98, "y": 112 } }, + { "infix": "3", "x": 27, "y": 178, "labelPos": { "x": 98, "y": 112 } }, + { "infix": "4", "x": 231, "y": 178, "labelPos": { "x": 98, "y": 112 } }, + { "infix": "5", "x": 27, "y": 312, "labelPos": { "x": 98, "y": 112 } }, + { "infix": "6", "x": 231, "y": 312, "labelPos": { "x": 98, "y": 112 } }, + { "infix": "7", "x": 27, "y": 447, "labelPos": { "x": 98, "y": 112 } }, + { "infix": "8", "x": 231, "y": 447, "labelPos": { "x": 98, "y": 112 } } + ] + }, + "scenarioCount": 8, + "scenarios": [ + { "voiceProlog": "chronicles_7/ABVOFL2" }, + { "voiceProlog": "chronicles_7/ABVOFL3" }, + { "voiceProlog": "chronicles_7/N1C_D" }, + { "voiceProlog": "chronicles_7/S1C" }, + { "voiceProlog": "chronicles_7/H3X2UAB" }, + { "voiceProlog": "chronicles_7/E2C" }, + { "voiceProlog": "chronicles_7/H3X2NBE" }, + { "voiceProlog": "chronicles_7/ABVOFW4", "voiceEpilog": "chronicles_7/ABVOAB1" } + ], + "loadingBackground": "chronicles_7/LoadBar", + "videoRim": "chronicles_7/INTRORIM", + "introVideo": "chronicles_7/Intro5" + }, + "MAPS/CHRONICLES/HC8_MAIN" : { // Heroes Chronicles 8 + "regions": + { + "background": "chronicles_8/CamBkHc", + "prefix": "chronicles_8/HcSc", + "suffix": ["1", "2", "3"], + "color_suffix_length": 0, + "desc": [ + { "infix": "1", "x": 27, "y": 43, "labelPos": { "x": 98, "y": 112 } }, + { "infix": "2", "x": 231, "y": 43, "labelPos": { "x": 98, "y": 112 } }, + { "infix": "3", "x": 27, "y": 178, "labelPos": { "x": 98, "y": 112 } }, + { "infix": "4", "x": 231, "y": 178, "labelPos": { "x": 98, "y": 112 } }, + { "infix": "5", "x": 27, "y": 312, "labelPos": { "x": 98, "y": 112 } }, + { "infix": "6", "x": 231, "y": 312, "labelPos": { "x": 98, "y": 112 } }, + { "infix": "7", "x": 27, "y": 447, "labelPos": { "x": 98, "y": 112 } }, + { "infix": "8", "x": 231, "y": 447, "labelPos": { "x": 98, "y": 112 } } + ] + }, + "scenarioCount": 8, + "scenarios": [ + { "voiceProlog": "chronicles_8/H3X2RNB" }, + { "voiceProlog": "chronicles_8/ABVOAB9" }, + { "voiceProlog": "chronicles_8/H3X2BBB" }, + { "voiceProlog": "chronicles_8/ABVODS1" }, + { "voiceProlog": "chronicles_8/H3X2ELA" }, + { "voiceProlog": "chronicles_8/E1B" }, + { "voiceProlog": "chronicles_8/H3X2BBD" }, + { "voiceProlog": "chronicles_8/H3X2ELE", "voiceEpilog": "chronicles_8/ABVOAB7" } + ], + "loadingBackground": "chronicles_8/LoadBar", + "videoRim": "chronicles_8/INTRORIM", + "introVideo": "chronicles_8/Intro6" + } +} 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/campaign_regions.json b/config/campaign_regions.json index 1ae7b89c1..bcba4875b 100644 --- a/config/campaign_regions.json +++ b/config/campaign_regions.json @@ -2,7 +2,7 @@ "campaign_regions": [ { "prefix": "G1", - "color_suffix_length": 1, + "colorSuffixLength": 1, "desc": [ { "infix": "A", "x": 57, "y": 314 }, { "infix": "B", "x": 137, "y": 309 }, @@ -12,7 +12,7 @@ { "prefix": "G2", - "color_suffix_length": 1, + "colorSuffixLength": 1, "desc": [ { "infix": "A", "x": 56, "y": 90 }, { "infix": "B", "x": 316, "y": 49 }, @@ -23,7 +23,7 @@ { "prefix": "G3", - "color_suffix_length": 1, + "colorSuffixLength": 1, "desc": [ { "infix": "A", "x": 289, "y": 376 }, { "infix": "B", "x": 60, "y": 147 }, @@ -33,7 +33,7 @@ { "prefix": "E1", - "color_suffix_length": 1, + "colorSuffixLength": 1, "desc": [ { "infix": "A", "x": 270, "y": 332 }, { "infix": "B", "x": 138, "y": 113 }, @@ -47,7 +47,7 @@ { "prefix": "E2", - "color_suffix_length": 1, + "colorSuffixLength": 1, "desc": [ { "infix": "A", "x": 131, "y": 202 }, { "infix": "B", "x": 60, "y": 145 }, @@ -58,7 +58,7 @@ { "prefix": "N1", - "color_suffix_length": 1, + "colorSuffixLength": 1, "desc": [ { "infix": "A", "x": 42, "y": 94 }, { "infix": "B", "x": 309, "y": 290 }, @@ -68,7 +68,7 @@ { "prefix": "S1", - "color_suffix_length": 1, + "colorSuffixLength": 1, "desc": [ { "infix": "A", "x": 263, "y": 199 }, { "infix": "B", "x": 182, "y": 210 }, @@ -78,7 +78,7 @@ { "prefix": "BR", - "color_suffix_length": 2, + "colorSuffixLength": 2, "desc": [ { "infix": "A", "x": 18, "y": 233 }, { "infix": "B", "x": 125, "y": 381 }, @@ -89,7 +89,7 @@ { "prefix": "IS", - "color_suffix_length": 2, + "colorSuffixLength": 2, "desc": [ { "infix": "A", "x": 294, "y": 399 }, { "infix": "B", "x": 183, "y": 293 }, @@ -100,7 +100,7 @@ { "prefix": "KR", - "color_suffix_length": 2, + "colorSuffixLength": 2, "desc": [ { "infix": "A", "x": 148, "y": 323 }, { "infix": "B", "x": 192, "y": 235 }, @@ -111,7 +111,7 @@ { "prefix": "NI", - "color_suffix_length": 2, + "colorSuffixLength": 2, "desc": [ { "infix": "A", "x": 118, "y": 111 }, { "infix": "B", "x": 223, "y": 145 }, @@ -122,7 +122,7 @@ { "prefix": "TA", - "color_suffix_length": 2, + "colorSuffixLength": 2, "desc": [ { "infix": "A", "x": 228, "y": 233 }, { "infix": "B", "x": 147, "y": 194 }, @@ -132,7 +132,7 @@ { "prefix": "AR", - "color_suffix_length": 2, + "colorSuffixLength": 2, "desc": [ { "infix": "A", "x": 135, "y": 238 }, { "infix": "B", "x": 135, "y": 121 }, @@ -147,7 +147,7 @@ { "prefix": "HS", - "color_suffix_length": 2, + "colorSuffixLength": 2, "desc": [ { "infix": "A", "x": 141, "y": 326 }, { "infix": "B", "x": 238, "y": 275 }, @@ -158,7 +158,7 @@ { "prefix": "BB", - "color_suffix_length": 2, + "colorSuffixLength": 2, "desc": [ { "infix": "A", "x": 167, "y": 342 }, { "infix": "B", "x": 217, "y": 263 }, @@ -170,7 +170,7 @@ { "prefix": "NB", - "color_suffix_length": 2, + "colorSuffixLength": 2, "desc": [ { "infix": "A", "x": 6, "y": 292 }, { "infix": "B", "x": 161, "y": 334 }, @@ -181,7 +181,7 @@ { "prefix": "EL", - "color_suffix_length": 2, + "colorSuffixLength": 2, "desc": [ { "infix": "A", "x": 11, "y": 73 }, { "infix": "B", "x": 0, "y": 241 }, @@ -192,7 +192,7 @@ { "prefix": "RN", - "color_suffix_length": 2, + "colorSuffixLength": 2, "desc": [ { "infix": "A", "x": 84, "y": 319 }, { "infix": "B", "x": 194, "y": 275 }, @@ -203,7 +203,7 @@ { "prefix": "UA", - "color_suffix_length": 2, + "colorSuffixLength": 2, "desc": [ { "infix": "A", "x": 157, "y": 409 }, { "infix": "B", "x": 62, "y": 346 }, @@ -222,7 +222,7 @@ { "prefix": "SP", - "color_suffix_length": 2, + "colorSuffixLength": 2, "desc": [ { "infix": "A", "x": 7, "y": 295 }, { "infix": "B", "x": 44, "y": 141 }, 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/castle.json b/config/factions/castle.json index 5026120a1..03b682cb2 100644 --- a/config/factions/castle.json +++ b/config/factions/castle.json @@ -119,7 +119,7 @@ "dwellingUpLvl7": { "animation" : "TBCSUP_6.def", "x" : 303, "y" : 0, "z" : -1, "border" : "TOCSANG2.bmp", "area" : "TZCSANG2.bmp" } }, - "musicTheme" : "music/CstleTown", + "musicTheme" : [ "music/CstleTown" ], "defaultTavern" : 5, "tavernVideo" : "TAVERN.BIK", "guildBackground" : "TPMAGE.bmp", @@ -147,35 +147,65 @@ ], "horde" : [ 2, -1 ], "mageGuild" : 4, - "warMachine" : "ballista", "moatAbility" : "castleMoat", // primaryResource not specified so town get both Wood and Ore for resource bonus "buildings" : { - "mageGuild1": { "id" : 0 }, - "mageGuild2": { "id" : 1, "upgrades" : "mageGuild1" }, - "mageGuild3": { "id" : 2, "upgrades" : "mageGuild2" }, - "mageGuild4": { "id" : 3, "upgrades" : "mageGuild3" }, - "tavern": { "id" : 5 }, - "shipyard": { "id" : 6 }, - "fort": { "id" : 7 }, - "citadel": { "id" : 8, "upgrades" : "fort" }, - "castle": { "id" : 9, "upgrades" : "citadel" }, - "villageHall": { "id" : 10, "mode" : "auto", "produce": { "gold": 500 } }, - "townHall": { "id" : 11, "upgrades" : "villageHall", "requires" : [ "tavern" ], "produce": { "gold": 1000 } }, - "cityHall": { "id" : 12, "upgrades" : "townHall", "requires" : [ "allOf", [ "mageGuild1" ], [ "marketplace" ], [ "blacksmith" ] ], "produce": { "gold": 2000 } }, - "capitol": { "id" : 13, "upgrades" : "cityHall", "requires" : [ "castle" ], "produce": { "gold": 4000 } }, - "marketplace": { "id" : 14 }, - "resourceSilo": { "id" : 15, "requires" : [ "marketplace" ], "produce": { "ore": 1, "wood": 1 } }, - "blacksmith": { "id" : 16 }, + "mageGuild1": { }, + "mageGuild2": { }, + "mageGuild3": { }, + "mageGuild4": { }, + "tavern": { }, + "shipyard": { }, + "fort": { }, + "citadel": { }, + "castle": { }, + "villageHall": { }, + "townHall": { }, + "cityHall": { }, + "capitol": { }, + "marketplace": { }, + "resourceSilo": { "produce": { "ore": 1, "wood": 1 } }, + "blacksmith": { "warMachine" : "ballista" }, - "special1": { "type" : "lighthouse", "requires" : [ "shipyard" ] }, + "special1": { + "bonuses": [ + { + "propagator": "PLAYER_PROPAGATOR", + "type": "MOVEMENT", + "subtype": "heroMovementSea", + "val": 500 + } + ], + "requires" : [ "shipyard" ] + }, "horde1": { "id" : 18, "upgrades" : "dwellingLvl3" }, "horde1Upgr": { "id" : 19, "upgrades" : "dwellingUpLvl3", "requires" : [ "horde1" ], "mode" : "auto" }, "ship": { "id" : 20, "upgrades" : "shipyard" }, - "special2": { "type" : "stables", "requires" : [ "dwellingLvl4" ] }, - "special3": { "type" : "brotherhoodOfSword", "upgrades" : "tavern" }, + "special2": { + "requires" : [ "dwellingLvl4" ], + "configuration" : { + "visitMode" : "bonus", + "rewards" : [ + { + "message" : "@core.genrltxt.580", + "movePoints" : 400, + "bonuses" : [ { "type" : "MOVEMENT", "subtype" : "heroMovementLand", "val" : 400, "valueType" : "ADDITIVE_VALUE", "duration" : "ONE_WEEK"} ] + } + ] + } + }, + "special3": { + "upgradeReplacesBonuses" : true, + "bonuses": [ + { + "type": "MORALE", + "val": 2 + } + ], + "upgrades" : "tavern" + }, "grail": { "id" : 26, "mode" : "grail", "produce": { "gold": 5000 }, "bonuses": [ { "type": "MORALE", "val": 2, "propagator": "PLAYER_PROPAGATOR" } ] }, "dwellingLvl1": { "id" : 30, "requires" : [ "fort" ] }, diff --git a/config/factions/conflux.json b/config/factions/conflux.json index fd431f6b1..a611b32c3 100644 --- a/config/factions/conflux.json +++ b/config/factions/conflux.json @@ -123,7 +123,7 @@ "dwellingUpLvl7": { "animation" : "TBELUP_6.def", "x" : 43, "y" : 0, "z" : -2, "border" : "TOELUP_6.bmp", "area" : "TZELUP_6.bmp" } }, - "musicTheme" : "music/ElemTown", + "musicTheme" : [ "music/ElemTown" ], "defaultTavern" : 5, "tavernVideo" : "TAVERN.BIK", "guildBackground" : "TPMAGE.bmp", @@ -152,34 +152,33 @@ "horde" : [ 0, -1 ], "mageGuild" : 5, "primaryResource" : "mercury", - "warMachine" : "ballista", "moatAbility" : "castleMoat", "buildings" : { - "mageGuild1": { "id" : 0 }, - "mageGuild2": { "id" : 1, "upgrades" : "mageGuild1" }, - "mageGuild3": { "id" : 2, "upgrades" : "mageGuild2" }, - "mageGuild4": { "id" : 3, "upgrades" : "mageGuild3" }, - "mageGuild5": { "id" : 4, "upgrades" : "mageGuild4" }, - "tavern": { "id" : 5 }, - "shipyard": { "id" : 6 }, - "fort": { "id" : 7 }, - "citadel": { "id" : 8, "upgrades" : "fort" }, - "castle": { "id" : 9, "upgrades" : "citadel" }, - "villageHall": { "id" : 10, "mode" : "auto", "produce": { "gold": 500 } }, - "townHall": { "id" : 11, "upgrades" : "villageHall", "requires" : [ "tavern" ], "produce": { "gold": 1000 } }, - "cityHall": { "id" : 12, "upgrades" : "townHall", "requires" : [ "allOf", [ "mageGuild1" ], [ "marketplace" ], [ "blacksmith" ] ], "produce": { "gold": 2000 } }, - "capitol": { "id" : 13, "upgrades" : "cityHall", "requires" : [ "castle" ], "produce": { "gold": 4000 } }, - "marketplace": { "id" : 14 }, - "resourceSilo": { "id" : 15, "requires" : [ "marketplace" ], "produce": { "mercury": 1 } }, - "blacksmith": { "id" : 16 }, + "mageGuild1": { }, + "mageGuild2": { }, + "mageGuild3": { }, + "mageGuild4": { }, + "mageGuild5": { }, + "tavern": { }, + "shipyard": { }, + "fort": { }, + "citadel": { }, + "castle": { }, + "villageHall": { }, + "townHall": { }, + "cityHall": { }, + "capitol": { }, + "marketplace": { }, + "resourceSilo": { "produce": { "mercury": 1 } }, + "blacksmith": { "warMachine" : "ballista" }, - "special1": { "type" : "artifactMerchant", "requires" : [ "marketplace" ] }, + "special1": { "requires" : [ "marketplace" ], "marketModes" : ["resource-artifact", "artifact-resource"] }, "horde1": { "id" : 18, "upgrades" : "dwellingLvl1" }, "horde1Upgr": { "id" : 19, "upgrades" : "dwellingUpLvl1", "requires" : [ "horde1" ], "mode" : "auto" }, "ship": { "id" : 20, "upgrades" : "shipyard" }, - "special2": { "type" : "magicUniversity", "requires" : [ "mageGuild1" ] }, + "special2": { "requires" : [ "mageGuild1" ], "marketModes" : ["resource-skill"] }, "grail": { "id" : 26, "mode" : "grail", "produce": { "gold": 5000 }}, "extraTownHall": { "id" : 27, "requires" : [ "townHall" ], "mode" : "auto" }, "extraCityHall": { "id" : 28, "requires" : [ "cityHall" ], "mode" : "auto" }, diff --git a/config/factions/dungeon.json b/config/factions/dungeon.json index f864b282e..54cafa27b 100644 --- a/config/factions/dungeon.json +++ b/config/factions/dungeon.json @@ -119,7 +119,7 @@ "dwellingUpLvl7": { "animation" : "TBDNUP_6.def", "x" : 550, "y" : 0, "z" : -1, "border" : "TODDRA2A.bmp", "area" : "TZDDRA2A.bmp" } }, - "musicTheme" : "music/Dungeon", + "musicTheme" : [ "music/Dungeon" ], "defaultTavern" : 5, "tavernVideo" : "TAVERN.BIK", "guildBackground" : "TPMAGE.bmp", @@ -148,35 +148,61 @@ "horde" : [ 0, -1 ], "mageGuild" : 5, "primaryResource" : "sulfur", - "warMachine" : "ballista", "moatAbility" : "dungeonMoat", - "buildings" : { - "mageGuild1": { "id" : 0 }, - "mageGuild2": { "id" : 1, "upgrades" : "mageGuild1" }, - "mageGuild3": { "id" : 2, "upgrades" : "mageGuild2" }, - "mageGuild4": { "id" : 3, "upgrades" : "mageGuild3" }, - "mageGuild5": { "id" : 4, "upgrades" : "mageGuild4" }, - "tavern": { "id" : 5 }, - "fort": { "id" : 7 }, - "citadel": { "id" : 8, "upgrades" : "fort" }, - "castle": { "id" : 9, "upgrades" : "citadel" }, - "villageHall": { "id" : 10, "mode" : "auto", "produce": { "gold": 500 } }, - "townHall": { "id" : 11, "upgrades" : "villageHall", "requires" : [ "tavern" ], "produce": { "gold": 1000 } }, - "cityHall": { "id" : 12, "upgrades" : "townHall", "requires" : [ "allOf", [ "mageGuild1" ], [ "marketplace" ], [ "blacksmith" ] ], "produce": { "gold": 2000 } }, - "capitol": { "id" : 13, "upgrades" : "cityHall", "requires" : [ "castle" ], "produce": { "gold": 4000 } }, - "marketplace": { "id" : 14 }, - "resourceSilo": { "id" : 15, "requires" : [ "marketplace" ] }, - "blacksmith": { "id" : 16 }, + "mageGuild1": { }, + "mageGuild2": { }, + "mageGuild3": { }, + "mageGuild4": { }, + "mageGuild5": { }, + "tavern": { }, + "fort": { }, + "citadel": { }, + "castle": { }, + "villageHall": { }, + "townHall": { }, + "cityHall": { }, + "capitol": { }, + "marketplace": { }, + "resourceSilo": { "produce": { "sulfur": 1 } }, + "blacksmith": { "warMachine" : "ballista" }, - "special1": { "type" : "artifactMerchant", "requires" : [ "marketplace" ] }, + "special1": { "requires" : [ "marketplace" ], "marketModes" : ["resource-artifact", "artifact-resource"] }, "horde1": { "id" : 18, "upgrades" : "dwellingLvl1" }, "horde1Upgr": { "id" : 19, "upgrades" : "dwellingUpLvl1", "requires" : [ "horde1" ], "mode" : "auto" }, - "special2": { "type" : "manaVortex", "requires" : [ "mageGuild1" ] }, + "special2": { + "requires" : [ "mageGuild1" ], + "configuration" : { + "resetParameters" : { + "period" : 7, + "visitors" : true + }, + "visitMode" : "once", + "rewards" : [ + { + "limiter" : { + "noneOf" : [ { "manaPercentage" : 200 } ] + }, + "message" : "@core.genrltxt.579", + "manaPercentage" : 200 + } + ] + } + }, "special3": { "type" : "portalOfSummoning" }, - "special4": { "type" : "experienceVisitingBonus" }, + "special4": { + "configuration" : { + "visitMode" : "hero", + "rewards" : [ + { + "message" : "@core.genrltxt.583", + "heroExperience" : 1000 + } + ] + } + }, "grail": { "id" : 26, "mode" : "grail", "produce": { "gold": 5000 }, "bonuses": [ { "type": "PRIMARY_SKILL", "subtype": "primarySkill.spellpower", "val": 12 } ] }, diff --git a/config/factions/fortress.json b/config/factions/fortress.json index 38002253a..d238f9d52 100644 --- a/config/factions/fortress.json +++ b/config/factions/fortress.json @@ -119,7 +119,7 @@ "dwellingUpLvl7": { "animation" : "TBFRUP_6.def", "x" : 587, "y" : 263, "z" : 5, "border" : "TOFHYD2A.bmp", "area" : "TZFHYD2A.bmp" } }, - "musicTheme" : "music/FortressTown", + "musicTheme" : [ "music/FortressTown" ], "defaultTavern" : 5, "tavernVideo" : "TAVERN.BIK", "guildBackground" : "TPMAGE.bmp", @@ -147,34 +147,62 @@ ], "horde" : [ 0, -1 ], "mageGuild" : 3, - "warMachine" : "firstAidTent", "moatAbility" : "fortressMoat", // primaryResource not specified so town get both Wood and Ore for resource bonus "buildings" : { - "mageGuild1": { "id" : 0 }, - "mageGuild2": { "id" : 1, "upgrades" : "mageGuild1" }, - "mageGuild3": { "id" : 2, "upgrades" : "mageGuild2" }, - "tavern": { "id" : 5 }, - "shipyard": { "id" : 6 }, - "fort": { "id" : 7 }, - "citadel": { "id" : 8, "upgrades" : "fort" }, - "castle": { "id" : 9, "upgrades" : "citadel" }, - "villageHall": { "id" : 10, "mode" : "auto", "produce": { "gold": 500 } }, - "townHall": { "id" : 11, "upgrades" : "villageHall", "requires" : [ "tavern" ], "produce": { "gold": 1000 } }, - "cityHall": { "id" : 12, "upgrades" : "townHall", "requires" : [ "allOf", [ "mageGuild1" ], [ "marketplace" ], [ "blacksmith" ] ], "produce": { "gold": 2000 } }, - "capitol": { "id" : 13, "upgrades" : "cityHall", "requires" : [ "castle" ], "produce": { "gold": 4000 } }, - "marketplace": { "id" : 14 }, - "resourceSilo": { "id" : 15, "requires" : [ "marketplace" ], "produce": { "wood": 1, "ore": 1 } }, - "blacksmith": { "id" : 16 }, + "mageGuild1": { }, + "mageGuild2": { }, + "mageGuild3": { }, + "tavern": { }, + "shipyard": { }, + "fort": { }, + "citadel": { }, + "castle": { }, + "villageHall": { }, + "townHall": { }, + "cityHall": { }, + "capitol": { }, + "marketplace": { }, + "resourceSilo": { "produce": { "wood": 1, "ore": 1 } }, + "blacksmith": { "warMachine" : "firstAidTent" }, - "special1": { "type" : "defenceVisitingBonus", "requires" : [ "allOf", [ "townHall" ], [ "special2" ] ] }, + "special1": { + "requires" : [ "allOf", [ "townHall" ], [ "special2" ] ], + "configuration" : { + "visitMode" : "hero", + "rewards" : [ + { + "message" : "@core.genrltxt.585", + "primary" : { "defence" : 1 } + } + ] + } + }, "horde1": { "id" : 18, "upgrades" : "dwellingLvl1" }, "horde1Upgr": { "id" : 19, "upgrades" : "dwellingUpLvl1", "requires" : [ "horde1" ], "mode" : "auto" }, "ship": { "id" : 20, "upgrades" : "shipyard" }, - "special2": { "type" : "defenseGarrisonBonus", "requires" : [ "fort" ] }, - "special3": { "type" : "attackGarrisonBonus", "requires" : [ "special2" ] }, + "special2": { + "bonuses": [ + { + "type": "PRIMARY_SKILL", + "subtype": "primarySkill.attack", + "val": 2 + } + ], + "requires" : [ "fort" ] + }, + "special3": { + "bonuses": [ + { + "type": "PRIMARY_SKILL", + "subtype": "primarySkill.defence", + "val": 2 + } + ], + "requires" : [ "special2" ] + }, "grail": { "id" : 26, "mode" : "grail", "produce": { "gold": 5000 }, "bonuses": [ { "type": "PRIMARY_SKILL", "subtype": "primarySkill.attack", "val": 10 }, diff --git a/config/factions/inferno.json b/config/factions/inferno.json index 9a8c86912..04cd9dfc3 100644 --- a/config/factions/inferno.json +++ b/config/factions/inferno.json @@ -120,7 +120,7 @@ "dwellingUpLvl7": { "animation" : "TBINUP_6.def", "x" : 420, "y" : 105, "z" : -1, "border" : "TOIDVL2.bmp", "area" : "TZIDVL2.bmp" } }, - "musicTheme" : "music/InfernoTown", + "musicTheme" : [ "music/InfernoTown" ], "defaultTavern" : 5, "tavernVideo" : "TAVERN.BIK", "guildBackground" : "TPMAGE.bmp", @@ -149,33 +149,52 @@ "horde" : [ 0, 2 ], "mageGuild" : 5, "primaryResource" : "mercury", - "warMachine" : "ammoCart", "moatAbility" : "infernoMoat", "buildings" : { - "mageGuild1": { "id" : 0 }, - "mageGuild2": { "id" : 1, "upgrades" : "mageGuild1" }, - "mageGuild3": { "id" : 2, "upgrades" : "mageGuild2" }, - "mageGuild4": { "id" : 3, "upgrades" : "mageGuild3" }, - "mageGuild5": { "id" : 4, "upgrades" : "mageGuild4" }, - "tavern": { "id" : 5 }, - "fort": { "id" : 7 }, - "citadel": { "id" : 8, "upgrades" : "fort" }, - "castle": { "id" : 9, "upgrades" : "citadel" }, - "villageHall": { "id" : 10, "mode" : "auto", "produce": { "gold": 500 } }, - "townHall": { "id" : 11, "upgrades" : "villageHall", "requires" : [ "tavern" ], "produce": { "gold": 1000 } }, - "cityHall": { "id" : 12, "upgrades" : "townHall", "requires" : [ "allOf", [ "mageGuild1" ], [ "marketplace" ], [ "blacksmith" ] ], "produce": { "gold": 2000 } }, - "capitol": { "id" : 13, "upgrades" : "cityHall", "requires" : [ "castle" ], "produce": { "gold": 4000 } }, - "marketplace": { "id" : 14 }, - "resourceSilo": { "id" : 15, "requires" : [ "marketplace" ], "produce": { "mercury": 1 } }, - "blacksmith": { "id" : 16 }, + "mageGuild1": { }, + "mageGuild2": { }, + "mageGuild3": { }, + "mageGuild4": { }, + "mageGuild5": { }, + "tavern": { }, + "fort": { }, + "citadel": { }, + "castle": { }, + "villageHall": { }, + "townHall": { }, + "cityHall": { }, + "capitol": { }, + "marketplace": { }, + "resourceSilo": { "produce": { "mercury": 1 } }, + "blacksmith": { "warMachine" : "ammoCart" }, "horde1": { "id" : 18, "upgrades" : "dwellingLvl1" }, "horde1Upgr": { "id" : 19, "upgrades" : "dwellingUpLvl1", "requires" : [ "horde1" ], "mode" : "auto" }, - "special2": { "type" : "spellPowerGarrisonBonus", "requires" : [ "fort" ] }, + "special2": { + "bonuses": [ + { + "type": "PRIMARY_SKILL", + "subtype": "primarySkill.spellpower", + "val": 2 + } + ], + "requires" : [ "fort" ] + }, "special3": { "type" : "castleGate", "requires" : [ "citadel" ] }, - "special4": { "type" : "spellPowerVisitingBonus", "requires" : [ "mageGuild1" ] }, + "special4": { + "requires" : [ "mageGuild1" ], + "configuration" : { + "visitMode" : "hero", + "rewards" : [ + { + "message" : "@core.genrltxt.582", + "primary" : { "spellpower" : 1 } + } + ] + } + }, "horde2": { "id" : 24, "upgrades" : "dwellingLvl3" }, "horde2Upgr": { "id" : 25, "upgrades" : "dwellingUpLvl3", "requires" : [ "horde2" ], "mode" : "auto" }, "grail": { "id" : 26, "mode" : "grail", "produce": { "gold": 5000 }}, diff --git a/config/factions/necropolis.json b/config/factions/necropolis.json index 29951540d..8485e092b 100644 --- a/config/factions/necropolis.json +++ b/config/factions/necropolis.json @@ -124,7 +124,7 @@ "dwellingUpLvl7": { "animation" : "TBNCUP_6.def", "x" : 662, "y" : 23, "border" : "TONBON2.bmp", "area" : "TZNBON2.bmp" } }, - "musicTheme" : "music/NecroTown", + "musicTheme" : [ "music/NecroTown" ], "defaultTavern" : 5, "tavernVideo" : "TAVERN.BIK", "guildBackground" : "TPMAGE.bmp", @@ -152,29 +152,28 @@ ], "horde" : [ 0, -1 ], "mageGuild" : 5, - "warMachine" : "firstAidTent", "moatAbility" : "necropolisMoat", // primaryResource not specified so town get both Wood and Ore for resource bonus "buildings" : { - "mageGuild1": { "id" : 0 }, - "mageGuild2": { "id" : 1, "upgrades" : "mageGuild1" }, - "mageGuild3": { "id" : 2, "upgrades" : "mageGuild2" }, - "mageGuild4": { "id" : 3, "upgrades" : "mageGuild3" }, - "mageGuild5": { "id" : 4, "upgrades" : "mageGuild4" }, - "tavern": { "id" : 5 }, - "shipyard": { "id" : 6 }, - "fort": { "id" : 7 }, - "citadel": { "id" : 8, "upgrades" : "fort" }, - "castle": { "id" : 9, "upgrades" : "citadel" }, - "villageHall": { "id" : 10, "mode" : "auto", "produce": { "gold": 500 } }, - "townHall": { "id" : 11, "upgrades" : "villageHall", "requires" : [ "tavern" ], "produce": { "gold": 1000 } }, - "cityHall": { "id" : 12, "upgrades" : "townHall", "requires" : [ "allOf", [ "mageGuild1" ], [ "marketplace" ], [ "blacksmith" ] ], "produce": { "gold": 2000 } }, - "capitol": { "id" : 13, "upgrades" : "cityHall", "requires" : [ "castle" ], "produce": { "gold": 4000 } }, - "marketplace": { "id" : 14 }, - "resourceSilo": { "id" : 15, "requires" : [ "marketplace" ], "produce": { "ore": 1, "wood": 1 } }, - "blacksmith": { "id" : 16 }, + "mageGuild1": { }, + "mageGuild2": { }, + "mageGuild3": { }, + "mageGuild4": { }, + "mageGuild5": { }, + "tavern": { }, + "shipyard": { }, + "fort": { }, + "citadel": { }, + "castle": { }, + "villageHall": { }, + "townHall": { }, + "cityHall": { }, + "capitol": { }, + "marketplace": { }, + "resourceSilo": { "produce": { "ore": 1, "wood": 1 } }, + "blacksmith": { "warMachine" : "firstAidTent" }, "special1": { "requires" : [ "fort" ], "bonuses": [ { "type": "DARKNESS", "val": 20 } ] }, "horde1": { "id" : 18, "upgrades" : "dwellingLvl1", "requires" : [ "special3" ] }, @@ -182,7 +181,7 @@ "ship": { "id" : 20, "upgrades" : "shipyard" }, "special2": { "requires" : [ "mageGuild1" ], "bonuses": [ { "type": "UNDEAD_RAISE_PERCENTAGE", "val": 10, "propagator": "PLAYER_PROPAGATOR" } ] }, - "special3": { "type" : "creatureTransformer", "requires" : [ "dwellingLvl1" ] }, + "special3": { "requires" : [ "dwellingLvl1" ], "marketModes" : ["creature-undead"] }, "grail": { "id" : 26, "mode" : "grail", "produce": { "gold": 5000 }, "bonuses": [ { "type": "UNDEAD_RAISE_PERCENTAGE", "val": 20, "propagator": "PLAYER_PROPAGATOR" } ] }, diff --git a/config/factions/rampart.json b/config/factions/rampart.json index 8893fe56a..42bd462f9 100644 --- a/config/factions/rampart.json +++ b/config/factions/rampart.json @@ -123,7 +123,7 @@ "dwellingUpLvl7": { "animation" : "TBRMUP_6.def", "x" : 502, "y" : 5, "z" : -5, "border" : "TORDR2AA.bmp", "area" : "TZRDR2AA.bmp" } }, - "musicTheme" : "music/Rampart", + "musicTheme" : [ "music/Rampart" ], "defaultTavern" : 5, "tavernVideo" : "TAVERN.BIK", "guildBackground" : "TPMAGE.bmp", @@ -152,32 +152,39 @@ "horde" : [ 1, 4 ], "mageGuild" : 5, "primaryResource" : "crystal", - "warMachine" : "firstAidTent", "moatAbility" : "rampartMoat", "buildings" : { - "mageGuild1": { "id" : 0 }, - "mageGuild2": { "id" : 1, "upgrades" : "mageGuild1" }, - "mageGuild3": { "id" : 2, "upgrades" : "mageGuild2" }, - "mageGuild4": { "id" : 3, "upgrades" : "mageGuild3" }, - "mageGuild5": { "id" : 4, "upgrades" : "mageGuild4" }, - "tavern": { "id" : 5 }, - "fort": { "id" : 7 }, - "citadel": { "id" : 8, "upgrades" : "fort" }, - "castle": { "id" : 9, "upgrades" : "citadel" }, - "villageHall": { "id" : 10, "mode" : "auto", "produce": { "gold": 500 } }, - "townHall": { "id" : 11, "upgrades" : "villageHall", "requires" : [ "tavern" ], "produce": { "gold": 1000 } }, - "cityHall": { "id" : 12, "upgrades" : "townHall", "requires" : [ "allOf", [ "mageGuild1" ], [ "marketplace" ], [ "blacksmith" ] ], "produce": { "gold": 2000 } }, - "capitol": { "id" : 13, "upgrades" : "cityHall", "requires" : [ "castle" ], "produce": { "gold": 4000 } }, - "marketplace": { "id" : 14 }, - "resourceSilo": { "id" : 15, "requires" : [ "marketplace" ], "produce": { "crystal": 1 } }, - "blacksmith": { "id" : 16 }, + "mageGuild1": { }, + "mageGuild2": { }, + "mageGuild3": { }, + "mageGuild4": { }, + "mageGuild5": { }, + "tavern": { }, + "fort": { }, + "citadel": { }, + "castle": { }, + "villageHall": { }, + "townHall": { }, + "cityHall": { }, + "capitol": { }, + "marketplace": { }, + "resourceSilo": { "produce": { "crystal": 1 } }, + "blacksmith": { "warMachine" : "firstAidTent" }, "special1": { "type" : "mysticPond" }, "horde1": { "id" : 18, "upgrades" : "dwellingLvl2" }, "horde1Upgr": { "id" : 19, "upgrades" : "dwellingUpLvl2", "requires" : [ "horde1" ], "mode" : "auto" }, - "special2": { "type" : "fountainOfFortune", "upgrades" : "special1" }, + "special2": { + "bonuses": [ + { + "type": "LUCK", + "val": 2 + } + ], + "upgrades" : "special1" + }, "special3": { "type" : "treasury", "requires" : [ "horde1" ] }, "horde2": { "id" : 24, "upgrades" : "dwellingLvl5" }, "horde2Upgr": { "id" : 25, "upgrades" : "dwellingUpLvl5", "requires" : [ "horde2" ], "mode" : "auto" }, @@ -192,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/factions/stronghold.json b/config/factions/stronghold.json index 5e8afda84..66af50a94 100644 --- a/config/factions/stronghold.json +++ b/config/factions/stronghold.json @@ -117,7 +117,7 @@ "dwellingUpLvl7": { "animation" : "TBSTUP_6.def", "x" : 604, "y" : 0, "border" : "TOSBEH2A.bmp", "area" : "TZSBEH2A.bmp" } }, - "musicTheme" : "music/Stronghold", + "musicTheme" : [ "music/Stronghold" ], "defaultTavern" : 5, "tavernVideo" : "TAVERN.BIK", "guildBackground" : "TPMAGE.bmp", @@ -145,33 +145,43 @@ ], "horde" : [ 0, -1 ], "mageGuild" : 3, - "warMachine" : "ammoCart", "moatAbility" : "strongholdMoat", // primaryResource not specified so town get both Wood and Ore for resource bonus "buildings" : { - "mageGuild1": { "id" : 0 }, - "mageGuild2": { "id" : 1, "upgrades" : "mageGuild1" }, - "mageGuild3": { "id" : 2, "upgrades" : "mageGuild2" }, - "tavern": { "id" : 5 }, - "fort": { "id" : 7 }, - "citadel": { "id" : 8, "upgrades" : "fort" }, - "castle": { "id" : 9, "upgrades" : "citadel" }, - "villageHall": { "id" : 10, "mode" : "auto", "produce": { "gold": 500 } }, - "townHall": { "id" : 11, "upgrades" : "villageHall", "requires" : [ "tavern" ], "produce": { "gold": 1000 } }, - "cityHall": { "id" : 12, "upgrades" : "townHall", "requires" : [ "allOf", [ "mageGuild1" ], [ "marketplace" ], [ "blacksmith" ] ], "produce": { "gold": 2000 } }, - "capitol": { "id" : 13, "upgrades" : "cityHall", "requires" : [ "castle" ], "produce": { "gold": 4000 } }, - "marketplace": { "id" : 14 }, - "resourceSilo": { "id" : 15, "requires" : [ "marketplace" ], "produce": { "ore": 1, "wood": 1 } }, - "blacksmith": { "id" : 16 }, + "mageGuild1": { }, + "mageGuild2": { }, + "mageGuild3": { }, + "tavern": { }, + "fort": { }, + "citadel": { }, + "castle": { }, + "villageHall": { }, + "townHall": { }, + "cityHall": { }, + "capitol": { }, + "marketplace": { }, + "resourceSilo": { "produce": { "ore": 1, "wood": 1 } }, + "blacksmith": { "warMachine" : "ammoCart" }, "special1": { "type" : "escapeTunnel", "requires" : [ "fort" ] }, "horde1": { "id" : 18, "upgrades" : "dwellingLvl1" }, "horde1Upgr": { "id" : 19, "upgrades" : "dwellingUpLvl1", "requires" : [ "horde1" ], "mode" : "auto" }, - "special2": { "type" : "freelancersGuild", "requires" : [ "marketplace" ] }, - "special3": { "type" : "ballistaYard", "requires" : [ "blacksmith" ] }, - "special4": { "type" : "attackVisitingBonus", "requires" : [ "fort" ] }, + "special2": { "requires" : [ "marketplace" ], "marketModes" : ["creature-resource"] }, + "special3": { "warMachine" : "ballista", "requires" : [ "blacksmith" ] }, + "special4": { + "requires" : [ "fort" ], + "configuration" : { + "visitMode" : "hero", + "rewards" : [ + { + "message" : "@core.genrltxt.584", + "primary" : { "attack" : 1 } + } + ] + } + }, "grail": { "id" : 26, "mode" : "grail", "produce": { "gold": 5000 }, "bonuses": [ { "type": "PRIMARY_SKILL", "subtype": "primarySkill.attack", "val": 20 } ] }, diff --git a/config/factions/tower.json b/config/factions/tower.json index 488759b91..04741ba0d 100644 --- a/config/factions/tower.json +++ b/config/factions/tower.json @@ -118,7 +118,7 @@ "dwellingUpLvl7": { "animation" : "TBTWUP_6.def", "x" : 75, "y" : 91, "z" : -1, "border" : "TOTTIT2.bmp", "area" : "TZTTIT2.bmp" } }, - "musicTheme" : "music/TowerTown", + "musicTheme" : [ "music/TowerTown" ], "defaultTavern" : 5, "tavernVideo" : "TAVERN.BIK", "guildBackground" : "TPMAGE.bmp", @@ -147,34 +147,44 @@ "horde" : [ 1, -1 ], "primaryResource" : "gems", "mageGuild" : 5, - "warMachine" : "ammoCart", "moatAbility" : "towerMoat", "buildings" : { - "mageGuild1": { "id" : 0 }, - "mageGuild2": { "id" : 1, "upgrades" : "mageGuild1" }, - "mageGuild3": { "id" : 2, "upgrades" : "mageGuild2" }, - "mageGuild4": { "id" : 3, "upgrades" : "mageGuild3" }, - "mageGuild5": { "id" : 4, "upgrades" : "mageGuild4" }, - "tavern": { "id" : 5 }, - "fort": { "id" : 7 }, - "citadel": { "id" : 8, "upgrades" : "fort" }, - "castle": { "id" : 9, "upgrades" : "citadel" }, - "villageHall": { "id" : 10, "mode" : "auto", "produce" : { "gold": 500 } }, - "townHall": { "id" : 11, "upgrades" : "villageHall", "requires" : [ "tavern" ], "produce" : { "gold": 1000 } }, - "cityHall": { "id" : 12, "upgrades" : "townHall", "requires" : [ "allOf", [ "mageGuild1" ], [ "marketplace" ], [ "blacksmith" ] ], "produce": { "gold": 2000 } }, - "capitol": { "id" : 13, "upgrades" : "cityHall", "requires" : [ "castle" ], "produce" : { "gold": 4000 } }, - "marketplace": { "id" : 14 }, - "resourceSilo": { "id" : 15, "requires" : [ "marketplace" ], "produce" : { "gems": 1 } }, - "blacksmith": { "id" : 16 }, + "mageGuild1": { }, + "mageGuild2": { }, + "mageGuild3": { }, + "mageGuild4": { }, + "mageGuild5": { }, + "tavern": { }, + "fort": { }, + "citadel": { }, + "castle": { }, + "villageHall": { }, + "townHall": { }, + "cityHall": { }, + "capitol": { }, + "marketplace": { }, + "resourceSilo": { "produce" : { "gems": 1 } }, + "blacksmith": { "warMachine" : "ammoCart" }, - "special1": { "type" : "artifactMerchant", "requires" : [ "marketplace" ] }, + "special1": { "requires" : [ "marketplace" ], "marketModes" : ["resource-artifact", "artifact-resource"] }, "horde1": { "id" : 18, "upgrades" : "dwellingLvl2" }, "horde1Upgr": { "id" : 19, "upgrades" : "dwellingUpLvl2", "requires" : [ "horde1" ], "mode" : "auto" }, - "special2": { "type" : "lookoutTower", "height" : "high", "requires" : [ "fort" ] }, + "special2": { "height" : "high", "requires" : [ "fort" ] }, "special3": { "type" : "library", "requires" : [ "mageGuild1" ] }, - "special4": { "type" : "knowledgeVisitingBonus", "requires" : [ "mageGuild1" ] }, + "special4": { + "requires" : [ "mageGuild1" ], + "configuration" : { + "visitMode" : "hero", + "rewards" : [ + { + "message" : "@core.genrltxt.581", + "primary" : { "knowledge" : 1 } + } + ] + } + }, "grail": { "height" : "skyship", "produce" : { "gold": 5000 }, "bonuses": [ { "type": "PRIMARY_SKILL", "subtype": "primarySkill.knowledge", "val": 15 } ] }, "dwellingLvl1": { "id" : 30, "requires" : [ "fort" ] }, diff --git a/config/fonts.json b/config/fonts.json index 7aa8360cb..e40da48f6 100644 --- a/config/fonts.json +++ b/config/fonts.json @@ -5,12 +5,12 @@ "bitmap" : [ "BIGFONT", // Mostly used for window titles - "CALLI10R", // Unused in VCMI - "CREDITS", // Used for credits menu - "HISCORE", // Unused in VCMI + "CALLI10R", // Only in World View menu + "CREDITS", // Only in Credits menu + "HISCORE", // Only in High Scores menu "MEDFONT", // Some titles "SMALFONT", // Most of the messages - "TIMES08R", // Used to display amounts on creature card + "TIMES08R", // Unused in VCMI "TINY", // Some text "VERD10B" // Unused in VCMI ], @@ -19,19 +19,24 @@ // Should be in format: // : // "file" - file to load font from, must be in data/ directory - // "size" - point size of font + // "size" - point size of font. Can be defined in two forms: + // a) single number, e.g. 10. In this case, game will automatically multiply font size by upscaling factor when xBRZ is in use, + // so xbrz 2x will use 20px, xbrz 3x will use 30px, etc + // b) list of scaling factors for each scaling mode, e.g. [ 10, 16, 22, 26]. In this case game will select point size according to xBRZ scaling factor + // so unscaled mode will use 10px, xbrz2 will use 16px, and xbrz3 will use 22 // "style" - italic and\or bold, indicates font style - // "blend" - if set to true, font will be antialiased + // "outline" - if set, black shadow will be generated around entire text (instead of only bottom-right side) + // "noShadow" - if set, this font will not drop any shadow "trueType": { - //"BIGFONT" : { "file" : "LiberationSerif-Bold.ttf", "size" : 22, "blend" : true}, - //"CALLI10R" : { "file" : "Georgia.ttf", "size" : 10}, - //"CREDITS" : { "file" : "LiberationSerif-Bold.ttf", "size" : 28}, - //"HISCORE" : { "file" : "Georgia.ttf", "size" : 13}, - //"MEDFONT" : { "file" : "LiberationSerif-Bold.ttf", "size" : 16}, // breaks messages (from map events) - //"SMALFONT" : { "file" : "LiberationSerif-Regular.ttf", "size" : 13, "blend" : true}, - //"TIMES08R" : { "file" : "LiberationSerif-Regular.ttf", "size" : 11, "blend" : true}, - //"TINY" : { "file" : "LiberationSerif-Regular.ttf", "size" : 11, "blend" : true}, - //"VERD10B" : { "file" : "Georgia.ttf", "size" : 13} + "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" : [ 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, 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 8f49551f6..ac40a419f 100644 --- a/config/gameConfig.json +++ b/config/gameConfig.json @@ -53,10 +53,13 @@ "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", "config/objects/rewardableBonusing.json", "config/objects/rewardableOncePerHero.json", "config/objects/rewardableOncePerWeek.json", @@ -271,6 +274,10 @@ "portraitYoungYog" : 162 } }, + "chronicles" : { + "supported" : false, + "iconIndex" : 2 + }, "jsonVCMI" : { "supported" : true, "iconIndex" : 3 @@ -296,7 +303,9 @@ // number of artifacts that can fit in a backpack. -1 is unlimited. "backpackSize" : -1, // if heroes are invitable in tavern - "tavernInvite" : false + "tavernInvite" : false, + // minimal primary skills for heroes + "minimalPrimarySkills": [ 0, 0, 1, 1] }, "towns": @@ -304,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": @@ -327,8 +350,79 @@ // limit of damage reduction that can be achieved by overpowering defense points "defensePointDamageFactorCap": 0.7, // If set to true, double-wide creatures will trigger obstacle effect when moving one tile forward or backwards - "oneHexTriggersObstacles": false - }, + "oneHexTriggersObstacles": false, + // Allow area shooters with SPELL_LIKE_ATTACK bonus such as liches or magogs to target empty hexes + "areaShotCanTargetEmptyHex" : false, + + // Positions of units on start of the combat + // If battle does not defines specific configuration, 'default' configuration will be used + // Configuration must define either 'attackerUnits' list of 7 elements or both 'attackerUnitsLoose' and 'attackerUnitsTight' lists of 7 elements, 1..7 elements each + // Similarly, for defender configuration must have either 'defenderUnits' or both 'defenderUnitsLoose' and 'defenderUnitsTight' + "layouts" : { + "default" : { + "tacticsAllowed" : true, + "obstaclesAllowed" : true, + "attackerCommander" : 88, + "defenderCommander" : 98, + "attackerWarMachines" : [ 52, 18, 154, 120 ], + "defenderWarMachines" : [ 66, 32, 168, 134 ], + "attackerUnitsLoose": [ + [ 86 ], + [ 35, 137 ], + [ 35, 86, 137 ], + [ 1, 69, 103, 171 ], + [ 1, 35, 86, 137, 171 ], + [ 1, 35, 69, 103, 137, 171 ], + [ 1, 35, 69, 86, 103, 137, 171 ] + ], + "defenderUnitsLoose": [ + [ 100 ], + [ 49, 151 ], + [ 49, 100, 151 ], + [ 15, 83, 117, 185 ], + [ 15, 49, 100, 151, 185 ], + [ 15, 49, 83, 117, 151, 185 ], + [ 15, 49, 83, 100, 117, 151, 185 ] + ], + "attackerUnitsTight": [ + [ 86 ], + [ 69, 103 ], + [ 69, 86, 103 ], + [ 35, 69, 103, 137 ], + [ 35, 69, 86, 103, 137 ], + [ 1, 35, 69, 103, 137, 171 ], + [ 1, 35, 69, 86, 103, 137, 171 ] + ], + "defenderUnitsTight": [ + [ 100 ], + [ 83, 117 ], + [ 83, 100, 117 ], + [ 49, 83, 117, 151 ], + [ 49, 83, 100, 117, 151 ], + [ 15, 49, 83, 117, 151, 185 ], + [ 15, 49, 83, 100, 117, 151, 185 ] + ] + }, + // Configuration for creature banks with single-tile enemies + "creatureBankNarrow" : { + "tacticsAllowed" : false, + "obstaclesAllowed" : false, + "attackerCommander" : 95, + "defenderCommander" : 8, + "attackerUnits": [ 57, 61, 90, 93, 96, 125, 129 ], + "defenderUnits": [ 15, 185, 172, 2, 100, 87, 8 ] + }, + // Configuration for creature banks with double-wide enemies + "creatureBankWide" : { + "tacticsAllowed" : false, + "obstaclesAllowed" : false, + "attackerCommander" : 95, + "defenderCommander" : 8, + "attackerUnits": [ 57, 61, 90, 93, 96, 125, 129 ], + "defenderUnits": [ 15, 185, 171, 1, 100, 86, 8 ] + } + } + }, "creatures": { @@ -339,6 +433,7 @@ // if stack experience is on, creatures on map will get specified amount of experience daily "dailyStackExperience" : 100, // if enabled, double growth, plague and creature weeks can happen randomly. Has no effect on weeks by "Deity of Fire" + // NOTE: on HotA maps, this setting has no effect. Value provided in map will be used instead. "allowRandomSpecialWeeks" : true, // if enabled, every creature can get double growth month, ignoring predefined list "allowAllForDoubleMonth" : false @@ -393,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": { @@ -476,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/portraits.json b/config/heroes/portraits.json index d1fa147aa..dbe961f7a 100644 --- a/config/heroes/portraits.json +++ b/config/heroes/portraits.json @@ -203,4 +203,4 @@ "skills" : [], "specialty" : {} } -} \ No newline at end of file +} diff --git a/config/heroes/portraitsChronicles.json b/config/heroes/portraitsChronicles.json new file mode 100644 index 000000000..ec3a69c21 --- /dev/null +++ b/config/heroes/portraitsChronicles.json @@ -0,0 +1,176 @@ +{ + "portraitTarnumBarbarian" : // 163 + { + "class" : "barbarian", + "special" : true, + "images": { + "large" : "HPL137", + "small" : "HPS137", + "specialtySmall" : "default", + "specialtyLarge" : "default" + }, + "texts" : { + "name" : "", + "biography" : "", + "specialty" : { + "description" : "", + "tooltip" : "", + "name" : "" + } + }, + "army" : [ + { + "creature" : "goblin", + "min" : 1, + "max" : 1 + } + ], + "skills" : [], + "specialty" : {} + }, + "portraitTarnumKnight" : // 164 + { + "class" : "knight", + "special" : true, + "images": { + "large" : "HPL138", + "small" : "HPS138", + "specialtySmall" : "default", + "specialtyLarge" : "default" + }, + "texts" : { + "name" : "", + "biography" : "", + "specialty" : { + "description" : "", + "tooltip" : "", + "name" : "" + } + }, + "army" : [ + { + "creature" : "pikeman", + "min" : 1, + "max" : 1 + } + ], + "skills" : [], + "specialty" : {} + }, + "portraitTarnumWizard" : // 165 + { + "class" : "wizard", + "special" : true, + "images": { + "large" : "HPL139", + "small" : "HPS139", + "specialtySmall" : "default", + "specialtyLarge" : "default" + }, + "texts" : { + "name" : "", + "biography" : "", + "specialty" : { + "description" : "", + "tooltip" : "", + "name" : "" + } + }, + "army" : [ + { + "creature" : "enchanter", + "min" : 1, + "max" : 1 + } + ], + "skills" : [], + "specialty" : {} + }, + "portraitTarnumRanger" : // 166 + { + "class" : "ranger", + "special" : true, + "images": { + "large" : "HPL140", + "small" : "HPS140", + "specialtySmall" : "default", + "specialtyLarge" : "default" + }, + "texts" : { + "name" : "", + "biography" : "", + "specialty" : { + "description" : "", + "tooltip" : "", + "name" : "" + } + }, + "army" : [ + { + "creature" : "sharpshooter", + "min" : 1, + "max" : 1 + } + ], + "skills" : [], + "specialty" : {} + }, + "portraitTarnumOverlord" : // 167 + { + "class" : "overlord", + "special" : true, + "images": { + "large" : "HPL141", + "small" : "HPS141", + "specialtySmall" : "default", + "specialtyLarge" : "default" + }, + "texts" : { + "name" : "", + "biography" : "", + "specialty" : { + "description" : "", + "tooltip" : "", + "name" : "" + } + }, + "army" : [ + { + "creature" : "troglodyte", + "min" : 1, + "max" : 1 + } + ], + "skills" : [], + "specialty" : {} + }, + "portraitTarnumBeastmaster" : // 168 + { + "class" : "beastmaster", + "special" : true, + "images": { + "large" : "HPL142", + "small" : "HPS142", + "specialtySmall" : "default", + "specialtyLarge" : "default" + }, + "texts" : { + "name" : "", + "biography" : "", + "specialty" : { + "description" : "", + "tooltip" : "", + "name" : "" + } + }, + "army" : [ + { + "creature" : "gnoll", + "min" : 1, + "max" : 1 + } + ], + "skills" : [], + "specialty" : {} + } +} 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 ffb7a9c79..dc69e117b 100644 --- a/config/objects/creatureBanks.json +++ b/config/objects/creatureBanks.json @@ -1,7 +1,7 @@ { "creatureBank" : { "index" :16, - "handler": "bank", + "handler" : "configurable", "lastReservedIndex" : 6, "base" : { "sounds" : { @@ -21,106 +21,98 @@ "value" : 3000, "rarity" : 100 }, - "levels": [ + "onGuardedMessage" : 32, + "onVisitedMessage" : 33, + "visitMode" : "once", + "selectMode" : "selectFirst", + "guardsLayout" : "creatureBankNarrow", + "rewards" : [ { - "chance": 30, - "guards": [ - { "amount": 4, "type": "cyclop" }, - { "amount": 4, "type": "cyclop" }, - { "amount": 4, "type": "cyclop", "upgradeChance": 50 }, - { "amount": 4, "type": "cyclop" }, - { "amount": 4, "type": "cyclop" } + "message" : 34, + "appearChance" : { "min" : 0, "max" : 30 }, + "guards" : [ + { "amount" : 4, "type" : "cyclop" }, + { "amount" : 4, "type" : "cyclop" }, + { "amount" : 4, "type" : "cyclop", "upgradeChance" : 50 }, + { "amount" : 4, "type" : "cyclop" }, + { "amount" : 4, "type" : "cyclop" } ], - "combat_value": 506, - "reward" : { - "value": 10000, - "resources": - { - "wood" : 4, - "mercury" : 4, - "ore" : 4, - "sulfur" : 4, - "crystal" : 4, - "gems" : 4 - } + "resources" : + { + "wood" : 4, + "mercury" : 4, + "ore" : 4, + "sulfur" : 4, + "crystal" : 4, + "gems" : 4 } }, { - "chance": 30, - "guards": [ - { "amount": 6, "type": "cyclop" }, - { "amount": 6, "type": "cyclop" }, - { "amount": 6, "type": "cyclop", "upgradeChance": 50 }, - { "amount": 6, "type": "cyclop" }, - { "amount": 6, "type": "cyclop" } + "message" : 34, + "appearChance" : { "min" : 30, "max" : 60 }, + "guards" : [ + { "amount" : 6, "type" : "cyclop" }, + { "amount" : 6, "type" : "cyclop" }, + { "amount" : 6, "type" : "cyclop", "upgradeChance" : 50 }, + { "amount" : 6, "type" : "cyclop" }, + { "amount" : 6, "type" : "cyclop" } ], - "combat_value": 760, - "reward" : { - "value": 15000, - "resources": - { - "wood" : 6, - "mercury" : 6, - "ore" : 6, - "sulfur" : 6, - "crystal" : 6, - "gems" : 6 - } + "resources" : + { + "wood" : 6, + "mercury" : 6, + "ore" : 6, + "sulfur" : 6, + "crystal" : 6, + "gems" : 6 } }, { - "chance": 30, - "guards": [ - { "amount": 8, "type": "cyclop" }, - { "amount": 8, "type": "cyclop" }, - { "amount": 8, "type": "cyclop", "upgradeChance": 50 }, - { "amount": 8, "type": "cyclop" }, - { "amount": 8, "type": "cyclop" } + "message" : 34, + "appearChance" : { "min" : 60, "max" : 90 }, + "guards" : [ + { "amount" : 8, "type" : "cyclop" }, + { "amount" : 8, "type" : "cyclop" }, + { "amount" : 8, "type" : "cyclop", "upgradeChance" : 50 }, + { "amount" : 8, "type" : "cyclop" }, + { "amount" : 8, "type" : "cyclop" } ], - "combat_value": 1013, - "reward" : { - "value": 20000, - "resources": - { - "wood" : 8, - "mercury" : 8, - "ore" : 8, - "sulfur" : 8, - "crystal" : 8, - "gems" : 8 - } + "resources" : + { + "wood" : 8, + "mercury" : 8, + "ore" : 8, + "sulfur" : 8, + "crystal" : 8, + "gems" : 8 } }, { - "chance": 10, - "guards": [ - { "amount": 10, "type": "cyclop" }, - { "amount": 10, "type": "cyclop" }, - { "amount": 10, "type": "cyclop", "upgradeChance": 50 }, - { "amount": 10, "type": "cyclop" }, - { "amount": 10, "type": "cyclop" } + "message" : 34, + "appearChance" : { "min" : 90, "max" : 100 }, + "guards" : [ + { "amount" : 10, "type" : "cyclop" }, + { "amount" : 10, "type" : "cyclop" }, + { "amount" : 10, "type" : "cyclop", "upgradeChance" : 50 }, + { "amount" : 10, "type" : "cyclop" }, + { "amount" : 10, "type" : "cyclop" } ], - "combat_value": 1266, - "reward" : { - "value": 25000, - "resources": - { - "wood" : 10, - "mercury" : 10, - "ore" : 10, - "sulfur" : 10, - "crystal" : 10, - "gems" : 10 - } + "resources" : + { + "wood" : 10, + "mercury" : 10, + "ore" : 10, + "sulfur" : 10, + "crystal" : 10, + "gems" : 10 } } ] }, "dwarvenTreasury" : { "index" : 1, - "resetDuration" : 0, "name" : "Dwarven Treasury", "aiValue" : 2000, "sounds" : { @@ -130,88 +122,80 @@ "value" : 2000, "rarity" : 100 }, - "levels": [ + "onGuardedMessage" : 32, + "onVisitedMessage" : 33, + "visitMode" : "once", + "selectMode" : "selectFirst", + "guardsLayout" : "creatureBankNarrow", + "rewards" : [ { - "chance": 30, - "guards": [ - { "amount": 10, "type": "dwarf" }, - { "amount": 10, "type": "dwarf" }, - { "amount": 10, "type": "dwarf", "upgradeChance": 50 }, - { "amount": 10, "type": "dwarf" }, - { "amount": 10, "type": "dwarf" } + "message" : 34, + "appearChance" : { "min" : 0, "max" : 30 }, + "guards" : [ + { "amount" : 10, "type" : "dwarf" }, + { "amount" : 10, "type" : "dwarf" }, + { "amount" : 10, "type" : "dwarf", "upgradeChance" : 50 }, + { "amount" : 10, "type" : "dwarf" }, + { "amount" : 10, "type" : "dwarf" } ], - "combat_value": 194, - "reward" : { - "value": 3500, - "resources": - { - "crystal" : 2, - "gold" : 2500 - } + "resources" : + { + "crystal" : 2, + "gold" : 2500 } }, { - "chance": 30, - "guards": [ - { "amount": 15, "type": "dwarf" }, - { "amount": 15, "type": "dwarf" }, - { "amount": 15, "type": "dwarf", "upgradeChance": 50 }, - { "amount": 15, "type": "dwarf" }, - { "amount": 15, "type": "dwarf" } + "message" : 34, + "appearChance" : { "min" : 30, "max" : 60 }, + "guards" : [ + { "amount" : 15, "type" : "dwarf" }, + { "amount" : 15, "type" : "dwarf" }, + { "amount" : 15, "type" : "dwarf", "upgradeChance" : 50 }, + { "amount" : 15, "type" : "dwarf" }, + { "amount" : 15, "type" : "dwarf" } ], - "combat_value": 291, - "reward" : { - "value": 5500, - "resources": - { - "crystal" : 3, - "gold" : 4000 - } + "resources" : + { + "crystal" : 3, + "gold" : 4000 } }, { - "chance": 30, - "guards": [ - { "amount": 20, "type": "dwarf" }, - { "amount": 20, "type": "dwarf" }, - { "amount": 20, "type": "dwarf", "upgradeChance": 50 }, - { "amount": 20, "type": "dwarf" }, - { "amount": 20, "type": "dwarf" } + "message" : 34, + "appearChance" : { "min" : 60, "max" : 90 }, + "guards" : [ + { "amount" : 20, "type" : "dwarf" }, + { "amount" : 20, "type" : "dwarf" }, + { "amount" : 20, "type" : "dwarf", "upgradeChance" : 50 }, + { "amount" : 20, "type" : "dwarf" }, + { "amount" : 20, "type" : "dwarf" } ], - "combat_value": 388, - "reward" : { - "value": 7500, - "resources": - { - "crystal" : 5, - "gold" : 5000 - } + "resources" : + { + "crystal" : 5, + "gold" : 5000 } }, { - "chance": 10, - "guards": [ - { "amount": 30, "type": "dwarf" }, - { "amount": 30, "type": "dwarf" }, - { "amount": 30, "type": "dwarf", "upgradeChance": 50 }, - { "amount": 30, "type": "dwarf" }, - { "amount": 30, "type": "dwarf" } + "message" : 34, + "appearChance" : { "min" : 90, "max" : 100 }, + "guards" : [ + { "amount" : 30, "type" : "dwarf" }, + { "amount" : 30, "type" : "dwarf" }, + { "amount" : 30, "type" : "dwarf", "upgradeChance" : 50 }, + { "amount" : 30, "type" : "dwarf" }, + { "amount" : 30, "type" : "dwarf" } ], - "combat_value": 582, - "reward" : { - "value": 12500, - "resources": - { - "crystal" : 10, - "gold" : 7500 - } + "resources" : + { + "crystal" : 10, + "gold" : 7500 } } ] }, "griffinConservatory" : { "index" : 2, - "resetDuration" : 0, "name" : "Griffin Conservatory", "aiValue" : 9000, "sounds" : { @@ -221,72 +205,65 @@ "value" : 2000, "rarity" : 100 }, - "levels": [ + "onGuardedMessage" : 32, + "onVisitedMessage" : 33, + "visitMode" : "once", + "selectMode" : "selectFirst", + "guardsLayout" : "creatureBankWide", + "rewards" : [ { - "chance": 30, - "guards": [ - { "amount": 10, "type": "griffin" }, - { "amount": 10, "type": "griffin" }, - { "amount": 10, "type": "griffin", "upgradeChance": 50 }, - { "amount": 10, "type": "griffin" }, - { "amount": 10, "type": "griffin" } + "message" : 34, + "appearChance" : { "min" : 0, "max" : 30 }, + "guards" : [ + { "amount" : 10, "type" : "griffin" }, + { "amount" : 10, "type" : "griffin" }, + { "amount" : 10, "type" : "griffin", "upgradeChance" : 50 }, + { "amount" : 10, "type" : "griffin" }, + { "amount" : 10, "type" : "griffin" } ], - "combat_value": 351, - "reward" : { - "value": 3000, - "creatures": [ { "amount": 1, "type": "angel" } ] - } + "creatures" : [ { "amount" : 1, "type" : "angel" } ] }, { - "chance": 30, - "guards": [ - { "amount": 20, "type": "griffin" }, - { "amount": 20, "type": "griffin" }, - { "amount": 20, "type": "griffin", "upgradeChance": 50 }, - { "amount": 20, "type": "griffin" }, - { "amount": 20, "type": "griffin" } + "message" : 34, + "appearChance" : { "min" : 30, "max" : 60 }, + "guards" : [ + { "amount" : 20, "type" : "griffin" }, + { "amount" : 20, "type" : "griffin" }, + { "amount" : 20, "type" : "griffin", "upgradeChance" : 50 }, + { "amount" : 20, "type" : "griffin" }, + { "amount" : 20, "type" : "griffin" } ], - "combat_value": 702, - "reward" : { - "value": 6000, - "creatures": [ { "amount": 2, "type": "angel" } ] - } + "creatures" : [ { "amount" : 2, "type" : "angel" } ] }, { - "chance": 30, - "guards": [ - { "amount": 30, "type": "griffin" }, - { "amount": 30, "type": "griffin" }, - { "amount": 30, "type": "griffin", "upgradeChance": 50 }, - { "amount": 30, "type": "griffin" }, - { "amount": 30, "type": "griffin" } + "message" : 34, + "appearChance" : { "min" : 60, "max" : 90 }, + "guards" : [ + { "amount" : 30, "type" : "griffin" }, + { "amount" : 30, "type" : "griffin" }, + { "amount" : 30, "type" : "griffin", "upgradeChance" : 50 }, + { "amount" : 30, "type" : "griffin" }, + { "amount" : 30, "type" : "griffin" } ], - "combat_value": 1053, - "reward" : { - "value": 9000, - "creatures": [ { "amount": 3, "type": "angel" } ] - } + "creatures" : [ { "amount" : 3, "type" : "angel" } ] }, { - "chance": 10, - "guards": [ - { "amount": 40, "type": "griffin" }, - { "amount": 40, "type": "griffin" }, - { "amount": 40, "type": "griffin", "upgradeChance": 50 }, - { "amount": 40, "type": "griffin" }, - { "amount": 40, "type": "griffin" } + "message" : 34, + "appearChance" : { "min" : 90, "max" : 100 }, + "guards" : [ + { "amount" : 40, "type" : "griffin" }, + { "amount" : 40, "type" : "griffin" }, + { "amount" : 40, "type" : "griffin", "upgradeChance" : 50 }, + { "amount" : 40, "type" : "griffin" }, + { "amount" : 40, "type" : "griffin" } ], - "combat_value": 1404, - "reward" : { - "value": 12000, - "creatures": [ { "amount": 4, "type": "angel" } ] - } + "creatures" : [ { "amount" : 4, "type" : "angel" } ] } ] }, - "inpCache" : { + "impCache" : { + "compatibilityIdentifiers" : [ "inpCache" ], "index" : 3, - "resetDuration" : 0, "name" : "Imp Cache", "aiValue" : 1500, "sounds" : { @@ -296,87 +273,79 @@ "value" : 5000, "rarity" : 100 }, - "levels": [ + "onGuardedMessage" : 32, + "onVisitedMessage" : 33, + "visitMode" : "once", + "selectMode" : "selectFirst", + "guardsLayout" : "creatureBankNarrow", + "rewards" : [ { - "chance": 30, - "guards": [ - { "amount": 20, "type": "imp" }, - { "amount": 20, "type": "imp" }, - { "amount": 20, "type": "imp", "upgradeChance": 50 }, - { "amount": 20, "type": "imp" }, - { "amount": 20, "type": "imp" } + "message" : 34, + "appearChance" : { "min" : 0, "max" : 30 }, + "guards" : [ + { "amount" : 20, "type" : "imp" }, + { "amount" : 20, "type" : "imp" }, + { "amount" : 20, "type" : "imp", "upgradeChance" : 50 }, + { "amount" : 20, "type" : "imp" }, + { "amount" : 20, "type" : "imp" } ], - "combat_value": 100, - "reward" : { - "value": 2000, - "resources": - { - "gold" : 1000 - } + "resources" : + { + "gold" : 1000 } }, { - "chance": 30, - "guards": [ - { "amount": 30, "type": "imp" }, - { "amount": 30, "type": "imp" }, - { "amount": 30, "type": "imp", "upgradeChance": 50 }, - { "amount": 30, "type": "imp" }, - { "amount": 30, "type": "imp" } + "message" : 34, + "appearChance" : { "min" : 30, "max" : 60 }, + "guards" : [ + { "amount" : 30, "type" : "imp" }, + { "amount" : 30, "type" : "imp" }, + { "amount" : 30, "type" : "imp", "upgradeChance" : 50 }, + { "amount" : 30, "type" : "imp" }, + { "amount" : 30, "type" : "imp" } ], - "combat_value": 150, - "reward" : { - "value": 3000, - "resources": - { - "mercury" : 3, - "gold" : 1500 - } + "resources" : + { + "mercury" : 3, + "gold" : 1500 } }, { - "chance": 30, - "guards": [ - { "amount": 40, "type": "imp" }, - { "amount": 40, "type": "imp" }, - { "amount": 40, "type": "imp", "upgradeChance": 50 }, - { "amount": 40, "type": "imp" }, - { "amount": 40, "type": "imp" } + "message" : 34, + "appearChance" : { "min" : 60, "max" : 90 }, + "guards" : [ + { "amount" : 40, "type" : "imp" }, + { "amount" : 40, "type" : "imp" }, + { "amount" : 40, "type" : "imp", "upgradeChance" : 50 }, + { "amount" : 40, "type" : "imp" }, + { "amount" : 40, "type" : "imp" } ], - "combat_value": 200, - "reward" : { - "value": 4000, - "resources": - { - "mercury" : 4, - "gold" : 2000 - } + "resources" : + { + "mercury" : 4, + "gold" : 2000 } }, { - "chance": 10, - "guards": [ - { "amount": 60, "type": "imp" }, - { "amount": 60, "type": "imp" }, - { "amount": 60, "type": "imp", "upgradeChance": 50 }, - { "amount": 60, "type": "imp" }, - { "amount": 60, "type": "imp" } + "message" : 34, + "appearChance" : { "min" : 90, "max" : 100 }, + "guards" : [ + { "amount" : 60, "type" : "imp" }, + { "amount" : 60, "type" : "imp" }, + { "amount" : 60, "type" : "imp", "upgradeChance" : 50 }, + { "amount" : 60, "type" : "imp" }, + { "amount" : 60, "type" : "imp" } ], - "combat_value": 300, - "reward" : { - "value": 6000, - "resources": - { - "mercury" : 6, - "gold" : 3000 - } + "resources" : + { + "mercury" : 6, + "gold" : 3000 } } ] }, "medusaStore" : { "index" : 4, - "resetDuration" : 0, "name" : "Medusa Stores", "aiValue" : 1500, "sounds" : { @@ -386,88 +355,80 @@ "value" : 1500, "rarity" : 100 }, - "levels": [ + "onGuardedMessage" : 32, + "onVisitedMessage" : 33, + "visitMode" : "once", + "selectMode" : "selectFirst", + "guardsLayout" : "creatureBankWide", + "rewards" : [ { - "chance": 30, - "guards": [ - { "amount": 4, "type": "medusa" }, - { "amount": 4, "type": "medusa" }, - { "amount": 4, "type": "medusa", "upgradeChance": 50 }, - { "amount": 4, "type": "medusa" }, - { "amount": 4, "type": "medusa" } + "message" : 34, + "appearChance" : { "min" : 0, "max" : 30 }, + "guards" : [ + { "amount" : 4, "type" : "medusa" }, + { "amount" : 4, "type" : "medusa" }, + { "amount" : 4, "type" : "medusa", "upgradeChance" : 50 }, + { "amount" : 4, "type" : "medusa" }, + { "amount" : 4, "type" : "medusa" } ], - "combat_value": 207, - "reward" : { - "value": 4500, - "resources": - { - "sulfur" : 5, - "gold" : 2000 - } + "resources" : + { + "sulfur" : 5, + "gold" : 2000 } }, { - "chance": 30, - "guards": [ - { "amount": 6, "type": "medusa" }, - { "amount": 6, "type": "medusa" }, - { "amount": 6, "type": "medusa", "upgradeChance": 50 }, - { "amount": 6, "type": "medusa" }, - { "amount": 6, "type": "medusa" } + "message" : 34, + "appearChance" : { "min" : 30, "max" : 60 }, + "guards" : [ + { "amount" : 6, "type" : "medusa" }, + { "amount" : 6, "type" : "medusa" }, + { "amount" : 6, "type" : "medusa", "upgradeChance" : 50 }, + { "amount" : 6, "type" : "medusa" }, + { "amount" : 6, "type" : "medusa" } ], - "combat_value": 310, - "reward" : { - "value": 6000, - "resources": - { - "sulfur" : 6, - "gold" : 3000 - } + "resources" : + { + "sulfur" : 6, + "gold" : 3000 } }, { - "chance": 30, - "guards": [ - { "amount": 8, "type": "medusa" }, - { "amount": 8, "type": "medusa" }, - { "amount": 8, "type": "medusa", "upgradeChance": 50 }, - { "amount": 8, "type": "medusa" }, - { "amount": 8, "type": "medusa" } + "message" : 34, + "appearChance" : { "min" : 60, "max" : 90 }, + "guards" : [ + { "amount" : 8, "type" : "medusa" }, + { "amount" : 8, "type" : "medusa" }, + { "amount" : 8, "type" : "medusa", "upgradeChance" : 50 }, + { "amount" : 8, "type" : "medusa" }, + { "amount" : 8, "type" : "medusa" } ], - "combat_value": 414, - "reward" : { - "value": 8000, - "resources": - { - "sulfur" : 8, - "gold" : 4000 - } + "resources" : + { + "sulfur" : 8, + "gold" : 4000 } }, { - "chance": 10, - "guards": [ - { "amount": 10, "type": "medusa" }, - { "amount": 10, "type": "medusa" }, - { "amount": 10, "type": "medusa", "upgradeChance": 50 }, - { "amount": 10, "type": "medusa" }, - { "amount": 10, "type": "medusa" } + "message" : 34, + "appearChance" : { "min" : 90, "max" : 100 }, + "guards" : [ + { "amount" : 10, "type" : "medusa" }, + { "amount" : 10, "type" : "medusa" }, + { "amount" : 10, "type" : "medusa", "upgradeChance" : 50 }, + { "amount" : 10, "type" : "medusa" }, + { "amount" : 10, "type" : "medusa" } ], - "combat_value": 517, - "reward" : { - "value": 10000, - "resources": - { - "sulfur" : 10, - "gold" : 5000 - } + "resources" : + { + "sulfur" : 10, + "gold" : 5000 } } ] }, "nagaBank" : { "index" : 5, - "resetDuration" : 0, "name" : "Naga Bank", "aiValue" : 3000, "sounds" : { @@ -477,88 +438,80 @@ "value" : 3000, "rarity" : 100 }, - "levels": [ + "onGuardedMessage" : 32, + "onVisitedMessage" : 33, + "visitMode" : "once", + "selectMode" : "selectFirst", + "guardsLayout" : "creatureBankWide", + "rewards" : [ { - "chance": 30, - "guards": [ - { "amount": 2, "type": "naga" }, - { "amount": 2, "type": "naga" }, - { "amount": 2, "type": "naga", "upgradeChance": 50 }, - { "amount": 2, "type": "naga" }, - { "amount": 2, "type": "naga" } + "message" : 34, + "appearChance" : { "min" : 0, "max" : 30 }, + "guards" : [ + { "amount" : 2, "type" : "naga" }, + { "amount" : 2, "type" : "naga" }, + { "amount" : 2, "type" : "naga", "upgradeChance" : 50 }, + { "amount" : 2, "type" : "naga" }, + { "amount" : 2, "type" : "naga" } ], - "combat_value": 403, - "reward" : { - "value": 8000, - "resources": - { - "gems" : 8, - "gold" : 4000 - } + "resources" : + { + "gems" : 8, + "gold" : 4000 } }, { - "chance": 30, - "guards": [ - { "amount": 3, "type": "naga" }, - { "amount": 3, "type": "naga" }, - { "amount": 3, "type": "naga", "upgradeChance": 50 }, - { "amount": 3, "type": "naga" }, - { "amount": 3, "type": "naga" } + "message" : 34, + "appearChance" : { "min" : 30, "max" : 60 }, + "guards" : [ + { "amount" : 3, "type" : "naga" }, + { "amount" : 3, "type" : "naga" }, + { "amount" : 3, "type" : "naga", "upgradeChance" : 50 }, + { "amount" : 3, "type" : "naga" }, + { "amount" : 3, "type" : "naga" } ], - "combat_value": 605, - "reward" : { - "value": 12000, - "resources": - { - "gems" : 12, - "gold" : 6000 - } + "resources" : + { + "gems" : 12, + "gold" : 6000 } }, { - "chance": 30, - "guards": [ - { "amount": 4, "type": "naga" }, - { "amount": 4, "type": "naga" }, - { "amount": 4, "type": "naga", "upgradeChance": 50 }, - { "amount": 4, "type": "naga" }, - { "amount": 4, "type": "naga" } + "message" : 34, + "appearChance" : { "min" : 60, "max" : 90 }, + "guards" : [ + { "amount" : 4, "type" : "naga" }, + { "amount" : 4, "type" : "naga" }, + { "amount" : 4, "type" : "naga", "upgradeChance" : 50 }, + { "amount" : 4, "type" : "naga" }, + { "amount" : 4, "type" : "naga" } ], - "combat_value": 806, - "reward" : { - "value": 16000, - "resources": - { - "gems" : 16, - "gold" : 8000 - } + "resources" : + { + "gems" : 16, + "gold" : 8000 } }, { - "chance": 10, - "guards": [ - { "amount": 6, "type": "naga" }, - { "amount": 6, "type": "naga" }, - { "amount": 6, "type": "naga", "upgradeChance": 50 }, - { "amount": 6, "type": "naga" }, - { "amount": 6, "type": "naga" } + "message" : 34, + "appearChance" : { "min" : 90, "max" : 100 }, + "guards" : [ + { "amount" : 6, "type" : "naga" }, + { "amount" : 6, "type" : "naga" }, + { "amount" : 6, "type" : "naga", "upgradeChance" : 50 }, + { "amount" : 6, "type" : "naga" }, + { "amount" : 6, "type" : "naga" } ], - "combat_value": 1210, - "reward" : { - "value": 24000, - "resources": - { - "gems" : 24, - "gold" : 12000 - } + "resources" : + { + "gems" : 24, + "gold" : 12000 } } ] }, "dragonFlyHive" : { "index" : 6, - "resetDuration" : 0, "name" : "Dragon Fly Hive", "aiValue" : 9000, "sounds" : { @@ -568,66 +521,59 @@ "value" : 9000, "rarity" : 100 }, - "levels": [ + "onGuardedMessage" : 32, + "onVisitedMessage" : 33, + "visitMode" : "once", + "selectMode" : "selectFirst", + "guardsLayout" : "creatureBankNarrow", + "rewards" : [ { - "chance": 30, - "guards": [ - { "amount": 6, "type": "fireDragonFly" }, - { "amount": 6, "type": "fireDragonFly" }, - { "amount": 6, "type": "fireDragonFly" }, - { "amount": 6, "type": "fireDragonFly" }, - { "amount": 6, "type": "fireDragonFly" } + "message" : 34, + "appearChance" : { "min" : 0, "max" : 30 }, + "guards" : [ + { "amount" : 6, "type" : "fireDragonFly" }, + { "amount" : 6, "type" : "fireDragonFly" }, + { "amount" : 6, "type" : "fireDragonFly" }, + { "amount" : 6, "type" : "fireDragonFly" }, + { "amount" : 6, "type" : "fireDragonFly" } ], - "combat_value": 154, - "reward" : { - "value": 3200, - "creatures": [ { "amount": 4, "type": "wyvern" } ] - } + "creatures" : [ { "amount" : 4, "type" : "wyvern" } ] }, { - "chance": 30, - "guards": [ - { "amount": 9, "type": "fireDragonFly" }, - { "amount": 9, "type": "fireDragonFly" }, - { "amount": 9, "type": "fireDragonFly" }, - { "amount": 9, "type": "fireDragonFly" }, - { "amount": 9, "type": "fireDragonFly" } + "message" : 34, + "appearChance" : { "min" : 30, "max" : 60 }, + "guards" : [ + { "amount" : 9, "type" : "fireDragonFly" }, + { "amount" : 9, "type" : "fireDragonFly" }, + { "amount" : 9, "type" : "fireDragonFly" }, + { "amount" : 9, "type" : "fireDragonFly" }, + { "amount" : 9, "type" : "fireDragonFly" } ], - "combat_value": 230, - "reward" : { - "value": 4800, - "creatures": [ { "amount": 6, "type": "wyvern" } ] - } + "creatures" : [ { "amount" : 6, "type" : "wyvern" } ] }, { - "chance": 30, - "guards": [ - { "amount": 12, "type": "fireDragonFly" }, - { "amount": 12, "type": "fireDragonFly" }, - { "amount": 12, "type": "fireDragonFly" }, - { "amount": 12, "type": "fireDragonFly" }, - { "amount": 12, "type": "fireDragonFly" } + "message" : 34, + "appearChance" : { "min" : 60, "max" : 90 }, + "guards" : [ + { "amount" : 12, "type" : "fireDragonFly" }, + { "amount" : 12, "type" : "fireDragonFly" }, + { "amount" : 12, "type" : "fireDragonFly" }, + { "amount" : 12, "type" : "fireDragonFly" }, + { "amount" : 12, "type" : "fireDragonFly" } ], - "combat_value": 307, - "reward" : { - "value": 6400, - "creatures": [ { "amount": 8, "type": "wyvern" } ] - } + "creatures" : [ { "amount" : 8, "type" : "wyvern" } ] }, { - "chance": 10, - "guards": [ - { "amount": 18, "type": "fireDragonFly" }, - { "amount": 18, "type": "fireDragonFly" }, - { "amount": 18, "type": "fireDragonFly" }, - { "amount": 18, "type": "fireDragonFly" }, - { "amount": 18, "type": "fireDragonFly" } + "message" : 34, + "appearChance" : { "min" : 90, "max" : 100 }, + "guards" : [ + { "amount" : 18, "type" : "fireDragonFly" }, + { "amount" : 18, "type" : "fireDragonFly" }, + { "amount" : 18, "type" : "fireDragonFly" }, + { "amount" : 18, "type" : "fireDragonFly" }, + { "amount" : 18, "type" : "fireDragonFly" } ], - "combat_value": 461, - "reward" : { - "value": 9600, - "creatures": [ { "amount": 12, "type": "wyvern" } ] - } + "creatures" : [ { "amount" : 12, "type" : "wyvern" } ] } ] } @@ -635,7 +581,7 @@ }, "shipwreck" : { "index" :85, - "handler": "bank", + "handler" : "configurable", "base" : { "sounds" : { "visit" : ["ROGUE"] @@ -644,7 +590,7 @@ "types" : { "shipwreck" : { "index" : 0, - "resetDuration" : 0, + "blockedVisitable" : true, "coastVisitable" : true, "name" : "Shipwreck", @@ -653,80 +599,78 @@ "value" : 2000, "rarity" : 100 }, - "levels": [ + "visitMode" : "once", + "selectMode" : "selectFirst", + "onGuardedMessage" : 122, + "onVisited" : [ { - "chance": 30, - "guards": [ - { "amount": 2, "type": "wight" }, - { "amount": 2, "type": "wight" }, - { "amount": 2, "type": "wight" }, - { "amount": 2, "type": "wight" }, - { "amount": 2, "type": "wight" } + "message" : 123, // Such a despicable act reduces your army's morale. + "bonuses" : [ { "type" : "MORALE", "val" : -1, "duration" : "ONE_BATTLE", "description" : 99 } ] + } + ], + "guardsLayout" : "creatureBankNarrow", + "rewards" : [ + { + "message" : 34, + "appearChance" : { "min" : 0, "max" : 30 }, + "guards" : [ + { "amount" : 2, "type" : "wight" }, + { "amount" : 2, "type" : "wight" }, + { "amount" : 2, "type" : "wight" }, + { "amount" : 2, "type" : "wight" }, + { "amount" : 2, "type" : "wight" } ], - "combat_value": 31, - "reward" : { - "value": 2000, - "resources": - { - "gold" : 2000 - } + "resources" : + { + "gold" : 2000 } }, { - "chance": 30, - "guards": [ - { "amount": 3, "type": "wight" }, - { "amount": 3, "type": "wight" }, - { "amount": 3, "type": "wight" }, - { "amount": 3, "type": "wight" }, - { "amount": 3, "type": "wight" } + "message" : 34, + "appearChance" : { "min" : 30, "max" : 60 }, + "guards" : [ + { "amount" : 3, "type" : "wight" }, + { "amount" : 3, "type" : "wight" }, + { "amount" : 3, "type" : "wight" }, + { "amount" : 3, "type" : "wight" }, + { "amount" : 3, "type" : "wight" } ], - "combat_value": 46, - "reward" : { - "value": 3000, - "resources": - { - "gold" : 3000 - } + "resources" : + { + "gold" : 3000 } }, { - "chance": 30, - "guards": [ - { "amount": 5, "type": "wight" }, - { "amount": 5, "type": "wight" }, - { "amount": 5, "type": "wight" }, - { "amount": 5, "type": "wight" }, - { "amount": 5, "type": "wight" } + "message" : 34, + "appearChance" : { "min" : 60, "max" : 90 }, + "guards" : [ + { "amount" : 5, "type" : "wight" }, + { "amount" : 5, "type" : "wight" }, + { "amount" : 5, "type" : "wight" }, + { "amount" : 5, "type" : "wight" }, + { "amount" : 5, "type" : "wight" } ], - "combat_value": 77, - "reward" : { - "value": 5000, - "resources": - { - "gold" : 4000 - }, - "artifacts": [ { "class" : "TREASURE" } ] - } + "resources" : + { + "gold" : 4000 + }, + "artifacts" : [ { "class" : "TREASURE" } ] }, { - "chance": 10, - "guards": [ - { "amount": 10, "type": "wight" }, - { "amount": 10, "type": "wight" }, - { "amount": 10, "type": "wight" }, - { "amount": 10, "type": "wight" }, - { "amount": 10, "type": "wight" } + "message" : 34, + "appearChance" : { "min" : 90, "max" : 100 }, + "guards" : [ + { "amount" : 10, "type" : "wight" }, + { "amount" : 10, "type" : "wight" }, + { "amount" : 10, "type" : "wight" }, + { "amount" : 10, "type" : "wight" }, + { "amount" : 10, "type" : "wight" } ], - "combat_value": 154, - "reward" : { - "value": 7000, - "resources": - { - "gold" : 5000 - }, - "artifacts": [ { "class" : "MINOR" } ] - } + "resources" : + { + "gold" : 5000 + }, + "artifacts" : [ { "class" : "MINOR" } ] } ] } @@ -734,7 +678,7 @@ }, "derelictShip" : { "index" :24, - "handler": "bank", + "handler" : "configurable", "base" : { "sounds" : { "visit" : ["ROGUE"] @@ -743,7 +687,7 @@ "types" : { "derelictShip" : { "index" : 0, - "resetDuration" : 0, + "blockedVisitable" : true, "name" : "Derelict Ship", "aiValue" : 4000, @@ -751,81 +695,76 @@ "value" : 4000, "rarity" : 20 }, - "levels": [ + "visitMode" : "once", + "selectMode" : "selectFirst", + "onGuardedMessage" : 41, + "onVisited" : [ { - "chance": 30, - "guards": [ - { "amount": 4, "type": "waterElemental" }, - { "amount": 4, "type": "waterElemental" }, - { "amount": 4, "type": "waterElemental" }, - { "amount": 4, "type": "waterElemental" }, - { "amount": 4, "type": "waterElemental" } + "message" : 42, // Such a despicable act reduces your army's morale. + "bonuses" : [ { "type" : "MORALE", "val" : -1, "duration" : "ONE_BATTLE", "description" : 101 } ] + } + ], + "guardsLayout" : "creatureBankWide", + "rewards" : [ + { + "message" : 43, + "appearChance" : { "min" : 0, "max" : 30 }, + "guards" : [ + { "amount" : 4, "type" : "waterElemental" }, + { "amount" : 4, "type" : "waterElemental" }, + { "amount" : 4, "type" : "waterElemental" }, + { "amount" : 4, "type" : "waterElemental" }, + { "amount" : 4, "type" : "waterElemental" } ], - "combat_value": 138, - "reward" : { - "value": 3000, - "resources": - { - "gold" : 3000 - } + "resources" : + { + "gold" : 3000 } }, { - "chance": 30, - "guards": [ - { "amount": 6, "type": "waterElemental" }, - { "amount": 6, "type": "waterElemental" }, - { "amount": 6, "type": "waterElemental" }, - { "amount": 6, "type": "waterElemental" }, - { "amount": 6, "type": "waterElemental" } + "message" : 43, + "appearChance" : { "min" : 30, "max" : 60 }, + "guards" : [ + { "amount" : 6, "type" : "waterElemental" }, + { "amount" : 6, "type" : "waterElemental" }, + { "amount" : 6, "type" : "waterElemental" }, + { "amount" : 6, "type" : "waterElemental" }, + { "amount" : 6, "type" : "waterElemental" } ], - "combat_value": 207, - "reward" : { - "value": 4000, - "resources": - { - "gold" : 3000 - }, - "artifacts": [ { "class" : "TREASURE" } ] - } + "resources" : { + "gold" : 3000 + }, + "artifacts" : [ { "class" : "TREASURE" } ] }, { - "chance": 30, - "guards": [ - { "amount": 8, "type": "waterElemental" }, - { "amount": 8, "type": "waterElemental" }, - { "amount": 8, "type": "waterElemental" }, - { "amount": 8, "type": "waterElemental" }, - { "amount": 8, "type": "waterElemental" } + "message" : 43, + "appearChance" : { "min" : 60, "max" : 90 }, + "guards" : [ + { "amount" : 8, "type" : "waterElemental" }, + { "amount" : 8, "type" : "waterElemental" }, + { "amount" : 8, "type" : "waterElemental" }, + { "amount" : 8, "type" : "waterElemental" }, + { "amount" : 8, "type" : "waterElemental" } ], - "combat_value": 276, - "reward" : { - "value": 5000, - "resources": - { - "gold" : 4000 - }, - "artifacts": [ { "class" : "TREASURE" } ] - } + "resources" : { + "gold" : 4000 + }, + "artifacts" : [ { "class" : "TREASURE" } ] }, { - "chance": 10, - "guards": [ - { "amount": 12, "type": "waterElemental" }, - { "amount": 12, "type": "waterElemental" }, - { "amount": 12, "type": "waterElemental" }, - { "amount": 12, "type": "waterElemental" }, - { "amount": 12, "type": "waterElemental" } + "message" : 43, + "appearChance" : { "min" : 90, "max" : 100 }, + "guards" : [ + { "amount" : 12, "type" : "waterElemental" }, + { "amount" : 12, "type" : "waterElemental" }, + { "amount" : 12, "type" : "waterElemental" }, + { "amount" : 12, "type" : "waterElemental" }, + { "amount" : 12, "type" : "waterElemental" } ], - "combat_value": 414, - "reward" : { - "value": 8000, - "resources": - { - "gold" : 6000 - }, - "artifacts": [ { "class" : "MINOR" } ] - } + "resources" : { + "gold" : 6000 + }, + "artifacts" : [ { "class" : "MINOR" } ] } ] } @@ -833,7 +772,7 @@ }, "crypt" : { "index" :84, - "handler": "bank", + "handler" : "configurable", "base" : { "sounds" : { "ambient" : ["LOOPDEAD"], @@ -843,85 +782,84 @@ "types" : { "crypt" : { "index" : 0, - "resetDuration" : 0, "name" : "Crypt", "aiValue" : 1500, + "rmg" : { "value" : 1000, "rarity" : 100 }, - "levels": [ + + "visitMode" : "once", + "selectMode" : "selectFirst", + "onGuardedMessage" : 119, // Do you want to search the graves? + "onVisited" : [ { - "chance": 30, - "guards": [ - { "amount": 10, "type": "skeleton" }, - { "amount": 10, "type": "walkingDead" }, - { "amount": 10, "type": "walkingDead" }, - { "amount": 10, "type": "skeleton" }, - { "amount": 10, "type": "skeleton" } + "message" : 120, // Such a despicable act reduces your army's morale. + "bonuses" : [ { "type" : "MORALE", "val" : -1, "duration" : "ONE_BATTLE" } ] + } + ], + "guardsLayout" : "creatureBankNarrow", + "rewards" : [ + { + "appearChance" : { "min" : 0, "max" : 30 }, + "guards" : [ + { "amount" : 10, "type" : "skeleton" }, + { "amount" : 10, "type" : "walkingDead" }, + { "amount" : 10, "type" : "walkingDead" }, + { "amount" : 10, "type" : "skeleton" }, + { "amount" : 10, "type" : "skeleton" } ], - "combat_value": 75, - "reward" : { - "value": 1500, - "resources": - { - "gold" : 1500 - } + "message" : 121, // you search the graves and find something + "resources" : + { + "gold" : 1500 } }, { - "chance": 30, - "guards": [ - { "amount": 13, "type": "skeleton" }, - { "amount": 10, "type": "walkingDead" }, - { "amount": 5, "type": "wight" }, - { "amount": 10, "type": "walkingDead" }, - { "amount": 12, "type": "skeleton" } + "appearChance" : { "min" : 30, "max" : 60 }, + "guards" : [ + { "amount" : 13, "type" : "skeleton" }, + { "amount" : 10, "type" : "walkingDead" }, + { "amount" : 5, "type" : "wight" }, + { "amount" : 10, "type" : "walkingDead" }, + { "amount" : 12, "type" : "skeleton" } ], - "combat_value": 94, - "reward" : { - "value": 2000, - "resources": - { - "gold" : 2000 - } + "message" : 121, // you search the graves and find something + "resources" : + { + "gold" : 2000 } }, { - "chance": 30, - "guards": [ - { "amount": 20, "type": "skeleton" }, - { "amount": 20, "type": "walkingDead" }, - { "amount": 10, "type": "wight" }, - { "amount": 5, "type": "vampire" } + "appearChance" : { "min" : 60, "max" : 90 }, + "guards" : [ + { "amount" : 20, "type" : "skeleton" }, + { "amount" : 20, "type" : "walkingDead" }, + { "amount" : 10, "type" : "wight" }, + { "amount" : 5, "type" : "vampire" } ], - "combat_value": 169, - "reward" : { - "value": 3500, - "resources": - { - "gold" : 2500 - }, - "artifacts": [ { "class" : "TREASURE" } ] - } + "message" : 121, // you search the graves and find something + "resources" : + { + "gold" : 2500 + }, + "artifacts" : [ { "class" : "TREASURE" } ] }, { - "chance": 10, - "guards": [ - { "amount": 20, "type": "skeleton" }, - { "amount": 20, "type": "walkingDead" }, - { "amount": 10, "type": "wight" }, - { "amount": 10, "type": "vampire" } + "appearChance" : { "min" : 90, "max" : 100 }, + "guards" : [ + { "amount" : 20, "type" : "skeleton" }, + { "amount" : 20, "type" : "walkingDead" }, + { "amount" : 10, "type" : "wight" }, + { "amount" : 10, "type" : "vampire" } ], - "combat_value": 225, - "reward" : { - "value": 6000, - "resources": - { - "gold" : 5000 - }, - "artifacts": [ { "class" : "TREASURE" } ] - } + "message" : 121, // you search the graves and find something + "resources" : + { + "gold" : 5000 + }, + "artifacts" : [ { "class" : "TREASURE" } ] } ] } @@ -929,7 +867,7 @@ }, "dragonUtopia" : { "index" :25, - "handler": "bank", + "handler" : "configurable", "base" : { "sounds" : { "ambient" : ["LOOPDRAG"], @@ -939,141 +877,99 @@ "types" : { "dragonUtopia" : { "index" : 0, - "resetDuration" : 0, + "name" : "Dragon Utopia", "aiValue" : 11000, "rmg" : { "value" : 10000, "rarity" : 100 }, - "levels": [ + + "visitMode" : "once", + "selectMode" : "selectFirst", + "onGuardedMessage" : 47, + "onVisitedMessage" : 33, + "guardsLayout" : "creatureBankWide", + "rewards" : [ { - "chance": 30, - "guards": [ - { "amount": 8, "type": "greenDragon" }, - { "amount": 5, "type": "redDragon" }, - { "amount": 2, "type": "goldDragon" }, - { "amount": 1, "type": "blackDragon" } + "message" : 34, + "appearChance" : { "min" : 0, "max" : 30 }, + "guards" : [ + { "amount" : 8, "type" : "greenDragon" }, + { "amount" : 5, "type" : "redDragon" }, + { "amount" : 2, "type" : "goldDragon" }, + { "amount" : 1, "type" : "blackDragon" } ], - "combat_value": 769, - "reward" : { - "value": 38000, - "resources": - { - "gold" : 20000 - }, - "artifacts": [ - { "class" : "TREASURE" }, - { "class" : "MINOR" }, - { "class" : "MAJOR" }, - { "class" : "RELIC" } - ] - } + "resources" : + { + "gold" : 20000 + }, + "artifacts" : [ + { "class" : "TREASURE" }, + { "class" : "MINOR" }, + { "class" : "MAJOR" }, + { "class" : "RELIC" } + ] }, { - "chance": 30, - "guards": [ - { "amount": 8, "type": "greenDragon" }, - { "amount": 6, "type": "redDragon" }, - { "amount": 3, "type": "goldDragon" }, - { "amount": 2, "type": "blackDragon" } + "message" : 34, + "appearChance" : { "min" : 30, "max" : 60 }, + "guards" : [ + { "amount" : 8, "type" : "greenDragon" }, + { "amount" : 6, "type" : "redDragon" }, + { "amount" : 3, "type" : "goldDragon" }, + { "amount" : 2, "type" : "blackDragon" } ], - "combat_value": 209, - "reward" : { - "value": 57000, - "resources": - { - "gold" : 30000 - }, - "artifacts": [ - { "class" : "MINOR" }, - { "class" : "MAJOR" }, - { "class" : "RELIC" }, - { "class" : "RELIC" } - ] - } + "resources" : + { + "gold" : 30000 + }, + "artifacts" : [ + { "class" : "MINOR" }, + { "class" : "MAJOR" }, + { "class" : "RELIC" }, + { "class" : "RELIC" } + ] }, { - "chance": 30, - "guards": [ - { "amount": 8, "type": "greenDragon" }, - { "amount": 6, "type": "redDragon" }, - { "amount": 4, "type": "goldDragon" }, - { "amount": 3, "type": "blackDragon" } + "message" : 34, + "appearChance" : { "min" : 60, "max" : 90 }, + "guards" : [ + { "amount" : 8, "type" : "greenDragon" }, + { "amount" : 6, "type" : "redDragon" }, + { "amount" : 4, "type" : "goldDragon" }, + { "amount" : 3, "type" : "blackDragon" } ], - "combat_value": 556, - "reward" : { - "value": 75000, - "resources": - { - "gold" : 40000 - }, - "artifacts": [ - { "class" : "MAJOR" }, - { "class" : "RELIC" }, - { "class" : "RELIC" }, - { "class" : "RELIC" } - ] - } + "resources" : + { + "gold" : 40000 + }, + "artifacts" : [ + { "class" : "MAJOR" }, + { "class" : "RELIC" }, + { "class" : "RELIC" }, + { "class" : "RELIC" } + ] }, { - "chance": 10, - "guards": [ - { "amount": 8, "type": "greenDragon" }, - { "amount": 7, "type": "redDragon" }, - { "amount": 6, "type": "goldDragon" }, - { "amount": 5, "type": "blackDragon" } + "message" : 34, + "appearChance" : { "min" : 90, "max" : 100 }, + "guards" : [ + { "amount" : 8, "type" : "greenDragon" }, + { "amount" : 7, "type" : "redDragon" }, + { "amount" : 6, "type" : "goldDragon" }, + { "amount" : 5, "type" : "blackDragon" } ], - "combat_value": 343, - "reward" : { - "value": 90000, - "resources": - { - "gold" : 50000 - }, - "artifacts": [ - { "class" : "RELIC" }, - { "class" : "RELIC" }, - { "class" : "RELIC" }, - { "class" : "RELIC" } - ] - } - } - ] - } - } - }, - "pyramid" : { - "index" :63, - "handler": "bank", - "base" : { - "sounds" : { - "visit" : ["MYSTERY"] - } - }, - "types" : { - "pyramid" : { - "index" : 0, - "resetDuration" : 0, - "name" : "Pyramid", - "aiValue" : 8000, - "rmg" : { - "value" : 5000, - "rarity" : 20 - }, - "levels": [ - { - "chance": 100, - "guards": [ - { "amount": 40, "type": "goldGolem" }, - { "amount": 10, "type": "diamondGolem" }, - { "amount": 10, "type": "diamondGolem" } - ], - "combat_value": 786, - "reward" : { - "value": 15000, - "spells" : [ { "level" : 5 } ] - } + "resources" : + { + "gold" : 50000 + }, + "artifacts" : [ + { "class" : "RELIC" }, + { "class" : "RELIC" }, + { "class" : "RELIC" }, + { "class" : "RELIC" } + ] } ] } 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 new file mode 100644 index 000000000..e1efd7b85 --- /dev/null +++ b/config/objects/pyramid.json @@ -0,0 +1,76 @@ +{ + "pyramid" : { + "index" :63, + "handler" : "configurable", + "base" : { + "sounds" : { + "visit" : ["MYSTERY"] + } + }, + "types" : { + "pyramid" : { + "index" : 0, + + "name" : "Pyramid", + "aiValue" : 8000, + "rmg" : { + "value" : 5000, + "rarity" : 20 + }, + + "variables" : { + "spell" : { + "gainedSpell" : { + "level": 5 + } + } + }, + + "onGuardedMessage" : 105, + "visitMode" : "once", + "selectMode" : "selectFirst", + "onVisited" : [ + { + "message" : 107, + "bonuses" : [ { "type" : "LUCK", "val" : -2, "duration" : "ONE_BATTLE", "description" : 70 } ] + } + ], + "guardsLayout" : "default", + "rewards" : [ + { + "limiter" : { + "canLearnSpells" : [ + "@gainedSpell" + ] + }, + "spells" : [ + "@gainedSpell" + ], + "message" : [ 106, "{%s}." ], // Upon defeating monsters, you learn new spell + "guards" : [ + { "amount" : 40, "type" : "goldGolem" }, + { "amount" : 10, "type" : "diamondGolem" }, + { "amount" : 10, "type" : "diamondGolem" } + ] + } + ], + "onEmpty" : [ + { + "limiter" : { + "artifacts" : [ + { + "type" : "spellBook" + } + ] + }, + "message" : [ 106, "{%s}. ", 108 ] // No Wisdom + }, + { + "message" : [ 106, "{%s}. ", 109 ] // No spellbook + } + ] + + } + } + } +} \ No newline at end of file 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/battlefield.json b/config/schemas/battlefield.json index 3135c2fb4..0a19116ae 100644 --- a/config/schemas/battlefield.json +++ b/config/schemas/battlefield.json @@ -24,6 +24,18 @@ "format" : "imageFile", "description" : "Background image for this battlefield" }, + "music" : + { + "description" : "Optional, filename for custom music to play during combat on this terrain", + "type" : "string", + "format" : "musicFile" + }, + "openingSound" : + { + "description" : "Optional, filename for custom sound to play during combat opening on this terrain", + "type" : "string", + "format" : "musicFile" + }, "impassableHexes" : { "type" : "array", "description" : "List of battle hexes that will be always blocked on this battlefield (e.g. ship to ship battles)", diff --git a/config/schemas/bonus.json b/config/schemas/bonus.json index 2789994c8..ffc1ab53b 100644 --- a/config/schemas/bonus.json +++ b/config/schemas/bonus.json @@ -179,7 +179,10 @@ "description" : "stacking" }, "description" : { - "type" : "string", + "anyOf" : [ + { "type" : "string" }, + { "type" : "number" } + ], "description" : "description" } } diff --git a/config/schemas/faction.json b/config/schemas/faction.json index 38f9f9e85..e25c286e4 100644 --- a/config/schemas/faction.json +++ b/config/schemas/faction.json @@ -89,14 +89,14 @@ "additionalProperties" : false, "required" : [ "mapObject", "buildingsIcons", "buildings", "creatures", "guildWindow", "names", - "hallBackground", "hallSlots", "horde", "mageGuild", "moatAbility", "defaultTavern", "tavernVideo", "guildBackground", "musicTheme", "siege", "structures", "townBackground", "warMachine" + "hallBackground", "hallSlots", "horde", "mageGuild", "moatAbility", "defaultTavern", "tavernVideo", "guildBackground", "musicTheme", "siege", "structures", "townBackground" ], "description" : "town", "properties" : { "creatures" : { "type" : "array", "minItems" : 7, - "maxItems" : 7, + "maxItems" : 8, "description" : "List of creatures available for recruitment on each level", "items" : { "type" : "array", @@ -133,10 +133,6 @@ "type" : "string", "description" : "Primary resource for this town. Produced by Silo and offered as starting bonus" }, - "warMachine" : { - "type" : "string", - "description" : "Identifier of war machine produced by blacksmith in town" - }, "horde" : { "type" : "array", "maxItems" : 2, @@ -151,9 +147,13 @@ "$ref" : "townSiege.json" }, "musicTheme" : { - "type" : "string", - "description" : "Path to town music theme", - "format" : "musicFile" + "type" : "array", + "description" : "Path to town music themes", + "minItems" : 1, + "items" : { + "type" : "string", + "format" : "musicFile" + } }, "tavernVideo" : { "type" : "string", 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 new file mode 100644 index 000000000..9d1b55b1e --- /dev/null +++ b/config/schemas/gameSettings.json @@ -0,0 +1,171 @@ +{ + "type" : "object", + "$schema" : "http://json-schema.org/draft-04/schema", + "title" : "VCMI game settings format", + "description" : "Format used to define game settings in VCMI", + "additionalProperties" : false, + "properties" : { + "textData" : { + "type" : "object", + "additionalProperties" : false, + "properties" : { + "heroClass" : { "type" : "number" }, + "artifact" : { "type" : "number" }, + "creature" : { "type" : "number" }, + "faction" : { "type" : "number" }, + "hero" : { "type" : "number" }, + "spell" : { "type" : "number" }, + "object" : { "type" : "number" }, + "terrain" : { "type" : "number" }, + "river" : { "type" : "number" }, + "road" : { "type" : "number" } + } + }, + "mapFormat" : { + "type" : "object", + "additionalProperties" : false, + "properties" : { + "restorationOfErathia" : { "type" : "object" }, + "armageddonsBlade" : { "type" : "object" }, + "shadowOfDeath" : { "type" : "object" }, + "chronicles" : { "type" : "object" }, + "jsonVCMI" : { "type" : "object" }, + "hornOfTheAbyss" : { "type" : "object" }, + "inTheWakeOfGods" : { "type" : "object" } + } + }, + "heroes" : { + "type" : "object", + "additionalProperties" : false, + "properties" : { + "perPlayerOnMapCap" : { "type" : "number" }, + "perPlayerTotalCap" : { "type" : "number" }, + "retreatOnWinWithoutTroops" : { "type" : "boolean" }, + "startingStackChances" : { "type" : "array" }, + "backpackSize" : { "type" : "number" }, + "tavernInvite" : { "type" : "boolean" }, + "minimalPrimarySkills" : { "type" : "array" } + } + }, + "towns" : { + "type" : "object", + "additionalProperties" : false, + "properties" : { + "buildingsPerTurnCap" : { "type" : "number" }, + "startingDwellingChances" : { "type" : "array" }, + "spellResearch" : { "type" : "boolean" }, + "spellResearchCost" : { "type" : "array" }, + "spellResearchPerDay" : { "type" : "array" }, + "spellResearchCostExponentPerResearch" : { "type" : "array" } + } + }, + "combat": { + "type" : "object", + "additionalProperties" : false, + "properties" : { + "goodMoraleDice" : { "type" : "array" }, + "badMoraleDice" : { "type" : "array" }, + "goodLuckDice" : { "type" : "array" }, + "badLuckDice" : { "type" : "array" }, + "backpackSize" : { "type" : "number" }, + "attackPointDamageFactor" : { "type" : "number" }, + "attackPointDamageFactorCap" : { "type" : "number" }, + "defensePointDamageFactor" : { "type" : "number" }, + "defensePointDamageFactorCap" : { "type" : "number" }, + "oneHexTriggersObstacles" : { "type" : "boolean" }, + "layouts" : { "type" : "object" }, + "areaShotCanTargetEmptyHex" : { "type" : "boolean" } + } + }, + "creatures": { + "type" : "object", + "additionalProperties" : false, + "properties" : { + "weeklyGrowthPercent" : { "type" : "number" }, + "weeklyGrowthCap" : { "type" : "number" }, + "dailyStackExperience" : { "type" : "number" }, + "allowRandomSpecialWeeks" : { "type" : "boolean" }, + "allowAllForDoubleMonth" : { "type" : "boolean" } + } + }, + "dwellings": { + "type" : "object", + "additionalProperties" : false, + "properties" : { + "accumulateWhenNeutral" : { "type" : "boolean" }, + "accumulateWhenOwned" : { "type" : "boolean" }, + "mergeOnRecruit" : { "type" : "boolean" } + } + }, + "markets": { + "type" : "object", + "additionalProperties" : false, + "properties" : { + "blackMarketRestockPeriod" : { "type" : "number" } + } + }, + "banks": { + "type" : "object", + "additionalProperties" : false, + "properties" : { + "showGuardsComposition" : { "type" : "boolean" } + } + }, + "modules": { + "type" : "object", + "additionalProperties" : false, + "properties" : { + "stackExperience" : { "type" : "boolean" }, + "stackArtifact" : { "type" : "boolean" }, + "commanders" : { "type" : "boolean" } + } + }, + "pathfinder": { + "type" : "object", + "additionalProperties" : false, + "properties" : { + "ignoreGuards" : { "type" : "boolean" }, + "useBoat" : { "type" : "boolean" }, + "useMonolithTwoWay" : { "type" : "boolean" }, + "useMonolithOneWayUnique" : { "type" : "boolean" }, + "useMonolithOneWayRandom" : { "type" : "boolean" }, + "useWhirlpool" : { "type" : "boolean" }, + "originalFlyRules" : { "type" : "boolean" } + } + }, + "resources": { + "type" : "object", + "additionalProperties" : false, + "properties" : { + "weeklyBonusesAI" : { "type" : "object" } + } + }, + + "spells": { + "type" : "object", + "additionalProperties" : false, + "properties" : { + "dimensionDoorOnlyToUncoveredTiles" : { "type" : "boolean" }, + "dimensionDoorExposesTerrainType" : { "type" : "boolean" }, + "dimensionDoorFailureSpendsPoints" : { "type" : "boolean" }, + "dimensionDoorTriggersGuards" : { "type" : "boolean" }, + "dimensionDoorTournamentRulesLimit" : { "type" : "boolean" } + } + }, + "bonuses": { + "type" : "object", + "additionalProperties" : false, + "properties" : { + "global" : { "type" : "object" }, + "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 e9ca88dd2..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,14 +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", - "additionalProperties" : { - "type" : "object", - "properties" : { - "type" : "object" - } - } + "oneOf" : [ + { + "type" : "object", + "$ref" : "gameSettings.json" + }, + { + "type" : "array", + "items" : { "type" : "string", "format" : "textFile" } + }, + ] }, "filesystem" : { "type" : "object", @@ -211,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 a055d279b..59d913440 100644 --- a/config/schemas/object.json +++ b/config/schemas/object.json @@ -17,7 +17,13 @@ "type" : "number" }, "handler" : { - "type" : "string" + "type" : "string", + "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", "flaggable", "magi", "mine", "obelisk", "pandora", "prison", "questGuard", "seerHut", "sign", + "siren", "monolith", "subterraneanGate", "whirlpool", "terrain" + ] }, "base" : { "type" : "object" diff --git a/config/schemas/objectType.json b/config/schemas/objectType.json index b470bc1cc..3d582bafa 100644 --- a/config/schemas/objectType.json +++ b/config/schemas/objectType.json @@ -34,6 +34,9 @@ } } }, + "name" : { + "type" : "string" + }, "templates" : { "type" : "object", "additionalProperties" : { diff --git a/config/schemas/rewardable.json b/config/schemas/rewardable.json new file mode 100644 index 000000000..afa4119e0 --- /dev/null +++ b/config/schemas/rewardable.json @@ -0,0 +1,339 @@ +{ + "type" : "object", + "$schema" : "http://json-schema.org/draft-04/schema", + "title" : "VCMI map object format", + "description" : "Description of map object class", + "required" : [ "rewards" ], + "additionalProperties" : false, + + "definitions" : { + "value" : { + "anyOf" : [ + { + "type" : "number" + }, + { + "type" : "string" // variable name + }, + { + "type" : "array", + "items" : { + "$ref" : "#/definitions/value" + } + }, + { + "type" : "object", + "additionalProperties" : true, + "properties" : { + "amount" : { "$ref" : "#/definitions/value" }, + "min" : { "$ref" : "#/definitions/value" }, + "max" : { "$ref" : "#/definitions/value" } + } + } + ] + }, + "identifier" : { + "anyOf" : [ + { + "type" : "string" + }, + { + "type" : "object", + "additionalProperties" : true, + "properties" : { + "type" : { + "$ref" : "#/definitions/identifier" + }, + "anyOf" : { + "type" : "array", + "items" : { + "$ref" : "#/definitions/identifier" + } + }, + "noneOf" : { + "type" : "array", + "items" : { + "$ref" : "#/definitions/identifier" + } + } + } + } + ] + }, + "identifierList" : { + "type" : "array", + "items" : { + "$ref" : "#/definitions/identifier" + } + }, + "identifierWithValueList" : { + "anyOf" : [ + { + "type" : "array", + "items" : { + "allOf" : [ + { "$ref" : "#/definitions/identifier" }, + { "$ref" : "#/definitions/value" } + ] + } + }, + { + "type" : "object", + "additionalProperties" : { + "$ref" : "#/definitions/value" + } + }, + ], + }, + "reward" : { + "type" : "object", + "additionalProperties" : false, + "properties" : { + "appearChance" : { + "type" : "object", + "additionalProperties" : false, + "properties" : { + "dice" : { "type" : "number" }, + "min" : { "type" : "number", "minimum" : 0, "exclusiveMaximum" : 100 }, + "max" : { "type" : "number", "exclusiveMinimum" : 0, "maximum" : 100 } + } + }, + + "guards" : { "$ref" : "#/definitions/identifierWithValueList" }, + + "limiter" : { "$ref" : "#/definitions/limiter" }, + "message" : { "$ref" : "#/definitions/message" }, + "description" : { "$ref" : "#/definitions/message" }, + + "heroExperience" : { "$ref" : "#/definitions/value" }, + "heroLevel" : { "$ref" : "#/definitions/value" }, + "movePercentage" : { "$ref" : "#/definitions/value" }, + "movePoints" : { "$ref" : "#/definitions/value" }, + "manaPercentage" : { "$ref" : "#/definitions/value" }, + "manaPoints" : { "$ref" : "#/definitions/value" }, + "manaOverflowFactor" : { "$ref" : "#/definitions/value" }, + + "removeObject" : { "type" : "boolean" }, + "bonuses" : { + "type":"array", + "description": "List of bonuses that will be granted to visiting hero", + "items": { "$ref" : "bonus.json" } + }, + + "resources" : { "$ref" : "#/definitions/identifierWithValueList" }, + "secondary" : { "$ref" : "#/definitions/identifierWithValueList" }, + "creatures" : { "$ref" : "#/definitions/identifierWithValueList" }, + "primary" : { "$ref" : "#/definitions/identifierWithValueList" }, + + "artifacts" : { "$ref" : "#/definitions/identifierList" }, + "spells" : { "$ref" : "#/definitions/identifierList" }, + + "spellCast" : { + "type" : "object", + "additionalProperties" : false, + "properties" : { + "spell" : { "$ref" : "#/definitions/identifier" }, + "schoolLevel" : { "type" : "number" } + } + }, + "revealTiles" : { + "type" : "object", + "additionalProperties" : false, + "properties" : { + "hide" : { "type" : "boolean" }, + "radius" : { "type" : "number" }, + "surface" : { "type" : "number" }, + "subterra" : { "type" : "number" }, + "water" : { "type" : "number" }, + "rock" : { "type" : "number" } + } + }, + "changeCreatures" : { + "type" : "object", + "additionalProperties" : { "type" : "string" } + } + } + }, + "limiter" : { + "type" : "object", + "additionalProperties" : false, + "properties" : { + "dayOfWeek" : { "$ref" : "#/definitions/value" }, + "daysPassed" : { "$ref" : "#/definitions/value" }, + "heroExperience" : { "$ref" : "#/definitions/value" }, + "heroLevel" : { "$ref" : "#/definitions/value" }, + "manaPercentage" : { "$ref" : "#/definitions/value" }, + "manaPoints" : { "$ref" : "#/definitions/value" }, + + "canLearnSkills" : { "type" : "boolean" }, + + "resources" : { "$ref" : "#/definitions/identifierWithValueList" }, + "secondary" : { "$ref" : "#/definitions/identifierWithValueList" }, + "creatures" : { "$ref" : "#/definitions/identifierWithValueList" }, + "primary" : { "$ref" : "#/definitions/identifierWithValueList" }, + + "canLearnSpells" : { "$ref" : "#/definitions/identifierList" }, + "heroClasses" : { "$ref" : "#/definitions/identifierList" }, + "artifacts" : { "$ref" : "#/definitions/identifierList" }, + "spells" : { "$ref" : "#/definitions/identifierList" }, + "colors" : { "$ref" : "#/definitions/identifierList" }, + "heroes" : { "$ref" : "#/definitions/identifierList" }, + + "anyOf" : { + "type" : "array", + "items" : { "$ref" : "#/definitions/limiter" } + }, + "allOf" : { + "type" : "array", + "items" : { "$ref" : "#/definitions/limiter" } + }, + "noneOf" : { + "type" : "array", + "items" : { "$ref" : "#/definitions/limiter" } + }, + } + }, + "message" : { + "anyOf" : [ + { + "type" : "array", + "items" : { + "anyOf" : [ + { "type" : "number" }, + { "type" : "string" } + ] + } + }, + { + "type" : "number" + }, + { + "type" : "string" + } + ] + }, + "variableList" : { + "type" : "object", + "additionalProperties" : { + "$ref" : "#/definitions/identifier" + } + } + }, + + "properties" : { + "rewards" : { + "type" : "array", + "items" : { "$ref" : "#/definitions/reward" } + }, + "onVisited" : { + "type" : "array", + "items" : { "$ref" : "#/definitions/reward" } + }, + "onEmpty" : { + "type" : "array", + "items" : { "$ref" : "#/definitions/reward" } + }, + + "variables" : { + "type" : "object", + "additionalProperties" : false, + "properties" : { + "number" : { + "type" : "object", + "additionalProperties" : { + "$ref" : "#/definitions/value" + } + }, + "artifact" : { + "$ref" : "#/definitions/variableList" + }, + "spell" : { + "$ref" : "#/definitions/variableList" + }, + "primarySkill" : { + "$ref" : "#/definitions/variableList" + }, + "secondarySkill" : { + "$ref" : "#/definitions/variableList" + }, + }, + }, + + "onGuardedMessage" : { + "$ref" : "#/definitions/message" + }, + "onSelectMessage" : { + "$ref" : "#/definitions/message" + }, + "description" : { + "$ref" : "#/definitions/message" + }, + "notVisitedTooltip" : { + "$ref" : "#/definitions/message" + }, + "visitedTooltip" : { + "$ref" : "#/definitions/message" + }, + "onVisitedMessage" : { + "$ref" : "#/definitions/message" + }, + "onEmptyMessage" : { + "$ref" : "#/definitions/message" + }, + + "canRefuse": { + "type" : "boolean" + }, + + "showScoutedPreview": { + "type" : "boolean" + }, + + "showInInfobox": { + "type" : "boolean" + }, + + "coastVisitable": { + "type" : "boolean" + }, + + "visitMode": { + "enum" : [ "unlimited", "once", "hero", "bonus", "limiter", "player" ], + "type" : "string" + }, + + "guardsLayout": { + "type" : "string" + }, + + "visitLimiter": { "$ref" : "#/definitions/limiter" }, + + "selectMode": { + "enum" : [ "selectFirst", "selectPlayer", "selectRandom", "selectAll" ], + "type" : "string" + }, + + "resetParameters" : { + "type" : "object", + "additionalProperties" : false, + "properties" : { + "visitors" : { "type" : "boolean" }, + "rewards" : { "type" : "boolean" }, + "period" : { "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/settings.json b/config/schemas/settings.json index 002500054..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", @@ -19,6 +19,7 @@ "additionalProperties" : false, "required" : [ "playerName", + "multiPlayerNames", "music", "sound", "saveRandomMaps", @@ -48,6 +49,10 @@ "type" : "string", "default" : "Player" }, + "multiPlayerNames" : { + "type" : "array", + "default" : [] + }, "music" : { "type" : "number", "default" : 88 @@ -144,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, @@ -161,7 +183,12 @@ "showfps", "targetfps", "vsync", - "scalingMode" + "fontsType", + "cursorScalingFactor", + "fontScalingFactor", + "upscalingFilter", + "fontUpscalingFilter", + "downscalingFilter" ], "properties" : { "resolution" : { @@ -169,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", @@ -225,7 +249,30 @@ "type" : "boolean", "default" : true }, - "scalingMode" : { + "fontsType" : { + "type" : "string", + "enum" : [ "auto", "original", "scalable" ], + "default" : "auto" + }, + "cursorScalingFactor" : { + "type" : "number", + "default" : 1 + }, + "fontScalingFactor" : { + "type" : "number", + "default" : 1 + }, + "fontUpscalingFilter" : { + "type" : "string", + "enum" : [ "nearest", "bilinear", "xbrz" ], + "default" : "nearest" + }, + "upscalingFilter" : { + "type" : "string", + "enum" : [ "auto", "none", "xbrz2", "xbrz3", "xbrz4" ], + "default" : "auto" + }, + "downscalingFilter" : { "type" : "string", "enum" : [ "nearest", "linear", "best" ], "default" : "best" @@ -305,7 +352,7 @@ "type" : "object", "additionalProperties" : false, "default" : {}, - "required" : [ "heroMoveTime", "enemyMoveTime", "scrollSpeedPixels", "heroReminder", "quickCombat", "objectAnimation", "terrainAnimation", "forceQuickCombat", "borderScroll", "leftButtonDrag", "smoothDragging", "backgroundDimLevel", "hideBackground", "backgroundDimSmallWindows" ], + "required" : [ "heroMoveTime", "enemyMoveTime", "scrollSpeedPixels", "heroReminder", "quickCombat", "objectAnimation", "terrainAnimation", "forceQuickCombat", "borderScroll", "leftButtonDrag", "rightButtonDrag", "smoothDragging", "backgroundDimLevel", "hideBackground", "backgroundDimSmallWindows" ], "properties" : { "heroMoveTime" : { "type" : "number", @@ -350,6 +397,10 @@ "type" : "boolean", "default" : false }, + "rightButtonDrag" : { + "type" : "boolean", + "default" : false + }, "smoothDragging" : { "type" : "boolean", "default" : true @@ -372,7 +423,7 @@ "type" : "object", "additionalProperties" : false, "default" : {}, - "required" : [ "speedFactor", "mouseShadow", "cellBorders", "stackRange", "movementHighlightOnHover", "rangeLimitHighlightOnHover", "showQueue", "swipeAttackDistance", "queueSize", "stickyHeroInfoWindows", "enableAutocombatSpells", "endWithAutocombat", "queueSmallSlots", "queueSmallOutside" ], + "required" : [ "speedFactor", "mouseShadow", "cellBorders", "stackRange", "movementHighlightOnHover", "rangeLimitHighlightOnHover", "showQueue", "swipeAttackDistance", "queueSize", "stickyHeroInfoWindows", "enableAutocombatSpells", "endWithAutocombat", "queueSmallSlots", "queueSmallOutside", "enableQuickSpellPanel" ], "properties" : { "speedFactor" : { "type" : "number", @@ -430,6 +481,10 @@ "queueSmallOutside" : { "type": "boolean", "default": false + }, + "enableQuickSpellPanel" : { + "type": "boolean", + "default": true } } }, @@ -437,7 +492,7 @@ "type" : "object", "additionalProperties" : false, "default" : {}, - "required" : [ "localHostname", "localPort", "remoteHostname", "remotePort", "playerAI", "alliedAI", "friendlyAI", "neutralAI", "enemyAI" ], + "required" : [ "localHostname", "localPort", "remoteHostname", "remotePort", "seed", "playerAI", "alliedAI", "friendlyAI", "neutralAI", "enemyAI" ], "properties" : { "localHostname" : { "type" : "string", @@ -455,6 +510,10 @@ "type" : "number", "default" : 3030 }, + "seed" : { + "type" : "number", + "default" : 0 + }, "playerAI" : { "type" : "string", "default" : "Nullkiller" @@ -469,7 +528,7 @@ }, "neutralAI" : { "type" : "string", - "default" : "StupidAI" + "default" : "BattleAI" }, "enemyAI" : { "type" : "string", @@ -537,7 +596,7 @@ }, "loggers" : { "type" : "array", - "default" : [ { "domain" : "global", "level" : "trace" } ], + "default" : [ { "domain" : "global", "level" : "trace" }, { "domain" : "rng", "level" : "info" } ], "items" : { "type" : "object", "additionalProperties" : false, @@ -561,7 +620,6 @@ "defaultRepositoryURL", "extraRepositoryURL", "extraRepositoryEnabled", - "enableInstalledMods", "autoCheckRepositories", "ignoreSslErrors", "updateOnStartup", @@ -574,7 +632,7 @@ }, "defaultRepositoryURL" : { "type" : "string", - "default" : "https://raw.githubusercontent.com/vcmi/vcmi-mods-repository/develop/vcmi-1.5.json", + "default" : "https://raw.githubusercontent.com/vcmi/vcmi-mods-repository/develop/vcmi-1.6.json", }, "extraRepositoryEnabled" : { "type" : "boolean", @@ -588,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 dff3bc2df..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,14 @@ "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." + }, "gainChance" : { "type" : "object", "description" : "Chance for this spell to appear in Mage Guild of a specific faction", diff --git a/config/schemas/template.json b/config/schemas/template.json index a1a353a6d..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"] + } + } + } } } }, @@ -99,7 +145,7 @@ "type": { "type" : "string", - "enum" : ["wide", "fictive", "repulsive"] + "enum" : ["wide", "fictive", "repulsive", "forcePortal"] } } }, @@ -131,6 +177,11 @@ "description" : "Maximal size of the map, e.g. 'm+u' or '120x120x1", "type": "string" }, + "settings" : { + "description" : "List of changed game settings by template", + "type" : "object", + "$ref" : "gameSettings.json" + }, "name" : { "description" : "Optional name - useful to have several template variations with same name", "type": "string" diff --git a/config/schemas/terrain.json b/config/schemas/terrain.json index 23aa598a3..980193db9 100644 --- a/config/schemas/terrain.json +++ b/config/schemas/terrain.json @@ -97,9 +97,13 @@ }, "music" : { - "type" : "string", - "description" : "Music filename to play on this terrain on adventure map", - "format" : "musicFile" + "description" : "Music filenames to play on this terrain on adventure map", + "type" : "array", + "minItems" : 1, + "items" : { + "type" : "string", + "format" : "musicFile" + } }, "sounds" : { diff --git a/config/schemas/townBuilding.json b/config/schemas/townBuilding.json index 7a86acdab..8a7635c55 100644 --- a/config/schemas/townBuilding.json +++ b/config/schemas/townBuilding.json @@ -31,11 +31,12 @@ "type" : "string" }, "description" : { - "description" : "Localizable decsription of this building", + "description" : "Localizable description of this building", "type" : "string" }, "type" : { "type" : "string", + "enum" : [ "mysticPond", "castleGate", "portalOfSummoning", "library", "escapeTunnel", "treasury", "bank" ], "description" : "Subtype for some special buildings" }, "mode" : { @@ -56,6 +57,34 @@ "description" : "Optional, indicates that this building upgrades another base building", "type" : "string" }, + "upgradeReplacesBonuses" : { + "description" : "If set to true, this building will replace all bonuses from base building, leaving only bonuses defined by this building", + "type" : "boolean" + }, + "manualHeroVisit" : { + "description" : "If set to true, this building will not automatically activate on new day or on entering town and needs to be activated manually on click", + "type" : "boolean" + }, + "configuration" : { + "description" : "Optional, configuration of building that can be activated by visiting hero", + "$ref" : "rewardable.json" + }, + "fortifications" : { + "type" : "object", + "additionalProperties" : false, + "description" : "Fortifications provided by this buildings, if any", + "properties" : { + "citadelShooter" : { "type" : "string", "description" : "Creature ID of shooter located in central keep (citadel). Used only if citadel is present." }, + "upperTowerShooter" : { "type" : "string", "description" : "Creature ID of shooter located in upper tower. Used only if upper tower is present." }, + "lowerTowerShooter" : { "type" : "string", "description" : "Creature ID of shooter located in lower tower. Used only if lower tower is present." }, + + "wallsHealth" : { "type" : "number", "description" : "Maximum health of destructible walls. Walls are only present if their health is above zero" }, + "citadelHealth" : { "type" : "number", "description" : "Maximum health of central tower or 0 if not present. Requires walls presence." }, + "upperTowerHealth" : { "type" : "number", "description" : "Maximum health of upper tower or 0 if not present. Requires walls presence." }, + "lowerTowerHealth" : { "type" : "number", "description" : "Maximum health of lower tower or 0 if not present. Requires walls presence." }, + "hasMoat" : { "type" : "boolean","description" : "If set to true, moat will be placed in front of the walls. Requires walls presence." } + } + }, "cost" : { "type" : "object", "additionalProperties" : false, @@ -84,24 +113,22 @@ "gems" : { "type" : "number"} } }, - "overrides" : { - "type" : "array", - "items" : [ - { - "description" : "The buildings which bonuses should be overridden with bonuses of the current building", - "type" : "string" - } - ] + "warMachine" : { + "type" : "string", + "description" : "Artifact ID of a war machine that can be purchased in this building, if any" }, "bonuses" : { "type" : "array", - "description" : "Bonuses, provided by this special building on build using bonus system", + "description" : "Bonuses that are provided by this building in any town where this building has been built. Only affects town itself (including siege), to propagate effect to player or team please use bonus propagators", "items" : { "$ref" : "bonus.json" } }, - "onVisitBonuses" : { + "marketModes" : { "type" : "array", - "description" : "Bonuses, provided by this special building on hero visit and applied to the visiting hero", - "items" : { "$ref" : "bonus.json" } + "items" : { + "type" : "string", + "enum" : [ "resource-resource", "resource-player", "creature-resource", "resource-artifact", "artifact-resource", "artifact-experience", "creature-experience", "creature-undead", "resource-skill"], + }, + "description" : "List of modes available in this market" } } } diff --git a/config/shortcutsConfig.json b/config/shortcutsConfig.json index ebf1463fb..e6c91b12c 100644 --- a/config/shortcutsConfig.json +++ b/config/shortcutsConfig.json @@ -40,6 +40,8 @@ "adventureSetHeroAwake": "W", "adventureThievesGuild": "G", "adventureToggleGrid": "F6", + "adventureToggleVisitable": [], + "adventureToggleBlocked": [], "adventureToggleMapLevel": "U", "adventureToggleSleep": [], "adventureTrackHero": "F5", @@ -54,6 +56,8 @@ "adventureZoomIn": "Keypad +", "adventureZoomOut": "Keypad -", "adventureZoomReset": "Backspace", + "adventureSearch": "Ctrl+F", + "adventureSearchContinue": "Alt+F", "battleAutocombat": "A", "battleAutocombatEnd": "Q", "battleCastSpell": "C", @@ -64,6 +68,19 @@ "battleOpenHoveredUnit": "V", "battleRetreat": "R", "battleSelectAction": "S", + "battleToggleQuickSpell": "T", + "battleSpellShortcut0": "1", + "battleSpellShortcut1": "2", + "battleSpellShortcut2": "3", + "battleSpellShortcut3": "4", + "battleSpellShortcut4": "5", + "battleSpellShortcut5": "6", + "battleSpellShortcut6": "7", + "battleSpellShortcut7": "8", + "battleSpellShortcut8": "9", + "battleSpellShortcut9": "0", + "battleSpellShortcut10": "N", + "battleSpellShortcut11": "M", "battleSurrender": "S", "battleTacticsEnd": [ "Return", "Keypad Enter"], "battleTacticsNext": "Space", @@ -122,6 +139,7 @@ "heroToggleTactics": "B", "highScoresCampaigns": "C", "highScoresReset": "R", + "highScoresStatistics": ".", "highScoresScenarios": "S", "kingdomHeroesTab": "H", "kingdomTownsTab": "T", @@ -135,10 +153,11 @@ "lobbyRandomMap": "R", "lobbyRandomTown": "T", "lobbyRandomTownVs": "V", + "lobbyHandicap": "H", "lobbyReplayVideo": "R", "lobbySaveGame": [ "S", "Return", "Keypad Enter"], "lobbySelectScenario": "S", - "lobbyToggleChat": "H", + "lobbyToggleChat": "C", "lobbyTurnOptions": "T", "mainMenuBack": [ "B", "Escape" ], "mainMenuCampaign": "C", @@ -226,6 +245,15 @@ "townOpenThievesGuild": "G", "townOpenVisitingHero": "Ctrl+H", "townSwapArmies": "Space", + "listHeroUp": "Ctrl+PageUp", + "listHeroDown": "Ctrl+PageDown", + "listHeroTop": "Ctrl+Home", + "listHeroBottom": "Ctrl+End", + "listHeroDismiss": "Delete", + "listTownUp": "Ctrl+PageUp", + "listTownDown": "Ctrl+PageDown", + "listTownTop": "Ctrl+Home", + "listTownBottom": "Ctrl+End", // Controller-specific "mouseCursorX": [], 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/config/terrains.json b/config/terrains.json index e649fcdb2..4bbf8f5ad 100644 --- a/config/terrains.json +++ b/config/terrains.json @@ -5,7 +5,7 @@ "moveCost" : 100, "minimapUnblocked" : [ 82, 56, 8 ], "minimapBlocked" : [ 57, 40, 8 ], - "music" : "Dirt.mp3", + "music" : [ "Dirt.mp3" ], "tiles" : "DIRTTL", "type" : ["SURFACE"], "shortIdentifier" : "dt", @@ -21,7 +21,7 @@ "moveCost" : 150, "minimapUnblocked" : [ 222, 207, 140 ], "minimapBlocked" : [ 165, 158, 107 ], - "music" : "Sand.mp3", + "music" : [ "Sand.mp3" ], "tiles" : "SANDTL", "type" : ["SURFACE"], "shortIdentifier" : "sa", @@ -38,7 +38,7 @@ "moveCost" : 100, "minimapUnblocked" : [ 0, 65, 0 ], "minimapBlocked" : [ 0, 48, 0 ], - "music" : "Grass.mp3", + "music" : [ "Grass.mp3" ], "tiles" : "GRASTL", "type" : ["SURFACE"], "shortIdentifier" : "gr", @@ -53,7 +53,7 @@ "moveCost" : 150, "minimapUnblocked" : [ 181, 199, 198 ], "minimapBlocked" : [ 140, 158, 156 ], - "music" : "Snow.mp3", + "music" : [ "Snow.mp3" ], "tiles" : "SNOWTL", "type" : ["SURFACE"], "shortIdentifier" : "sn", @@ -68,7 +68,7 @@ "moveCost" : 175, "minimapUnblocked" : [ 74, 134, 107 ], "minimapBlocked" : [ 33, 89, 66 ], - "music" : "Swamp.mp3", + "music" : [ "Swamp.mp3" ], "tiles" : "SWMPTL", "type" : ["SURFACE"], "shortIdentifier" : "sw", @@ -83,7 +83,7 @@ "moveCost" : 125, "minimapUnblocked" : [ 132, 113, 49 ], "minimapBlocked" : [ 99, 81, 33 ], - "music" : "Rough.mp3", + "music" : [ "Rough.mp3" ], "tiles" : "ROUGTL", "type" : ["SURFACE"], "shortIdentifier" : "rg", @@ -98,7 +98,7 @@ "moveCost" : 100, "minimapUnblocked" : [ 132, 48, 0 ], "minimapBlocked" : [ 90, 8, 0 ], - "music" : "Underground.mp3", + "music" : [ "Underground.mp3" ], "tiles" : "SUBBTL", "type" : [ "SUB" ], "shortIdentifier" : "sb", @@ -114,7 +114,7 @@ "moveCost" : 100, "minimapUnblocked" : [ 74, 73, 74 ], "minimapBlocked" : [ 41, 40, 41 ], - "music" : "Lava.mp3", + "music" : [ "Lava.mp3" ], "tiles" : "LAVATL", "type" : ["SUB", "SURFACE"], "shortIdentifier" : "lv", @@ -133,7 +133,7 @@ "moveCost" : 100, "minimapUnblocked" : [ 8, 81, 148 ], "minimapBlocked" : [ 8, 81, 148 ], - "music" : "Water.mp3", + "music" : [ "Water.mp3" ], "tiles" : "WATRTL", "type" : [ "WATER" ], "shortIdentifier" : "wt", @@ -156,7 +156,7 @@ "moveCost" : -1, "minimapUnblocked" : [ 0, 0, 0 ], "minimapBlocked" : [ 0, 0, 0 ], - "music" : "Underground.mp3", // Impossible in H3 + "music" : [ "Underground.mp3" ], // Impossible in H3 "tiles" : "ROCKTL", "type" : [ "ROCK" ], "shortIdentifier" : "rc", diff --git a/config/widgets/extraOptionsTab.json b/config/widgets/extraOptionsTab.json index f0b939192..8a3e3b74b 100644 --- a/config/widgets/extraOptionsTab.json +++ b/config/widgets/extraOptionsTab.json @@ -6,7 +6,7 @@ { "name": "background", "type": "picture", - "image": "ADVOPTBK", + "image": "AdventureOptionsBackgroundClear", "position": {"x": 0, "y": 6} }, { @@ -35,31 +35,6 @@ "rect": {"x": 60, "y": 48, "w": 320, "h": 0}, "adoptHeight": true }, - { - "type": "transparentFilledRectangle", - "rect": {"x": 54, "y": 127, "w": 335, "h": 2}, - "color": [24, 41, 90, 255] - }, - { - "type": "transparentFilledRectangle", - "rect": {"x": 159, "y": 90, "w": 2, "h": 38}, - "color": [24, 41, 90, 255] - }, - { - "type": "transparentFilledRectangle", - "rect": {"x": 235, "y": 90, "w": 2, "h": 38}, - "color": [24, 41, 90, 255] - }, - { - "type": "transparentFilledRectangle", - "rect": {"x": 311, "y": 90, "w": 2, "h": 38}, - "color": [24, 41, 90, 255] - }, - { - "type": "transparentFilledRectangle", - "rect": {"x": 55, "y": 556, "w": 335, "h": 19}, - "color": [24, 41, 90, 255] - }, { "name": "ExtraOptionsButtons", "type" : "verticalLayout", diff --git a/config/widgets/mapOverview.json b/config/widgets/mapOverview.json index 9aff4aab7..24c87ff37 100644 --- a/config/widgets/mapOverview.json +++ b/config/widgets/mapOverview.json @@ -7,7 +7,7 @@ "name": "background", "type": "texture", "image": "DIBOXBCK", - "rect": {"w": 428, "h": 379} + "rect": {"w": 428, "h": 429} }, { "type": "boxWithBackground", @@ -34,6 +34,15 @@ "text": "", "position": {"x": 214, "y": 40} }, + { + "type": "label", + "name": "version", + "font": "small", + "alignment": "right", + "color": "green", + "text": "", + "position": {"x": 418, "y": 48} + }, { "type": "boxWithBackground", "rect": {"x": 5, "y": 55, "w": 418, "h": 20} @@ -115,12 +124,37 @@ "font": "medium", "alignment": "center", "color": "yellow", - "text": "vcmi.lobby.filepath", + "text": "vcmi.lobby.author", "position": {"x": 214, "y": 314} }, { "type": "boxWithBackground", - "rect": {"x": 5, "y": 329, "w": 418, "h": 45} + "rect": {"x": 5, "y": 329, "w": 418, "h": 20} + }, + { + "type": "label", + "name": "author", + "font": "small", + "alignment": "center", + "color": "white", + "text": "", + "position": {"x": 214, "y": 339} + }, + { + "type": "boxWithBackground", + "rect": {"x": 5, "y": 354, "w": 418, "h": 20} + }, + { + "type": "label", + "font": "medium", + "alignment": "center", + "color": "yellow", + "text": "vcmi.lobby.filepath", + "position": {"x": 214, "y": 364} + }, + { + "type": "boxWithBackground", + "rect": {"x": 5, "y": 379, "w": 418, "h": 45} }, { "type": "textBox", @@ -129,7 +163,7 @@ "alignment": "center", "color": "white", "text": "", - "rect": {"x": 10, "y": 334, "w": 408, "h": 35} + "rect": {"x": 10, "y": 384, "w": 408, "h": 35} } ], diff --git a/config/widgets/settings/adventureOptionsTab.json b/config/widgets/settings/adventureOptionsTab.json index 57886e5e2..3cddfb4ac 100644 --- a/config/widgets/settings/adventureOptionsTab.json +++ b/config/widgets/settings/adventureOptionsTab.json @@ -364,13 +364,25 @@ }, { "text": "vcmi.adventureOptions.leftButtonDrag.hover", - "created" : "desktop" + "created" : "keyboardMouse" }, { "text": "vcmi.adventureOptions.smoothDragging.hover" } ] }, + { + "type": "verticalLayout", + "customType": "labelDescription", + "position": {"x": 225, "y": 415}, + "items": + [ + { + "text": "vcmi.adventureOptions.rightButtonDrag.hover", + "created" : "keyboardMouse" + } + ] + }, { "type": "verticalLayout", "customType": "checkbox", @@ -411,7 +423,7 @@ "name": "leftButtonDragCheckbox", "help": "vcmi.adventureOptions.leftButtonDrag", "callback": "leftButtonDragChanged", - "created" : "desktop" + "created" : "keyboardMouse" }, { "name": "smoothDraggingCheckbox", @@ -419,6 +431,20 @@ "callback": "smoothDraggingChanged" } ] + }, + { + "type": "verticalLayout", + "customType": "checkbox", + "position": {"x": 190, "y": 413}, + "items": + [ + { + "name": "rightButtonDragCheckbox", + "help": "vcmi.adventureOptions.rightButtonDrag", + "callback": "rightButtonDragChanged", + "created" : "keyboardMouse" + } + ] } ] } diff --git a/config/widgets/settings/battleOptionsTab.json b/config/widgets/settings/battleOptionsTab.json index 08480233f..0fdcd7757 100644 --- a/config/widgets/settings/battleOptionsTab.json +++ b/config/widgets/settings/battleOptionsTab.json @@ -6,11 +6,6 @@ "items": [ - { - "name": "lineCreatureInfo", - "type": "horizontalLine", - "rect": { "x" : 5, "y" : 289, "w": 365, "h": 3} - }, { "name": "lineAnimationSpeed", "type": "horizontalLine", @@ -21,11 +16,6 @@ "text": "core.genrltxt.396", // Auto-combat options "position": {"x": 380, "y": 55} }, - { - "type" : "labelTitle", - "text": "core.genrltxt.397", // Creature info - "position": {"x": 10, "y": 265} - }, /////////////////////////////////////// Right section - Auto-combat settings (NOT IMPLEMENTED) { "name": "autoCombatLabels", @@ -105,32 +95,6 @@ ] }, /////////////////////////////////////// Left section - checkboxes - { - "name": "creatureInfoLabels", - "type" : "verticalLayout", - "customType" : "labelDescription", - "position": {"x": 45, "y": 295}, - "items": - [ - { - "text": "core.genrltxt.402", - }, - { - "text": "core.genrltxt.403", - } - ] - }, - { - "name": "creatureInfoCheckboxes", - "type" : "verticalLayout", - "customType" : "checkboxFake", - "position": {"x": 10, "y": 293}, - "items": - [ - {}, - {} - ] - }, { "name": "generalOptionsLabels", "type" : "verticalLayout", @@ -153,6 +117,9 @@ { "text": "vcmi.battleOptions.showStickyHeroInfoWindows.hover", }, + { + "text": "vcmi.battleOptions.showQuickSpell.hover", + }, { "text": "core.genrltxt.406", }, @@ -192,6 +159,11 @@ "help": "vcmi.battleOptions.showStickyHeroInfoWindows", "callback": "showStickyHeroWindowsChanged" }, + { + "name": "showQuickSpellCheckbox", + "help": "vcmi.battleOptions.showQuickSpell", + "callback": "showQuickSpellChanged" + }, { "name": "mouseShadowCheckbox", "help": "core.help.429", diff --git a/debian/changelog b/debian/changelog index 58b0f9705..1f3298ff6 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +vcmi (1.6.0) jammy; urgency=medium + + * New upstream release + + -- Ivan Savenko Fri, 20 Dec 2024 12:00:00 +0200 + vcmi (1.5.7) jammy; urgency=medium * New upstream release diff --git a/docs/Readme.md b/docs/Readme.md index 8b4a392c2..4d2fcd7b9 100644 --- a/docs/Readme.md +++ b/docs/Readme.md @@ -1,28 +1,25 @@ -[![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](https://github.com/vcmi/vcmi/actions/workflows/github.yml/badge.svg?branch=develop&event=push)](https://github.com/vcmi/vcmi/actions/workflows/github.yml?query=branch%3Adevelop+event%3Apush) +[![Github Downloads](https://img.shields.io/github/downloads/vcmi/vcmi/1.6.0/total)](https://github.com/vcmi/vcmi/releases/tag/1.6.0) +[![Github Downloads](https://img.shields.io/github/downloads/vcmi/vcmi/total)](https://github.com/vcmi/vcmi/releases) + VCMI is an open-source recreation of Heroes of Might & Magic III engine, giving it new and extended possibilities.

-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 +28,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

@@ -52,12 +52,15 @@ Please see corresponding installation guide articles for details for your platfo - [Cheat codes](players/Cheat_Codes.md) - [Privacy Policy](players/Privacy_Policy.md) +## Documentation and guidelines for translators + +- [Translations](translators/Translations.md) + ## Documentation and guidelines for game modders - [Modding Guidelines](modders/Readme.md) - [Mod File Format](modders/Mod_File_Format.md) - [Bonus Format](modders/Bonus_Format.md) -- [Translations](modders/Translations.md) - [Map Editor](modders/Map_Editor.md) - [Campaign Format](modders/Campaign_Format.md) - [Configurable Widgets](modders/Configurable_Widgets.md) @@ -65,6 +68,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) @@ -73,6 +77,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) @@ -90,6 +95,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 93117004e..6fd7910ab 100644 --- a/docs/developers/AI.md +++ b/docs/developers/AI.md @@ -1,21 +1,24 @@ +# AI + There are two types of AI: adventure and battle. **Adventure AIs** are responsible for moving heroes across the map and developing towns **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 *. -Units have bonuses. Nearly everything aspect of a unit is configured in the form of bonuses. Attack, defense, health, retalitation, 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. +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. -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: @@ -30,3 +33,75 @@ BattleAI's most important classes are the following: - BattleEvaluator - is a top level logic layer which also adds spellcasts and movement to unreachable targets BattleAI itself handles all the rest and issues actual commands + +## Nullkiller AI + +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 + +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 +* Pathfinder - core thing used to calculate paths including bypassing monsters, quests, using advmap spells +* Graph - experimental thing connecting all objects into a network by paths (using common hero characteristics), does not work without maphack, allows simplified faster paths calculation. Possibly can be used for something else +* ArmyManager - for now only helps calculating best army from two CreatureSet objects. Later may be responsible for forming ideal army for each main hero so that we know what we need to build and buy. +* ObjectClusterizer - aggregates all objects into clusters depending on which object blocks way towards them. +* DeepDecomposer - sometimes pathfinder may return path through some object which canno be simply bypassed but instead it requires something to be done first. DeepDecomposer allows to detalizing such paths. Examples: building a boat requires capturing shipyard, bypassing bordergate requires visiting masterkey tent. See AbstractGoal +* FuzzyEngines - looks like some legacy from VCAI +* 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 +* BuyArmy - buys specific amount of army in AIValue in specific town +* DigAtTile - for grail, not implemented yet +* DismissHero - sometimes we may want to get rid of some scout +* ExchangeSwapTownHeroes - puts specifc hero in garrison or extracts hero from garrison. Also makes possible upgrades and buys army in town (used by defence and startup behaviors) +* ExecuteHeroChain - moves hero accross some path (or a few heroes forming a chain in order to move army to the target hero), can bypass simple obstacles like monsters, garrisons +* ExploreNeighborTile - after AI visits initial tile for exploration - makes a few sequential explorations of nearby tiles to save some performance +* RecruitHero - recruits specific hero in specifc town +* SaveResources - locks some resources for later use during next day +* 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 +* BuyArmyBehavior - buys army in towns +* GatherArmyBehavior - picks army from towns and brings it to main hero by scout, or main itslef goes for it +* RecruitHeroBehavior - recruits hero it it is either stronger than any main or there is something to gather +* StartupBehavior - scripted behavior which helps a bit on the first day. It keeps main hero in town garrison and accumulates army from initial heroes bought in tavern +* StayAtTownBehavior - stay at town to gain mana from mage guild +* 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. +Sometimes such decomposition may form a loop of abstract goals and will be discarded in such case. Generally the current architecture attempts to avoid decomposition as quite a heavy operation. + +Composition - a goal which can be both elementar (a set of tasks) or abstract (contains unresolved abstract goal at the end). Compositions express a chain of tasks in order to achieve some reward. They consist of sequences. Each sequence is a vector of goals. Only last sequence is actually executed or decomposed. All the rest adds value to reward evaluator. + +Marker - a goal used to just add value (reward) into some composition. We want to capture some shipyard not just because but in order to capture a town (or something else) later. Thus when we are capturing a shipyard we should know that later we will unlock town so we contribute towards town reward as well. diff --git a/docs/developers/Bonus_System.md b/docs/developers/Bonus_System.md index 370bf9ea4..c3b86a646 100644 --- a/docs/developers/Bonus_System.md +++ b/docs/developers/Bonus_System.md @@ -1,11 +1,13 @@ +# Bonus System + The bonus system of VCMI is a set of mechanisms that make handling of different bonuses for heroes, towns, players and units easier. The system consists of a set of nodes representing objects that can be a source or a subject of a bonus and two directed acyclic graphs (DAGs) representing inheritance and propagation of bonuses. Core of bonus system is defined in HeroBonus.h file. ## Propagation and inheritance 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. @@ -13,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 @@ -24,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). @@ -52,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 +```json "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 13db490aa..c2517ba26 100644 --- a/docs/developers/Building_Android.md +++ b/docs/developers/Building_Android.md @@ -1,26 +1,28 @@ -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. +# Building Android + +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: -``` +```sh git clone --recurse-submodules https://github.com/vcmi/vcmi.git ``` @@ -29,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) @@ -36,10 +39,10 @@ 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) -``` +```sh conan install -c tools.android:ndk_path=/path/to/ndk ... ``` diff --git a/docs/developers/Building_Linux.md b/docs/developers/Building_Linux.md index 68284a98a..7fa79e487 100644 --- a/docs/developers/Building_Linux.md +++ b/docs/developers/Building_Linux.md @@ -1,27 +1,27 @@ -# Compiling VCMI +# Building VCMI for Linux - Current baseline requirement for building is Ubuntu 20.04 - Supported C++ compilers for UNIX-like systems are GCC 9+ and Clang 13+ Older distributions and compilers might work, but they aren't tested by Github CI (Actions) -# Installing dependencies +## Installing dependencies -## Prerequisites +### Prerequisites 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) +### On Debian-based systems (e.g. Ubuntu) For Ubuntu and Debian you need to install this list of packages: @@ -31,101 +31,123 @@ Alternatively if you have VCMI installed from repository or PPA you can use: `sudo apt-get build-dep vcmi` -## On RPM-based distributions (e.g. Fedora) +### On RPM-based distributions (e.g. Fedora) `sudo yum install cmake gcc-c++ SDL2-devel SDL2_image-devel SDL2_ttf-devel SDL2_mixer-devel boost boost-devel boost-filesystem boost-system boost-thread boost-program-options boost-locale boost-iostreams zlib-devel ffmpeg-devel ffmpeg-libs qt5-qtbase-devel tbb-devel luajit-devel liblzma-devel libsqlite3-devel fuzzylite-devel ccache` NOTE: `fuzzylite-devel` package is no longer available in recent version of Fedora, for example Fedora 38. It's not a blocker because VCMI bundles fuzzylite lib in its source code. -## On Arch-based distributions +### On Arch-based distributions On Arch-based distributions, there is a development package available for VCMI on the AUR. -It can be found at: +It can be found at Information about building packages from the Arch User Repository (AUR) can be found at the Arch wiki. -# Getting the sources +### On NixOS or Nix + +On NixOS or any system with nix available, [it is recommended](https://nixos.wiki/wiki/C) to use nix-shell. Create a shell.nix file with the following content: + +```nix +with import {}; +stdenv.mkDerivation { + name = "build"; + nativeBuildInputs = [ cmake ]; + buildInputs = [ + cmake clang clang-tools llvm ccache ninja + boost zlib minizip xz + SDL2 SDL2_ttf SDL2_net SDL2_image SDL2_sound SDL2_mixer SDL2_gfx + ffmpeg tbb vulkan-headers libxkbcommon + qt6.full luajit + ]; +} +``` + +And put it into build directory. Then run `nix-shell` before running any build commands. + +## Getting the sources We recommend the following directory structure: - . - ├── vcmi -> contains sources and is under git control - └── build -> contains build output, makefiles, object files,... +```text +. +├── vcmi -> contains sources and is under git control +└── build -> contains build output, makefiles, object files,... +``` -Out-of-source builds keep the local repository clean so one doesn't have to manually exclude files generated during the build from commits. - -You can get latest sources with: +You can get the latest source code with: `git clone -b develop --recursive https://github.com/vcmi/vcmi.git` -# Compilation +## Compilation -## Configuring Makefiles +### Configuring Makefiles ```sh -mkdir build && cd build +mkdir build +cd build cmake -S ../vcmi ``` -# Additional options that you may want to use: +> [!NOTE] +> The `../vcmi` is not a typo, it will place Makefiles into the build dir as the build dir is your working dir when calling CMake. -## To enable debugging: -`cmake -S ../vcmi -D CMAKE_BUILD_TYPE=Debug` +See [CMake](CMake.md) for a list of options -**Notice**: The ../vcmi/ is not a typo, it will place makefile scripts into the build dir as the build dir is your working dir when calling CMake. +### Trigger build -## To use ccache: -`cmake -S ../vcmi -D ENABLE_CCACHE:BOOL=ON` +```sh +cmake --build . -j8 +``` -## Trigger build +(-j8 = compile with 8 threads, you can specify any value. ) -`cmake --build . -- -j2` -(-j2 = compile with 2 threads, you can specify any value) +This will generate `vcmiclient`, `vcmiserver`, `vcmilauncher` as well as .so libraries in the `build/bin/` directory. -That will generate vcmiclient, vcmiserver, vcmilauncher as well as .so libraries in the **build/bin/** directory. +## Packaging -# Package building +### RPM package -## RPM package +The first step is to prepare a RPM build environment. On Fedora systems you can follow this guide: -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 - -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 ``` -NOTE: the stock ffmpeg from Fedora repo is no good as it has stripped lots of codecs +> [!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 @@ -136,7 +158,7 @@ For other distributions that uses RPM, chances are there might be a spec file fo Available root environments and their names are listed in /etc/mock. -## Debian/Ubuntu +### Debian/Ubuntu 1. Install debhelper and devscripts packages diff --git a/docs/developers/Building_Windows.md b/docs/developers/Building_Windows.md index 257ef14ba..8df20783d 100644 --- a/docs/developers/Building_Windows.md +++ b/docs/developers/Building_Windows.md @@ -1,7 +1,10 @@ -# Preparations +# Building VCMI for Windows + +## Preparations + Windows builds can be made in more than one way and with more than one tool. This guide focuses on the simplest building process using Microsoft Visual Studio 2022 -# Prerequisites +## Prerequisites - Windows Vista or newer. - [Microsoft Visual Studio](https://visualstudio.microsoft.com/downloads/) @@ -9,64 +12,69 @@ 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 +### Choose an installation directory Create a directory for VCMI development, eg. `C:\VCMI` We will call this directory `%VCMI_DIR%` 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)` -# Install VCMI dependencies +## Install VCMI dependencies You have two options: to use pre-built libraries or build your own. We strongly recommend start with using pre-built ones. -## Option A. Use pre-built Vcpkg +### Option A. Use pre-built Vcpkg -### Download and unpack archive +#### 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 +#### 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%` -## Option B. Build Vcpkg on your own +### Option B. Build Vcpkg on your own Please be aware that if you're running 32-bit Windows version, then this is impossible due to Be aware that building Vcpkg might take a lot of time depend on your CPU model and 10-20GB of disk space. -### Create initial directory +#### Create initial directory -### Clone vcpkg +#### 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 +#### 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` @@ -75,13 +83,14 @@ From command line use: For the list of the packages used you can also consult [vcmi-deps-windows readme](https://github.com/vcmi/vcmi-deps-windows) in case this article gets outdated a bit. -# Install CCache +## Install CCache Extract `ccache` to a folder of your choosing, add the folder to the `PATH` environment variable and log out and back in. -# Build VCMI +## Build VCMI #### From GIT GUI + - Open SourceTree - File -> Clone - select `https://github.com/vcmi/vcmi/` as source @@ -91,33 +100,37 @@ 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 +### 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 +### 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/ +### Compile VCMI with MinGW via MSYS2 + +- 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. -# Create VCMI installer (This step is not required for just building & development) +## Create VCMI installer (This step is not required for just building & development) Make sure NSIS is installed to default directory or have registry entry so CMake can find it. After you build VCMI execute following commands from `%VCMI_DIR%/build`. @@ -125,14 +138,16 @@ After you build VCMI execute following commands from `%VCMI_DIR%/build`. - for release build: `cpack` - for debug build: `cpack -C Debug` -# Troubleshooting and workarounds +## Troubleshooting and workarounds Vcpkg might be very unstable due to limited popularity and fact of using bleeding edge packages (such as most recent Boost). Using latest version of dependencies could also expose both problems in VCMI code or library interface changes that developers not checked yet. So if you're built Vcpkg yourself and can't get it working please try to use binary package. 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 6a0f97b8c..f95f8f294 100644 --- a/docs/developers/Building_iOS.md +++ b/docs/developers/Building_iOS.md @@ -1,16 +1,18 @@ +# Building VCMI for iOS + ## Requirements 1. **macOS** 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 Clone with submodules. Example for command line: -``` +```sh git clone --recurse-submodules https://github.com/vcmi/vcmi.git ``` @@ -34,9 +36,8 @@ There're a few [CMake presets](https://cmake.org/cmake/help/latest/manual/cmake- Open terminal and `cd` to the directory with source code. Configuration example for device with Conan: -``` -cmake --preset ios-device-conan \ - -D BUNDLE_IDENTIFIER_PREFIX=com.MY-NAME +```sh +cmake --preset ios-device-conan -D BUNDLE_IDENTIFIER_PREFIX=com.MY-NAME ``` By default build directory containing Xcode project will appear at `../build-ios-device-conan`, but you can change it with `-B` option. @@ -45,7 +46,7 @@ If you want to speed up the recompilation, add `-D ENABLE_CCACHE=ON` ### Building for device -To be able to build for iOS device, you must also specify codesigning settings. If you don't know your development team ID, open the generated Xcode project, open project settings (click **VCMI** with blue icon on the very top in the left panel with files), select **vcmiclient** target, open **Signing & Capabilities** tab and select yout team. Now you can copy the value from **Build Settings** tab - `DEVELOPMENT_TEAM` variable (paste it in the Filter field on the right) - click the greenish value - Other... - copy. Now you can pass it in `CMAKE_XCODE_ATTRIBUTE_DEVELOPMENT_TEAM` variable when configuring the project to avoid selecting the team manually every time CMake re-generates the project. +To be able to build for iOS device, you must also specify codesigning settings. If you don't know your development team ID, open the generated Xcode project, open project settings (click **VCMI** with blue icon on the very top in the left panel with files), select **vcmiclient** target, open **Signing & Capabilities** tab and select your team. Now you can copy the value from **Build Settings** tab - `DEVELOPMENT_TEAM` variable (paste it in the Filter field on the right) - click the greenish value - Other... - copy. Now you can pass it in `CMAKE_XCODE_ATTRIBUTE_DEVELOPMENT_TEAM` variable when configuring the project to avoid selecting the team manually every time CMake re-generates the project. Advanced users who know exact private key and provisioning profile to sign with, can use `CMAKE_XCODE_ATTRIBUTE_CODE_SIGN_IDENTITY` and `CMAKE_XCODE_ATTRIBUTE_PROVISIONING_PROFILE_SPECIFIER` variables instead. In this case you must also pass `-D CMAKE_XCODE_ATTRIBUTE_CODE_SIGN_STYLE=Manual`. @@ -59,7 +60,7 @@ You must also install game files, see [Installation on iOS](../players/Installat ### From command line -``` +```sh cmake --build --target vcmiclient -- -quiet ``` diff --git a/docs/developers/Building_macOS.md b/docs/developers/Building_macOS.md index 67571f74d..b5a309581 100644 --- a/docs/developers/Building_macOS.md +++ b/docs/developers/Building_macOS.md @@ -1,3 +1,5 @@ +# Building VCMI for macOS + ## Requirements 1. C++ toolchain, either of: @@ -13,7 +15,7 @@ Clone with submodules. Example for command line: -``` +```sh git clone --recurse-submodules https://github.com/vcmi/vcmi.git ``` @@ -89,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 new file mode 100644 index 000000000..e4adadd48 --- /dev/null +++ b/docs/developers/CMake.md @@ -0,0 +1,21 @@ +# CMake options + +* `-D CMAKE_BUILD_TYPE=Debug` + * 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 + ```text + CompileFlags: + CompilationDatabase: build + ``` + and place it here: + ```text + . + ├── vcmi -> contains sources and is under git control + ├── build -> contains build output, makefiles, object files,... + └── .clangd + ``` +* `-D ENABLE_CCACHE:BOOL=ON` + * Speeds up recompilation +* `-G Ninja` + * 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 72f11ba0e..ee3384670 100644 --- a/docs/developers/Code_Structure.md +++ b/docs/developers/Code_Structure.md @@ -1,6 +1,8 @@ +# Code Structure + The code of VCMI is divided into several main parts: client, server, lib and AIs, each one in a separate binary file. -# The big picture +## The big picture VCMI contains three core projects: VCMI_lib (dll / so), VCMI_client (executable) and VCMI_server (executable). Server handles all game mechanics and events. Client presents game state and events to player and collects input from him. @@ -8,48 +10,49 @@ During the game, we have one (and only one) server and one or more (one for each Important: State of the game and its mechanics are synchronized between clients and server. All changes to the game state or mechanics must be done by server which will send appropriate notices to clients. -## Game state +### Game state It's basically CGameState class object and everything that's accessible from it: map (with objects), player statuses, game options, etc. -## Bonus system +### Bonus system One of the more important pieces of VCMI is the [bonus system](Bonus_System.md). It's described in a separate article. -## Configuration +### Configuration Most of VCMI configuration files uses Json format and located in "config" directory -### Json parser and writer +#### Json parser and writer -# Client +## Client -## Main purposes of client +### 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 -## Rendering of graphics +- 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 relies heavily on SDL. Currently we do not have any wrapper for SDL internal structures and most of rendering is about blitting surfaces using SDL_BlitSurface. We have a few function that make rendering easier or make specific parts of rendering (like printing text). They are places in client/SDL_Extensions and client/SDL_Framerate (the second one contains code responsible for keeping appropriate framerate, it should work more smart than just SDL_Delay(miliseconds)). +### Rendering of graphics + +Rendering of graphics relies heavily on SDL. Currently we do not have any wrapper for SDL internal structures and most of rendering is about blitting surfaces using SDL_BlitSurface. We have a few function that make rendering easier or make specific parts of rendering (like printing text). They are places in client/SDL_Extensions and client/SDL_Framerate (the second one contains code responsible for keeping appropriate framerate, it should work more smart than just SDL_Delay(milliseconds)). In rendering, Interface object system is quite helpful. Its base is CIntObject class that is basically a base class for our library of GUI components and other objects. -# Server +## Server -## Main purposes of server +### Main purposes of server 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 +## Lib -## Main purposes of lib +### Main purposes of lib VCMI_Lib is a library that contains code common to server and client, so we avoid it's duplication. Important: the library code is common for client and server and used by them, but the library instance (in opposition to the library as file) is not shared by them! Both client and server create their own "copies" of lib with all its class instances. @@ -57,22 +60,22 @@ 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 +#### Serialization The serialization framework can serialize basic types, several standard containers among smart pointers and custom objects. Its design is based on the [boost serialization libraries](http://www.boost.org/doc/libs/1_52_0/libs/serialization/doc/index.html). In addition to the basic functionality it provides light-weight transfer of CGObjectInstance objects by sending only the index/id. Serialization page for all the details. -### Wrapped namespace examples +#### Wrapped namespace examples -#### Inside the lib +##### Inside the lib Both header and implementation of a new class inside the lib should have the following structure: @@ -81,7 +84,7 @@ Both header and implementation of a new class inside the lib should have the fol `` `VCMI_LIB_NAMESPACE_END` -#### Headers outside the lib +##### Headers outside the lib Forward declarations of the lib in headers of other parts of the project need to be wrapped in the macros: @@ -92,31 +95,30 @@ Forward declarations of the lib in headers of other parts of the project need to `` `` - -#### New project part +##### 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: -# Artificial Intelligence +## Artificial Intelligence -## StupidAI +### StupidAI Stupid AI is recent and used battle AI. -## Adventure AI +### Adventure AI VCAI module is currently developed agent-based system driven by goals and heroes. -## Programming challenge +### Programming challenge -## Fuzzy logic +### Fuzzy logic -VCMI includes [FuzzyLite](http://code.google.com/p/fuzzy-lite/) library to make use of fuzzy rule-based algorithms. They are useful to handle uncertanity and resemble human behaviour who takes decisions based on rough observations. FuzzyLite is linked as separate static library in AI/FuzzyLite.lib file. +VCMI includes [FuzzyLite](http://code.google.com/p/fuzzy-lite/) library to make use of fuzzy rule-based algorithms. They are useful to handle uncertainty and resemble human behaviour who takes decisions based on rough observations. FuzzyLite is linked as separate static library in AI/FuzzyLite.lib file. -# Utilities +## Utilities -## Launcher +### Launcher -## Duels +### 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 a7537acbe..5c1706cc3 100644 --- a/docs/developers/Coding_Guidelines.md +++ b/docs/developers/Coding_Guidelines.md @@ -1,8 +1,10 @@ +# Coding Guidelines + ## C++ Standard 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 @@ -18,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(); @@ -28,7 +30,7 @@ if(a) Bad: -``` cpp +```cpp if(a) { code(); code(); @@ -39,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(); } @@ -56,7 +58,7 @@ Unless there are either multiple hierarchical conditions being used or that the Good: -``` cpp +```cpp if(a) { if(b) @@ -66,7 +68,7 @@ if(a) Bad: -``` cpp +```cpp if(a) if(b) code(); @@ -76,7 +78,7 @@ If there are brackets inside the body, outside brackets are required. Good: -``` cpp +```cpp if(a) { for(auto elem : list) @@ -88,7 +90,7 @@ if(a) Bad: -``` cpp +```cpp if(a) for(auto elem : list) { @@ -100,7 +102,7 @@ If "else" branch has brackets then "if" should also have brackets even if it is Good: -``` cpp +```cpp if(a) { code(); @@ -116,7 +118,7 @@ else Bad: -``` cpp +```cpp if(a) code(); else @@ -132,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(); @@ -146,7 +148,7 @@ else Bad: -``` cpp +```cpp if(a) code(); else @@ -158,7 +160,7 @@ When defining a method, use a new line for the brace, like this: Good: -``` cpp +```cpp void method() { } @@ -166,7 +168,7 @@ void method() Bad: -``` cpp +```cpp void Method() { } ``` @@ -177,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; ``` @@ -197,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]; ``` @@ -212,14 +214,14 @@ Do not use spaces before parentheses. Good: -``` cpp +```cpp if(a) code(); ``` Bad: -``` cpp +```cpp if (a) code(); ``` @@ -228,7 +230,7 @@ Do not use extra spaces around conditions inside parentheses. Good: -``` cpp +```cpp if(a && b) code(); @@ -238,7 +240,7 @@ if(a && (b || c)) Bad: -``` cpp +```cpp if( a && b ) code(); @@ -250,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(); @@ -271,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; ``` @@ -287,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); ``` @@ -303,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; @@ -319,7 +321,7 @@ When 'private:', 'public:' and other labels are not on the line after opening br Good: -``` cpp +```cpp class CClass { int id; @@ -331,7 +333,7 @@ public: Bad: -``` cpp +```cpp class CClass { int id; @@ -342,7 +344,7 @@ public: Good: -``` cpp +```cpp class CClass { protected: @@ -355,7 +357,7 @@ public: Bad: -``` cpp +```cpp class CClass { @@ -371,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) { @@ -385,7 +387,7 @@ Switch statements have the case at the same indentation as the switch. Good: -``` cpp +```cpp switch(alignment) { case EAlignment::EVIL: @@ -405,7 +407,7 @@ default: Bad: -``` cpp +```cpp switch(alignment) { case EAlignment::EVIL: @@ -445,7 +447,7 @@ break; Good: -``` cpp +```cpp auto lambda = [this, a, &b](int3 & tile, int index) -> bool { do_that(); @@ -454,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();}; ``` @@ -462,7 +464,7 @@ Empty parameter list is required even if function takes no arguments. Good: -``` cpp +```cpp auto lambda = []() { do_that(); @@ -471,7 +473,7 @@ auto lambda = []() Bad: -``` cpp +```cpp auto lambda = [] { do_that(); @@ -482,7 +484,7 @@ Do not use inline lambda expressions inside if-else, for and other conditions. Good: -``` cpp +```cpp auto lambda = []() { do_that(); @@ -495,7 +497,7 @@ if(lambda) Bad: -``` cpp +```cpp if([]() { do_that(); @@ -509,7 +511,7 @@ Do not pass inline lambda expressions as parameter unless it's the last paramete Good: -``` cpp +```cpp auto lambda = []() { do_that(); @@ -519,7 +521,7 @@ obj->someMethod(lambda, true); Bad: -``` cpp +```cpp obj->someMethod([]() { do_that(); @@ -528,7 +530,7 @@ obj->someMethod([]() Good: -``` cpp +```cpp obj->someMethod(true, []() { do_that(); @@ -541,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; @@ -553,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; @@ -564,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; @@ -584,7 +586,7 @@ template void serialize(Handler & h, const int version) Bad: -``` cpp +```cpp template void serialize(Handler & h, const int version) { h & identifier; @@ -602,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 * @@ -620,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 * @@ -650,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 @@ -663,7 +665,7 @@ else // Do something. Bad: -``` cpp +```cpp if(a) { code();//Do something @@ -678,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) { @@ -690,7 +692,7 @@ if(a) Bad: -``` cpp +```cpp // Do something if(a) { @@ -704,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) @@ -715,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 { @@ -725,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; @@ -736,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 ///
/// /// @@ -747,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 @@ -759,7 +761,7 @@ The line length for c++ source code is 120 columns. If your function declaration ### Warnings -Avoid use of #pragma to disable warnings. Compile at warning level 3. Avoid commiting code with new warnings. +Avoid use of #pragma to disable warnings. Compile at warning level 3. Avoid committing code with new warnings. ### File/directory naming @@ -773,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); ``` @@ -789,20 +791,20 @@ The name of the type should be logged first, e.g. {TYPE_NAME: members...}. The m Avoid code duplication or don't repeat yourself(DRY) is the most important aspect in programming. Code duplication of any kind can lead to inconsistency and is much harder to maintain. If one part of the system gets changed you have to change the code in several places. This process is error-prone and leads often to problems. Here you can read more about the DRY principle: [](http://en.wikipedia.org/wiki/Don%27t_repeat_yourself) -### Do not use uncommon abbrevations +### Do not use uncommon abbreviations -Do not use uncommon abbrevations for class, method, parameter and global object names. +Do not use uncommon abbreviations for class, method, parameter and global object names. Bad: -``` cpp +```cpp CArt * getRandomArt(...) class CIntObject ``` Good: -``` cpp +```cpp CArtifact * getRandomArtifact(...) class CInterfaceObject ``` @@ -825,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, @@ -846,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?) ``` @@ -860,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 185c85af5..fb29795f8 100644 --- a/docs/developers/Conan.md +++ b/docs/developers/Conan.md @@ -1,4 +1,4 @@ -# Using dependencies from Conan +# Conan Dependencies [Conan](https://conan.io/) is a package manager for C/C++. We provide prebuilt binary dependencies for some platforms that are used by our CI, but they can also be consumed by users to build VCMI. However, it's not required to use only the prebuilt binaries: you can build them from source as well. @@ -27,12 +27,12 @@ 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: +2. Download the binaries archive and unpack it to `~/.conan` directory from - - [macOS](https://github.com/vcmi/vcmi-deps-macos/releases/latest): pick **intel.txz** if you have Intel Mac, otherwise - **intel-cross-arm.txz** - - [iOS](https://github.com/vcmi/vcmi-ios-deps/releases/latest) - - [Windows](https://github.com/vcmi/vcmi-deps-windows-conan/releases/latest): pick **vcmi-deps-windows-conan-w64.tgz** if you want x86_64, otherwise pick **vcmi-deps-windows-conan-w32.tgz** - - [Android](https://github.com/vcmi/vcmi-dependencies/releases): current limitation is that building works only on a macOS host due to Qt 5 for Android being compiled on macOS. Simply delete directory `~/.conan/data/qt/5.15.x/_/_/package` (`5.15.x` is a placeholder) after unpacking the archive and build Qt from source. Alternatively, if you have (or are [willing to build](https://github.com/vcmi/vcmi-ios-deps#note-for-arm-macs)) Qt host tools for your platform, then simply replace those in the archive with yours and most probably it would work. + - macOS: pick **dependencies-mac-intel.txz** if you have Intel Mac, otherwise - **dependencies-mac-arm.txz** + - iOS: pick ***dependencies-ios.txz*** + - Windows: currently only mingw is supported. Pick **dependencies-mingw.tgz** if you want x86_64, otherwise pick **dependencies-mingw-32.tgz** + - Android: current limitation is that building works only on a macOS host due to Qt 5 for Android being compiled on macOS. Simply delete directory `~/.conan/data/qt/5.15.x/_/_/package` (`5.15.x` is a placeholder) after unpacking the archive and build Qt from source. Alternatively, if you have (or are [willing to build](https://github.com/vcmi/vcmi-ios-deps#note-for-arm-macs)) Qt host tools for your platform, then simply replace those in the archive with yours and most probably it would work. 3. Only if you have Apple Silicon Mac and trying to build for macOS or iOS: @@ -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. @@ -126,7 +126,7 @@ In these examples only the minimum required amount of options is passed to `cmak ### Use our prebuilt binaries to build for macOS x86_64 with Xcode -``` +```sh conan install . \ --install-folder=conan-generated \ --no-imports \ @@ -143,7 +143,7 @@ cmake -S . -B build -G Xcode \ If you also want to build the missing binaries from source, use `--build=missing` instead of `--build=never`. -``` +```sh conan install . \ --install-folder=~/my-dir \ --no-imports \ @@ -158,7 +158,7 @@ cmake -S . -B build \ ### Use our prebuilt binaries to build for iOS arm64 device with custom preset -``` +```sh conan install . \ --install-folder=~/my-dir \ --no-imports \ @@ -205,7 +205,7 @@ ubuntu Next steps are identical both in WSL and in real Ubuntu 22.04 -```bash +```sh sudo pip3 install conan sudo apt install cmake build-essential sed -i 's/x86_64-w64-mingw32/i686-w64-mingw32/g' CI/mingw-ubuntu/before-install.sh diff --git a/docs/developers/Development_with_Qt_Creator.md b/docs/developers/Development_with_Qt_Creator.md index 1153ae016..66c621781 100644 --- a/docs/developers/Development_with_Qt_Creator.md +++ b/docs/developers/Development_with_Qt_Creator.md @@ -1,4 +1,4 @@ -# Introduction to Qt Creator +# Development with Qt Creator Qt Creator is the recommended IDE for VCMI development on Linux distributions, but it may be used on other operating systems as well. It has the following advantages compared to other IDEs: @@ -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 e2ead1fb6..5c43c91ed 100644 --- a/docs/developers/Logging_API.md +++ b/docs/developers/Logging_API.md @@ -1,28 +1,30 @@ -# Features +# Logging API -- 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 +## Features -# Class diagram +- 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 ![Logging_Class_Diagram](https://github.com/IvanSavenko/vcmi/assets/1576820/31c9b14e-a055-4b07-87fe-00da43430a2b) 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 +## Usage -## Setup settings.json +### Setup settings.json -``` javascript +```json { "logging" : { "console" : { @@ -51,7 +53,7 @@ Some notes: The above code is an example on how to configure logging. It sets the log level to debug globally and the log level of the domain ai to trace. In addition, it tells the console to log debug messages as well with the threshold attribute. Finally, it configures the console so that it logs network trace messages in magenta. -## Configuration +### Configuration The following code shows how the logging system can be configured: @@ -66,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 @@ -74,22 +76,23 @@ 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 +### 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")); ``` -## Logging +### Logging Logging can be done via two ways, stream-like or function-like. @@ -102,24 +105,24 @@ 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 +### How to trace execution The program execution can be traced by using the macros TRACE_BEGIN, TRACE_END and their \_PARAMS counterparts. This can be important if you want to analyze the operations/internal workings of the AI or the communication of the client-server. In addition, it can help you to find bugs on a foreign VCMI installation with a custom mod configuration. @@ -135,16 +138,16 @@ The program execution can be traced by using the macros TRACE_BEGIN, TRACE_END a } ``` -# Concepts +## Concepts -## Domain +### Domain 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: @@ -158,4 +161,4 @@ global, level=debug ai, level=not set, effective level=debug ai.battle, level=trace, effective level=trace -The same technique is applied to the console colors. If you want to have another debug color for the domain ai, you can explicitely set a color for that domain and level. +The same technique is applied to the console colors. If you want to have another debug color for the domain ai, you can explicitly set a color for that domain and level. diff --git a/docs/developers/Lua_Scripting_System.md b/docs/developers/Lua_Scripting_System.md index 2c4be7387..9968d7206 100644 --- a/docs/developers/Lua_Scripting_System.md +++ b/docs/developers/Lua_Scripting_System.md @@ -1,6 +1,8 @@ -# Configuration +# Lua Scripting System -``` javascript +## Configuration + +```json { //general purpose script, Lua or ERM, runs on server "myScript": @@ -32,13 +34,13 @@ } ``` -# Lua +## Lua -## API Reference +### API Reference TODO **In near future Lua API may change drastically several times. Information here may be outdated** -### Globals +#### Globals - DATA - persistent table - - DATA.ERM contains ERM state, anything else is free to use. @@ -61,9 +63,9 @@ TODO **In near future Lua API may change drastically several times. Information - -TODO require(":relative.path.to.module") - loads module from same mod - logError(text) - backup error log function -### Low level events API +#### Low level events API -``` Lua +```lua -- Each event type must be loaded first local PlayerGotTurn = require("events.PlayerGotTurn") @@ -79,81 +81,81 @@ sub2 = PlayerGotTurn.subscribeBefore(EVENT_BUS, function(event) end) ``` -### Lua standard library +#### Lua standard library VCMI uses LuaJIT, which is Lua 5.1 API, see [upstream documentation](https://www.lua.org/manual/5.1/manual.html) Following libraries are supported -- base -- table -- string -- math -- bit +- base +- table +- string +- math +- bit -# ERM +## ERM -## Features +### 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 +### Bugs -- TODO Broken XOR support (clashes with \`X\` option) +- TODO Broken XOR support (clashes with \`X\` option) -## Triggers +### 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 +### Receivers -### VCMI +#### 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 +#### ERA -- DONE !!if !!el !!en -- TODO !!br !!co -- TODO !!SN:X +- DONE !!if !!el !!en +- TODO !!br !!co +- TODO !!SN:X -### WoG +#### 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 Действия героя в бою @@ -199,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 7bcadc437..35407d496 100644 --- a/docs/developers/Networking.md +++ b/docs/developers/Networking.md @@ -1,14 +1,18 @@ -# The big picture +# Networking + +## The big picture 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: -``` + +```cpp 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. @@ -19,48 +23,54 @@ Following connections can be established during game lifetime: - game client -> lobby server: This connection is used to access global lobby, for multiplayer over Internet. Created when player logs into a lobby (Multiplayer -> Connect to global service) - match server -> lobby server: This connection is established when player creates new multiplayer room via lobby. It is used by lobby server to send commands to match server -# Gameplay communication +## Gameplay communication For gameplay, VCMI serializes data into a binary stream. See [Serialization](Serialization.md) for more information. -# Global lobby communication +## 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: -``` + +```cpp int32_t messageSize; char jsonString[messageSize]; ``` Every message must be a struct (json object) that contains "type" field. Unlike rest of VCMI codebase, this message is validated as strict json, without any extensions, such as comments. -## Communication flow +### 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 -### New Account Creation +#### New Account Creation - client -> lobby: `clientRegister` - lobby -> client: `accountCreated` -### Login +#### Login + - client -> lobby: `clientLogin` - lobby -> client: `loginSuccess` - lobby -> client: `chatHistory` - lobby -> client: `activeAccounts` - lobby -> client: `activeGameRooms` -### Chat Message +#### Chat Message + - client -> lobby: `sendChatMessage` - lobby -> every client: `chatMessage` -### New Game Room +#### New Game Room + - client starts match server instance - match -> lobby: `serverLogin` - lobby -> match: `loginSuccess` @@ -70,28 +80,33 @@ Notes: - lobby -> every client: `activeAccounts` - lobby -> every client: `activeGameRooms` -### Joining a game room +#### Joining a game room + See [#Proxy mode](proxy-mode) -### Leaving a game room +#### 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 +#### Logout + - client closes connection - lobby -> every client: `activeAccounts` -## Proxy mode +### Proxy mode 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 @@ -99,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 7a9d7810c..33dca93d7 100644 --- a/docs/developers/RMG_Description.md +++ b/docs/developers/RMG_Description.md @@ -1,46 +1,48 @@ -# Fundamentals +# RMG Description + +## Fundamentals Random maps are represented by undirected graph of zones linked with connections. On maps with water, a single extra water zone is created. -## Modifiers +### Modifiers Zone filling process is split into multiple phases, each of them represented as a modifier. A modifier can require other modifiers to finish their job before launching. A modifier might be preceded by other modifier from every zone, or many modifiers from all zones. For instance, placing underground rock requires all underground zones to finish treasure placement first. -## Thread pool +### Thread pool A queue of Modifiers jobs is created in roughly topological order, so that Modificators with no dependencies are placed first. The queue is iterated in a circular manner and if Modificator with no remaining preceders is found, it is picked for execution in a separate thread. After job completion, Modifier is erased from dependencies of Modifiers which depend on it. -# Placing Zones +## Placing Zones -## Generating distance graph +### Generating distance graph Based on zone connections, a simple distance graph is created using Dijkstra algorithm. -## Initial zone placement +### Initial zone placement Based on distance graph, zones are placed one by one on N x N grid of size just enough to fit all the zones (for instance, 5 zones are placed on a 3 x 3 grid and 24 zones on 5 x 5 grid). Adjacent zones are placed close while distant zones are placed as far away from each other as possible. -## Iterative optimization +### Iterative optimization Finally, zones are moved from their initial positions using Fruchterman-Reingold algorithm. It assumes all the zones are soft spheres, which attract connected zones as springs but push back not overlapping zones crossing their borders. These forces are summend and determine the vector shift of the zone position. The algorithm uses classic "simulated annealing" approach - zones start with high "temperature" (are very soft and squishy) and then gradually become colder (harder) and push away overlapping zones with stronger force. To prevent getting stuck in local minima, sometimes most misplaced zones swap placed manually. -## Penrose Tiling +### Penrose Tiling Using iterative subdivision, a set of Penrose tiling vertices ic created at random orientation and centered over middle of the map. All the tiles on a map are assigned to the closes vertex. Then, every vertex is assigned to the closest zone, creating irregular shapes. -# Zone connections +## Zone connections Directly adjacent zones are connected with a guard and a road, and overlapping zones on different levels are connected with Subterranean Gate. Zones which are not directly adjacent might be covered through water zone. If a zone shouldn't be connected with nearby zones adjacent to body of water, it's coast is sealed with obstacles. -## Water routes +### Water routes **TODO** -## Fractalization +### Fractalization Every zone starts with at least one free tile in the center. Now, from every tiles at a distant greater than some number, a random tile is chosen. From it, algorithm routes a free path connected to already existing free paths. Process is repetaed until no tiles distant from free paths are left. Tiles used for connections are marked `free` and nothing else can be placed on them. @@ -48,28 +50,28 @@ Zones with type `junction` are not fractalized. The remaining tiles that are not obstacles (such as zone edges) are marked as `possible"` so they can be either filled with treasures or left `free.` -# Treasures +## Treasures Every object or treasure pile in the zone is placed as far away from existing objects as possible. This includes towns and zone guards placed first. Zone keeps a priority queue of tiles sorted by their distance to closest object. Whenever an object is placed, these distances are updated. -## Treasure generation +### Treasure generation Treasures are separated in value ranges. The highest range is picked first. A large number of treasure piles is generated, then RMG tries to fit each of treasure piles into a zone. Then, lower treasure ranges are generated -## Treasure placement +### Treasure placement New treasure pile can't be closer to any previous object than some distance determined by total treasure density and value. Lower value piles and placed with lower minimal distance. Any new treasure is placed so that it can't join two previously separated blocked islands, to prevent sealing a gap and ensuring entire zone is passable. -# Obstacles +## Obstacles After all the treasures are placed, tiles marked as `possible` are iteratively stripped and cleared from lose appendages, leaving `free` space. Then remaining ones are marked as `blocked`, and covered with obstacles. -## Biomes +### Biomes For every zone, a few random obstacle sets are selected. [Details](https://github.com/vcmi/vcmi/blob/develop/docs/modders/Entities_Format/Biome_Format.md) -## Filling space +### 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 4f805af28..56b428f1e 100644 --- a/docs/developers/Serialization.md +++ b/docs/developers/Serialization.md @@ -1,24 +1,26 @@ -# Introduction +# Serialization + +## Introduction The serializer translates between objects living in our code (like int or CGameState\*) and stream of bytes. Having objects represented as a stream of bytes is useful. Such bytes can send through the network connection (so client and server can communicate) or written to the disk (savegames). VCMI uses binary format. The primitive types are simply copied from memory, more complex structures are represented as a sequence of primitives. -## Typical tasks +### Typical tasks -### Bumping a version number +#### Bumping a version number Different major version of VCMI likely change the format of the save game. Every save game needs a version identifier, that loading can work properly. Backward compatibility isn't supported for now. The version identifier is a constant named version in Connection.h and should be updated every major VCMI version or development version if the format has been changed. Do not change this constant if it's not required as it leads to full rebuilds. Why should the version be updated? If VCMI cannot detect "invalid" save games the program behaviour is random and undefined. It mostly results in a crash. The reason can be anything from null pointer exceptions, index out of bounds exceptions(ok, they aren't available in c++, but you know what I mean:) or invalid objects loading(too much elements in a vector, etc...) This should be avoided at least for public VCMI releases. -### Adding a new class +#### Adding a new class If you want your class to be serializable (eg. being storable in a savegame) you need to define a serialize method template, as described in [#User types](#user-types) Additionally, if your class is part of one of registered object hierarchies (basically: if it derives from CGObjectInstance, IPropagator, ILimiter, CBonusSystemNode, CPack) it needs to be registered. Just add an appropriate entry in the `RegisterTypes.h` file. See polymorphic serialization for more information. -# How does it work +## How does it work -## Primitive types +### Primitive types They are simply stored in a binary form, as in memory. Compatibility is ensued through the following means: @@ -27,21 +29,21 @@ They are simply stored in a binary form, as in memory. Compatibility is ensued t It's not "really" portable, yet it works properly across all platforms we currently support. -## Dependant types +### Dependant types -### Pointers +#### Pointers Storing pointers mechanics can be and almost always is customized. See [#Additional features](additional-features). In the most basic form storing pointer simply sends the object state and loading pointer allocates an object (using "new" operator) and fills its state with the stored data. -### Arrays +#### Arrays Serializing array is simply serializing all its elements. -## Standard library types +### Standard library types -### STL Containers +#### STL Containers First the container size is stored, then every single contained element. @@ -56,7 +58,7 @@ Supported STL types include: `pair` `map` -### Smart pointers +#### Smart pointers Smart pointers at the moment are treated as the raw C-style pointers. This is very bad and dangerous for shared_ptr and is expected to be fixed somewhen in the future. @@ -65,20 +67,20 @@ The list of supported data types from standard library: `shared_ptr (partial!!!)` `unique_ptr` -### Boost +#### Boost Additionally, a few types for Boost are supported as well: `variant` `optional` -## User types +### User types To make the user-defined type serializable, it has to provide a template method serialize. The first argument (typed as template parameter) is a reference to serializer. The second one is version number. Serializer provides an operator& that is internally expanded to `<<` when serialziing or `>>` when deserializing. -Serializer provides a public bool field `saving`that set to true during serialziation and to false for deserialziation. +Serializer provides a public bool field `saving`that set to true during serialization and to false for deserialization. Typically, serializing class involves serializing all its members (given that they are serializable). Sample: @@ -98,7 +100,7 @@ struct DLL_LINKAGE Rumor }; ``` -## Backwards compatibility +### Backwards compatibility Serializer, before sending any data, stores its version number. It is passed as the parameter to the serialize method, so conditional code ensuring backwards compatibility can be added. @@ -126,21 +128,21 @@ struct DLL_LINKAGE Rumor }; ``` -## Serializer classes +### Serializer classes -### Common information +#### Common information Serializer classes provide iostream-like interface with operator `<<` for serialization and operator `>>` for deserialization. Serializer upon creation will retrieve/store some metadata (version number, endianness), so even if no object is actually serialized, some data will be passed. -### Serialization to file +#### Serialization to file CLoadFile/CSaveFile classes allow to read data to file and store data to file. They take filename as the first parameter in constructor and, optionally, the minimum supported version number (default to the current version). If the construction fails (no file or wrong file) the exception is thrown. -### Networking +#### Networking -See [Networking](Networking.md) +See [Networking](Networking.md) -## Additional features +### Additional features Here is the list of additional custom features serialzier provides. Most of them can be turned on and off. @@ -149,7 +151,7 @@ Here is the list of additional custom features serialzier provides. Most of them - Stack instance serialization — enabled by sendStackInstanceByIds flag. - Smart pointer serialization — enabled by smartPointerSerialization flag. -### Polymorphic serialization +#### Polymorphic serialization Serializer is to recognize the true type of object under the pointer if classes of that hierarchy were previously registered. @@ -170,7 +172,7 @@ Class hierarchies that are now registered to benefit from this feature are mostl It is crucial that classes are registered in the same order in the both serializers (storing and loading). -### Vectorized list member serialization +#### Vectorized list member serialization Both client and server store their own copies of game state and VLC (handlers with data from config). Many game logic objects are stored in the vectors and possess a unique id number that represent also their position in such vector. @@ -219,7 +221,7 @@ Important: this means that the object state is not serialized. This feature makes sense only for server-client network communication. -### Stack instance serialization +#### Stack instance serialization This feature works very much like the vectorised object serialization. It is like its special case for stack instances that are not vectorised (each hero owns its map). When this option is turned on, sending CStackInstance\* will actually send an owning object (town, hero, garrison, etc) id and the stack slot position. @@ -227,7 +229,7 @@ For this to work, obviously, both sides of the connection need to have exactly t This feature depends on vectorised member serialization being turned on. (Sending owning object by id.) -### Smart pointer serialization +#### Smart pointer serialization Note: name is unfortunate, this feature is not about smart pointers (like shared-ptr and unique_ptr). It is for raw C-style pointers, that happen to point to the same object. @@ -257,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 eaa6108e0..32456c1a4 100644 --- a/docs/maintainers/Release_Process.md +++ b/docs/maintainers/Release_Process.md @@ -1,10 +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`. @@ -12,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` @@ -32,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 @@ -43,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 220df1bff..1dbd6a7d0 100644 --- a/docs/maintainers/Ubuntu_PPA.md +++ b/docs/maintainers/Ubuntu_PPA.md @@ -1,4 +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) @@ -12,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 a4a0d9896..8b3c44082 100644 --- a/docs/modders/Animation_Format.md +++ b/docs/modders/Animation_Format.md @@ -1,12 +1,14 @@ +# Animation Format + 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 +```json { // Base path of all images in animation. Optional. // Can be used to avoid using long path to images @@ -56,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 +```json +{ "basepath" : "interface/MyButton", // all images are located in this directory "images" : @@ -78,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 +```json +{ "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 cf276ed7a..c82448116 100644 --- a/docs/modders/Bonus/Bonus_Duration_Types.md +++ b/docs/modders/Bonus/Bonus_Duration_Types.md @@ -1,15 +1,17 @@ +# Bonus Duration Types + 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 6c422acb8..304f6c043 100644 --- a/docs/modders/Bonus/Bonus_Limiters.md +++ b/docs/modders/Bonus/Bonus_Limiters.md @@ -1,3 +1,5 @@ +# Bonus Limiters + ## Predefined Limiters The limiters take no parameters: @@ -13,7 +15,7 @@ The limiters take no parameters: Example: -``` javascript +```json "limiters" : [ "SHOOTER_ONLY" ] ``` @@ -23,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 +```json "limiters" : [ { "type" : "HAS_ANOTHER_BONUS_LIMITER", @@ -48,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 @@ -69,24 +72,24 @@ Parameters: Parameters: -- Faction identifier +- Faction identifier ### CREATURE_TERRAIN_LIMITER Parameters: -- Terrain identifier +- Terrain identifier Example: -``` javascript +```json "limiters": [ { "type":"CREATURE_TYPE_LIMITER", "parameters": [ "angel", true ] } ], ``` -``` javascript +```json "limiters" : [ { "type" : "CREATURE_TERRAIN_LIMITER", "parameters" : ["sand"] @@ -104,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 +```json "limiters" : [ "noneOf", "IS_UNDEAD", @@ -119,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 73b8b3004..2a3e93e20 100644 --- a/docs/modders/Bonus/Bonus_Propagators.md +++ b/docs/modders/Bonus/Bonus_Propagators.md @@ -1,8 +1,10 @@ +# Bonus Propagators + ## 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 447632847..c3b5567ef 100644 --- a/docs/modders/Bonus/Bonus_Range_Types.md +++ b/docs/modders/Bonus/Bonus_Range_Types.md @@ -1,3 +1,5 @@ +# Bonus Range Types + ## List of all Bonus range types - NO_LIMIT @@ -8,10 +10,10 @@ TODO: ONLY_MELEE_FIGHT / ONLY_DISTANCE_FIGHT range types work ONLY with creature For replacing ONLY_ENEMY_ARMY alias, you should use the following parameters of bonus: -``` +```text "propagator": "BATTLE_WIDE", "propagationUpdater" : "BONUS_OWNER_UPDATER", "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 44fd6ed10..c23140132 100644 --- a/docs/modders/Bonus/Bonus_Sources.md +++ b/docs/modders/Bonus/Bonus_Sources.md @@ -1,3 +1,5 @@ +# Bonus Sources + ## List of all possible bonus sources - ARTIFACT @@ -17,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 ed5635b69..640c05e54 100644 --- a/docs/modders/Bonus/Bonus_Types.md +++ b/docs/modders/Bonus/Bonus_Types.md @@ -1,4 +1,6 @@ -The bonuses were grouped according to their original purpose. The bonus system allows them to propagate freely betwen the nodes, however they may not be recognized properly beyond the scope of original use. +# Bonus Types + +The bonuses were grouped according to their original purpose. The bonus system allows them to propagate freely between the nodes, however they may not be recognized properly beyond the scope of original use. ## General-purpose bonuses @@ -47,7 +49,7 @@ Bonus that does not account for propagation and gives specific amount of extra r Bonus that does not account for propagation and gives extra resources per day with amount multiplied by number of owned towns - subtype: resource identifier -- val: - base resource amount to be multipled times number of owned towns +- val: - base resource amount to be multiplied times number of owned towns ## Hero bonuses @@ -55,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 @@ -126,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 +```json { "type" : "IMPROVED_NECROMANCY", "subtype" : "creature.walkingDead", @@ -156,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 @@ -254,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 +```json "disruptingRay" : { "addInfo" : -2, "subtype" : "spell.disruptingRay", @@ -269,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 +```json "fortune" : { "addInfo" : 3, "subtype" : "spell.fortune", @@ -344,14 +346,14 @@ 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 In battle, army affected by this bonus will cast spell at the very start of the battle. Spell is always cast at expert level. -- subtype: spell identifer +- subtype: spell identifier - val: duration of the spell, in rounds ### FREE_SHIP_BOARDING @@ -381,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 @@ -400,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 @@ -441,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 @@ -500,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) @@ -515,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 @@ -566,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 @@ -603,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 @@ -617,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 @@ -659,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 @@ -750,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 @@ -806,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 @@ -816,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 @@ -832,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 @@ -876,7 +887,7 @@ Affected unit receives decreased damage from spells of specific school (Golems) Affected unit receives increased damage from specific spell - val: increase to damage, percentage -- subtype: spell identifer +- subtype: spell identifier ### MIND_IMMUNITY @@ -990,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 @@ -1008,11 +1019,20 @@ Dummy bonus that acts as marker for Dendroid's Bind ability Dummy skill for alternative upgrades mod -### TOWN_MAGIC_WELL +### THIEVES_GUILD_ACCESS -Internal bonus, do not use +Increases amount of information available in affected thieves guild (in town or in adventure map tavern). Does not affects adventure map object "Den of Thieves". You may want to use PLAYER_PROPAGATOR with this bonus to make its effect player wide. + +- val: additional number of 'levels' of information to grant access to ### LEVEL_COUNTER Internal bonus, do not use +### DISINTEGRATE + +When a unit affected by this bonus dies, no corpse is left behind + +### INVINCIBLE + +The unit affected by this bonus cannot be target of attacks or spells diff --git a/docs/modders/Bonus/Bonus_Updaters.md b/docs/modders/Bonus/Bonus_Updaters.md index d8a33b6de..82b5dd2cb 100644 --- a/docs/modders/Bonus/Bonus_Updaters.md +++ b/docs/modders/Bonus/Bonus_Updaters.md @@ -1,3 +1,5 @@ +# Bonus Updaters + TODO: this page may be incorrect or outdated Updaters come in two forms: simple and complex. Simple updaters take no @@ -8,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"` -` }` +```json +"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"` -` }` +```json +"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 @@ -54,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"` -` }` +```json +"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 f33be82c4..9b4a6e75b 100644 --- a/docs/modders/Bonus/Bonus_Value_Types.md +++ b/docs/modders/Bonus/Bonus_Value_Types.md @@ -1,23 +1,30 @@ +# Bonus Value Types + 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: + +```text +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)` +```text +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 f3a7c671c..be37fa558 100644 --- a/docs/modders/Bonus_Format.md +++ b/docs/modders/Bonus_Format.md @@ -1,8 +1,10 @@ +# Bonus Format + ## Full format All parameters but type are optional. -``` javascript +```json { // Type of the bonus. See Bonus Types for full list "type": "BONUS_TYPE", @@ -76,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 +```json "bonus" : { "type" : "HATE", @@ -88,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 cee6a9cb5..da815123b 100644 --- a/docs/modders/Building_Bonuses.md +++ b/docs/modders/Building_Bonuses.md @@ -1,124 +1,126 @@ +# Building Bonuses + Work-in-progress page do describe all bonuses provided by town buildings for future configuration. TODO: This page is outdated and may not represent VCMI 1.3 state -## unique buildings +### unique buildings Hardcoded functionalities, selectable but not configurable. In future 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 +```json "function" : "castleGates" ``` -## trade-related +### trade-related 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 +### hero visitables Buildings that give one or another bonus to visiting hero. All should be 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 +### generic functions Generic town-specific functions that can be implemented as part of CBuilding class. -### unlock guild level +#### unlock guild level -``` javascript +```json "guildLevels" : 1 ``` -### unlock hero recruitment +#### unlock hero recruitment -``` javascript +```json "allowsHeroPurchase" : true ``` -### unlock ship purchase +#### unlock ship purchase -``` javascript +```json "allowsShipPurchase" : true ``` -### unlock building purchase +#### unlock building purchase -``` javascript +```json "allowsBuildingPurchase" : true ``` -### unlocks creatures +#### unlocks creatures -``` javascript +```json "dwelling" : { "level" : 1, "creature" : "archer" } ``` -### creature growth bonus +#### creature growth bonus Turn into town bonus? What about creature-specific bonuses from hordes? -### gives resources +#### gives resources -``` javascript +```json "provides" : { "gold" : 500 } ``` -### gives guild spells +#### gives guild spells -``` javascript +```json "guildSpells" : [5, 0, 0, 0, 0] ``` -### gives thieves guild +#### gives thieves guild -``` javascript +```json "thievesGuildLevels" : 1 ``` -### gives fortifications +#### gives fortifications -``` javascript +```json "fortificationLevels" : 1 ``` -### gives war machine +#### gives war machine -``` javascript +```json "warMachine" : "ballista" ``` -## simple bonuses +### simple bonuses Bonuses that can be made part of CBuilding. Note that due to how bonus system works this bonuses won't be stackable. @@ -127,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 +```json "bonuses" : { "moraleToDefenders" : @@ -149,23 +151,23 @@ Includes: } ``` -## misc +### misc Some other properties of town building that does not fall under "bonus" category. -### unique building +#### unique building 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 +```json "onePerPlayer" : true ``` -### chance to be built on start +#### chance to be built on start -``` javascript +```json "prebuiltChance" : 75 -``` \ No newline at end of file +``` diff --git a/docs/modders/Campaign_Format.md b/docs/modders/Campaign_Format.md index d41713144..9def306b1 100644 --- a/docs/modders/Campaign_Format.md +++ b/docs/modders/Campaign_Format.md @@ -1,12 +1,15 @@ -# Introduction +# Campaign Format + +## 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 `00.json`. See also [Packing campaign](#packing-campaign) +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 + +```json { "version" : 1, @@ -27,26 +30,40 @@ Basic structure of this file is here, each section is described in details below `"version"` defines version of campaign file. Larger versions should have more features and flexibility, but may not be supported by older VCMI engines. See [compatibility table](#compatibility-table) -# Header properties +## Header properties In header are parameters describing campaign properties -```js + +```json ... "regions": {...}, "name": "Campaign name", "description": "Campaign description", + "author": "Author", + "authorContact": "Author contact", + "campaignVersion": "Campaign version", + "creationDateTime": "Creation date and time", "allowDifficultySelection": true, ``` - `"regions"` contains information about background and regions. See section [campaign regions](#regions-description) for more information - `"name"` is a human readable title of campaign - `"description"` is a human readable description of campaign +- `"author"` is the author of the campaign +- `"authorContact"` is a contact address for the author (e.g. email) +- `"campaignVersion"` is creator defined version +- `"creationDateTime"` unix time of campaign creation - `"allowDifficultySelection"` is a boolean field (`true`/`false`) which allows or disallows to choose difficulty before scenario start +- `"loadingBackground"` is for setting a different loading screen background +- `"introVideo"` is for defining an optional intro video +- `"outroVideo"` is for defining an optional outro video +- `"videoRim"` is for the Rim around the optional video (default is INTRORIM) -# Scenario description +## Scenario description Scenario description looks like follow: -```js + +```json { "map": "maps/SomeMap", "preconditions": [], @@ -63,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` @@ -79,10 +96,11 @@ Scenario description looks like follow: - `"playerColor"` defines color id of flag which player will play for. Possible values are `0: red, 1: blue, tan: 2, green: 3, orange: 4, purple: 5, teal: 6, pink: 7` - "bonuses" array of possible bonus objects, format depends on `"startOptions"` parameter -## Prolog/Epilog +### Prolog/Epilog Prolog and epilog properties are optional -```js + +```json { "video": "NEUTRALA.smk", //video to show "music": "musicFile.ogg", //music to play, should be located in music directory @@ -91,17 +109,17 @@ Prolog and epilog properties are optional } ``` -## Start options and bonuses +### Start options and bonuses -### None start option +#### None start option If `startOptions` is `none`, `bonuses` field will be ignored -### Bonus start option +#### Bonus start option If `startOptions` is `bonus`, bonus format may vary depending on its type. -```js +```json { "what": "", @@ -140,23 +158,25 @@ If `startOptions` is `bonus`, bonus format may vary depending on its type. - `"amount"`: amount of resources - `"hero"` can be specified as explicit hero name and as one of keywords: `strongest`, `generated` -### Crossover start option +#### Crossover start option If `startOptions` is `crossover`, heroes from specific scenario will be moved to this scenario. Bonus format is following -```js +```json { "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 +#### Hero start option If `startOptions` is `hero`, hero can be chosen as a starting bonus. Bonus format is following -```js + +```json { "playerColor": 0, "hero": "random" @@ -166,49 +186,40 @@ If `startOptions` is `hero`, hero can be chosen as a starting bonus. Bonus forma - `"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` - `"hero"` can be specified as explicit hero name and as one of keywords: `random` -## Regions description +### Regions description Predefined campaign regions are located in file `campaign_regions.json` -```js +```json { + "background": "ownRegionBackground.png", + "suffix": ["Enabled", "Selected", "Conquered"], "prefix": "G3", - "color_suffix_length": 1, + "colorSuffixLength": 1, "desc": [ - { "infix": "A", "x": 289, "y": 376 }, - { "infix": "B", "x": 60, "y": 147 }, - { "infix": "C", "x": 131, "y": 202 } + { "infix": "A", "x": 289, "y": 376, "labelPos": { "x": 98, "y": 112 } }, + { "infix": "B", "x": 60, "y": 147, "labelPos": { "x": 98, "y": 112 } }, + { "infix": "C", "x": 131, "y": 202, "labelPos": { "x": 98, "y": 112 } } ] }, ``` -- `"prefix"` used to identify all images related to campaign. In this example, background picture will be `G3_BG` -- `"inflix"` ised to identify all images related to region. In this example, it will be pictures starting from `G3A_..., G3B_..., G3C_..."` -- `"color_suffix_length"` identifies suffix length for region colourful frames. 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` +- `"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_..."` +- `"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` -# Packing campaign +## Packing campaign After campaign scenarios and campaign description are ready, you should pack them into *.vcmp file. -This file is basically headless gz archive. +This file is a zip archive. -Your campaign should be stored in some folder with json describing campaign information. -Place all your scenarios inside same folder and enumerate their filenames, e.g `01.vmap`, '02.vmap', etc. -``` -my-campaign/ -|-- 00.json -|-- 01.vmap -|-- 02.vmap -|-- 03.vmap -``` +The scenarios should be named as in `"map"` field from header. Subfolders are allowed. -If you use unix system, execute this command to pack your campaign: -``` -gzip -c -n ./* >> my-campaign.vcmp -``` +## Compatibility table -If you are using Windows system, try this https://gnuwin32.sourceforge.net/packages/gzip.htm - -# 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 a1d40f76a..2d968af49 100644 --- a/docs/modders/Configurable_Widgets.md +++ b/docs/modders/Configurable_Widgets.md @@ -1,10 +1,12 @@ -# Introduction +# Configurable Widgets + +## Introduction VCMI has capabilities to change some UI elements in your mods beyond only replacing one image with another. Not all UI elements are possible to modify currently, but development team is expanding them. Elements possible to modify are located in `config/widgets`. -# Tutorial +## Tutorial Let's take `extendedLobby` mod from `vcmi-extras` as an example for VCMI-1.4. [Example sources](https://github.com/vcmi-mods/vcmi-extras/tree/vcmi-1.4/Mods/extendedLobby). @@ -16,10 +18,11 @@ For options tab it introduces UI for chess timers. In this tutorial we will recreate options tab to support chess timers UI. -## Creating mod structure +### Creating mod structure To start making mod, create following folders structure; -``` + +```text extendedLobby/ |- content/ | |- sprites/ @@ -29,6 +32,7 @@ extendedLobby/ ``` File `mod.json` is generic and could look like this: + ```json { "name" : "Configurable UI tutorial mod", @@ -42,9 +46,9 @@ 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 +### Making layout for timer Let's copy `config/widgets/optionsTab.json` file from VCMI folder to `content/config/widgets/` folder from our mod. It defines UI for options tab as it designed in original game, we will keep everything related to player settings and will modify only timer area. @@ -62,6 +66,7 @@ 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 { "items" @@ -83,6 +88,7 @@ Let's find first element, which is label ``` And modify it a bit + ```json { "name": "labelTimer", //add name, only for convenience @@ -96,6 +102,7 @@ And modify it a bit ``` But we also need proper background image for this label. Add image widget BEFORE labelTimer widget: + ```json { "type": "picture", @@ -107,11 +114,12 @@ 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. -## Adding combo box +### Adding combo box Now, let's add combo box. @@ -264,12 +272,13 @@ Now specify items inside `dropDown` field Now we can press drop-down menu and even select elements. -## Switching timer modes +### Switching timer modes 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 "variables": { @@ -298,7 +307,7 @@ 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 @@ -322,12 +331,13 @@ This background must be visible for chess timer and hidden for classic timer. Ju It works and can switch elements, the only missing part is chess timer configuration. -## Chess timer configuration +### Chess timer configuration 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 { "name": "chessFieldBase", @@ -343,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` @@ -352,39 +363,39 @@ And what we want to do is to hide/show those fields when classic/chess times is We are done! You can find more information about configurable UI elements in documentation section. -# Documentation +## Documentation -## Types +### Types All fields have format `"key": value` There are different basic types, which can be used as value. -### Primitive types +#### Primitive types -Read JSON documentation for primitive types description: https://www.json.org/json-en.html +Read JSON documentation for primitive types description: -### Text +#### Text Load predefined text which can be localised, examples: `"vcmi.otherOptions.availableCreaturesAsDwellingLabel"` `"core.genrltxt.738"` -### Position +#### Position Point with two coordinates, example: `{ "x": 43, "y": -28 }` -### Rect +#### Rect Rectangle ares, example: `{ "x": 28, "y": 220, "w": 108, "h": 50 }` -### Text alignment +#### Text alignment Defines text alignment, can be one of values: `"center"`, `"left"`, `"right"` -### Color +#### Color Predefined colors: `"yellow"`, `"white"`, `"gold"`, `"green"`, `"orange"`, `"bright-yellow"` @@ -392,17 +403,17 @@ Predefined colors: To have custom color make an array of four elements in RGBA notation: `[255, 128, 0, 255]` -### Font +#### Font Predefined fonts: `"big"`, `"medium"`, `"small"`, `"tiny"`, `"calisto"` -### Hint text +#### Hint text Hint text is a pair of strings, one is usually shown in status bar when cursor hovers element, another hint while right button pressed. Each of elements is a [Text](#text) -``` +```json { "hover": "Text", "help": "Text @@ -413,21 +424,22 @@ If one string specified, it will be applied for both hover and help. `"text"` -### Shortcut +#### Shortcut String value defines shortcut. Some examples of shortcuts: `"globalAccept", "globalCancel", "globalReturn","globalFullscreen", "globalOptions", "globalBackspace", "globalMoveFocus"` Full list is TBD -### [VCMI-1.4] Player color +#### [VCMI-1.4] Player color One of predefined values: `"red"`, `"blue"`, `"tan"`, `"green"`, `"orange"`, `"purple"`, `"teal"`, `"pink"` -## Configurable objects +### Configurable objects Configurable object has following structure: + ```json { "items": [], @@ -445,9 +457,9 @@ Configurable object has following structure: `library` - same as above, but custom widgets are described in separate json, this parameter should contain path to library json is specified -## Basic widgets +### Basic widgets -### Label +#### Label `"type": "label"` @@ -465,7 +477,7 @@ Configurable object has following structure: `"maxWidth"`: int` optional, trim longer text -### [VCMI-1.4] Multi-line label +#### [VCMI-1.4] Multi-line label `"type": "multiLineLabel"` @@ -485,7 +497,7 @@ Configurable object has following structure: `"adoptHeight": bool` //if true, text area height will be adopted automatically based on content -### Label group +#### Label group `"type": "labelGroup"` @@ -505,7 +517,7 @@ Configurable object has following structure: `"text"`: [text](#text), -### TextBox +#### TextBox `"type": "textBox"` @@ -521,7 +533,7 @@ Configurable object has following structure: `"rect"`: [rect](#rect) -### Picture +#### Picture `"type": "picture"` @@ -535,7 +547,7 @@ Configurable object has following structure: `"playerColored", bool`, optional, if true will be colorised to current player -### Image +#### Image Use to show single frame from animation @@ -551,7 +563,7 @@ Use to show single frame from animation `"frame": integer` optional, specify animation frame -### Texture +#### Texture Filling area with texture @@ -563,7 +575,7 @@ Filling area with texture `"rect"`: [rect](#rect) -### TransparentFilledRectangle +#### TransparentFilledRectangle `"type": "transparentFilledRectangle"` @@ -575,7 +587,7 @@ Filling area with texture `"rect"`: [rect](#rect) -### Animation +#### Animation `"type": "animation"` @@ -601,7 +613,7 @@ Filling area with texture `"end": integer`, last frame -### [VCMI-1.4] Text input +#### [VCMI-1.4] Text input `"type": "textInput"` @@ -619,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) @@ -627,7 +639,7 @@ Filling area with texture `"callback": string` optional, callback to be called on text changed. Input text is passed to callback function as an argument. -### Button +#### Button `"type": "button"` @@ -649,7 +661,7 @@ Filling area with texture `"items": []` array of widgets to be shown as overlay (caption [label](#label), for example) -### Toggle button +#### Toggle button `"type": "toggleButton"` @@ -669,7 +681,7 @@ Filling area with texture `"items": []` array of widgets to be shown as overlay (caption [label](#label), for example) -### Toggle group +#### Toggle group Group of [toggle buttons](#toggle-button), when one is selected, other will be de-selected @@ -683,7 +695,7 @@ Group of [toggle buttons](#toggle-button), when one is selected, other will be d `"items": []` array of [toggle buttons](#toggle-button) -### Slider +#### Slider `"type": "slider"` @@ -709,7 +721,7 @@ Group of [toggle buttons](#toggle-button), when one is selected, other will be d `"panningStep": integer`, optional -### Combo box +#### Combo box `"type": "comboBox"` @@ -731,7 +743,7 @@ Group of [toggle buttons](#toggle-button), when one is selected, other will be d `"dropDown" : {}` description of [drop down](#drop-down) menu widget -### Drop down +#### Drop down Used only as special object for [combo box](#combo-box) @@ -746,28 +758,31 @@ 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 -### Layout +- `sliderMove` connect to slider callback to correctly navigate over elements + +#### Layout `"type": "layout"` `"name": "string"` optional, object name -## High-level widgets +### High-level widgets -## Custom widgets +### Custom widgets -# For developers +## For developers 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 @@ -779,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 { @@ -800,19 +815,19 @@ MyYesNoDialog::MyYesNoDialog(const JsonNode & config): } ``` -## Callbacks +### Callbacks -## Custom widgets +### Custom widgets 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"]); @@ -834,11 +849,11 @@ After that, if your JSON file has items with type "MyItem", the new Item element } ``` -## Variables +### Variables 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 4c4bec5ed..b071f4da4 100644 --- a/docs/modders/Difficulty.md +++ b/docs/modders/Difficulty.md @@ -1,3 +1,5 @@ +# Difficulty + Since VCMI 1.4.0 there are more capabilities to configure difficulty parameters. It means, that modders can give different bonuses to AI or human players depending on selected difficulty @@ -5,7 +7,7 @@ Difficulty configuration is located in [config/difficulty.json](../config/diffic ## Format summary -``` javascript +```json { "human": //parameters impacting human players only { @@ -13,7 +15,7 @@ Difficulty configuration is located in [config/difficulty.json](../config/diffic { //starting resources "resources": { "wood" : 30, "mercury": 15, "ore": 30, "sulfur": 15, "crystal": 15, "gems": 15, "gold": 30000, "mithril": 0 }, - //bonuses will be given to player globaly + //bonuses will be given to player globally "globalBonuses": [], //bonuses will be given to player every battle "battleBonuses": [] @@ -38,9 +40,9 @@ Difficulty configuration is located in [config/difficulty.json](../config/diffic It's possible to specify bonuses of two types: `globalBonuses` and `battleBonuses`. -Both are arrays containing any amount of bonuses, each can be described as usual bonus. See details in [bonus documenation](Bonus_Format.md). +Both are arrays containing any amount of bonuses, each can be described as usual bonus. See details in [bonus documentation](Bonus_Format.md). -`globalBonuses` are given to player on the begining and depending on bonus configuration, it can behave diffierently. +`globalBonuses` are given to player on the beginning and depending on bonus configuration, it can behave diffierently. `battleBonuses` are given to player during the battles, but *only for battles with neutral forces*. So it won't be provided to player for PvP battles and battles versus AI heroes/castles/garrisons. To avoid cumulative effects or unexpected behavior it's recommended to specify bonus `duration` as `ONE_BATTLE`. @@ -48,7 +50,7 @@ For both types of bonuses, `source` should be specified as `OTHER`. ## Example -```js +```json { //will give 150% extra health to all players' creatures if specified in "battleBonuses" array "type" : "STACK_HEALTH", "val" : 150, @@ -61,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 72299d2b9..9c0e2c71d 100644 --- a/docs/modders/Entities_Format/Artifact_Format.md +++ b/docs/modders/Entities_Format/Artifact_Format.md @@ -1,16 +1,18 @@ +# Artifact Format + Artifact bonuses use [Bonus Format](../Bonus_Format.md) ## Required data 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 +```json { // Type of this artifact - creature, hero or commander "type": ["HERO", "CREATURE", "COMMANDER"] @@ -65,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 9d769ec29..84b8184ce 100644 --- a/docs/modders/Entities_Format/Battle_Obstacle_Format.md +++ b/docs/modders/Entities_Format/Battle_Obstacle_Format.md @@ -1,4 +1,6 @@ -```jsonc +# Battle Obstacle Format + +```json // List of terrains on which this obstacle can be used "allowedTerrains" : [] @@ -22,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 2d8164228..7797b02ec 100644 --- a/docs/modders/Entities_Format/Battlefield_Format.md +++ b/docs/modders/Entities_Format/Battlefield_Format.md @@ -1,17 +1,25 @@ -```jsonc +# Battlefield Format + +```json // Human-readable name of the battlefield - "name" : "" + "name" : "", // If set to true, obstacles will be taken from "specialBattlefields" property of an obstacle // If set to false, obstacles will be taken from "allowedTerrains" instead - "isSpecial" : false + "isSpecial" : false, // List of bonuses that will affect all battles on this battlefield - "bonuses" : { BONUS_FORMAT } + "bonuses" : { BONUS_FORMAT }, // Background image for this battlefield - "graphics" : "" + "graphics" : "", + + // Optional, filename for custom music to play during combat on this terrain + "music" : "", + + // Optional, filename for custom sound to play during combat opening on this terrain + "openingSound" : "", // 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 + "impassableHexes" : [ 10, 20, 50 ], +``` diff --git a/docs/modders/Entities_Format/Biome_Format.md b/docs/modders/Entities_Format/Biome_Format.md index de1bb5830..7c2d76e5c 100644 --- a/docs/modders/Entities_Format/Biome_Format.md +++ b/docs/modders/Entities_Format/Biome_Format.md @@ -1,12 +1,14 @@ +# Biome Format + ## General description Biome is a new entity type added in VCMI 1.5.0. It defines a set of random map obstacles which will be generated together. For each zone different obstacle sets is randomized and then only obstacles from that set will be used to fill this zone. -The purpose is to create visually attractive and consistent maps, which will also vary between generations. It is advised to define a biome for a group of objects that look similiar and just fit each other visually. +The purpose is to create visually attractive and consistent maps, which will also vary between generations. It is advised to define a biome for a group of objects that look similar and just fit each other visually. 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 +```json "obstacleSetId" : { "biome" : { "terrain" : "grass", // Id or vector of Ids this obstacle set can spawn at @@ -36,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 ad58c66ad..66cb5a83d 100644 --- a/docs/modders/Entities_Format/Creature_Format.md +++ b/docs/modders/Entities_Format/Creature_Format.md @@ -1,25 +1,29 @@ +# Creature Format + +This page tells you what you need to do to make your creature work. For help, tips and advices, read the [creature help](Creature_Help.md). + ## Required data 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 +```json // camelCase unique creature identifier "creatureName" : { @@ -213,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 new file mode 100644 index 000000000..a2234e40b --- /dev/null +++ b/docs/modders/Entities_Format/Creature_Help.md @@ -0,0 +1,164 @@ +# Creature Help + +This page helps you to create a creature (i.e. a unit that fights in a battle) for a creature mod or inside a bigger mod like a [faction mod](Faction_Help.md). + +## 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 + +But you can create a configuration that directly reads your image files. Most of the existing mods are coded with `.def` files but direct images are recommended. + +## Make a playable creature + +First of all, retrieve an existing creature and be sure you can clone it and make it work independently without any new content. If it already fails, don't waste your time to draw the new animation. It should work first. + +## Battle render + +The sun is always at zenith, so the shadow is always behind. The reason is that the creature render may be mirrored. There was no strong rules in the original game but usually, the shadow is twice less higher than the creature. + +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: + +- [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) +- [Solus-the-knight](https://www.cgtrader.com/free-3d-models/character/man/solus-the-knight-low-poly-character) +- [Ancient-earth-golem](https://www.cgtrader.com/free-3d-models/character/fantasy-character/ancient-earth-golem-v2) +- [Shadow-golem-elemental](https://www.cgtrader.com/free-3d-models/character/fantasy-character/shadow-golem-elemental) +- [Earth-golem-elemental](https://www.cgtrader.com/free-3d-models/character/fantasy-character/earth-golem-elemental) +- [Kong-2021-rig](https://www.cgtrader.com/free-3d-models/character/sci-fi-character/kong-2021-rig) +- [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*: +- *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* + +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: + +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: + +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*: + +## Battle sound effect + +To create the audio effects, I recommend to use *Tango 2*: + +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. + +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: + +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: + +### 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: + +| | | | | | | | | | | +|---|---|---|---|---|---|---|---|---|---| +| 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | +| 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | ‍⬛ | 🟦 | 🟦 | +| 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | ‍⬛ | 🟦 | 🟦 | +| 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | ‍⬛ | ‍⬛ | ‍⬛ | 🟦 | +| 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | ‍⬛ | ‍⬛ | ‍⬛ | 🟦 | +| 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | ‍⬛ | ‍⬛ | ‍⬛ | ‍⬛ | ‍⬛ | +| 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | ‍⬛ | ‍⬛ | ‍⬛ | 🟦 | +| 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | +| 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | +| 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | + +Locate the top and its projection to the ground: + +| | | | | | | | | | | +|---|---|---|---|---|---|---|---|---|---| +| 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | +| 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | 🟥 | 🟦 | 🟦 | +| 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | ‍⬛ | 🟦 | 🟦 | +| 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | ‍⬛ | ‍⬛ | ‍⬛ | 🟦 | +| 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | ‍⬛ | ‍⬛ | ‍⬛ | 🟦 | +| 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | ‍⬛ | ‍⬛ | 🟥 | ‍⬛ | ‍⬛ | +| 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | ‍⬛ | ‍⬛ | ‍⬛ | 🟦 | +| 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | +| 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | +| 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | + +Then draw a rectangle triangle on the left: + +| | | | | | | | | | | +|---|---|---|---|---|---|---|---|---|---| +| 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | +| 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | 💟 | 🟦 | 🟦 | +| 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | 💟 | ‍⬛ | 🟦 | 🟦 | +| 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | 💟 | ‍⬛ | ‍⬛ | ‍⬛ | 🟦 | +| 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | 💟 | ‍⬛ | ‍⬛ | 🟦 | +| 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | ‍⬛ | ‍⬛ | 💟 | ‍⬛ | ‍⬛ | +| 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | ‍⬛ | ‍⬛ | ‍⬛ | 🟦 | +| 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | +| 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | +| 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | 🟦 | + +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 a7324a956..8b4b01431 100644 --- a/docs/modders/Entities_Format/Faction_Format.md +++ b/docs/modders/Entities_Format/Faction_Format.md @@ -1,49 +1,53 @@ +# Faction Format + +This page tells you what you need to do to make your faction work. For help, tips and advices, read the [faction help](Faction_Help.md). + ## Required data -In order to make functional town you also need: +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 (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 +```json // Unique faction identifier. "myFaction" : { @@ -104,7 +108,7 @@ Each town requires a set of buildings (Around 30-45 buildings) ## Town node -```jsonc +```json { // Field that describes behavior of map object part of town. Town-specific part of object format "mapObject" : @@ -150,8 +154,9 @@ Each town requires a set of buildings (Around 30-45 buildings) } } }, - // Path to town music theme, e.g. "music/castleTheme" - "musicTheme" : "", + // List of town music themes, e.g. [ "music/castleTheme" ] + // At least one music file is required + "musicTheme" : [ "" ], // List of structures which represents visible graphical objects on town screen. // See detailed description below @@ -244,9 +249,6 @@ Each town requires a set of buildings (Around 30-45 buildings) // maximum level of mage guild "mageGuild" : 4, - // war machine produced in town - "warMachine" : "ballista" - // Identifier of spell that will create effects for town moat during siege "moatAbility" : "castleMoat" } @@ -254,7 +256,7 @@ Each town requires a set of buildings (Around 30-45 buildings) ## Siege node -```jsonc +```json // 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" @@ -339,99 +341,8 @@ Each town requires a set of buildings (Around 30-45 buildings) ## Building node -```jsonc -{ - // Numeric identifier of this building - "id" : 0, - - // Localizable name of this building - "name" : "", - - // Localizable decsription of this building - "description" : "", - - // Optional, indicates that this building upgrades another base building - "upgrades" : "baseBuilding", - - // List of town buildings that must be built before this one. See below for full format - "requires" : [ "allOf", [ "mageGuild1" ], [ "tavern" ] ], - - // Resources needed to build building - "cost" : { ... }, - - // TODO: Document me: Subtype for some special buildings - "type" : "", - - // TODO: Document me: Height for lookout towers and some grails - "height" : "average" - - // Resources produced each day by this building - "produce" : { ... }, - - //determine how this building can be built. Possible values are: - // normal - default value. Fulfill requirements, use resources, spend one day - // auto - building appears when all requirements are built - // special - building can not be built manually - // grail - building reqires grail to be built - "mode" : "auto", - - // Buildings which bonuses should be overridden with bonuses of the current building - "overrides" : [ "anotherBuilding ] - - // Bonuses, provided by this special building on build using bonus system - "bonuses" : BONUS_FORMAT - - // Bonuses, provided by this special building on hero visit and applied to the visiting hero - "onVisitBonuses" : BONUS_FORMAT -} -``` - -Building requirements can be described using logical expressions: - -```jsonc -"requires" : -[ - "allOf", // Normal H3 "build all" mode - [ "mageGuild1" ], - [ - "noneOf", // available only when none of these building are built - [ "dwelling5A" ], - [ "dwelling5AUpgrade" ] - ], - [ - "anyOf", // any non-zero number of these buildings must be built - [ "tavern" ], - [ "blacksmith" ] - ] -] -``` +See [Town Building Format](Town_Building_Format.md) ## Structure node -```jsonc -{ - // Main animation file for this building - "animation" : "", - - // Horizontal position on town screen - "x" : 0, - - // Vertical position on town screen - "y" : 0, - - // used for blit order. Higher value places structure close to screen and drawn on top of buildings with lower values - "z" : 0, - - // Path to image with golden border around building, displayed when building is selected - "border" : "", - - // Path to image with area that indicate when building is selected - "area" : "", - - //TODO: describe me - "builds": "", - - // If upgrade, this building will replace parent animation but will not alter its behaviour - "hidden" : false -} -``` \ 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 new file mode 100644 index 000000000..ebbcd38fa --- /dev/null +++ b/docs/modders/Entities_Format/Faction_Help.md @@ -0,0 +1,111 @@ +# Faction Help + +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. + +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: + +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*: + +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*: + +## 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*: + +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*: + +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*: + +## 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*: +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* + +* [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* + +* [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* + +* [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* + +* [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* + +* [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* + +* [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* + +* [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* + +* [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)*. + +## 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. diff --git a/docs/modders/Entities_Format/Hero_Class_Format.md b/docs/modders/Entities_Format/Hero_Class_Format.md index 6bfb54efe..1a25cc01c 100644 --- a/docs/modders/Entities_Format/Hero_Class_Format.md +++ b/docs/modders/Entities_Format/Hero_Class_Format.md @@ -1,13 +1,15 @@ +# Hero Class Format + ## Required data 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 +```json // Unique identifier of hero class, camelCase "myClassName" : { @@ -104,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 472bf7153..1028bd86f 100644 --- a/docs/modders/Entities_Format/Hero_Type_Format.md +++ b/docs/modders/Entities_Format/Hero_Type_Format.md @@ -1,13 +1,15 @@ +# Hero Type Format + ## Required data 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 +```json "myHeroName" : { // Identifier of class this hero belongs to. Such as knight or battleMage @@ -44,7 +46,7 @@ In order to make functional hero you also need: // Tooltip visible on clicking icon. Can use {} symbols to change title to yellow // as well as escape sequences "\n" to add line breaks - "tooltip" : "{Magic Arrow}\n\nCasts powerfull magic arrows", + "tooltip" : "{Magic Arrow}\n\nCasts powerful magic arrows", // Name of your specialty "name" : "Magic Arrow" @@ -131,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 17cbbdf8d..7730cf750 100644 --- a/docs/modders/Entities_Format/River_Format.md +++ b/docs/modders/Entities_Format/River_Format.md @@ -1,9 +1,11 @@ +# River Format + ## Format -```jsonc +```json "newRiver" : { - // Two-letters unique indentifier for this river. Used in map format + // Two-letters unique identifier for this river. Used in map format "shortIdentifier" : "mr", // Human-readable name of the river @@ -26,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 bc4a85cd7..df48ade5e 100644 --- a/docs/modders/Entities_Format/Road_Format.md +++ b/docs/modders/Entities_Format/Road_Format.md @@ -1,9 +1,11 @@ +# Road Format + ## Format -```jsonc +```json "newRoad" : { - // Two-letters unique indentifier for this road. Used in map format + // Two-letters unique identifier for this road. Used in map format "shortIdentifier" : "mr", // Human-readable name of the road @@ -15,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 90bbf757c..d5be72bd0 100644 --- a/docs/modders/Entities_Format/Secondary_Skill_Format.md +++ b/docs/modders/Entities_Format/Secondary_Skill_Format.md @@ -1,6 +1,17 @@ +# Secondary Skill Format + ## Main format -```jsonc +```json +{ + // Skill be only be available on maps with water + "onlyOnWaterMap" : false, + // Skill is not available on maps at random + "special" : true +} +``` + +```json { "skillName": { @@ -44,7 +55,7 @@ level fields become optional if they equal "base" configuration. ## Skill level format -```jsonc +```json { // Localizable description // Use {xxx} for formatting @@ -76,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 +```json "core:tactics" : { "base" : { "effects" : { @@ -112,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 32d68afa0..9aa39403f 100644 --- a/docs/modders/Entities_Format/Spell_Format.md +++ b/docs/modders/Entities_Format/Spell_Format.md @@ -1,6 +1,8 @@ +# Spell Format + ## Main format -``` javascript +```json { "spellName": { @@ -63,6 +65,12 @@ // 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, + // If true, spell won't be available on a map without water "onlyOnWaterMap" : true, @@ -148,7 +156,7 @@ TODO -``` javascript +```json { "projectile": [ {"minimumAngle": 0 ,"defName":"C20SPX4"}, @@ -159,7 +167,7 @@ TODO ], "cast" : [] "hit":["C20SPX"], - "affect":[{"defName":"C03SPA0", "verticalPosition":"bottom"}, "C11SPA1"] + "affect":[{"defName":"C03SPA0", "verticalPosition":"bottom", "transparency" : 0.5}, "C11SPA1"] } ``` @@ -171,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 +```json "base":{ "range": 0 }, @@ -184,7 +192,7 @@ This will make spell affect single target on all levels except expert, where it TODO -``` javascript +```json { //Mandatory, localizable description. Use {xxx} for formatting @@ -254,7 +262,7 @@ Configurable spells ignore *offensive* flag, *effects* and *cumulativeEffects*. TODO -``` javascript +```json "mod:effectId":{ @@ -275,7 +283,7 @@ TODO TODO -``` javascript +```json "mod:effectId":{ @@ -296,7 +304,7 @@ TODO Configurable version of Clone spell. -``` javascript +```json "mod:effectId":{ @@ -312,7 +320,7 @@ TODO If effect is automatic, spell behave like offensive spell (uses power, levelPower etc) -``` javascript +```json "mod:effectId":{ @@ -360,7 +368,7 @@ TODO If effect is automatic, spell behave like \[de\]buff spell (effect and cumulativeEffects ignored) -``` javascript +```json "mod:effectId":{ @@ -381,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 736f7d5a1..e0a0fff43 100644 --- a/docs/modders/Entities_Format/Terrain_Format.md +++ b/docs/modders/Entities_Format/Terrain_Format.md @@ -1,9 +1,11 @@ +# Terrain Format + ## Format -```jsonc +```json "newTerrain" : { - // Two-letters unique indentifier for this terrain. Used in map format + // Two-letters unique identifier for this terrain. Used in map format "shortIdentifier" : "mt", // Human-readable name of the terrain @@ -11,7 +13,7 @@ // Type(s) of this terrain. // WATER - this terrain is water-like terrains that requires boat for movement - // ROCK - this terrain is unpassable "rock" terrain that is used for inacessible parts of underground layer + // ROCK - this terrain is unpassable "rock" terrain that is used for inaccessible parts of underground layer // SUB - this terrain can be placed in underground map layer by RMG // SURFACE - this terrain can be placed in surface map layer by RMG "type" : [ "WATER", "SUB", "ROCK", "SURFACE" ], @@ -48,8 +50,8 @@ // Color of terrain on minimap with unpassable objects. RGB triplet, 0-255 range "minimapBlocked" : [ 150, 100, 50 ], - // Music filename to play on this terrain on adventure map - "music" : "", + // List of music files to play on this terrain on adventure map. At least one file is required + "music" : [ "" ], "sounds" : { // List of ambient sounds for this terrain @@ -73,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 new file mode 100644 index 000000000..c97b87c7b --- /dev/null +++ b/docs/modders/Entities_Format/Town_Building_Format.md @@ -0,0 +1,310 @@ +# Town Building Format + +## Required data + +Each building requires following assets: + +- 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 + +```json +"special4": { + "requires" : [ "mageGuild1" ], + "name" : "Order of Fire", + "description" : "Increases spellpower of visiting hero", + "cost" : { + "mercury" : 5, + "gold" : 1000 + }, + "configuration" : { + "visitMode" : "hero", + "rewards" : [ + { + // NOTE: this forces vcmi to load string from H3 text file. In order to define own string simply write your own message without '@' symbol + "message" : "@core.genrltxt.582", + "primary" : { "spellpower" : 1 } + } + ] + } +} +``` + +##### Mana Vortex from Dungeon + +```json +"special2": { + "requires" : [ "mageGuild1" ], + "name" : "Mana Vortex", + "description" : "Doubles mana points of the first visiting hero each week", + "cost" : { + "gold" : 5000 + }, + "configuration" : { + "resetParameters" : { + "period" : 7, + "visitors" : true + }, + "visitMode" : "once", + "rewards" : [ + { + "limiter" : { + "noneOf" : [ { "manaPercentage" : 200 } ] + }, + "message" : "As you near the mana vortex your body is filled with new energy. You have doubled your normal spell points.", + "manaPercentage" : 200 + } + ] + } +} +``` + +#### Resource Silo with custom production + +```json +"resourceSilo": { + "name" : "Wood Resource Silo", + "description" : "Produces 2 wood every day", + "cost" : { + "wood" : 10, + "gold" : 5000 + }, + "produce" : { + "wood": 2 + } +}, +``` + +#### Brotherhood of Sword - bonuses in siege + +```json +"special3": { + // replaces +1 Morale bonus from Tavern + "upgradeReplacesBonuses" : true, + // Gives +2 bonus to morale to town (effective only during siege) + "bonuses": [ + { + "type": "MORALE", + "val": 2 + } + ], + "upgrades" : "tavern" +}, +``` + +#### Lighthouse - bonus to all heroes under player control + +```json +"special1": { + "bonuses": [ + { + "propagator": "PLAYER_PROPAGATOR", // bonus affects everything under player control + "type": "MOVEMENT", + "subtype": "heroMovementSea", + "val": 500 // +500 movement points + } + ], + "requires" : [ "shipyard" ] +}, +``` + +## Town Building node + +```json +{ + // Numeric identifier of this building + "id" : 0, + + // Localizable name of this building + "name" : "", + + // Localizable decsription of this building + "description" : "", + + // Optional, indicates that this building upgrades another base building + "upgrades" : "baseBuilding", + + // List of town buildings that must be built before this one. See below for full format + "requires" : [ "allOf", [ "mageGuild1" ], [ "tavern" ] ], + + // Resources needed to build building + "cost" : { + "wood" : 20, + "ore" : 20, + "gold" : 10000 + }, + + // Artifact ID of a war machine produced in this town building, if any + "warMachine" : "ballista", + + // Allows to define additional functionality of this building, usually using logic of one of original H3 town building + // Generally only needs to be specified for "special" buildings + // See 'List of unique town buildings' section below for detailed description of this field + "type" : "", + + // If set, building will have Lookout Tower logic - extend sight radius of a town. + // Possible values: + // low - increases town sight radius by 5 tiles + // average - sight radius extended by 15 tiles + // high - sight radius extended by 20 tiles + // skyship - entire map will be revealed + // If not set, building will not affect sight radius of a town + "height" : "average" + + // Resources produced each day by this building + "produce" : { + "sulfur" : 1, + "gold" : 2000 + }, + + // Optional, allows this building to add fortifications during siege + "fortifications" : { + // Maximum health of destructible walls. Walls are only present if their health is above zero". + // Presence of walls is required for all other fortification types + "wallsHealth" : 3, + + // If set to true, moat will be placed in front of the walls. Requires walls presence. + "hasMoat" : true + + // Maximum health of central tower or 0 if not present. Requires walls presence. + "citadelHealth" : 2, + // Maximum health of upper tower or 0 if not present. Requires walls presence. + "upperTowerHealth" : 2, + // Maximum health of lower tower or 0 if not present. Requires walls presence. + "lowerTowerHealth" : 2, + + // Creature ID of shooter located in central keep (citadel). Used only if citadel is present. + "citadelShooter" : "archer", + // Creature ID of shooter located in upper tower. Used only if upper tower is present. + "upperTowerShooter" : "archer", + // Creature ID of shooter located in lower tower. Used only if lower tower is present. + "lowerTowerShooter" : "archer", + }, + + //determine how this building can be built. Possible values are: + // normal - default value. Fulfill requirements, use resources, spend one day + // auto - building appears when all requirements are built + // special - building can not be built manually + // grail - building requires grail to be built + "mode" : "auto", + + // Buildings which bonuses should be overridden with bonuses of the current building + "overrides" : [ "anotherBuilding ] + + // Bonuses provided by this special building if this building or any of its upgrades are constructed in town + "bonuses" : [ BONUS_FORMAT ] + + // If set to true, this building will not automatically activate on new day or on entering town and needs to be activated manually on click + // Note that such building can only be activated by visiting hero, and not by garrisoned hero. + "manualHeroVisit" : false, + + // Bonuses provided by this special building if this building or any of its upgrades are constructed in town + "bonuses" : [ BONUS_FORMAT ] + + + // If the building is a market, it requires market mode. + "marketModes" : [ "resource-resource", "resource-player" ], +} +``` + +Building requirements can be described using logical expressions: + +```json +"requires" : +[ + "allOf", // Normal H3 "build all" mode + [ "mageGuild1" ], + [ + "noneOf", // available only when none of these building are built + [ "dwelling5A" ], + [ "dwelling5AUpgrade" ] + ], + [ + "anyOf", // any non-zero number of these buildings must be built + [ "tavern" ], + [ "blacksmith" ] + ] +] +``` + +### 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` +- `magicUniversity` +- `castleGate` +- `creatureTransformer` +- `portalOfSummoning` +- `library` +- `escapeTunnel` +- `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. + +### Town Structure node + +```json +{ + // Main animation file for this building + "animation" : "", + + // Horizontal position on town screen + "x" : 0, + + // Vertical position on town screen + "y" : 0, + + // used for blit order. Higher value places structure close to screen and drawn on top of buildings with lower values + "z" : 0, + + // Path to image with golden border around building, displayed when building is selected + "border" : "", + + // Path to image with area that indicate when building is selected + "area" : "", + + //TODO: describe me + "builds": "", + + // If upgrade, this building will replace parent animation but will not alter its behaviour + "hidden" : false +} +``` + +#### Markets in towns + +Market buildings require list of available [modes](../Map_Objects/Market.md) + +##### Marketplace + +```json + "marketplace": { "marketModes" : ["resource-resource", "resource-player"] }, +``` + +##### Artifact merchant + +```json + "special1": { "type" : "artifactMerchant", "requires" : [ "marketplace" ], "marketModes" : ["resource-artifact", "artifact-resource"] }, +``` diff --git a/docs/modders/File_Formats.md b/docs/modders/File_Formats.md new file mode 100644 index 000000000..900c90a25 --- /dev/null +++ b/docs/modders/File_Formats.md @@ -0,0 +1,93 @@ +# File Formats + +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 + +Transparency support: +VCMI supports transparency (alpha) channel, both in png and in bmp images. There may be cases where transparency is not fully supported. If you discover such cases, please report them. + +For performance reasons, please use alpha channel only in places where transparency is actually required and remove alpha channel from image othervice + +Palette support: +TODO: describe how palettes work in vcmi + +### Animations + +For animations VCMI supports .def format from Heroes III as well as alternative json-based. See [Animation Format](Animation_Format.md) for more details + +### Sounds + +For sounds VCMI currently supports: + +- .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 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 + +Starting from VCMI 1.6, following video container formats are supported by VCMI: + +- .bik - one of the formats used by Heroes III +- .smk - one of the formats used by Heroes III. Note that these videos generally have lower quality and are only used as fallback if no other formats are found +- .ogv - format used by Heroes III: HD Edition +- .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 +- vp9 - recommended, this format is improvement of vp9 format and should be used as a default option + +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 + +### Json + +For most of configuration files, VCMI uses [JSON format](http://en.wikipedia.org/wiki/Json) with some extensions from [JSON5](https://spec.json5.org/) format, such as comments. + +### Maps + +TODO: describe + +### Campaigns + +TODO: describe + +### Map Templates + +TODO: describe + +### Archives + +TODO: describe + +### Txt + +TODO: describe diff --git a/docs/modders/Game_Identifiers.md b/docs/modders/Game_Identifiers.md index 6317339d1..24e95b530 100644 --- a/docs/modders/Game_Identifiers.md +++ b/docs/modders/Game_Identifiers.md @@ -1,3 +1,5 @@ +# Game Identifiers + ## List of all game identifiers This is a list of all game identifiers available to modders. Note that only identifiers from base game have been included. For identifiers from mods please look up corresponding mod @@ -491,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 9a430c6a1..ceb6853a4 100644 --- a/docs/modders/Map_Editor.md +++ b/docs/modders/Map_Editor.md @@ -1,16 +1,18 @@ -# Interface +# Map Editor + +## Interface -# Create the map +## Create the map -## New map +### New map Create the new map by pressing **New** button from the toolbar -### Empty map +#### Empty map To create empty map, define its size by choosing option from drop-down list or enter required size manually in the text fields and press Ok button. Check **Two level map** option to create map with underground. `Note: there are no limits on map size but be careful with sizes larger predefined XL size. It will be processed quite long to create even empty map. Also, it will be difficult to work with the huge maps because of possible performance issues` @@ -19,7 +21,7 @@ Other parameters won't be used for empty map. -### Random map +#### Random map To generate random map, check the **Random map** option and configure map parameters. You can select template from the drop-down list. @@ -32,33 +34,33 @@ Templates are dynamically filtered depending on parameters you choose. -## Map load & save +### Map load & save 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 +## Views There are 3 buttons switching views Снимок экрана 2022-09-07 в 06 48 08 -### Ground/underground +#### Ground/underground **"U/G"** switches you between ground and underground -### Grid view +#### Grid view **Grid** show/hide grid -### Passability view +#### Passability view **Pass** show/hide passability map -# Setup terrain +## Setup terrain 1. Select brush you want @@ -73,9 +75,9 @@ There are 3 buttons switching views Снимок экра
 
 <img width= -### Drawing roads and rivers +#### 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. @@ -83,15 +85,16 @@ 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 -## About brushes * Buttons "1", "2", "4" - 1x1, 2x2, 4x4 brush sizes accordingly * Button "[]" - non-additive rectangle selection * Button "O" - lasso brush (not implemented yet) * Button "E" - object erase, not a brush -## Fill obstacles +### Fill obstacles Map editor supports automatic obstacle placement. Obstacle types are automatically selected for appropriate terrain types @@ -105,9 +108,9 @@ To do that, select area (see Setup terrains) and press **Fill** button from the `Note: obstacle placer may occupy few neighbour tiles outside of selected area` -# Manipulating objects +## Manipulating objects -## Adding new objects +### Adding new objects 1. Find the object you'd like to place in the object browser @@ -125,7 +128,7 @@ To do that, select area (see Setup terrains) and press **Fill** button from the **Right click over the scene - cancel object placement** -## Removing objects +### Removing objects 1. **Make sure that no one terrain brush is selected.** To de-select brush click on selected brush again. @@ -134,7 +137,7 @@ To do that, select area (see Setup terrains) and press **Fill** button from the 3. Press **"E"** button from the brush panel or press **delete** on keyboard -## Changing object's properties +### Changing object's properties 1. **Make sure that no one terrain brush is selected.** To de-select brush click on selected brush again. @@ -147,15 +150,15 @@ To do that, select area (see Setup terrains) and press **Fill** button from the 4. You are able to modify properties which are not gray `Note: sometimes there are empty editable fields` -### Assigning player to the object +#### Assigning player to the object Objects with flags can be assigned to the player. Find Owner property in the inspector for selected object, press twice to modify right cell. Type player number from **0 to 7 or type NEUTRAL** for neutral objects. -# Set up the map +## Set up the map You can modify general properties of the map -## Map name and description +### Map name and description 1. Open **Map** menu on the top and select **General** @@ -167,9 +170,9 @@ You can modify general properties of the map -# Player settings +## Player settings -Open **Map** menu on the top and select **Player settings" +Open **Map** menu on the top and select **Player settings** @@ -179,40 +182,41 @@ You will see a window with player settings. Combobox players defines amount of p -# Compatibility questions +## Compatibility questions -## Platform compatibility +### Platform compatibility vcmieditor is a cross-platform application, so in general can support all platforms, supported by VCMI. However, currently it doesn't support mobile platforms. -## Engine compatibility +### Engine compatibility vcmieditor is independent application so potentially it can be installed just in the folder with existing stable vcmi. However, on the initial stages of development compatibility was not preserved because major changes were needed to introduce into vcmi library. So it's recommended to download full package to use editor. -## Map compatibility +### Map compatibility vcmieditor haven't introduced any change into map format yet, so all maps made by vcmieditor can be easily played with any version of vcmi. At the same time, those maps can be open and read in the old map editor and vice verse - maps from old editor can be imported in the new editor. So, full compatibility is ensured here. -## Mod compatibilty +### Mod compatibility vcmieditor loads set of mods using exactly same mechanism as game uses and mod manipulations can be done using vcmilaucnher application, just enable or disable mods you want and open editor to use content from those mods. In regards on compatibility, of course you need to play maps with same set of mods as you used in the editor. Good part is that is maps don't use content from the mods (even mods were enabled), it can be played on vcmi without mods as well -# Working With Mods +## Working With Mods -## Enabling and disabling mods +### Enabling and disabling mods 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 There is no button to start map editor directly from launcher, however you may use this approach to control active mods from any version of vcmi. -## Placing objects from mods +### Placing objects from mods * All objects from mods will be automatically added into objects Browser. You can type mod name into filter field to find them. @@ -222,13 +226,13 @@ There is no button to start map editor directly from launcher, however you may u -## Playing maps with mods +### Playing maps with mods If you place any kind of objects from the mods, obviously, you need those mods to be installed to play the map. Also, you need to activate them. You also may have other mods being activated in addition to what was used during map designing. -### Mod versions +#### 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 0078b883b..f487c60f4 100644 --- a/docs/modders/Map_Object_Format.md +++ b/docs/modders/Map_Object_Format.md @@ -1,28 +1,22 @@ +# Map Object Format + ## Description 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 appearance will be copied from template -## Object types - -- [Rewardable](Map_Objects/Rewardable.md) - Visitable object which grants all kinds of rewards (gold, experience, Bonuses etc...) -- [Creature Bank](Map_Objects/Creature_Bank.md) - Object that grants award on defeating guardians -- [Dwelling](Map_Objects/Dwelling.md) - Object that allows recruitments of units outside of towns -- [Market](Map_Objects/Market.md) - Trading resources, artifacts, creatures and such -- [Boat](Map_Objects/Boat.md) - Object to move across different terrains, such as water - ## Object group format -``` javascript +```json { "myCoolObjectGroup": @@ -31,7 +25,8 @@ Full object consists from 3 parts: // human readable name, localized "name": "My cool object", - //defines C++/script class name that handles behavior of this object + // defines C++ class name that handles behavior of this object + // see Object Types section below for possible values "handler" : "mine", // default values, will be merged with each type during loading @@ -44,13 +39,71 @@ 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...) +- `bank` - see [Creature Bank](Map_Objects/Creature_Bank.md). Object that grants award on defeating guardians. Deprectated in favor of [Rewardable](Map_Objects/Rewardable.md) +- `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` +- `magi` +- `mine` +- `obelisk` +- `subterraneanGate` +- `whirlpool` +- `resource` +- `denOfThieves` +- `garrison` +- `keymaster` +- `pandora` +- `prison` +- `questGuard` +- `seerHut` +- `sign` +- `siren` +- `monolith` + +### Internal types + +These are internal types that are generally not available for modding and are handled by vcmi internally. + +- `hero` +- `town` +- `monster` +- `randomArtifact` +- `randomHero` +- `randomResource` +- `randomTown` +- `randomMonster` +- `randomDwelling` +- `artifact` +- `event` +- `heroPlaceholder` + ## Object type format -``` javascript +```json { "myCoolObject": { - // Additonal parameters that will be passed over to class that controls behavior of the object + // Additional parameters that will be passed over to class that controls behavior of the object // See object-specific properties of different object types "propertyA" : "value", "propertyB" : 12345 @@ -100,7 +153,7 @@ Full object consists from 3 parts: ## Object template format -``` javascript +```json { "myCoolObjectTemplate" : { @@ -149,4 +202,4 @@ Full object consists from 3 parts: "zIndex": 0 } } -``` \ No newline at end of file +``` diff --git a/docs/modders/Map_Objects/Boat.md b/docs/modders/Map_Objects/Boat.md index 1f17090f3..4c462ed38 100644 --- a/docs/modders/Map_Objects/Boat.md +++ b/docs/modders/Map_Objects/Boat.md @@ -1,4 +1,6 @@ -``` javascript +# Boat + +```json { // Layer on which this boat moves. Possible values: // "land" - same rules as movement of hero on land @@ -25,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 4a583f270..76a6c647e 100644 --- a/docs/modders/Map_Objects/Creature_Bank.md +++ b/docs/modders/Map_Objects/Creature_Bank.md @@ -1,7 +1,121 @@ +# Creature Bank + Reward types for clearing creature bank are limited to resources, creatures, artifacts and spell. Format of rewards is same as in [Rewardable Objects](Rewardable.md) -``` javascript +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. + +```json +{ + "name" : "Cyclops Stockpile", + + // Generic message to ask player whether he wants to attack a creature bank, can be replaced with custom string + "onGuardedMessage" : 32, + + // Generic message to inform player that bank was already cleared + "onVisitedMessage" : 33, + + // As an alternative to a generic message you can define 'reward' + // that will be granted for visiting already cleared bank, such as morale debuff + "onVisited" : [ + { + "message" : 123, // "Such a despicable act reduces your army's morale." + "bonuses" : [ { "type" : "MORALE", "val" : -1, "duration" : "ONE_BATTLE", "description" : 99 } ] + } + ], + "visitMode" : "once", // Banks never reset + // Defines layout of guards. To emulate H3 logic, + // use 'creatureBankNarrow' if guardian units are narrow (1-tile units) + // or, 'creatureBankWide' if defenders are double-hex units + "guardsLayout" : "creatureBankNarrow", + "rewards" : [ + { + "message" : 34, + "appearChance" : { "min" : 0, "max" : 30 }, + "guards" : [ + { "amount" : 4, "type" : "cyclop" }, + { "amount" : 4, "type" : "cyclop" }, + { "amount" : 4, "type" : "cyclop", "upgradeChance" : 50 }, + { "amount" : 4, "type" : "cyclop" }, + { "amount" : 4, "type" : "cyclop" } + ], + "resources" : { + "gold" : 4000 + } + }, + { + "message" : 34, + "appearChance" : { "min" : 30, "max" : 60 }, + "guards" : [ + { "amount" : 6, "type" : "cyclop" }, + { "amount" : 6, "type" : "cyclop" }, + { "amount" : 6, "type" : "cyclop", "upgradeChance" : 50 }, + { "amount" : 6, "type" : "cyclop" }, + { "amount" : 6, "type" : "cyclop" } + ], + "resources" : { + "gold" : 6000 + } + }, + { + "message" : 34, + "appearChance" : { "min" : 60, "max" : 90 }, + "guards" : [ + { "amount" : 8, "type" : "cyclop" }, + { "amount" : 8, "type" : "cyclop" }, + { "amount" : 8, "type" : "cyclop", "upgradeChance" : 50 }, + { "amount" : 8, "type" : "cyclop" }, + { "amount" : 8, "type" : "cyclop" } + ], + "resources" : { + "gold" : 8000 + } + }, + { + "message" : 34, + "appearChance" : { "min" : 90, "max" : 100 }, + "guards" : [ + { "amount" : 10, "type" : "cyclop" }, + { "amount" : 10, "type" : "cyclop" }, + { "amount" : 10, "type" : "cyclop", "upgradeChance" : 50 }, + { "amount" : 10, "type" : "cyclop" }, + { "amount" : 10, "type" : "cyclop" } + ], + "resources" : { + "gold" : 10000 + } + } + ] +}, +``` + +### 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` + +- If your object has non-zero `resetDuration`, replace with `resetParameters` entry + +- For each possible level, replace `chance` with `appearChance` entry + +- If you have `combat_value` or `field` entries inside 'reward' - remove them. These fields are unused in both 1.5 and in 1.6 + +- Rename `levels` entry to `rewards` + +- Add property `"visitMode" : "once"` +- Add property `"onGuardedMessage" : 119`, optionally - replace with custom message for object visit +- Add property `"onVisitedMessage" : 33`, optionally - custom message or morale debuff +- Add property `"message" : 34`, to every level of your reward, optionally - replace with custom message + +### Old format (1.5 or earlier) + +```json { /// If true, battle setup will be like normal - Attacking player on the left, enemy on the right "regularUnitPlacement" : true, @@ -61,4 +175,4 @@ Format of rewards is same as in [Rewardable Objects](Rewardable.md) ] } -``` \ No newline at end of file +``` diff --git a/docs/modders/Map_Objects/Dwelling.md b/docs/modders/Map_Objects/Dwelling.md index 84823d0b1..7ca9aaf57 100644 --- a/docs/modders/Map_Objects/Dwelling.md +++ b/docs/modders/Map_Objects/Dwelling.md @@ -1,4 +1,6 @@ -``` javascript +# Dwelling + +```json { /// 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) @@ -17,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..10acde5e3 --- /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 + +```json +{ + "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 3861db879..dc24ecb9f 100644 --- a/docs/modders/Map_Objects/Market.md +++ b/docs/modders/Map_Objects/Market.md @@ -1,3 +1,5 @@ +# Market + ## Market schema Since VCMI-1.3 it's possible to create customizable markets on adventure map. @@ -5,7 +7,7 @@ Markets can be added as any other object with special handler called "market". Here is schema describing such object -```js +```json "seafaringAcademy" : //object name { "handler" : "market", //market handler @@ -32,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 @@ -47,6 +50,7 @@ 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 "modes" : ["resource-resource", "resource-player"] ``` @@ -81,14 +85,14 @@ See [Secondary skills](Rewardable.md#secondary-skills) description for more deta ### Example for University of magic (e.g conflux building) -```js +```json "modes" : ["resource-skill"], "offer" : ["airMagic", "waterMagic", "earthMagic", "fireMagic"] ``` ### Example for regular University -```js +```json "modes" : ["resource-skill"], "offer" : [ //4 random skills except necromancy { "noneOf" : ["necromancy"] }, @@ -96,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 571401a70..aa9224e92 100644 --- a/docs/modders/Map_Objects/Rewardable.md +++ b/docs/modders/Map_Objects/Rewardable.md @@ -1,6 +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 + +```json { "baseObjectName" : { "name" : "Object name", @@ -32,7 +36,8 @@ Rewardable object is defined similarly to other objects, with key difference bei ``` ## Configurable object definition -```jsonc + +```json // List of potential rewards "rewards" : [ { @@ -115,6 +120,9 @@ Rewardable object is defined similarly to other objects, with key difference bei // Message that will be shown if there are multiple selectable awards to choose from "onSelectMessage" : "", +// Message that will be shown if object has undefeated guards +"onGuardedMessage" : "", + // Message that will be shown if this object has been already visited before "onVisitedMessage" : "{Warehouse of Crystal}\r\n\r\nThe owner of the storage is apologising: 'I am sorry Milord, no crystal here. Please, return next week!'", @@ -123,10 +131,22 @@ Rewardable object is defined similarly to other objects, with key difference bei "onVisited" : [ ] +// Layout of units in the battle (only used if guards are present) +// Predefined values: +// "default" - attacker is on the left, defender is on the right, war machine, tactics, and battlefield obstacles are present +// "creatureBankNarrow" - emulates H3 logic for banks with narrow (1-tile wide) units +// "creatureBankWide" - emulates H3 logic for banks with wide units that take 2 hexes +// Additionally, it is possible to define new layouts, see "layouts" field in (vcmi install)/config/gameConfig.json file +"guardsLayout" : "default" + // if true, then player can refuse from reward and don't select anything // Note that in this case object will not become "visited" and can still be revisited later "canRefuse": true, +// If set to true, then this object can be visited from land when placed next to a coast. +// NOTE: make sure that object also has "blockedVisitable" set to true. Othervice, behavior is undefined +"coastVisitable" : true + // Controls when object state will be reset, allowing potential revisits. See Reset Parameters definition section "resetParameters" : { } @@ -156,6 +176,7 @@ 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 "variables" : { "secondarySkill" : { @@ -170,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 @@ -177,6 +199,7 @@ Possible variable types: - secondarySkill To reference variable in limiter prepend variable name with '@' symbol: + ```json "secondary" : { "@gainedSkill" : 1 @@ -184,12 +207,14 @@ To reference variable in limiter prepend variable name with '@' symbol: ``` ## 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 +```json "resetParameters" : { "period" : 7, "visitors" : true, @@ -198,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 continious range for every value in 0-99 range. For example, object with 3 different rewards may want to define them as +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 +```json "appearChance": { // (Advanced) rewards with different dice number will get different dice number @@ -223,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 + +```json "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 +```json "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 +```json "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 +```json "resources": { "crystal" : 6, "gold" : -1000, }, ``` + - Alternative format that allows random selection of a resource type -```jsonc +```json "resources": [ { "anyOf" : [ "wood", "ore" ], @@ -272,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 +```json "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 +```json "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 +```json "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 +```json "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 +```json "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 +```json "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 +```json "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 +```json "primary": [ { // Specific primary skill @@ -359,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 + +```json "secondary": [ { // Specific skill @@ -399,6 +440,7 @@ Keep in mind, that all randomization is performed on map load and on object rese ``` ### 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 @@ -410,17 +452,18 @@ Keep in mind, that all randomization is performed on map load and on object rese "type" : "MORALE", "val" : 1, "duration" : "ONE_BATTLE", - "desription" : 94 + "description" : 94 } ] ``` ### 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 +```json "artifacts": [ "ribCage" ], @@ -430,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 +```json "artifacts": [ { "class" : "TREASURE", @@ -441,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 +```json "spells": [ "magicArrow" ], @@ -454,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 +```json "spells": [ { "level" : 1, @@ -468,7 +512,7 @@ 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 @@ -477,14 +521,14 @@ Keep in mind, that all randomization is performed on map load and on object rese ], ``` -canLearnSpells - ### 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 probabilty to receive upgraded creature -```jsonc +- It is possible to specify probability to receive upgraded creature + +```json "creatures" : [ { "type" : "archer", @@ -494,18 +538,37 @@ canLearnSpells ], ``` +### 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 + +```json +"guards" : [ + { "type" : "archer", "amount" : 20 }, + { "type" : "archer", "amount" : 20, "upgradeChance" : 30 }, + { "type" : "archer", "amount" : 20 } +], +``` + ### 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 +```json "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) @@ -537,28 +600,31 @@ canLearnSpells ``` ### Player color + - Can be used as limiter - Can NOT be used as reward - Only players with specific color can pass the limiter -```jsonc +```json "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 +```json "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 +```json "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 29e04f2b9..aabb3a59f 100644 --- a/docs/modders/Mod_File_Format.md +++ b/docs/modders/Mod_File_Format.md @@ -1,6 +1,8 @@ +# Mod File Format + ## Fields with description of mod -``` javascript +```json { // 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 @@ -46,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" : @@ -83,30 +91,31 @@ These are fields that are present only in local mod.json file -``` javascript +```json { // 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 @@ -115,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 ] } ``` @@ -186,13 +195,13 @@ These are fields that are present only in local mod.json file In addition to field listed above, it is possible to add following block for any language supported by VCMI. If such block is present, Launcher will use this information for displaying translated mod information and game will use provided json files to translate mod to specified language. See [Translations](Translations.md) for more information -``` +```json "" : { "name" : "", "description" : "", "author" : "", "translations" : [ - "config//.json" + "config/.json" ] }, ``` @@ -201,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 +```json { // URL to mod.json that describes this mod "mod" : "https://raw.githubusercontent.com/vcmi-mods/vcmi-extras/vcmi-1.4/mod.json", @@ -219,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 10c4eb141..52f714660 100644 --- a/docs/modders/Random_Map_Template.md +++ b/docs/modders/Random_Map_Template.md @@ -1,6 +1,8 @@ +# Random Map Template + ## Template format -``` javascript +```json /// Unique template name "Triangle" : { @@ -23,6 +25,16 @@ ///Optional parameter allowing to prohibit some water modes. All modes are allowed if parameter is not specified "allowedWaterContent" : ["none", "normal", "islands"] + + /// 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 + } + }, /// List of named zones, see below for format description "zones" : @@ -36,7 +48,7 @@ { "a" : "zoneA", "b" : "zoneB", "guard" : 5000, "road" : "false" }, { "a" : "zoneA", "b" : "zoneC", "guard" : 5000, "road" : "random" }, { "a" : "zoneB", "b" : "zoneC", "type" : "wide" } - //"type" can be "guarded" (default), "wide", "fictive" or "repulsive" + //"type" can be "guarded" (default), "wide", "fictive", "repulsive" or "forcePortal" //"wide" connections have no border, or guard. "fictive" and "repulsive" connections are virtual - //they do not create actual path, but only attract or repulse zones, respectively ] @@ -45,10 +57,14 @@ ## Zone format -``` javascript +```json { // 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 @@ -88,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"] @@ -119,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 33dee4330..783060c26 100644 --- a/docs/modders/Readme.md +++ b/docs/modders/Readme.md @@ -1,3 +1,5 @@ +# Modding Readme + ## Creating mod To make your own mod you need to create subdirectory in **/Mods/** with name that will be used as identifier for your mod. @@ -6,7 +8,7 @@ All content of your mod should go into **Content** directory, e.g. **Mods/myMod/ Example of how directory structure of your mod may look like: -``` +```text Mods/ myMod/ mod.json @@ -17,16 +19,18 @@ Example of how directory structure of your mod may look like: music/ - music files. Mp3 and ogg/vorbis are supported sounds/ - sound files, in wav format. sprites/ - animation, image sets (H3 .def files or VCMI .json files) - video/ - video files, .bik or .smk + 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 + ## Creating mod file All VCMI configuration files use [JSON format](http://en.wikipedia.org/wiki/Json) so you may want to familiarize yourself with it first. 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 +```json { "name" : "My test mod", "description" : "My test mod that add a lot of useless stuff into the game", @@ -41,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 @@ -48,18 +53,23 @@ 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](Entities_Format/Creature_Format.md) -- [Faction](Entities_Format/Faction_Format.md) +- [Creature Requirement](Entities_Format/Creature_Format.md) +- [Creature Help](Entities_Format/Creature_Help.md) +- [Faction Requirement](Entities_Format/Faction_Format.md) +- [Faction Help](Entities_Format/Faction_Help.md) - [Hero Class](Entities_Format/Hero_Class_Format.md) - [Hero Type](Entities_Format/Hero_Type_Format.md) - [Spell](Entities_Format/Spell_Format.md) - [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) @@ -68,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) @@ -90,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 + +```json "newCreature" : { // creature parameters } @@ -98,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 +```json /// "core" specifier refers to objects that exist in H3 "core:archer" : { /// This will set health of Archer to 10 @@ -117,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. @@ -126,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 @@ -139,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 +```json { // List of replaced images "images" : @@ -185,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 @@ -193,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 +```text +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 1ca939e36..067a59e84 100644 --- a/docs/players/Bug_Reporting_Guidelines.md +++ b/docs/players/Bug_Reporting_Guidelines.md @@ -1,4 +1,6 @@ -First of all, thanks for your support! If you report a bug we can fix it. But keep in mind that reporting your bugs appropriately makes our (developers') lifes easier. Here are a few guidelines that will help you write good bug reports. +# Bug Reporting Guidelines + +First of all, thanks for your support! If you report a bug we can fix it. But keep in mind that reporting your bugs appropriately makes our (developers') lives easier. Here are a few guidelines that will help you write good bug reports. ## Github bugtracker @@ -21,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 b8d8c6387..57d74c8cc 100644 --- a/docs/players/Cheat_Codes.md +++ b/docs/players/Cheat_Codes.md @@ -1,4 +1,6 @@ -## Cheat Codes +# Cheat Codes + +## Codes Similar to H3, VCMI provides cheat codes to make testing game more convenient. @@ -29,6 +31,7 @@ Gives specific creature in every slot, with optional amount. Examples: `nwclotsofguns` or `vcminoldor` or `vcmimachines` - give ballista, ammo cart and first aid tent `vcmiforgeofnoldorking` or `vcmiartifacts` - give all artifacts, except spell book, spell scrolls and war machines. Artifacts added via mods included +`vcmiscrolls` - give spell scrolls for every possible spells ### Movement points @@ -68,13 +71,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 @@ -87,12 +92,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: @@ -112,30 +119,36 @@ 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 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) -`get txt` - save game texts into .txt files matching original heroes 3 files -`def2bmp <.def file name>` - extract .def animation as BMP files -`extract ` - export file into directory used by other extraction 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) +`get txt` - save game texts into .txt files matching original heroes 3 files +`def2bmp <.def file name>` - extract .def animation as BMP files +`extract ` - export file into directory used by other extraction commands +`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 @@ -143,6 +156,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 059f4d45e..54df67881 100644 --- a/docs/players/Game_Mechanics.md +++ b/docs/players/Game_Mechanics.md @@ -1,10 +1,12 @@ +# Game Mechanics + ## List of features added in VCMI ### High resolutions VCMI supports resolutions higher than original game, which ran only in 800 x 600. It also allows a number of additional features: -- High-resolution screens of any ascpect ratio are supported. +- High-resolution screens of any aspect ratio are supported. - In-game GUI can be freely scaled - Adventure map can be freely zoomed @@ -27,7 +29,7 @@ The list of implemented cheat codes and console commands is [here](Cheat_codes.m ### Stack Experience module -VCMI natively suppoorts stack experience feature known from WoG. Any creature - old or modded - can get stack experience bonuses. However, this feature needs to be enabled as a part of WoG VCMI submod. +VCMI natively supports stack experience feature known from WoG. Any creature - old or modded - can get stack experience bonuses. However, this feature needs to be enabled as a part of WoG VCMI submod. Stack experience interface has been merged with regular creature window. Among old functionalities, it includes new useful info: @@ -56,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 @@ -80,7 +82,7 @@ VCMI itroduces custom Quest Log window. It can display info about Seer Hut or Qu ### Power rating -When hovering cursor over neutral stack on adventure map, you may notice additional info about relative threat this stack poses to curently selected hero. This feature has been originally introduced in Heroes of Might and Magic V. +When hovering cursor over neutral stack on adventure map, you may notice additional info about relative threat this stack poses to currently selected hero. This feature has been originally introduced in Heroes of Might and Magic V. ### Minor GUI features @@ -103,8 +105,8 @@ In combat, some creatures, such as Dragon or Cerberi, may attack enemies on mult - [LShift] + LClick – splits a half units from the selected stack into an empty slot. - [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 splitted 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] + 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] + [LShift] + LClick - dismiss selected stack` - Directly type numbers in the Split Stack window to split them in any way you wish @@ -172,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. @@ -183,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) @@ -195,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_Android.md b/docs/players/Installation_Android.md index cbbff127d..a28290a1b 100644 --- a/docs/players/Installation_Android.md +++ b/docs/players/Installation_Android.md @@ -1,3 +1,5 @@ +# Installation Android + ## Step 1: Download and install VCMI **This app requires original heroes 3 sod / complete files to operate, they are not supplied with this installer. it is recommended to purchase version from gog.com. Heroes 3 "hd edition" (steam version) files are not supported !!!** diff --git a/docs/players/Installation_Linux.md b/docs/players/Installation_Linux.md index 02f2e18c2..bfe324647 100644 --- a/docs/players/Installation_Linux.md +++ b/docs/players/Installation_Linux.md @@ -1,13 +1,16 @@ +# Installation Linux + VCMI requires data from original Heroes 3: Shadow of Death or Complete editions. Data from native Linux version made by LOKI will not work. -# Step 1: Binaries installation +## Step 1: Binaries installation ### Ubuntu - Latest stable build from PPA (recommended) Up-to-date releases can be found in our PPA here: To install VCMI from PPA use: -``` + +```sh sudo apt-add-repository ppa:vcmi/ppa sudo apt update sudo apt install vcmi @@ -18,29 +21,44 @@ 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: -``` + +```sh 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: -``` + +```sh 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: -``` + +```sh sudo apt-get update sudo apt-get install vcmi ``` + +### Fedora (40 or newer) + +Stable VCMI version is available in RPM Fusion repository. Learn how to enable it in [wiki](https://docs.fedoraproject.org/en-US/quick-docs/rpmfusion-setup/). To install VCMI from repository: + +```sh + sudo dnf update + sudo dnf install vcmi +``` + ### Flatpak (distribution-agnostic) Latest public release build can be installed via Flatpak. @@ -52,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 2c2b9ddf7..741f2774b 100644 --- a/docs/players/Installation_iOS.md +++ b/docs/players/Installation_iOS.md @@ -1,42 +1,77 @@ +# Installation iOS + You can run VCMI on iOS 12.0 and later, all devices are supported. If you wish to run on iOS 10 or 11, you should build from source, see [How to build VCMI (iOS)](../developers/Building_iOS.md). ## Step 1: Download and install VCMI -- The latest release (recommended): -- Daily builds: +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. -To run on a non-jailbroken device you need to sign the IPA file, you -have the following options: +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. -- (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 -- 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](https://github.com/indygreg/PyOxidizer/tree/main/tugger-code-signing). You'd still need to find a way to create signing assets (private key and provisioning profile) though. +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: -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: +![iTunes](images/itunes.jpg) - /Applications/Apple\ Configurator.app/Contents/MacOS/cfgutil install-app ~/Desktop/vcmi.ipa +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 -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. +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. -### Step 2.a: Installing data files with GOG offline installer +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. -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. +## Step 3: Configuration settings -### Step 2.b: Installing data files with Finder or Windows explorer +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 aternative options: + +- 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. + +The easiest way to install the ipa on your device is to 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 + +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: + +```sh +/Applications/Apple\ Configurator.app/Contents/MacOS/cfgutil install-app ~/Desktop/vcmi.ipa +``` + +## Alternative Step 2: Installing Heroes III data files + +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.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. @@ -45,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 f550d6e94..d327dc62e 100644 --- a/docs/players/Installation_macOS.md +++ b/docs/players/Installation_macOS.md @@ -1,13 +1,15 @@ -For iOS installation look here: (Installation on iOS)[Installation_iOS.md] +# Installation macOS ## 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. @@ -15,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 25a884435..b35074107 100644 --- a/docs/players/Privacy_Policy.md +++ b/docs/players/Privacy_Policy.md @@ -1,14 +1,16 @@ +# Privacy Policy + **Last Updated: 24th December, 2022** -### Glossary +## Glossary * VCMI team - a community of VCMI developers, mod makers and testers. It is not some officially registered organization. * VCMI app - an application provided by VCMI team. -### Single player +## Single player VCMI team does not collect any data produced by VCMI app. All game files, logs, saves, mods are stored in app's internal directory and will be removed upon app uninstallation. It should be possible to backup this data by standard ways provided by your device. -### Multiplayer +## 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/modders/Translations.md b/docs/translators/Translations.md similarity index 76% rename from docs/modders/Translations.md rename to docs/translators/Translations.md index 76c6b8801..c13c2b8ca 100644 --- a/docs/modders/Translations.md +++ b/docs/translators/Translations.md @@ -1,3 +1,5 @@ +# Translations + ## List of currently supported languages This is list of all languages that are currently supported by VCMI. If your languages is missing from the list and you wish to translate VCMI - please contact our team and we'll add support for your language in next release. @@ -21,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) @@ -30,12 +33,20 @@ 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 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 +- 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 + +This way, a contributor that is not a native speaker can do it for you in the future. If you have already existing Heroes III translation you can: @@ -47,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`): + +```json +[ + { + "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: @@ -62,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. @@ -72,8 +99,9 @@ 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/ -- Open `/launcher/translation/` directory, copy english.ts file and rename it to your language +- 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 - Select any untranslated string, enter translation in field below, and click "Done and Next" (Ctrl+Return) to navigate to next untranslated string - Once translation has been finished, save resulting file. @@ -89,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 @@ -126,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: -``` + +In order to display information in Launcher in language selected by user add following block into your `mod.json`: + +```json "" : { "name" : "", "description" : "", @@ -138,9 +174,10 @@ 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. -### Tranlating in-game strings +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 After you have exported translation and added mod information for your language, copy exported file to `/Content/config//.json`. @@ -149,9 +186,11 @@ 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 + +- 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 - Add new language into mod json format - in order to allow translation into new language @@ -159,18 +198,20 @@ Also, make full search for a name of an existing language to ensure that there a ### Updating translation of Launcher and Map Editor to include new strings -At the moment, build system will generate binary translation files (.qs) that can be opened by Qt. +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 -``` +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 ``` -This will remove any no longer existing lines from translation and add any new lines for all translations. If you want to keep old lines, remove `-no-obsolete` key from the command +This will remove any no longer existing lines from translation and add any new lines for all translations. If you want to keep old lines, remove `-no-obsolete` key from the command. There *may* be a way to do the same via QtCreator UI or via CMake, if you find one feel free to update this information. ### 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 eded843b7..b00a61525 100644 --- a/include/vcmi/Entity.h +++ b/include/vcmi/Entity.h @@ -26,11 +26,11 @@ 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; }; -class DLL_LINKAGE Entity +class DLL_LINKAGE Entity : boost::noncopyable { public: using IconRegistar = std::function; @@ -40,6 +40,7 @@ public: virtual int32_t getIndex() const = 0; virtual int32_t getIconIndex() const = 0; virtual std::string getJsonKey() const = 0; + virtual std::string getModScope() const = 0; virtual std::string getNameTranslated() const = 0; virtual std::string getNameTextID() const = 0; 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/Services.h b/include/vcmi/Services.h index ea7977331..9ca566e48 100644 --- a/include/vcmi/Services.h +++ b/include/vcmi/Services.h @@ -59,9 +59,7 @@ public: virtual const SkillService * skills() const = 0; virtual const BattleFieldService * battlefields() const = 0; virtual const ObstacleService * obstacles() const = 0; - virtual const IGameSettings * settings() const = 0; - - virtual void updateEntity(Metatype metatype, int32_t index, const JsonNode & data) = 0; + virtual const IGameSettings * engineSettings() const = 0; virtual const spells::effects::Registry * spellEffects() const = 0; virtual spells::effects::Registry * spellEffects() = 0; diff --git a/include/vcmi/events/ApplyDamage.h b/include/vcmi/events/ApplyDamage.h index 5ea190e4f..f395e3981 100644 --- a/include/vcmi/events/ApplyDamage.h +++ b/include/vcmi/events/ApplyDamage.h @@ -36,7 +36,7 @@ public: static Sub * getRegistry(); - virtual int64_t getInitalDamage() const = 0; + virtual int64_t getInitialDamage() const = 0; virtual int64_t getDamage() const = 0; virtual void setDamage(int64_t value) = 0; virtual const battle::Unit * getTarget() const = 0; diff --git a/include/vcmi/spells/Spell.h b/include/vcmi/spells/Spell.h index 1822639e1..7a3852a5b 100644 --- a/include/vcmi/spells/Spell.h +++ b/include/vcmi/spells/Spell.h @@ -45,6 +45,8 @@ 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/include/vstd/CLoggerBase.h b/include/vstd/CLoggerBase.h index 546b3d352..a1cf6edba 100644 --- a/include/vstd/CLoggerBase.h +++ b/include/vstd/CLoggerBase.h @@ -193,5 +193,6 @@ extern DLL_LINKAGE vstd::CLoggerBase * logNetwork; extern DLL_LINKAGE vstd::CLoggerBase * logAi; extern DLL_LINKAGE vstd::CLoggerBase * logAnim; extern DLL_LINKAGE vstd::CLoggerBase * logMod; +extern DLL_LINKAGE vstd::CLoggerBase * logRng; VCMI_LIB_NAMESPACE_END diff --git a/include/vstd/RNG.h b/include/vstd/RNG.h index 60e1dedc0..9a4e54a06 100644 --- a/include/vstd/RNG.h +++ b/include/vstd/RNG.h @@ -15,18 +15,36 @@ VCMI_LIB_NAMESPACE_BEGIN namespace vstd { -using TRandI64 = std::function; -using TRand = std::function; - class DLL_LINKAGE RNG { public: - virtual ~RNG() = default; - virtual TRandI64 getInt64Range(int64_t lower, int64_t upper) = 0; + /// Returns random number in range [lower, upper] + virtual int nextInt(int lower, int upper) = 0; - virtual TRand getDoubleRange(double lower, double upper) = 0; + /// Returns random number in range [lower, upper] + virtual int64_t nextInt64(int64_t lower, int64_t upper) = 0; + + /// Returns random number in range [lower, upper] + virtual double nextDouble(double lower, double upper) = 0; + + /// Returns random number in range [0, upper] + virtual int nextInt(int upper) = 0; + + /// Returns random number in range [0, upper] + virtual int64_t nextInt64(int64_t upper) = 0; + + /// Returns random number in range [0, upper] + virtual double nextDouble(double upper) = 0; + + /// Generates an integer between 0 and the maximum value it can hold. + /// Should be only used for seeding other generators + virtual int nextInt() = 0; + + /// Returns integer using binomial distribution + /// returned value is number of successfull coin flips with chance 'coinChance' out of 'coinsCount' attempts + virtual int nextBinomialInt(int coinsCount, double coinChance) = 0; }; } @@ -39,7 +57,7 @@ namespace RandomGeneratorUtil if(container.empty()) throw std::runtime_error("Unable to select random item from empty container!"); - return std::next(container.begin(), rand.getInt64Range(0, container.size() - 1)()); + return std::next(container.begin(), rand.nextInt64(0, container.size() - 1)); } template @@ -48,7 +66,7 @@ namespace RandomGeneratorUtil if(container.empty()) throw std::runtime_error("Unable to select random item from empty container!"); - return std::next(container.begin(), rand.getInt64Range(0, container.size() - 1)()); + return std::next(container.begin(), rand.nextInt64(0, container.size() - 1)); } template @@ -59,7 +77,7 @@ namespace RandomGeneratorUtil int64_t totalWeight = std::accumulate(container.begin(), container.end(), 0); assert(totalWeight > 0); - int64_t roll = rand.getInt64Range(0, totalWeight - 1)(); + int64_t roll = rand.nextInt64(0, totalWeight - 1); for (size_t i = 0; i < container.size(); ++i) { @@ -77,7 +95,7 @@ namespace RandomGeneratorUtil for(int64_t i = n-1; i>0; --i) { - std::swap(container.begin()[i],container.begin()[rand.getInt64Range(0, i)()]); + std::swap(container.begin()[i],container.begin()[rand.nextInt64(0, i)]); } } } diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index f128a9240..9a8e4897f 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -7,19 +7,21 @@ 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 + startGame/StartGameTab.cpp firstLaunch/firstlaunch_moc.cpp main.cpp helper.cpp + innoextract.cpp mainwindow_moc.cpp languages.cpp - launcherdirs.cpp - jsonutils.cpp updatedialog_moc.cpp prepare.cpp ) @@ -37,20 +39,22 @@ 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 + startGame/StartGameTab.h firstLaunch/firstlaunch_moc.h mainwindow_moc.h languages.h - launcherdirs.h - jsonutils.h updatedialog_moc.h main.h helper.h + innoextract.h prepare.h ) @@ -61,6 +65,7 @@ set(launcher_FORMS settingsView/csettingsview_moc.ui firstLaunch/firstlaunch_moc.ui mainwindow_moc.ui + startGame/StartGameTab.ui updatedialog_moc.ui ) @@ -79,6 +84,7 @@ set(launcher_TS "${translationsDir}/portuguese.ts" "${translationsDir}/russian.ts" "${translationsDir}/spanish.ts" + "${translationsDir}/swedish.ts" "${translationsDir}/ukrainian.ts" "${translationsDir}/vietnamese.ts" ) @@ -201,7 +207,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/VCMI_launcher.cbp b/launcher/VCMI_launcher.cbp deleted file mode 100644 index b7c1bedf3..000000000 --- a/launcher/VCMI_launcher.cbp +++ /dev/null @@ -1,110 +0,0 @@ - - - - - - diff --git a/launcher/VCMI_launcher.vcxproj b/launcher/VCMI_launcher.vcxproj deleted file mode 100644 index cd98d119e..000000000 --- a/launcher/VCMI_launcher.vcxproj +++ /dev/null @@ -1,172 +0,0 @@ - - - - - Debug - Win32 - - - RD - Win32 - - - - {5B6946C8-A24F-4223-8415-5E16A238ACED} - Win32Proj - VCMI_launcher - 10.0 - - - - Application - true - v142 - Unicode - - - Application - false - v142 - true - Unicode - - - - - - - - - - - - - - - - - .\GeneratedFiles;D:\Programowanie\Biblioteki\QT\5.1.1\msvc2012\include\QtGui;D:\Programowanie\Biblioteki\QT\5.1.1\msvc2012\include\QtCore;D:\Programowanie\Biblioteki\QT\5.1.1\msvc2012\include\QtANGLE;D:\Programowanie\Biblioteki\QT\5.1.1\msvc2012\include\QtWidgets;D:\Programowanie\Biblioteki\QT\5.1.1\msvc2012\include;$(IncludePath) - D:\Programowanie\Biblioteki\QT\5.1.1\msvc2012\lib;$(LibraryPath) - - - - - $(QTDIR)\include;$(QTDIR)\include\QtCore;$(QTDIR)\include\QtGui;$(QTDIR)\include\QtANGLE;$(QTDIR)\include\QtWidgets;.\GeneratedFiles;$(IncludePath) - - - $(QTDIR)\lib;$(LibraryPath) - $(VCMI_Out) - - - - Use - StdInc.h - /Zm150 %(AdditionalOptions) - - - - VCMI_lib.lib;Qt5Cored.lib;Qt5Guid.lib;Qt5Widgetsd.lib;Qt5Networkd.lib;kernel32.lib;user32.lib;gdi32.lib;winspool.lib;comdlg32.lib;advapi32.lib;shell32.lib;ole32.lib;oleaut32.lib;uuid.lib;odbc32.lib;odbccp32.lib;%(AdditionalDependencies) - ..\..\libs;.. - - - - - Use - Full - StdInc.h - %(PreprocessorDefinitions) - true - - - VCMI_lib.lib;Qt5Core.lib;Qt5Gui.lib;Qt5Widgets.lib;Qt5Network.lib;SDL2.lib;%(AdditionalDependencies) - $(VCMI_Out) - - - - - - - - - - - - - - - - - - - - - - - - - - - true - true - - - true - true - - - Create - Create - Create - Create - - - - - - - - D:\Programowanie\Biblioteki\QT\5.1.1\msvc2012\bin\moc.exe;%(FullPath) - Calling D:\Programowanie\Biblioteki\QT\5.1.1\msvc2012\bin\moc.exe for %(Filename) file... - .\GeneratedFiles\$(ConfigurationName)\moc_%(Filename).cpp - "D:\Programowanie\Biblioteki\QT\5.1.1\msvc2012\bin\moc.exe" "%(FullPath)" -o ".\GeneratedFiles\$(ConfigurationName)\moc_%(Filename).cpp" "-fStdInc.h" "-f..\..\%(RecursiveDir)%(Filename).h" -DUNICODE -DWIN32 -DWIN64 -DQT_DLL -DQT_CORE_LIB -DQT_GUI_LIB -DQT_WIDGETS_LIB -DQT_SVG_LIB "-I.\GeneratedFiles" "-I." "-ID:\Programowanie\Biblioteki\QT\5.1.1\msvc2012\include" "-I.\GeneratedFiles\$(ConfigurationName)\." "-ID:\Programowanie\Biblioteki\QT\5.1.1\msvc2012\include\QtCore" "-ID:\Programowanie\Biblioteki\QT\5.1.1\msvc2012\include\QtGui" "-ID:\Programowanie\Biblioteki\QT\5.1.1\msvc2012\include\QtWidgets" "-ID:\Programowanie\Biblioteki\QT\5.1.1\msvc2012\include\QtSvg" - D:\Programowanie\Biblioteki\QT\5.1.1\msvc2012\bin\moc.exe;%(FullPath) - Calling D:\Programowanie\Biblioteki\QT\5.1.1\msvc2012\bin\moc.exe for %(Filename) file... - .\GeneratedFiles\$(ConfigurationName)\moc_%(Filename).cpp - "D:\Programowanie\Biblioteki\QT\5.1.1\msvc2012\bin\moc.exe" "%(FullPath)" -o ".\GeneratedFiles\$(ConfigurationName)\moc_%(Filename).cpp" "-fStdInc.h" "-f..\..\%(RecursiveDir)%(Filename).h" -DUNICODE -DWIN32 -DWIN64 -DQT_DLL -DQT_CORE_LIB -DQT_GUI_LIB -DQT_WIDGETS_LIB -DQT_SVG_LIB "-I.\GeneratedFiles" "-I." "-ID:\Programowanie\Biblioteki\QT\5.1.1\msvc2012\include" "-I.\GeneratedFiles\$(ConfigurationName)\." "-ID:\Programowanie\Biblioteki\QT\5.1.1\msvc2012\include\QtCore" "-ID:\Programowanie\Biblioteki\QT\5.1.1\msvc2012\include\QtGui" "-ID:\Programowanie\Biblioteki\QT\5.1.1\msvc2012\include\QtWidgets" "-ID:\Programowanie\Biblioteki\QT\5.1.1\msvc2012\include\QtSvg" - $(QTDIR)\bin\moc.exe;%(FullPath) - Calling $(QTDIR)\bin\moc.exe for %(Filename) file... - .\GeneratedFiles\$(ConfigurationName)\moc_%(Filename).cpp - "$(QTDIR)\bin\moc.exe" "%(FullPath)" -o ".\GeneratedFiles\$(ConfigurationName)\moc_%(Filename).cpp" "-fStdInc.h" "-f..\..\%(RecursiveDir)%(Filename).h" -DUNICODE -DWIN32 -DWIN64 -DQT_DLL -DQT_NO_DEBUG -DNDEBUG -DQT_CORE_LIB -DQT_GUI_LIB -DQT_WIDGETS_LIB -DQT_SVG_LIB "-I.\GeneratedFiles" "-I." "-IC:\Qt\Qt5.8.0\5.8\msvc2015\include" "-I.\GeneratedFiles\$(ConfigurationName)\." "-IC:\Qt\Qt5.8.0\5.8\msvc2015\include\QtCore" "-IC:\Qt\Qt5.8.0\5.8\msvc2015\include\QtGui" "-IC:\Qt\Qt5.8.0\5.8\msvc2015\include\QtWidgets" "-IC:\Qt\Qt5.8.0\5.8\msvc2015\include\QtSvg" - D:\Programowanie\Biblioteki\QT\5.1.1\msvc2012\bin\moc.exe;%(FullPath) - Calling D:\Programowanie\Biblioteki\QT\5.1.1\msvc2012\bin\moc.exe for %(Filename) file... - .\GeneratedFiles\$(ConfigurationName)\moc_%(Filename).cpp - "D:\Programowanie\Biblioteki\QT\5.1.1\msvc2012\bin\moc.exe" "%(FullPath)" -o ".\GeneratedFiles\$(ConfigurationName)\moc_%(Filename).cpp" "-fStdInc.h" "-f..\..\%(RecursiveDir)%(Filename).h" -DUNICODE -DWIN32 -DWIN64 -DQT_DLL -DQT_NO_DEBUG -DNDEBUG -DQT_CORE_LIB -DQT_GUI_LIB -DQT_WIDGETS_LIB -DQT_SVG_LIB "-I.\GeneratedFiles" "-I." "-ID:\Programowanie\Biblioteki\QT\5.1.1\msvc2012\include" "-I.\GeneratedFiles\$(ConfigurationName)\." "-ID:\Programowanie\Biblioteki\QT\5.1.1\msvc2012\include\QtCore" "-ID:\Programowanie\Biblioteki\QT\5.1.1\msvc2012\include\QtGui" "-ID:\Programowanie\Biblioteki\QT\5.1.1\msvc2012\include\QtWidgets" "-ID:\Programowanie\Biblioteki\QT\5.1.1\msvc2012\include\QtSvg" - - - - - D:\Programowanie\Biblioteki\QT\5.1.1\msvc2012\bin\uic.exe;%(AdditionalInputs) - Uic%27ing %(Identity)... - .\GeneratedFiles\ui_%(Filename).h;%(Outputs) - "D:\Programowanie\Biblioteki\QT\5.1.1\msvc2012\bin\uic.exe" -o ".\GeneratedFiles\ui_%(Filename).h" "%(FullPath)" - D:\Programowanie\Biblioteki\QT\5.1.1\msvc2012\bin\uic.exe;%(AdditionalInputs) - Uic%27ing %(Identity)... - .\GeneratedFiles\ui_%(Filename).h;%(Outputs) - "D:\Programowanie\Biblioteki\QT\5.1.1\msvc2012\bin\uic.exe" -o ".\GeneratedFiles\ui_%(Filename).h" "%(FullPath)" - $(QTDIR)\bin\uic.exe;%(AdditionalInputs) - Uic%27ing %(Identity)... - .\GeneratedFiles\ui_%(Filename).h;%(Outputs) - "$(QTDIR)\bin\uic.exe" -o ".\GeneratedFiles\ui_%(Filename).h" "%(FullPath)" - D:\Programowanie\Biblioteki\QT\5.1.1\msvc2012\bin\uic.exe;%(AdditionalInputs) - Uic%27ing %(Identity)... - .\GeneratedFiles\ui_%(Filename).h;%(Outputs) - "D:\Programowanie\Biblioteki\QT\5.1.1\msvc2012\bin\uic.exe" -o ".\GeneratedFiles\ui_%(Filename).h" "%(FullPath)" - - - - - - - - - \ No newline at end of file diff --git a/launcher/eu.vcmi.VCMI.metainfo.xml b/launcher/eu.vcmi.VCMI.metainfo.xml index 4f8330e75..dba68657e 100644 --- a/launcher/eu.vcmi.VCMI.metainfo.xml +++ b/launcher/eu.vcmi.VCMI.metainfo.xml @@ -90,6 +90,7 @@ vcmilauncher.desktop + diff --git a/launcher/firstLaunch/firstlaunch_moc.cpp b/launcher/firstLaunch/firstlaunch_moc.cpp index a44f33122..6827b60a7 100644 --- a/launcher/firstLaunch/firstlaunch_moc.cpp +++ b/launcher/firstLaunch/firstlaunch_moc.cpp @@ -15,17 +15,13 @@ #include "modManager/cmodlistview_moc.h" #include "../../lib/CConfigHandler.h" -#include "../../lib/CGeneralTextHandler.h" -#include "../../lib/Languages.h" +#include "../../lib/texts/CGeneralTextHandler.h" +#include "../../lib/texts/Languages.h" #include "../../lib/VCMIDirs.h" #include "../../lib/filesystem/Filesystem.h" #include "../helper.h" #include "../languages.h" - -#ifdef ENABLE_INNOEXTRACT -#include "cli/extract.hpp" -#include "setup/version.hpp" -#endif +#include "../innoextract.h" #ifdef VCMI_IOS #include "ios/selectdirectory.h" @@ -299,13 +295,21 @@ bool FirstLaunchView::heroesDataDetect() QString FirstLaunchView::getHeroesInstallDir() { #ifdef VCMI_WINDOWS - QString gogPath = QSettings("HKEY_LOCAL_MACHINE\\SOFTWARE\\GOG.com\\Games\\1207658787", QSettings::NativeFormat).value("path").toString(); - if(!gogPath.isEmpty()) - return gogPath; + QVector> regKeys = { + { "HKEY_LOCAL_MACHINE\\SOFTWARE\\GOG.com\\Games\\1207658787", "path" }, // Gog on x86 system + { "HKEY_LOCAL_MACHINE\\SOFTWARE\\WOW6432Node\\GOG.com\\Games\\1207658787", "path" }, // Gog on x64 system + { "HKEY_LOCAL_MACHINE\\SOFTWARE\\New World Computing\\Heroes of Might and Magic® III\\1.0", "AppPath" }, // H3 Complete on x86 system + { "HKEY_LOCAL_MACHINE\\SOFTWARE\\WOW6432Node\\New World Computing\\Heroes of Might and Magic® III\\1.0", "AppPath" }, // H3 Complete on x64 system + { "HKEY_LOCAL_MACHINE\\SOFTWARE\\New World Computing\\Heroes of Might and Magic III\\1.0", "AppPath" }, // some localized H3 on x86 system + { "HKEY_LOCAL_MACHINE\\SOFTWARE\\WOW6432Node\\New World Computing\\Heroes of Might and Magic III\\1.0", "AppPath" }, // some localized H3 on x64 system + }; - QString cdPath = QSettings("HKEY_LOCAL_MACHINE\\SOFTWARE\\New World Computing\\Heroes of Might and Magic® III\\1.0", QSettings::NativeFormat).value("AppPath").toString(); - if(!cdPath.isEmpty()) - return cdPath; + for(auto & regKey : regKeys) + { + QString path = QSettings(regKey.first, QSettings::NativeFormat).value(regKey.second).toString(); + if(!path.isEmpty()) + return path; + } #endif return QString{}; } @@ -327,7 +331,7 @@ void FirstLaunchView::extractGogData() QFile tmpFile(file); if(!tmpFile.open(QIODevice::ReadOnly)) { - QMessageBox::critical(this, tr("File cannot opened"), tmpFile.errorString()); + QMessageBox::critical(this, tr("File cannot be opened"), tmpFile.errorString()); return QString{}; } QByteArray magicFile = tmpFile.read(magic.length()); @@ -363,13 +367,17 @@ void FirstLaunchView::extractGogData() return; // should not happen - but avoid deleting wrong folder in any case QString tmpFileExe = tempDir.filePath("h3_gog.exe"); + QString tmpFileBin = tempDir.filePath("h3_gog-1.bin"); QFile(fileExe).copy(tmpFileExe); - QFile(fileBin).copy(tempDir.filePath("h3_gog-1.bin")); + QFile(fileBin).copy(tmpFileBin); + + logGlobal->info("Installing exe '%s' ('%s')", tmpFileExe.toStdString(), fileExe.toStdString()); + logGlobal->info("Installing bin '%s' ('%s')", tmpFileBin.toStdString(), fileBin.toStdString()); QString errorText{}; - auto isGogGalaxyExe = [](QString fileExe) { - QFile file(fileExe); + auto isGogGalaxyExe = [](QString fileToTest) { + QFile file(fileToTest); quint64 fileSize = file.size(); if(fileSize > 10 * 1024 * 1024) @@ -379,51 +387,22 @@ void FirstLaunchView::extractGogData() return false; QByteArray data = file.readAll(); - const QByteArray magicId{(const char*)u"GOG Galaxy", 20}; + const QByteArray magicId{reinterpret_cast(u"GOG Galaxy"), 20}; return data.contains(magicId); }; if(isGogGalaxyExe(tmpFileExe)) - errorText = tr("You've provided GOG Galaxy installer! This file doesn't contain the game. Please download the offline backup game installer!"); + errorText = tr("You've provided a GOG Galaxy installer! This file doesn't contain the game. Please download the offline backup game installer!"); - ::extract_options o; - o.extract = true; - - // standard settings - o.gog_galaxy = true; - o.codepage = 0U; - o.output_dir = tempDir.path().toStdString(); - o.extract_temp = true; - o.extract_unknown = true; - o.filenames.set_expand(true); - - o.preserve_file_times = true; // also correctly closes file -> without it: on Windows the files are not written completly - - try - { - if(errorText.isEmpty()) - process_file(tmpFileExe.toStdString(), o, [this](float progress) { - ui->progressBarGog->setValue(progress * 100); - qApp->processEvents(); - }); - } - catch(const std::ios_base::failure & e) - { - errorText = tr("Stream error while extracting files!\nerror reason: "); - errorText += e.what(); - } - catch(const format_error & e) - { - errorText = e.what(); - } - catch(const std::runtime_error & e) - { - errorText = e.what(); - } - catch(const setup::version_error &) - { - errorText = tr("Not a supported Inno Setup installer!"); - } + if(errorText.isEmpty()) + errorText = Innoextract::extract(tmpFileExe, tempDir.path(), [this](float progress) { + ui->progressBarGog->setValue(progress * 100); + qApp->processEvents(); + }); + + QString hashError; + if(!errorText.isEmpty()) + hashError = Innoextract::getHashError(tmpFileExe, tmpFileBin, fileExe, fileBin); ui->progressBarGog->setVisible(false); ui->pushButtonGogInstall->setVisible(true); @@ -433,7 +412,11 @@ void FirstLaunchView::extractGogData() if(!errorText.isEmpty() || dirData.empty() || QDir(tempDir.filePath(dirData.front())).entryList({"*.lod"}, QDir::Filter::Files).empty()) { if(!errorText.isEmpty()) + { QMessageBox::critical(this, tr("Extracting error!"), errorText, QMessageBox::Ok, QMessageBox::Ok); + if(!hashError.isEmpty()) + QMessageBox::critical(this, tr("Hash error!"), hashError, QMessageBox::Ok, QMessageBox::Ok); + } else QMessageBox::critical(this, tr("No Heroes III data!"), tr("Selected files do not contain Heroes III data!"), QMessageBox::Ok, QMessageBox::Ok); tempDir.removeRecursively(); @@ -479,7 +462,7 @@ void FirstLaunchView::copyHeroesData(const QString & path, bool move) QStringList dirMaps = sourceRoot.entryList({"maps"}, QDir::Filter::Dirs); QStringList dirMp3 = sourceRoot.entryList({"mp3"}, QDir::Filter::Dirs); - const auto noDataMessage = tr("Failed to detect valid Heroes III data in chosen directory.\nPlease select directory with installed Heroes III data."); + const auto noDataMessage = tr("Failed to detect valid Heroes III data in chosen directory.\nPlease select the directory with installed Heroes III data."); if(dirData.empty()) { QMessageBox::critical(this, tr("Heroes III data not found!"), noDataMessage); @@ -503,12 +486,12 @@ void FirstLaunchView::copyHeroesData(const QString & path, bool move) if (!hdFiles.empty()) { // HD Edition contains only RoE data so we can't use even unmodified files from it - QMessageBox::critical(this, tr("Heroes III data not found!"), tr("Heroes III: HD Edition files are not supported by VCMI.\nPlease select directory with Heroes III: Complete Edition or Heroes III: Shadow of Death.")); + QMessageBox::critical(this, tr("Heroes III data not found!"), tr("Heroes III: HD Edition files are not supported by VCMI.\nPlease select the directory with Heroes III: Complete Edition or Heroes III: Shadow of Death.")); return; } // RoE or some other unsupported edition. Demo version? - QMessageBox::critical(this, tr("Heroes III data not found!"), tr("Unknown or unsupported Heroes III version found.\nPlease select directory with Heroes III: Complete Edition or Heroes III: Shadow of Death.")); + QMessageBox::critical(this, tr("Heroes III data not found!"), tr("Unknown or unsupported Heroes III version found.\nPlease select the directory with Heroes III: Complete Edition or Heroes III: Shadow of Death.")); return; } @@ -569,15 +552,13 @@ void FirstLaunchView::modPresetUpdate() QString FirstLaunchView::findTranslationModName() { - if (!getModView()) + auto * mainWindow = dynamic_cast(QApplication::activeWindow()); + auto status = mainWindow->getTranslationStatus(); + + if (status == ETranslationStatus::ACTIVE || status == ETranslationStatus::NOT_AVAILABLE) return QString(); QString preferredlanguage = QString::fromStdString(settings["general"]["language"].String()); - QString installedlanguage = QString::fromStdString(settings["session"]["language"].String()); - - if (preferredlanguage == installedlanguage) - return QString(); - return getModView()->getTranslationModName(preferredlanguage); } diff --git a/launcher/firstLaunch/firstlaunch_moc.h b/launcher/firstLaunch/firstlaunch_moc.h index a35aa4820..a5fb752f3 100644 --- a/launcher/firstLaunch/firstlaunch_moc.h +++ b/launcher/firstLaunch/firstlaunch_moc.h @@ -55,8 +55,6 @@ class FirstLaunchView : public QWidget bool checkCanInstallExtras(); bool checkCanInstallMod(const QString & modID); - void installMod(const QString & modID); - public: explicit FirstLaunchView(QWidget * parent = nullptr); diff --git a/launcher/firstLaunch/firstlaunch_moc.ui b/launcher/firstLaunch/firstlaunch_moc.ui index a5e54b104..d195d9ce4 100644 --- a/launcher/firstLaunch/firstlaunch_moc.ui +++ b/launcher/firstLaunch/firstlaunch_moc.ui @@ -96,7 +96,7 @@ - 1 + 2 @@ -177,9 +177,9 @@ Thank you for installing VCMI! -Before you can start playing, there are a few more steps that need to be completed. +Before you can start playing, there are a few more steps to complete. -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. +Please remember that 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! @@ -307,7 +307,7 @@ Heroes® of Might and Magic® III HD is currently not supported! - 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 + You can manually copy directories Maps, Data, and Mp3 from the original game directory to the VCMI data directory that you can see on top of this page Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop @@ -501,8 +501,8 @@ Heroes® of Might and Magic® III HD is currently not supported! - 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. + If you own Heroes III on gog.com, you can download a backup offline installer from gog.com. VCMI will then import Heroes III data using the offline installer. +Offline installer consists of two files: ".exe" and ".bin" - you must download both. Qt::PlainText @@ -785,7 +785,7 @@ Offline installer consists of two parts, .exe and .bin. Make sure you download b - Install mod that provides various interface improvements, such as better interface for random maps and selectable actions in battles + Install mod that provides various interface improvements, such as a better interface for random maps and selectable actions in battles true @@ -809,9 +809,9 @@ Offline installer consists of two parts, .exe and .bin. Make sure you download b In The Wake of Gods - + :/icons/mod-disabled.png - :icons/mod-enabled.png:icons/mod-disabled.png + :icons/mod-enabled.png:/icons/mod-disabled.png true @@ -876,6 +876,8 @@ Offline installer consists of two parts, .exe and .bin. Make sure you download b - + + + 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/innoextract.cpp b/launcher/innoextract.cpp new file mode 100644 index 000000000..f20ae641e --- /dev/null +++ b/launcher/innoextract.cpp @@ -0,0 +1,166 @@ +/* + * innoextract.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 "innoextract.h" + +#ifdef ENABLE_INNOEXTRACT +#include "cli/extract.hpp" +#include "setup/version.hpp" +#endif + +QString Innoextract::extract(QString installer, QString outDir, std::function cb) +{ + QString errorText{}; + +#ifdef ENABLE_INNOEXTRACT + ::extract_options o; + o.extract = true; + + // standard settings + o.gog_galaxy = true; + o.codepage = 0U; + o.output_dir = outDir.toStdString(); + o.extract_temp = true; + o.extract_unknown = true; + o.filenames.set_expand(true); + + o.preserve_file_times = true; // also correctly closes file -> without it: on Windows the files are not written completely + + try + { + process_file(installer.toStdString(), o, cb); + } + catch(const std::ios_base::failure & e) + { + errorText = tr("Stream error while extracting files!\nerror reason: "); + errorText += e.what(); + } + catch(const format_error & e) + { + errorText = e.what(); + } + catch(const std::runtime_error & e) + { + errorText = e.what(); + } + catch(const setup::version_error &) + { + errorText = tr("Not a supported Inno Setup installer!"); + } +#else + errorText = tr("VCMI was compiled without innoextract support, which is needed to extract exe files!"); +#endif + + return errorText; +} + +QString Innoextract::getHashError(QString exeFile, QString binFile, QString exeFileOriginal, QString binFileOriginal) +{ + enum filetype + { + H3_COMPLETE, CHR + }; + struct data + { + filetype type; + std::string language; + int exeSize; + int binSize; + std::string exe; + std::string bin; + }; + + struct fileinfo { + std::string hash; + int size = 0; + }; + + std::vector knownHashes = { + // { H3_COMPLETE, "english", 973162040, 0, "7cf1ecec73e8c2f2c2619415cd16749be5641942", "" }, // setup_homm_3_complete_4.0_(10665).exe + // { H3_COMPLETE, "french", ???, 0, "7e5a737c51530a1888033d188ab0635825ee622f", "" }, // setup_homm_3_complete_french_4.0_(10665).exe + { H3_COMPLETE, "english", 822520, 1005040617, "66646a353b06417fa12c6384405688c84a315cc1", "c624e2071f4e35386765ab044ad5860ac245b7f4" }, // setup_heroes_of_might_and_magic_3_complete_4.0_(28740).exe + { H3_COMPLETE, "french", 824960, 997305870, "072f1d4466ff16444d8c7949c6530448a9c53cfa", "9b6b451d2bd2f8b4be159e62fa6d32e87ee10455" }, // setup_heroes_of_might_and_magic_3_complete_4.0_(french)_(28740).exe + { H3_COMPLETE, "polish", 822288, 849286313, "74ffde00156dd5a8e237668f87213387f0dd9c7c", "2523cf9943043ae100186f89e4ebf7c28be09804" }, // setup_heroes_of_might_and_magic_3_complete_4.0_(polish)_(28740).exe + { H3_COMPLETE, "russian", 821608, 980398466, "88ccae41e66da58ba4ad62024d97dfe69084f825", "58f1b3c813a1953992bba1f9855c47d01c897db8" }, // setup_heroes_of_might_and_magic_3_complete_4.0_(russian)_(28740).exe + { H3_COMPLETE, "english", 820288, 1006275333, "ca68adb8c2d8c6b3afa17a595ad70c2cec062b5a", "2715e10e91919d05377d39fd879d43f9f0cb9f87" }, // setup_heroes_of_might_and_magic_3_complete_4.0_(3.2)_gog_0.1_(77075).exe + { H3_COMPLETE, "french", 822688, 998540653, "fbb300eeef52f5d81a571a178723b19313e3856d", "4f4d90ff2f60968616766237664744bc54754500" }, // setup_heroes_of_might_and_magic_3_complete_4.0_(3.2)_gog_0.1_(french)_(77075).exe + { H3_COMPLETE, "polish", 819904, 851750601, "a413b0b9f3d5ca3e1a57e84a42de28c67d77b1a7", "fd9fe58bcbb8b442e8cfc299d90f1d503f281d40" }, // setup_heroes_of_might_and_magic_3_complete_4.0_(3.2)_gog_0.1_(polish)_(77075).exe + { H3_COMPLETE, "russian", 819416, 981633128, "e84eedf62fe2e5f9171a7e1ce6e99315a09ce41f", "49cc683395c0cf80830bfa66e42bb5dfdb7aa124" }, // setup_heroes_of_might_and_magic_3_complete_4.0_(3.2)_gog_0.1_(russian)_(77075).exe + { CHR, "english", 485694752, 0, "44e4fc2c38261a1c2a57d5198f44493210e8fc1a", "" }, // setup_heroes_chronicles_chapter1_2.1.0.42.exe + { CHR, "english", 493102840, 0, "b479a3272cf4b57a6b7fc499df5eafb624dcd6de", "" }, // setup_heroes_chronicles_chapter2_2.1.0.43.exe + { CHR, "english", 470364128, 0, "5ad36d822e1700c9ecf93b78652900a52518146b", "" }, // setup_heroes_chronicles_chapter3_2.1.0.41.exe + { CHR, "english", 469211296, 0, "5deb374a2e188ed14e8f74ad1284c45e46adf760", "" }, // setup_heroes_chronicles_chapter4_2.1.0.42.exe + { CHR, "english", 447497560, 0, "a6daa6ed56c840f3be7ad6ad920a2f9f2439acc8", "" }, // setup_heroes_chronicles_chapter5_2.1.0.42.exe + { CHR, "english", 447430456, 0, "93a42dd24453f36e7020afc61bca05b8461a3f04", "" }, // setup_heroes_chronicles_chapter6_2.1.0.42.exe + { CHR, "english", 481583720, 0, "d74b042015f3c5b667821c5d721ac3d2fdbf43fc", "" }, // setup_heroes_chronicles_chapter7_2.1.0.42.exe + { CHR, "english", 462976008, 0, "9039050e88b9dabcdb3ffa74b33e6aa86a20b7d9", "" }, // setup_heroes_chronicles_chapter8_2.1.0.42.exe + }; + + auto doHash = [](QFile f){ + fileinfo tmp; + + if(f.open(QFile::ReadOnly)) { + QCryptographicHash hash(QCryptographicHash::Algorithm::Sha1); + if(hash.addData(&f)) + tmp.hash = hash.result().toHex().toLower().toStdString(); + tmp.size = f.size(); + } + + return tmp; + }; + + fileinfo exeInfo; + fileinfo binInfo; + fileinfo exeInfoOriginal; + fileinfo binInfoOriginal; + + exeInfo = doHash(QFile(exeFile)); + if(!binFile.isEmpty()) + binInfo = doHash(QFile(binFile)); + exeInfoOriginal = doHash(QFile(exeFileOriginal)); + if(!binFileOriginal.isEmpty()) + binInfoOriginal = doHash(QFile(binFileOriginal)); + + if(exeInfo.hash.empty() || (!binFile.isEmpty() && binInfo.hash.empty())) + return QString{}; // hashing not possible -> previous error is enough + + QString hashOutput = tr("SHA1 hash of provided files:\nExe (%1 bytes):\n%2").arg(QString::number(exeInfo.size), QString::fromStdString(exeInfo.hash)); + if(!binInfo.hash.empty()) + hashOutput += tr("\nBin (%1 bytes):\n%2").arg(QString::number(binInfo.size), QString::fromStdString(binInfo.hash)); + + if((!exeInfoOriginal.hash.empty() && exeInfo.hash != exeInfoOriginal.hash) || (!binInfoOriginal.hash.empty() && !binFile.isEmpty() && !binFileOriginal.isEmpty() && binInfo.hash != binInfoOriginal.hash)) + return tr("Internal copy process failed. Enough space on device?\n\n%1").arg(hashOutput); + + QString foundKnown; + QString exeLang; + QString binLang; + auto find = [exeInfo, binInfo](const data & d) { return (!d.exe.empty() && d.exe == exeInfo.hash) || (!d.bin.empty() && d.bin == binInfo.hash);}; + auto it = std::find_if(knownHashes.begin(), knownHashes.end(), find); + while(it != knownHashes.end()){ + auto lang = QString::fromStdString((*it).language); + foundKnown += "\n" + (exeInfo.hash == (*it).exe ? tr("Exe") : tr("Bin")) + " - " + lang; + if(exeInfo.hash == (*it).exe) + exeLang = lang; + else + binLang = lang; + it = std::find_if(++it, knownHashes.end(), find); + } + + if(!exeLang.isEmpty() && !binLang.isEmpty() && exeLang != binLang && !binFile.isEmpty()) + return tr("Language mismatch!\n%1\n\n%2").arg(foundKnown, hashOutput); + else if((!exeLang.isEmpty() || !binLang.isEmpty()) && !binFile.isEmpty()) + return tr("Only one file known! Maybe files are corrupted? Please download again.\n%1\n\n%2").arg(foundKnown, hashOutput); + else if(!exeLang.isEmpty() && binFile.isEmpty()) + return QString{}; + else if(!exeLang.isEmpty() && !binFile.isEmpty() && exeLang == binLang) + return QString{}; + + return tr("Unknown files! Maybe files are corrupted? Please download again.\n\n%1").arg(hashOutput); +} diff --git a/launcher/innoextract.h b/launcher/innoextract.h new file mode 100644 index 000000000..e61871bff --- /dev/null +++ b/launcher/innoextract.h @@ -0,0 +1,17 @@ +/* + * innoextract.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 + +class Innoextract : public QObject +{ +public: + static QString extract(QString installer, QString outDir, std::function cb = nullptr); + static QString getHashError(QString exeFile, QString binFile, QString exeFileOriginal, QString binFileOriginal); +}; diff --git a/launcher/jsonutils.h b/launcher/jsonutils.h deleted file mode 100644 index 791711eb0..000000000 --- a/launcher/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/launcher/languages.cpp b/launcher/languages.cpp index d035c4fb1..174f0eb39 100644 --- a/launcher/languages.cpp +++ b/launcher/languages.cpp @@ -11,8 +11,8 @@ #include "languages.h" #include "../lib/CConfigHandler.h" -#include "../lib/Languages.h" -#include "../lib/CGeneralTextHandler.h" +#include "../lib/texts/Languages.h" +#include "../lib/texts/CGeneralTextHandler.h" #include #include diff --git a/launcher/main.cpp b/launcher/main.cpp index 1a1b9bf6d..019ee87e6 100644 --- a/launcher/main.cpp +++ b/launcher/main.cpp @@ -108,19 +108,21 @@ void startEditor(const QStringList & args) #ifndef VCMI_MOBILE void startExecutable(QString name, const QStringList & args) { + // Start vcmiclient and vcmieditor with QProcess::start() instead of QProcess::startDetached() + // since startDetached() results in a missing terminal prompt after quitting vcmiclient. + // QProcess::start() causes the launcher window to freeze while the child process is running, so we hide it in + // MainWindow::on_startGameButton_clicked() and MainWindow::on_startEditorButton_clicked() QProcess process; - - // Start the executable - if(process.startDetached(name, args)) - { - qApp->quit(); - } - else - { + process.setProcessChannelMode(QProcess::ForwardedChannels); + process.start(name, args); + process.waitForFinished(-1); + + if (process.exitStatus() != QProcess::NormalExit || process.exitCode() != 0) { QMessageBox::critical(qApp->activeWindow(), - QObject::tr("Error starting executable"), - QObject::tr("Failed to start %1\nReason: %2").arg(name, process.errorString()) - ); + QObject::tr("Error starting executable"), + QObject::tr("Failed to start %1\nReason: %2").arg(name, process.errorString())); } + + qApp->quit(); } #endif diff --git a/launcher/mainwindow_moc.cpp b/launcher/mainwindow_moc.cpp index c7194906b..01d483176 100644 --- a/launcher/mainwindow_moc.cpp +++ b/launcher/mainwindow_moc.cpp @@ -15,9 +15,9 @@ #include "../lib/CConfigHandler.h" #include "../lib/VCMIDirs.h" -#include "../lib/Languages.h" #include "../lib/filesystem/Filesystem.h" #include "../lib/logging/CBasicLogConfigurator.h" +#include "../lib/texts/Languages.h" #include "updatedialog_moc.h" #include "main.h" @@ -47,7 +47,6 @@ void MainWindow::computeSidePanelSizes() ui->modslistButton, ui->settingsButton, ui->aboutButton, - ui->startEditorButton, ui->startGameButton }; @@ -78,11 +77,12 @@ MainWindow::MainWindow(QWidget * parent) ui->setupUi(this); + setAcceptDrops(true); + setWindowIcon(QIcon{":/icons/menu-game.png"}); ui->modslistButton->setIcon(QIcon{":/icons/menu-mods.png"}); ui->settingsButton->setIcon(QIcon{":/icons/menu-settings.png"}); ui->aboutButton->setIcon(QIcon{":/icons/about-project.png"}); - ui->startEditorButton->setIcon(QIcon{":/icons/menu-editor.png"}); ui->startGameButton->setIcon(QIcon{":/icons/menu-game.png"}); #ifndef VCMI_MOBILE @@ -101,16 +101,12 @@ MainWindow::MainWindow(QWidget * parent) } #endif -#ifndef ENABLE_EDITOR - ui->startEditorButton->hide(); -#endif - computeSidePanelSizes(); bool h3DataFound = CResourceHandler::get()->existsResource(ResourcePath("DATA/GENRLTXT.TXT")); if (h3DataFound && setupCompleted) - ui->tabListWidget->setCurrentIndex(TabRows::MODS); + ui->tabListWidget->setCurrentIndex(TabRows::START); else enterSetup(); @@ -147,7 +143,6 @@ void MainWindow::detectPreferredLanguage() void MainWindow::enterSetup() { ui->startGameButton->setEnabled(false); - ui->startEditorButton->setEnabled(false); ui->settingsButton->setEnabled(false); ui->aboutButton->setEnabled(false); ui->modslistButton->setEnabled(false); @@ -160,16 +155,27 @@ void MainWindow::exitSetup() writer->Bool() = true; ui->startGameButton->setEnabled(true); - ui->startEditorButton->setEnabled(true); ui->settingsButton->setEnabled(true); ui->aboutButton->setEnabled(true); ui->modslistButton->setEnabled(true); ui->tabListWidget->setCurrentIndex(TabRows::MODS); } +void MainWindow::switchToStartTab() +{ + ui->startGameButton->setEnabled(true); + ui->startGameButton->setChecked(true); + ui->tabListWidget->setCurrentIndex(TabRows::START); + + auto* startGameTabWidget = qobject_cast(ui->tabListWidget->widget(TabRows::START)); + if(startGameTabWidget) + startGameTabWidget->refreshState(); +} + void MainWindow::switchToModsTab() { ui->startGameButton->setEnabled(true); + ui->modslistButton->setChecked(true); ui->tabListWidget->setCurrentIndex(TabRows::MODS); } @@ -196,17 +202,7 @@ MainWindow::~MainWindow() void MainWindow::on_startGameButton_clicked() { - startGame({}); -} - -void MainWindow::on_startEditorButton_clicked() -{ - startEditor({}); -} - -const CModList & MainWindow::getModList() const -{ - return ui->modlistView->getModList(); + switchToStartTab(); } CModListView * MainWindow::getModView() @@ -231,19 +227,124 @@ void MainWindow::on_aboutButton_clicked() ui->tabListWidget->setCurrentIndex(TabRows::ABOUT); } +void MainWindow::dragEnterEvent(QDragEnterEvent* event) +{ + if(event->mimeData()->hasUrls()) + for(const auto & url : event->mimeData()->urls()) + for(const auto & ending : QStringList({".zip", ".h3m", ".h3c", ".vmap", ".vcmp", ".json", ".exe"})) + if(url.fileName().endsWith(ending, Qt::CaseInsensitive)) + { + event->acceptProposedAction(); + return; + } +} + +void MainWindow::dropEvent(QDropEvent* event) +{ + const QMimeData* mimeData = event->mimeData(); + + if(mimeData->hasUrls()) + { + const QList urlList = mimeData->urls(); + for (const auto & url : urlList) + manualInstallFile(url.toLocalFile()); + } +} + +void MainWindow::manualInstallFile(QString filePath) +{ + if(filePath.endsWith(".zip", Qt::CaseInsensitive) || filePath.endsWith(".exe", Qt::CaseInsensitive)) + switchToModsTab(); + + QString fileName = QFileInfo{filePath}.fileName(); + if(filePath.endsWith(".zip", Qt::CaseInsensitive)) + { + QString filenameClean = fileName.toLower() + // mod name currently comes from zip file -> remove suffixes from github zip download + .replace(QRegularExpression("-[0-9a-f]{40}"), "") + .replace(QRegularExpression("-vcmi-.+\\.zip"), ".zip") + .replace("-main.zip", ".zip"); + + getModView()->downloadFile(filenameClean, QUrl::fromLocalFile(filePath), "mods"); + } + else if(filePath.endsWith(".json", Qt::CaseInsensitive)) + { + QDir configDir(QString::fromStdString(VCMIDirs::get().userConfigPath().string())); + QStringList configFile = configDir.entryList({fileName}, QDir::Filter::Files); // case insensitive check + if(!configFile.empty()) + { + auto dialogResult = QMessageBox::warning(this, tr("Replace config file?"), tr("Do you want to replace %1?").arg(configFile[0]), QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + if(dialogResult == QMessageBox::Yes) + { + const auto configFilePath = configDir.filePath(configFile[0]); + QFile::remove(configFilePath); + QFile::copy(filePath, configFilePath); + + // reload settings + Helper::loadSettings(); + for(const auto widget : qApp->allWidgets()) + if(auto settingsView = qobject_cast(widget)) + settingsView->loadSettings(); + + getModView()->reload(); + } + } + } + else + getModView()->installFiles(QStringList{filePath}); +} + +ETranslationStatus MainWindow::getTranslationStatus() +{ + QString preferredlanguage = QString::fromStdString(settings["general"]["language"].String()); + QString installedlanguage = QString::fromStdString(settings["session"]["language"].String()); + + if (preferredlanguage == installedlanguage) + return ETranslationStatus::ACTIVE; + + QString modName = getModView()->getTranslationModName(preferredlanguage); + + if (modName.isEmpty()) + return ETranslationStatus::NOT_AVAILABLE; + + if (!getModView()->isModInstalled(modName)) + return ETranslationStatus::NOT_INSTALLLED; + + if (!getModView()->isModEnabled(modName)) + return ETranslationStatus::DISABLED; + + return ETranslationStatus::ACTIVE; +} + void MainWindow::updateTranslation() { #ifdef ENABLE_QT_TRANSLATIONS - const std::string translationFile = settings["general"]["language"].String() + ".qm"; - logGlobal->info("Loading translation '%s'", translationFile); + const std::string translationFile = settings["general"]["language"].String()+ ".qm"; + QString translationFileResourcePath = QString{":/translation/%1"}.arg(translationFile.c_str()); - if (!translator.load(QString{":/translation/%1"}.arg(translationFile.c_str()))) + logGlobal->info("Loading translation %s", translationFile); + + if(!QFile::exists(translationFileResourcePath)) { - logGlobal->error("Failed to load translation"); + logGlobal->debug("Translation file %s does not exist", translationFileResourcePath.toStdString()); + return; + } + + if (!translator.load(translationFileResourcePath)) + { + logGlobal->error("Failed to load translation file %s", translationFileResourcePath.toStdString()); + return; + } + + if(translationFile == "english.qm") + { + // translator doesn't need to be installed for English return; } if (!qApp->installTranslator(&translator)) - logGlobal->error("Failed to install translator"); + { + logGlobal->error("Failed to install translator for translation file %s", translationFileResourcePath.toStdString()); + } #endif } diff --git a/launcher/mainwindow_moc.h b/launcher/mainwindow_moc.h index 6581086ff..1f4685cac 100644 --- a/launcher/mainwindow_moc.h +++ b/launcher/mainwindow_moc.h @@ -23,6 +23,14 @@ class QTableWidgetItem; class CModList; class CModListView; +enum class ETranslationStatus : int8_t +{ + NOT_AVAILABLE, // translation for this language was not found in mod list. Could also happen if player is offline or disabled repository checkout + NOT_INSTALLLED, // translation mod found, but it is not installed + DISABLED, // translation mod found, and installed, but toggled off + ACTIVE // translation mod active OR game is already in specified language (e.g. English H3 for players with English language) +}; + class MainWindow : public QMainWindow { Q_OBJECT @@ -40,13 +48,13 @@ class MainWindow : public QMainWindow SETTINGS = 1, SETUP = 2, ABOUT = 3, + START = 4, }; public: explicit MainWindow(QWidget * parent = nullptr); ~MainWindow() override; - const CModList & getModList() const; CModListView * getModView(); void updateTranslation(); @@ -56,6 +64,13 @@ public: void enterSetup(); void exitSetup(); void switchToModsTab(); + void switchToStartTab(); + + void dragEnterEvent(QDragEnterEvent* event) override; + void dropEvent(QDropEvent *event) override; + + void manualInstallFile(QString filePath); + ETranslationStatus getTranslationStatus(); protected: void changeEvent(QEvent * event) override; @@ -66,6 +81,5 @@ public slots: private slots: void on_modslistButton_clicked(); void on_settingsButton_clicked(); - void on_startEditorButton_clicked(); void on_aboutButton_clicked(); }; diff --git a/launcher/mainwindow_moc.ui b/launcher/mainwindow_moc.ui index aab701f2c..99648d40e 100644 --- a/launcher/mainwindow_moc.ui +++ b/launcher/mainwindow_moc.ui @@ -29,6 +29,57 @@ + + + + + 1 + 10 + + + + + 0 + 0 + + + + + 16777215 + 16777215 + + + + + true + + + + Game + + + + 64 + 64 + + + + true + + + true + + + true + + + Qt::ToolButtonTextUnderIcon + + + true + + + @@ -62,7 +113,7 @@ true - true + false true @@ -146,8 +197,8 @@ - 32 - 32 + 48 + 48 @@ -180,104 +231,6 @@ - - - - - 1 - 5 - - - - - 0 - 0 - - - - - 16777215 - 16777215 - - - - - 75 - true - - - - Map Editor - - - - 32 - 32 - - - - false - - - false - - - Qt::ToolButtonTextUnderIcon - - - false - - - - - - - - 1 - 10 - - - - - 0 - 0 - - - - - 16777215 - 16777215 - - - - - 75 - true - - - - Start game - - - - 64 - 64 - - - - false - - - false - - - Qt::ToolButtonTextUnderIcon - - - false - - - @@ -292,12 +245,13 @@ - 3 + 4 + @@ -329,6 +283,12 @@
aboutProject/aboutproject_moc.h
1 + + StartGameTab + QWidget +
startGame/StartGameTab.h
+ 1 +
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 new file mode 100644 index 000000000..c15e621c7 --- /dev/null +++ b/launcher/modManager/chroniclesextractor.cpp @@ -0,0 +1,259 @@ +/* + * chroniclesextractor.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 "chroniclesextractor.h" + +#include "../../lib/VCMIDirs.h" +#include "../../lib/filesystem/CArchiveLoader.h" + +#include "../innoextract.h" + +ChroniclesExtractor::ChroniclesExtractor(QWidget *p, std::function cb) : + parent(p), cb(cb) +{ +} + +bool ChroniclesExtractor::createTempDir() +{ + tempDir = QDir(pathToQString(VCMIDirs::get().userDataPath())); + if(tempDir.cd("tmp")) + { + tempDir.removeRecursively(); // remove if already exists (e.g. previous run) + tempDir.cdUp(); + } + tempDir.mkdir("tmp"); + if(!tempDir.cd("tmp")) + return false; // should not happen - but avoid deleting wrong folder in any case + + return true; +} + +void ChroniclesExtractor::removeTempDir() +{ + tempDir.removeRecursively(); +} + +int ChroniclesExtractor::getChronicleNo() +{ + QStringList appDirCandidates = tempDir.entryList({"app"}, QDir::Filter::Dirs); + + if (!appDirCandidates.empty()) + { + QDir appDir = tempDir.filePath(appDirCandidates.front()); + + for (size_t i = 1; i < chronicles.size(); ++i) + { + QString chronicleName = chronicles.at(i); + QStringList chroniclesDirCandidates = appDir.entryList({chronicleName}, QDir::Filter::Dirs); + + if (!chroniclesDirCandidates.empty()) + return i; + } + } + QMessageBox::critical(parent, tr("Invalid file selected"), tr("You have to select a Heroes Chronicles installer file!")); + return 0; +} + +bool ChroniclesExtractor::extractGogInstaller(QString file) +{ + QString errorText = Innoextract::extract(file, tempDir.path(), [this](float progress) { + float overallProgress = ((1.0 / static_cast(fileCount)) * static_cast(extractionFile)) + (progress / static_cast(fileCount)); + if(cb) + cb(overallProgress); + }); + + if(!errorText.isEmpty()) + { + QString hashError = Innoextract::getHashError(file, {}, {}, {}); + QMessageBox::critical(parent, tr("Extracting error!"), errorText); + if(!hashError.isEmpty()) + QMessageBox::critical(parent, tr("Hash error!"), hashError, QMessageBox::Ok, QMessageBox::Ok); + return false; + } + + return true; +} + +void ChroniclesExtractor::createBaseMod() const +{ + QDir dir(pathToQString(VCMIDirs::get().userDataPath() / "Mods")); + dir.mkdir("chronicles"); + dir.cd("chronicles"); + dir.mkdir("Mods"); + + QJsonObject mod + { + { "modType", "Expansion" }, + { "name", tr("Heroes Chronicles") }, + { "description", tr("Heroes Chronicles") }, + { "author", "3DO" }, + { "version", "1.0" }, + { "contact", "vcmi.eu" }, + { "heroes", QJsonArray({"config/portraitsChronicles.json"}) }, + { "settings", QJsonObject({{"mapFormat", QJsonObject({{"chronicles", QJsonObject({{ + {"supported", true}, + {"portraits", QJsonObject({ + {"portraitTarnumBarbarian", 163}, + {"portraitTarnumKnight", 164}, + {"portraitTarnumWizard", 165}, + {"portraitTarnumRanger", 166}, + {"portraitTarnumOverlord", 167}, + {"portraitTarnumBeastmaster", 168}, + })}, + }})}})}})}, + }; + + QFile jsonFile(dir.filePath("mod.json")); + jsonFile.open(QFile::WriteOnly); + jsonFile.write(QJsonDocument(mod).toJson()); + + for(auto & dataPath : VCMIDirs::get().dataPaths()) + { + auto file = pathToQString(dataPath / "config" / "heroes" / "portraitsChronicles.json"); + auto destFolder = VCMIDirs::get().userDataPath() / "Mods" / "chronicles" / "content" / "config"; + auto destFile = pathToQString(destFolder / "portraitsChronicles.json"); + if(QFile::exists(file)) + { + QDir().mkpath(pathToQString(destFolder)); + QFile::remove(destFile); + QFile::copy(file, destFile); + } + } +} + +void ChroniclesExtractor::createChronicleMod(int no) +{ + QDir dir(pathToQString(VCMIDirs::get().userDataPath() / "Mods" / "chronicles" / "Mods" / ("chronicles_" + std::to_string(no)))); + dir.removeRecursively(); + dir.mkpath("."); + + QString tmpChronicles = chronicles.at(no); + + QJsonObject mod + { + { "modType", "Expansion" }, + { "name", QString("%1 - %2").arg(no).arg(tmpChronicles) }, + { "description", tr("Heroes Chronicles %1 - %2").arg(no).arg(tmpChronicles) }, + { "author", "3DO" }, + { "version", "1.0" }, + { "contact", "vcmi.eu" }, + }; + + QFile jsonFile(dir.filePath("mod.json")); + jsonFile.open(QFile::WriteOnly); + jsonFile.write(QJsonDocument(mod).toJson()); + + dir.cd("content"); + + extractFiles(no); +} + +void ChroniclesExtractor::extractFiles(int no) const +{ + QString tmpChronicles = chronicles.at(no); + + std::string chroniclesDir = "chronicles_" + std::to_string(no); + QDir tmpDir = tempDir.filePath(tempDir.entryList({"app"}, QDir::Filter::Dirs).front()); + tmpDir.setPath(tmpDir.filePath(tmpDir.entryList({QString(tmpChronicles)}, QDir::Filter::Dirs).front())); + tmpDir.setPath(tmpDir.filePath(tmpDir.entryList({"data"}, QDir::Filter::Dirs).front())); + auto basePath = VCMIDirs::get().userDataPath() / "Mods" / "chronicles" / "Mods" / chroniclesDir / "content"; + QDir outDirDataPortraits(pathToQString(VCMIDirs::get().userDataPath() / "Mods" / "chronicles" / "content" / "Data")); + QDir outDirData(pathToQString(basePath / "Data" / chroniclesDir)); + QDir outDirSprites(pathToQString(basePath / "Sprites" / chroniclesDir)); + QDir outDirVideo(pathToQString(basePath / "Video" / chroniclesDir)); + QDir outDirSounds(pathToQString(basePath / "Sounds" / chroniclesDir)); + QDir outDirMaps(pathToQString(basePath / "Maps" / "Chronicles")); + + auto extract = [](QDir scrDir, QDir dest, QString file, std::vector files = {}){ + CArchiveLoader archive("", scrDir.filePath(scrDir.entryList({file}).front()).toStdString(), false); + for(auto & entry : archive.getEntries()) + if(files.empty()) + archive.extractToFolder(dest.absolutePath().toStdString(), "", entry.second, true); + else + { + for(const auto & item : files) + if(boost::algorithm::to_lower_copy(entry.second.name).find(boost::algorithm::to_lower_copy(item)) != std::string::npos) + archive.extractToFolder(dest.absolutePath().toStdString(), "", entry.second, true); + } + }; + + extract(tmpDir, outDirData, "xBitmap.lod"); + extract(tmpDir, outDirData, "xlBitmap.lod"); + extract(tmpDir, outDirSprites, "xSprite.lod"); + extract(tmpDir, outDirSprites, "xlSprite.lod"); + extract(tmpDir, outDirVideo, "xVideo.vid"); + extract(tmpDir, outDirSounds, "xSound.snd"); + + tmpDir.cdUp(); + if(tmpDir.entryList({"maps"}, QDir::Filter::Dirs).size()) // special case for "The World Tree": the map is in the "Maps" folder instead of inside the lod + { + QDir tmpDirMaps = tmpDir.filePath(tmpDir.entryList({"maps"}, QDir::Filter::Dirs).front()); + for(const auto & entry : tmpDirMaps.entryList()) + QFile(tmpDirMaps.filePath(entry)).copy(outDirData.filePath(entry)); + } + + tmpDir.cdUp(); + QDir tmpDirData = tmpDir.filePath(tmpDir.entryList({"data"}, QDir::Filter::Dirs).front()); + auto tarnumPortraits = std::vector{"HPS137", "HPS138", "HPS139", "HPS140", "HPS141", "HPS142", "HPL137", "HPL138", "HPL139", "HPL140", "HPL141", "HPL142"}; + extract(tmpDirData, outDirDataPortraits, "bitmap.lod", tarnumPortraits); + extract(tmpDirData, outDirData, "lbitmap.lod", std::vector{"INTRORIM"}); + + if(!outDirMaps.exists()) + outDirMaps.mkpath("."); + QString campaignFileName = "Hc" + QString::number(no) + "_Main.h3c"; + QFile(outDirData.filePath(outDirData.entryList({"Main.h3c"}).front())).copy(outDirMaps.filePath(campaignFileName)); +} + +void ChroniclesExtractor::installChronicles(QStringList exe) +{ + logGlobal->info("Installing Chronicles"); + + extractionFile = -1; + fileCount = exe.size(); + for(QString f : exe) + { + extractionFile++; + + logGlobal->info("Creating temporary directory"); + if(!createTempDir()) + continue; + + logGlobal->info("Copying offline installer"); + // FIXME: this is required at the moment for Android (and possibly iOS) + // Incoming file names are in content URI form, e.g. content://media/internal/chronicles.exe + // Qt can handle those like it does regular files + // however, innoextract fails to open such files + // so make a copy in directory to which vcmi always has full access and operate on it + QString filepath = tempDir.filePath("chr.exe"); + QFile(f).copy(filepath); + QFile file(filepath); + + logGlobal->info("Extracting offline installer"); + if(!extractGogInstaller(filepath)) + continue; + + logGlobal->info("Detecting Chronicle"); + int chronicleNo = getChronicleNo(); + if(!chronicleNo) + continue; + + logGlobal->info("Creating base Chronicle mod"); + createBaseMod(); + + logGlobal->info("Creating Chronicle mod"); + createChronicleMod(chronicleNo); + + logGlobal->info("Removing temporary directory"); + removeTempDir(); + } + + logGlobal->info("Chronicles installed"); +} diff --git a/launcher/modManager/chroniclesextractor.h b/launcher/modManager/chroniclesextractor.h new file mode 100644 index 000000000..2e55a7677 --- /dev/null +++ b/launcher/modManager/chroniclesextractor.h @@ -0,0 +1,48 @@ +/* + * chroniclesextractor.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 "../StdInc.h" + +class ChroniclesExtractor : public QObject +{ + Q_OBJECT + + QWidget *parent; + std::function cb; + + QDir tempDir; + int extractionFile; + int fileCount; + + bool createTempDir(); + void removeTempDir(); + int getChronicleNo(); + bool extractGogInstaller(QString filePath); + void createBaseMod() const; + void createChronicleMod(int no); + void extractFiles(int no) const; + + const QStringList chronicles = { + {}, // fake 0th "chronicle", to create 1-based list + "Warlords of the Wasteland", + "Conquest of the Underworld", + "Masters of the Elements", + "Clash of the Dragons", + "The World Tree", + "The Fiery Moon", + "Revolt of the Beastmasters", + "The Sword of Frost", + }; +public: + void installChronicles(QStringList exe); + + ChroniclesExtractor(QWidget *p, std::function cb = nullptr); +}; 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 1adf9e5b6..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 verison 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/cmodlistmodel_moc.cpp b/launcher/modManager/cmodlistmodel_moc.cpp deleted file mode 100644 index 258ffbd83..000000000 --- a/launcher/modManager/cmodlistmodel_moc.cpp +++ /dev/null @@ -1,295 +0,0 @@ -/* - * cmodlistmodel_moc.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 "cmodlistmodel_moc.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) - : QAbstractItemModel(parent) -{ -} - -QString CModListModel::modIndexToName(const QModelIndex & index) const -{ - if(index.isValid()) - { - return modNameToID.at(index.internalId()); - } - return ""; -} - - -QString CModListModel::modTypeName(QString modTypeID) const -{ - static const QMap modTypes = { - {"Translation", tr("Translation")}, - {"Town", tr("Town") }, - {"Test", tr("Test") }, - {"Templates", tr("Templates") }, - {"Spells", tr("Spells") }, - {"Music", tr("Music") }, - {"Maps", tr("Maps") }, - {"Sounds", tr("Sounds") }, - {"Skills", tr("Skills") }, - {"Other", tr("Other") }, - {"Objects", tr("Objects") }, - {"Mechanical", tr("Mechanics") }, - {"Mechanics", tr("Mechanics") }, - {"Themes", tr("Interface") }, - {"Interface", tr("Interface") }, - {"Heroes", tr("Heroes") }, - {"Graphic", tr("Graphical") }, - {"Graphical", tr("Graphical") }, - {"Expansion", tr("Expansion") }, - {"Creatures", tr("Creatures") }, - {"Compatibility", tr("Compatibility") }, - {"Artifacts", tr("Artifacts") }, - {"AI", tr("AI") }, - }; - - if (modTypes.contains(modTypeID)) - return modTypes[modTypeID]; - return tr("Other"); -} - -QVariant CModListModel::getValue(const CModEntry & mod, int field) const -{ - switch(field) - { - case ModFields::STATUS_ENABLED: - return mod.getModStatus() & (ModStatus::ENABLED | ModStatus::INSTALLED); - - case ModFields::STATUS_UPDATE: - return mod.getModStatus() & (ModStatus::UPDATEABLE | ModStatus::INSTALLED); - - case ModFields::NAME: - return mod.getValue("name"); - - case ModFields::TYPE: - return modTypeName(mod.getValue("modType").toString()); - - default: - return QVariant(); - } -} - -QVariant CModListModel::getText(const CModEntry & mod, int field) const -{ - switch(field) - { - case ModFields::STATUS_ENABLED: - case ModFields::STATUS_UPDATE: - return ""; - default: - return getValue(mod, field); - } -} - -QVariant CModListModel::getIcon(const CModEntry & 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); - - if(field == ModFields::STATUS_UPDATE && mod.isUpdateable()) - return QIcon(ModStatus::iconUpdate); - if(field == ModFields::STATUS_UPDATE && !mod.isInstalled()) - return QIcon(ModStatus::iconDownload); - - return QVariant(); -} - -QVariant CModListModel::getTextAlign(int field) const -{ - return QVariant(Qt::AlignLeft | Qt::AlignVCenter); -} - -QVariant CModListModel::data(const QModelIndex & index, int role) const -{ - if(index.isValid()) - { - auto mod = getMod(modIndexToName(index)); - - switch(role) - { - case Qt::DecorationRole: - return getIcon(mod, index.column()); - case Qt::DisplayRole: - return getText(mod, index.column()); - case Qt::TextAlignmentRole: - return getTextAlign(index.column()); - case ModRoles::ValueRole: - return getValue(mod, index.column()); - case ModRoles::ModNameRole: - return mod.getName(); - } - } - return QVariant(); -} - -int CModListModel::rowCount(const QModelIndex & index) const -{ - if(index.isValid()) - return modIndex[modIndexToName(index)].size(); - return modIndex[""].size(); -} - -int CModListModel::columnCount(const QModelIndex &) const -{ - return ModFields::COUNT; -} - -Qt::ItemFlags CModListModel::flags(const QModelIndex &) const -{ - return Qt::ItemIsSelectable | Qt::ItemIsEnabled; -} - -QVariant CModListModel::headerData(int section, Qt::Orientation orientation, int role) const -{ - static const QString header[ModFields::COUNT] = - { - QT_TRANSLATE_NOOP("ModFields", "Name"), - QT_TRANSLATE_NOOP("ModFields", ""), // status icon - QT_TRANSLATE_NOOP("ModFields", ""), // status icon - QT_TRANSLATE_NOOP("ModFields", "Type"), - }; - - if(role == Qt::DisplayRole && orientation == Qt::Horizontal) - return QCoreApplication::translate("ModFields", header[section].toStdString().c_str()); - return QVariant(); -} - -void CModListModel::reloadRepositories() -{ - beginResetModel(); - endResetModel(); -} - -void CModListModel::resetRepositories() -{ - beginResetModel(); - CModList::resetRepositories(); - endResetModel(); -} - -void CModListModel::modChanged(QString modID) -{ - int index = modNameToID.indexOf(modID); - QModelIndex parent = this->parent(createIndex(0, 0, index)); - int row = modIndex[modIndexToName(parent)].indexOf(modID); - emit dataChanged(createIndex(row, 0, index), createIndex(row, 4, index)); -} - -void CModListModel::endResetModel() -{ - modNameToID = getModList(); - modIndex.clear(); - for(const QString & str : modNameToID) - { - if(str.contains('.')) - { - modIndex[str.section('.', 0, -2)].append(str); - } - else - { - modIndex[""].append(str); - } - } - QAbstractItemModel::endResetModel(); -} - -QModelIndex CModListModel::index(int row, int column, const QModelIndex & parent) const -{ - if(parent.isValid()) - { - if(modIndex[modIndexToName(parent)].size() > row) - return createIndex(row, column, modNameToID.indexOf(modIndex[modIndexToName(parent)][row])); - } - else - { - if(modIndex[""].size() > row) - return createIndex(row, column, modNameToID.indexOf(modIndex[""][row])); - } - return QModelIndex(); -} - -QModelIndex CModListModel::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 - { - if(entry.key() != "" && entry.value().indexOf(modID) != -1) - { - return createIndex(entry.value().indexOf(modID), child.column(), modNameToID.indexOf(entry.key())); - } - } - return QModelIndex(); -} - -void CModFilterModel::setTypeFilter(int filteredType, int filterMask) -{ - this->filterMask = filterMask; - this->filteredType = filteredType; - invalidateFilter(); -} - -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()); -} - -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()) - return false; - - if(filterMatchesThis(index)) - { - return true; - } - - for(size_t i = 0; i < base->rowCount(index); i++) - { - if(filterMatchesThis(base->index((int)i, 0, index))) - return true; - } - - QModelIndex parent = source_parent; - while(parent.isValid()) - { - if(filterMatchesThis(parent)) - return true; - parent = parent.parent(); - } - return false; -} - -CModFilterModel::CModFilterModel(CModListModel * model, QObject * parent) - : QSortFilterProxyModel(parent), base(model), filteredType(ModStatus::MASK_NONE), filterMask(ModStatus::MASK_NONE) -{ - setSourceModel(model); - setSortRole(ModRoles::ValueRole); -} diff --git a/launcher/modManager/cmodlistview_moc.cpp b/launcher/modManager/cmodlistview_moc.cpp index 1a4aa2348..3bc5b7e98 100644 --- a/launcher/modManager/cmodlistview_moc.cpp +++ b/launcher/modManager/cmodlistview_moc.cpp @@ -17,28 +17,37 @@ #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" #include "../../lib/CConfigHandler.h" -#include "../../lib/Languages.h" +#include "../../lib/VCMIDirs.h" +#include "../../lib/filesystem/Filesystem.h" +#include "../../lib/json/JsonUtils.h" #include "../../lib/modding/CModVersion.h" +#include "../../lib/texts/CGeneralTextHandler.h" +#include "../../lib/texts/Languages.h" -static double mbToBytes(double mb) -{ - return mb * 1024 * 1024; -} +#include 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->setRepositoryData(cachedRepositoryData); + + modModel = new ModStateItemModel(modStateModel, this); + manager = std::make_unique(modStateModel); } void CModListView::changeEvent(QEvent *event) @@ -46,35 +55,11 @@ void CModListView::changeEvent(QEvent *event) if(event->type() == QEvent::LanguageChange) { ui->retranslateUi(this); - modModel->reloadRepositories(); + modModel->reloadViewModel(); } QWidget::changeEvent(event); } -void CModListView::dragEnterEvent(QDragEnterEvent* event) -{ - if(event->mimeData()->hasUrls()) - for(const auto & url : event->mimeData()->urls()) - for(const auto & ending : QStringList({".zip", ".h3m", ".h3c", ".vmap", ".vcmp", ".json"})) - if(url.fileName().endsWith(ending, Qt::CaseInsensitive)) - { - event->acceptProposedAction(); - return; - } -} - -void CModListView::dropEvent(QDropEvent* event) -{ - const QMimeData* mimeData = event->mimeData(); - - if(mimeData->hasUrls()) - { - const QList urlList = mimeData->urls(); - for (const auto & url : urlList) - manualInstallFile(url.toLocalFile()); - } -} - void CModListView::setupFilterModel() { filterModel = new CModFilterModel(modModel, this); @@ -126,14 +111,15 @@ CModListView::CModListView(QWidget * parent) { ui->setupUi(this); - setAcceptDrops(true); - ui->uninstallButton->setIcon(QIcon{":/icons/mod-delete.png"}); ui->enableButton->setIcon(QIcon{":/icons/mod-enabled.png"}); ui->disableButton->setIcon(QIcon{":/icons/mod-disabled.png"}); ui->updateButton->setIcon(QIcon{":/icons/mod-update.png"}); ui->installButton->setIcon(QIcon{":/icons/mod-download.png"}); + ui->splitter->setStyleSheet("QSplitter::handle {background: palette('window');}"); + + disableModInfo(); setupModModel(); setupFilterModel(); setupModsView(); @@ -141,14 +127,9 @@ CModListView::CModListView(QWidget * parent) ui->progressWidget->setVisible(false); dlManager = nullptr; + modModel->reloadViewModel(); if(settings["launcher"]["autoCheckRepositories"].Bool()) - { loadRepositories(); - } - else - { - manager->resetRepositories(); - } #ifdef VCMI_MOBILE for(auto * scrollWidget : { @@ -163,9 +144,15 @@ CModListView::CModListView(QWidget * parent) #endif } +void CModListView::reload() +{ + modStateModel->reloadLocalState(); + modModel->reloadViewModel(); +} + void CModListView::loadRepositories() { - manager->resetRepositories(); + accumulatedRepositoryData.clear(); QStringList repositories; @@ -175,7 +162,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; @@ -198,11 +185,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()); @@ -217,7 +214,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 = "

    "; @@ -227,7 +224,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) @@ -236,37 +233,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 @@ -280,29 +299,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) @@ -321,52 +351,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); @@ -389,6 +424,8 @@ void CModListView::dataChanged(const QModelIndex & topleft, const QModelIndex & void CModListView::selectMod(const QModelIndex & index) { + ui->tabWidget->setCurrentIndex(0); + if(!index.isValid()) { disableModInfo(); @@ -396,7 +433,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)); @@ -404,24 +444,23 @@ 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 = getModsToInstall(modName); + QStringList unavailableDependencies = findUnavailableMods(notInstalledDependencies); + bool translationMismatch = mod.isTranslation() && CGeneralTextHandler::getPreferredLanguage() != mod.getBaseLanguage().toStdString(); + bool modIsBeingDownloaded = enqueuedModDownloads.contains(mod.getID()); - 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() && !modIsBeingDownloaded); + ui->uninstallButton->setEnabled(true); + ui->updateButton->setEnabled(unavailableDependencies.empty() && !modIsBeingDownloaded); loadScreenshots(); } @@ -454,149 +493,127 @@ 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->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); + } + + if (modStateModel->isModExists(potentialToInstall) && !modStateModel->isModInstalled(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(); + doUpdateMod(modName); - assert(findInvalidDependencies(modName).empty()); + ui->updateButton->setEnabled(false); +} - for(auto & name : modModel->getRequirements(modName)) +void CModListView::doUpdateMod(const QString & modName) +{ + auto targetMod = modStateModel->getMod(modName); + + if(targetMod.isUpdateAvailable()) + downloadMod(targetMod); + + 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()) + downloadMod(mod); } } 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); + reload(); } - emit modsChanged(); checkManagerErrors(); } @@ -604,89 +621,21 @@ void CModListView::on_installButton_clicked() { QString modName = ui->allModsView->currentIndex().data(ModRoles::ModNameRole).toString(); - assert(findInvalidDependencies(modName).empty()); + doInstallMod(modName); - for(auto & name : modModel->getRequirements(modName)) - { - auto mod = modModel->getMod(name); - if(mod.isAvailable()) - downloadFile(name + ".zip", mod.getValue("download").toString(), name, mbToBytes(mod.getValue("downloadSize").toDouble())); - else if(!mod.isEnabled()) - 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); - } - } + ui->installButton->setEnabled(false); } -void CModListView::on_installFromFileButton_clicked() +void CModListView::downloadMod(const ModState & mod) { - // iOS can't display modal dialogs when called directly on button press - // https://bugreports.qt.io/browse/QTBUG-98651 - QTimer::singleShot(0, this, [this] - { - QString filter = tr("All supported files") + " (*.h3m *.vmap *.h3c *.vcmp *.zip *.json);;" + tr("Maps") + " (*.h3m *.vmap);;" + tr("Campaigns") + " (*.h3c *.vcmp);;" + tr("Configs") + " (*.json);;" + tr("Mods") + " (*.zip)"; - QStringList files = QFileDialog::getOpenFileNames(this, tr("Select files (configs, mods, maps, campaigns) to install..."), QDir::homePath(), filter); + if (enqueuedModDownloads.contains(mod.getID())) + return; - for(const auto & file : files) - { - manualInstallFile(file); - } - }); + enqueuedModDownloads.push_back(mod.getID()); + downloadFile(mod.getID() + ".zip", mod.getDownloadUrl(), mod.getName(), mod.getDownloadSizeBytes()); } -void CModListView::manualInstallFile(QString filePath) -{ - QString fileName = QFileInfo{filePath}.fileName(); - if(filePath.endsWith(".zip", Qt::CaseInsensitive)) - downloadFile(fileName.toLower() - // mod name currently comes from zip file -> remove suffixes from github zip download - .replace(QRegularExpression("-[0-9a-f]{40}"), "") - .replace(QRegularExpression("-vcmi-.+\\.zip"), ".zip") - .replace("-main.zip", ".zip") - , QUrl::fromLocalFile(filePath), "mods"); - else if(filePath.endsWith(".json", Qt::CaseInsensitive)) - { - QDir configDir(QString::fromStdString(VCMIDirs::get().userConfigPath().string())); - QStringList configFile = configDir.entryList({fileName}, QDir::Filter::Files); // case insensitive check - if(!configFile.empty()) - { - auto dialogResult = QMessageBox::warning(this, tr("Replace config file?"), tr("Do you want to replace %1?").arg(configFile[0]), QMessageBox::Yes | QMessageBox::No, QMessageBox::No); - if(dialogResult == QMessageBox::Yes) - { - const auto configFilePath = configDir.filePath(configFile[0]); - QFile::remove(configFilePath); - QFile::copy(filePath, configFilePath); - - // reload settings - Helper::loadSettings(); - for(auto widget : qApp->allWidgets()) - if(auto settingsView = qobject_cast(widget)) - settingsView->loadSettings(); - manager->loadMods(); - manager->loadModSettings(); - } - } - } - else - downloadFile(fileName, QUrl::fromLocalFile(filePath), fileName); -} - -void CModListView::downloadFile(QString file, QString url, QString description, qint64 size) -{ - downloadFile(file, QUrl{url}, description, size); -} - -void CModListView::downloadFile(QString file, QUrl url, QString description, qint64 size) +void CModListView::downloadFile(QString file, QUrl url, QString description, qint64 sizeBytes) { if(!dlManager) { @@ -701,13 +650,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) @@ -756,6 +705,7 @@ void CModListView::downloadFinished(QStringList savedFiles, QStringList failedFi doInstallFiles = true; } + enqueuedModDownloads.clear(); dlManager->deleteLater(); dlManager = nullptr; @@ -766,7 +716,6 @@ void CModListView::downloadFinished(QStringList savedFiles, QStringList failedFi installFiles(savedFiles); hideProgressBar(); - emit modsChanged(); } void CModListView::hideProgressBar() @@ -784,7 +733,8 @@ void CModListView::installFiles(QStringList files) QStringList mods; QStringList maps; QStringList images; - QVector repositories; + QStringList exe; + bool repositoryFilesEnqueued = false; // TODO: some better way to separate zip's with mods and downloaded repository files for(QString filename : files) @@ -793,42 +743,101 @@ void CModListView::installFiles(QStringList files) mods.push_back(filename); else if(filename.endsWith(".h3m", Qt::CaseInsensitive) || filename.endsWith(".h3c", Qt::CaseInsensitive) || filename.endsWith(".vmap", Qt::CaseInsensitive) || filename.endsWith(".vcmp", Qt::CaseInsensitive)) maps.push_back(filename); + if(filename.endsWith(".exe", Qt::CaseInsensitive)) + exe.push_back(filename); else if(filename.endsWith(".json", Qt::CaseInsensitive)) { //download and merge additional files - auto repoData = JsonUtils::JsonFromFile(filename).toMap(); - if(repoData.value("name").isNull()) + JsonNode 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()) + auto modNameLower = boost::algorithm::to_lower_copy(modName); + auto modJsonUrl = modJson["mod"]; + if(!modJsonUrl.isNull()) { - downloadFile(key + ".json", modjson.toString(), tr("mods repository index")); + downloadFile(QString::fromStdString(modName + ".json"), QString::fromStdString(modJsonUrl.String()), tr("mods repository index")); + repositoryFilesEnqueued = true; } + + accumulatedRepositoryData[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); + JsonUtils::merge(accumulatedRepositoryData[modNameLower], repoData); } - repositories.push_back(repoData); } else if(filename.endsWith(".png", Qt::CaseInsensitive)) images.push_back(filename); } - if (!repositories.empty()) - manager->loadRepositories(repositories); + if (!accumulatedRepositoryData.isNull() && !repositoryFilesEnqueued) + { + logGlobal->info("Installing repository: started"); + manager->setRepositoryData(accumulatedRepositoryData); + modModel->reloadViewModel(); + accumulatedRepositoryData.clear(); + + static const QString repositoryCachePath = CLauncherDirs::downloadsPath() + "/repositoryCache.json"; + JsonUtils::jsonToFile(repositoryCachePath, modStateModel->getRepositoryData()); + logGlobal->info("Installing repository: ended"); + } if(!mods.empty()) + { + logGlobal->info("Installing mods: started"); installMods(mods); + reload(); + logGlobal->info("Installing mods: ended"); + } if(!maps.empty()) + { + logGlobal->info("Installing maps: started"); installMaps(maps); + logGlobal->info("Installing maps: ended"); + } + + if(!exe.empty()) + { + logGlobal->info("Installing chronicles: started"); + ui->progressBar->setFormat(tr("Installing Heroes Chronicles")); + ui->progressWidget->setVisible(true); + ui->pushButton->setEnabled(false); + + float prog = 0.0; + + auto futureExtract = std::async(std::launch::async, [this, exe, &prog]() + { + ChroniclesExtractor ce(this, [&prog](float progress) { prog = progress; }); + ce.installChronicles(exe); + reload(); + enableModByName("chronicles"); + return true; + }); + + while(futureExtract.wait_for(std::chrono::milliseconds(10)) != std::future_status::ready) + { + emit extractionProgress(static_cast(prog * 1000.f), 1000); + qApp->processEvents(); + } + + if(futureExtract.get()) + { + hideProgressBar(); + ui->pushButton->setEnabled(true); + ui->progressWidget->setVisible(false); + //update + reload(); + } + logGlobal->info("Installing chronicles: ended"); + } if(!images.empty()) loadScreenshots(); @@ -837,6 +846,7 @@ void CModListView::installFiles(QStringList files) void CModListView::installMods(QStringList archives) { QStringList modNames; + QStringList modsToEnable; for(QString archive : archives) { @@ -847,70 +857,47 @@ 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->isModExists(mod) && modStateModel->getMod(mod).isInstalled()) { - // enable mod if installed and enabled - if(entry.isEnabled()) + logGlobal->info("Uninstalling old version of mod '%s'", mod.toStdString()); + 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); - } + reload(); // FIXME: better way that won't reset selection for(int i = 0; i < modNames.size(); i++) { + logGlobal->info("Installing mod '%s'", modNames[i].toStdString()); ui->progressBar->setFormat(tr("Installing mod %1").arg(modNames[i])); manager->installMod(modNames[i], archives[i]); } - std::function enableMod; + reload(); - enableMod = [&](QString modName) + if (!modsToEnable.empty()) { - 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(); for(QString archive : archives) + { + logGlobal->info("Erasing archive '%s'", archive.toStdString()); QFile::remove(archive); + } } void CModListView::installMaps(QStringList maps) @@ -919,6 +906,7 @@ void CModListView::installMaps(QStringList maps) for(QString map : maps) { + logGlobal->info("Importing map '%s'", map.toStdString()); QFile(map).rename(destDir + map.section('/', -1, -1)); } } @@ -960,11 +948,17 @@ void CModListView::loadScreenshots() { if(ui->tabWidget->currentIndex() == 2) { + if(!ui->allModsView->currentIndex().isValid()) + { + // select the first mod, so we can access its data + ui->allModsView->setCurrentIndex(filterModel->index(0, 0)); + } + 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); @@ -998,46 +992,77 @@ 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 : getModsToInstall(modName)) { - auto mod = modModel->getMod(name); - if(!mod.isInstalled()) - downloadFile(name + ".zip", mod.getValue("download").toString(), name, mbToBytes(mod.getValue("downloadSize").toDouble())); + auto mod = modStateModel->getMod(name); + if(mod.isAvailable()) + downloadMod(mod); + else if(!modStateModel->isModEnabled(name)) + enableModByName(name); } } 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 = modStateModel->getMod(modName); + return mod.isInstalled(); +} + +QStringList CModListView::getInstalledChronicles() +{ + QStringList result; + + for(const auto & modName : modStateModel->getAllMods()) + { + auto mod = modStateModel->getMod(modName); + if (!mod.isInstalled()) + continue; + + if (mod.getTopParentID() != "chronicles") + continue; + + result += modName; + } + + return result; +} + +QStringList CModListView::getUpdateableMods() +{ + QStringList result; + + for(const auto & modName : modStateModel->getAllMods()) + { + auto mod = modStateModel->getMod(modName); + if (mod.isUpdateAvailable()) + result.push_back(modName); + } + + return result; } 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; @@ -1052,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; @@ -1080,15 +1104,46 @@ 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; } } + +void CModListView::createNewPreset(const QString & presetName) +{ + modStateModel->createNewPreset(presetName); +} + +void CModListView::deletePreset(const QString & presetName) +{ + modStateModel->deletePreset(presetName); +} + +void CModListView::activatePreset(const QString & presetName) +{ + modStateModel->activatePreset(presetName); + reload(); +} + +void CModListView::renamePreset(const QString & oldPresetName, const QString & newPresetName) +{ + modStateModel->renamePreset(oldPresetName, newPresetName); +} + +QStringList CModListView::getAllPresets() const +{ + return modStateModel->getAllPresets(); +} + +QString CModListView::getActivePreset() const +{ + return modStateModel->getActivePreset(); +} diff --git a/launcher/modManager/cmodlistview_moc.h b/launcher/modManager/cmodlistview_moc.h index 7cfe44012..6dbd198ce 100644 --- a/launcher/modManager/cmodlistview_moc.h +++ b/launcher/modManager/cmodlistview_moc.h @@ -17,23 +17,28 @@ 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; + JsonNode accumulatedRepositoryData; + + QStringList enqueuedModDownloads; void setupModModel(); void setupFilterModel(); @@ -42,31 +47,21 @@ 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); - - 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); + QStringList findUnavailableMods(QStringList candidates); 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); @@ -75,26 +70,54 @@ public: void loadScreenshots(); void loadRepositories(); + void reload(); + void disableModInfo(); void selectMod(const QModelIndex & index); - const CModList & getModList() const; - // First Launch View interface /// install mod by name void doInstallMod(const QString & modName); + /// update mod by name + void doUpdateMod(const QString & modName); + /// returns true if mod is available in repository and can be installed bool isModAvailable(const QString & modName); /// finds translation mod for specified languages. Returns empty string on error QString getTranslationModName(const QString & language); + /// finds all already imported Heroes Chronicles mods (if any) + QStringList getInstalledChronicles(); + + /// finds all mods that can be updated + QStringList getUpdateableMods(); + + void createNewPreset(const QString & presetName); + + void deletePreset(const QString & presetName); + + void activatePreset(const QString & presetName); + + void renamePreset(const QString & oldPresetName, const QString & newPresetName); + + QStringList getAllPresets() const; + + QString getActivePreset() const; + /// returns true if mod is currently enabled bool isModEnabled(const QString & modName); + /// returns true if mod is currently installed + bool isModInstalled(const QString & modName); + + void downloadMod(const ModState & mod); + void downloadFile(QString file, QUrl url, QString description, qint64 sizeBytes = 0); + void installFiles(QStringList mods); + public slots: void enableModByName(QString modName); void disableModByName(QString modName); @@ -109,31 +132,17 @@ private slots: void hideProgressBar(); void on_lineEdit_textChanged(const QString & arg1); - void on_comboBox_currentIndexChanged(int index); - void on_enableButton_clicked(); - void on_disableButton_clicked(); - void on_updateButton_clicked(); - void on_uninstallButton_clicked(); - void on_installButton_clicked(); - - void on_installFromFileButton_clicked(); - void on_pushButton_clicked(); - void on_refreshButton_clicked(); - void on_allModsView_activated(const QModelIndex & index); - void on_tabWidget_currentChanged(int index); - void on_screenshotsList_clicked(const QModelIndex & index); - void on_allModsView_doubleClicked(const QModelIndex &index); private: diff --git a/launcher/modManager/cmodlistview_moc.ui b/launcher/modManager/cmodlistview_moc.ui index dd50b6929..0957aee7f 100644 --- a/launcher/modManager/cmodlistview_moc.ui +++ b/launcher/modManager/cmodlistview_moc.ui @@ -42,6 +42,9 @@ Filter + + true + @@ -191,7 +194,9 @@ <html><head><meta name="qrichtext" content="1" /><meta charset="utf-8" /><style type="text/css"> p, li { white-space: pre-wrap; } hr { height: 1px; border-width: 0; } -</style></head><body style=" font-family:'.AppleSystemUIFont'; font-size:13pt; font-weight:400; font-style:normal;"> +li.unchecked::marker { content: "\2610"; } +li.checked::marker { content: "\2612"; } +</style></head><body style=" font-family:'Sans Serif'; font-size:9pt; font-weight:400; font-style:normal;"> <p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px; font-family:'Ubuntu'; font-size:11pt;"><br /></p></body></html> @@ -317,6 +322,9 @@ hr { height: 1px; border-width: 0; } 0 + + Qt::AlignCenter + true @@ -349,41 +357,6 @@ hr { height: 1px; border-width: 0; } 6 - - - - - 0 - 0 - - - - - 51 - 0 - - - - - 170 - 16777215 - - - - Install from file - - - - icons:mod-download.pngicons:mod-download.png - - - - 20 - 20 - - - - diff --git a/launcher/modManager/cmodmanager.cpp b/launcher/modManager/cmodmanager.cpp deleted file mode 100644 index a290ff760..000000000 --- a/launcher/modManager/cmodmanager.cpp +++ /dev/null @@ -1,382 +0,0 @@ -/* - * cmodmanager.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 "cmodmanager.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 "../jsonutils.h" -#include "../launcherdirs.h" - -#include - -namespace -{ -QString detectModArchive(QString path, QString modName, std::vector & filesToExtract) -{ - try { - ZipArchive archive(qstringToPath(path)); - filesToExtract = archive.listFiles(); - } - catch (const std::runtime_error & e) - { - logGlobal->error("Failed to open zip archive. Reason: %s", e.what()); - return ""; - } - - QString modDirName; - - for(int folderLevel : {0, 1}) //search in subfolder if there is no mod.json in the root - { - for(auto file : filesToExtract) - { - QString filename = QString::fromUtf8(file.c_str()); - modDirName = filename.section('/', 0, folderLevel); - - if(filename == modDirName + "/mod.json") - { - return modDirName; - } - } - } - - logGlobal->error("Failed to detect mod path in archive!"); - logGlobal->debug("List of file in archive:"); - for(auto file : filesToExtract) - logGlobal->debug("%s", file.c_str()); - - return ""; -} -} - - -CModManager::CModManager(CModList * modList) - : modList(modList) -{ - loadMods(); - loadModSettings(); -} - -QString CModManager::settingsPath() -{ - return pathToQString(VCMIDirs::get().userConfigPath() / "modSettings.json"); -} - -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["storedLocaly"].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) -{ - recentErrors.push_back(QString("%1: %2").arg(modname).arg(message)); - return false; -} - -QStringList CModManager::getErrors() -{ - QStringList ret = recentErrors; - recentErrors.clear(); - return ret; -} - -bool CModManager::installMod(QString modname, QString archivePath) -{ - return canInstallMod(modname) && doInstallMod(modname, archivePath); -} - -bool CModManager::uninstallMod(QString modname) -{ - return canUninstallMod(modname) && doUninstallMod(modname); -} - -bool CModManager::enableMod(QString modname) -{ - return canEnableMod(modname) && doEnableMod(modname, true); -} - -bool CModManager::disableMod(QString modname) -{ - return canDisableMod(modname) && doEnableMod(modname, false); -} - -bool CModManager::canInstallMod(QString modname) -{ - auto mod = modList->getMod(modname); - - if(mod.isSubmod()) - return addError(modname, tr("Can not install submod")); - - if(mod.isInstalled()) - return addError(modname, tr("Mod is already installed")); - return true; -} - -bool CModManager::canUninstallMod(QString modname) -{ - auto mod = modList->getMod(modname); - - if(mod.isSubmod()) - return addError(modname, tr("Can not uninstall submod")); - - if(!mod.isInstalled()) - return addError(modname, tr("Mod is not installed")); - - return true; -} - -bool CModManager::canEnableMod(QString modname) -{ - auto mod = modList->getMod(modname); - - if(mod.isEnabled()) - return addError(modname, tr("Mod is already enabled")); - - if(!mod.isInstalled()) - return addError(modname, tr("Mod must be installed first")); - - //check for compatibility - 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(!modList->hasMod(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) -{ - auto mod = modList->getMod(modname); - - if(mod.isDisabled()) - 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) -{ - const auto destDir = CLauncherDirs::modsPath() + QChar{'/'}; - - if(!QFile(archivePath).exists()) - return addError(modname, tr("Mod archive is missing")); - - if(localMods.contains(modname)) - return addError(modname, tr("Mod with such name is already installed")); - - std::vector filesToExtract; - QString modDirName = ::detectModArchive(archivePath, modname, filesToExtract); - if(!modDirName.size()) - return addError(modname, tr("Mod archive is invalid or corrupted")); - - std::atomic filesCounter = 0; - - auto futureExtract = std::async(std::launch::async, [&archivePath, &destDir, &filesCounter, &filesToExtract]() - { - const auto destDirFsPath = qstringToPath(destDir); - ZipArchive archive(qstringToPath(archivePath)); - for (auto const & file : filesToExtract) - { - if (!archive.extract(destDirFsPath, file)) - return false; - ++filesCounter; - } - return true; - }); - - while(futureExtract.wait_for(std::chrono::milliseconds(10)) != std::future_status::ready) - { - emit extractionProgress(filesCounter, filesToExtract.size()); - qApp->processEvents(); - } - - if(!futureExtract.get()) - { - removeModDir(destDir + modDirName); - return addError(modname, tr("Failed to extract mod data")); - } - - //rename folder and fix the path - QDir extractedDir(destDir + modDirName); - auto rc = QFile::rename(destDir + modDirName, destDir + modname); - if (rc) - extractedDir.setPath(destDir + modname); - - //there are possible excessive files - remove them - QString upperLevel = modDirName.section('/', 0, 0); - if(upperLevel != modDirName) - removeModDir(destDir + upperLevel); - - CResourceHandler::get("initial")->updateFilteredFiles([](const std::string &) { return true; }); - loadMods(); - modList->reloadRepositories(); - - return true; -} - -bool CModManager::doUninstallMod(QString modname) -{ - ResourcePath resID(std::string("Mods/") + modname.toStdString(), EResType::DIRECTORY); - // Get location of the mod, in case-insensitive way - QString modDir = pathToQString(*CResourceHandler::get()->getResourceName(resID)); - - if(!QDir(modDir).exists()) - return addError(modname, tr("Data with this mod was not found")); - - QDir modFullDir(modDir); - 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(); - - return true; -} - -bool CModManager::removeModDir(QString path) -{ - // issues 2673 and 2680 its why you do not recursively remove without sanity check - QDir checkDir(path); - QDir dir(path); - - if(!checkDir.cdUp() || QString::compare("Mods", checkDir.dirName(), Qt::CaseInsensitive)) - return false; -#ifndef VCMI_MOBILE // ios and android applications are stored in the isolated container - if(!checkDir.cdUp() || QString::compare("vcmi", checkDir.dirName(), Qt::CaseInsensitive)) - return false; - - if(!dir.absolutePath().contains("vcmi", Qt::CaseInsensitive)) - return false; -#endif - if(!dir.absolutePath().contains("Mods", Qt::CaseInsensitive)) - return false; - - return dir.removeRecursively(); -} 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/modstatecontroller.cpp b/launcher/modManager/modstatecontroller.cpp new file mode 100644 index 000000000..df799a8e1 --- /dev/null +++ b/launcher/modManager/modstatecontroller.cpp @@ -0,0 +1,276 @@ +/* + * modstatecontroller.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 "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/IdentifierStorage.h" +#include "../../lib/json/JsonNode.h" +#include "../../lib/texts/CGeneralTextHandler.h" + +#include "../vcmiqt/jsonutils.h" +#include "../vcmiqt/launcherdirs.h" + +#include + +namespace +{ +QString detectModArchive(QString path, QString modName, std::vector & filesToExtract) +{ + try { + ZipArchive archive(qstringToPath(path)); + filesToExtract = archive.listFiles(); + } + catch (const std::runtime_error & e) + { + logGlobal->error("Failed to open zip archive. Reason: %s", e.what()); + return ""; + } + + QString modDirName; + + for(int folderLevel : {0, 1}) //search in subfolder if there is no mod.json in the root + { + for(const auto & file : filesToExtract) + { + QString filename = QString::fromUtf8(file.c_str()); + modDirName = filename.section('/', 0, folderLevel); + + if(filename == modDirName + "/mod.json") + { + return modDirName; + } + } + } + + logGlobal->error("Failed to detect mod path in archive!"); + logGlobal->debug("List of file in archive:"); + for(const auto & file : filesToExtract) + logGlobal->debug("%s", file.c_str()); + + return ""; +} +} + + +ModStateController::ModStateController(std::shared_ptr modList) + : modList(modList) +{ +} + +ModStateController::~ModStateController() = default; + +void ModStateController::setRepositoryData(const JsonNode & repomap) +{ + modList->setRepositoryData(repomap); +} + +bool ModStateController::addError(QString modname, QString message) +{ + recentErrors.push_back(QString("%1: %2").arg(modname).arg(message)); + return false; +} + +QStringList ModStateController::getErrors() +{ + QStringList ret = recentErrors; + recentErrors.clear(); + return ret; +} + +bool ModStateController::installMod(QString modname, QString archivePath) +{ + return canInstallMod(modname) && doInstallMod(modname, archivePath); +} + +bool ModStateController::uninstallMod(QString modname) +{ + return canUninstallMod(modname) && doUninstallMod(modname); +} + +bool ModStateController::enableMods(QStringList modlist) +{ + for (const auto & modname : modlist) + if (!canEnableMod(modname)) + return false; + + modList->doEnableMods(modlist); + return true; +} + +bool ModStateController::disableMod(QString modname) +{ + if (!canDisableMod(modname)) + return false; + modList->doDisableMod(modname); + return true; +} + +bool ModStateController::canInstallMod(QString modname) +{ + if (!modList->isModExists(modname)) + return true; // for installation of unknown mods, e.g. via "Install from file" option + + auto mod = modList->getMod(modname); + + if(mod.isSubmod()) + return addError(modname, tr("Can not install submod")); + + if(mod.isInstalled()) + return addError(modname, tr("Mod is already installed")); + return true; +} + +bool ModStateController::canUninstallMod(QString modname) +{ + auto mod = modList->getMod(modname); + + if(mod.isSubmod()) + return addError(modname, tr("Can not uninstall submod")); + + if(!mod.isInstalled()) + return addError(modname, tr("Mod is not installed")); + + return true; +} + +bool ModStateController::canEnableMod(QString modname) +{ + auto mod = modList->getMod(modname); + + if(modList->isModEnabled(modname)) + return addError(modname, tr("Mod is already enabled")); + + if(!mod.isInstalled()) + return addError(modname, tr("Mod must be installed first")); + + //check for compatibility + if(!mod.isCompatible()) + return addError(modname, tr("Mod is not compatible, please update VCMI and check the latest mod revisions")); + + 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->isModExists(modEntry)) // required mod is not available + return addError(modname, tr("Required mod %1 is missing").arg(modEntry)); + } + + return true; +} + +bool ModStateController::canDisableMod(QString modname) +{ + auto mod = modList->getMod(modname); + + if(!modList->isModEnabled(modname)) + return addError(modname, tr("Mod is already disabled")); + + if(!mod.isInstalled()) + return addError(modname, tr("Mod must be installed first")); + + return true; +} + +bool ModStateController::doInstallMod(QString modname, QString archivePath) +{ + const auto destDir = CLauncherDirs::modsPath() + QChar{'/'}; + + if(!QFile(archivePath).exists()) + return addError(modname, tr("Mod archive is missing")); + + std::vector filesToExtract; + QString modDirName = ::detectModArchive(archivePath, modname, filesToExtract); + if(!modDirName.size()) + return addError(modname, tr("Mod archive is invalid or corrupted")); + + std::atomic filesCounter = 0; + + auto futureExtract = std::async(std::launch::async, [&archivePath, &destDir, &filesCounter, &filesToExtract]() + { + const auto destDirFsPath = qstringToPath(destDir); + ZipArchive archive(qstringToPath(archivePath)); + for(const auto & file : filesToExtract) + { + if (!archive.extract(destDirFsPath, file)) + return false; + ++filesCounter; + } + return true; + }); + + while(futureExtract.wait_for(std::chrono::milliseconds(10)) != std::future_status::ready) + { + emit extractionProgress(filesCounter, filesToExtract.size()); + qApp->processEvents(); + } + + if(!futureExtract.get()) + { + removeModDir(destDir + modDirName); + return addError(modname, tr("Failed to extract mod data")); + } + + //rename folder and fix the path + QDir extractedDir(destDir + modDirName); + auto rc = QFile::rename(destDir + modDirName, destDir + modname); + if (rc) + extractedDir.setPath(destDir + modname); + + //there are possible excessive files - remove them + QString upperLevel = modDirName.section('/', 0, 0); + if(upperLevel != modDirName) + removeModDir(destDir + upperLevel); + + return true; +} + +bool ModStateController::doUninstallMod(QString modname) +{ + ResourcePath resID(std::string("Mods/") + modname.toStdString(), EResType::DIRECTORY); + // Get location of the mod, in case-insensitive way + QString modDir = pathToQString(*CResourceHandler::get()->getResourceName(resID)); + + if(!QDir(modDir).exists()) + return addError(modname, tr("Data with this mod was not found")); + + QDir modFullDir(modDir); + if(!removeModDir(modDir)) + return addError(modname, tr("Mod is located in a protected directory, please remove it manually:\n") + modFullDir.absolutePath()); + + return true; +} + +bool ModStateController::removeModDir(QString path) +{ + // issues 2673 and 2680 its why you do not recursively remove without sanity check + QDir checkDir(path); + QDir dir(path); + + if(!checkDir.cdUp() || QString::compare("Mods", checkDir.dirName(), Qt::CaseInsensitive)) + return false; +#ifndef VCMI_MOBILE // ios and android applications are stored in the isolated container + if(!checkDir.cdUp() || QString::compare("vcmi", checkDir.dirName(), Qt::CaseInsensitive)) + return false; + + if(!dir.absolutePath().contains("vcmi", Qt::CaseInsensitive)) + return false; +#endif + if(!dir.absolutePath().contains("Mods", Qt::CaseInsensitive)) + return false; + + return dir.removeRecursively(); +} diff --git a/launcher/modManager/cmodmanager.h b/launcher/modManager/modstatecontroller.h similarity index 68% rename from launcher/modManager/cmodmanager.h rename to launcher/modManager/modstatecontroller.h index 987d1a580..fa28982ab 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,35 +9,33 @@ */ #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; bool addError(QString modname, QString message); 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 setRepositoryData(const JsonNode & repositoriesList); QStringList getErrors(); @@ -46,7 +44,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/modstateitemmodel_moc.cpp b/launcher/modManager/modstateitemmodel_moc.cpp new file mode 100644 index 000000000..9d9bfe8c1 --- /dev/null +++ b/launcher/modManager/modstateitemmodel_moc.cpp @@ -0,0 +1,326 @@ +/* + * modstateview_moc.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 "modstateitemmodel_moc.h" + +#include "modstatemodel.h" + +#include + +ModStateItemModel::ModStateItemModel(std::shared_ptr model, QObject * parent) + : QAbstractItemModel(parent) + , model(model) +{ +} + +QString ModStateItemModel::modIndexToName(const QModelIndex & index) const +{ + if(index.isValid()) + { + return modNameToID.at(index.internalId()); + } + return ""; +} + + +QString ModStateItemModel::modTypeName(QString modTypeID) const +{ + static const QStringList modTypes = { + QT_TR_NOOP("Translation"), + QT_TR_NOOP("Town"), + QT_TR_NOOP("Test"), + QT_TR_NOOP("Templates"), + QT_TR_NOOP("Spells"), + QT_TR_NOOP("Music"), + QT_TR_NOOP("Maps"), + QT_TR_NOOP("Sounds"), + QT_TR_NOOP("Skills"), + QT_TR_NOOP("Other"), + QT_TR_NOOP("Objects"), + QT_TR_NOOP("Mechanics"), + QT_TR_NOOP("Interface"), + QT_TR_NOOP("Heroes"), + QT_TR_NOOP("Graphical"), + QT_TR_NOOP("Expansion"), + QT_TR_NOOP("Creatures"), + QT_TR_NOOP("Compatibility") , + QT_TR_NOOP("Artifacts"), + QT_TR_NOOP("AI"), + }; + + if (modTypes.contains(modTypeID)) + return tr(modTypeID.toStdString().c_str()); + return tr("Other"); +} + +QVariant ModStateItemModel::getValue(const ModState & mod, int field) const +{ + switch(field) + { + case ModFields::STATUS_ENABLED: + return model->isModEnabled(mod.getID()); + + case ModFields::STATUS_UPDATE: + return model->isModUpdateAvailable(mod.getID()); + + case ModFields::NAME: + return mod.getName(); + + case ModFields::TYPE: + return modTypeName(mod.getType()); + + default: + return QVariant(); + } +} + +QVariant ModStateItemModel::getText(const ModState & mod, int field) const +{ + switch(field) + { + case ModFields::STATUS_ENABLED: + case ModFields::STATUS_UPDATE: + return ""; + default: + return getValue(mod, field); + } +} + +QVariant ModStateItemModel::getIcon(const ModState & mod, int field) const +{ + 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_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 ModStateItemModel::getTextAlign(int field) const +{ + return QVariant(Qt::AlignLeft | Qt::AlignVCenter); +} + +QVariant ModStateItemModel::data(const QModelIndex & index, int role) const +{ + if(index.isValid()) + { + auto mod = model->getMod(modIndexToName(index)); + + switch(role) + { + case Qt::DecorationRole: + return getIcon(mod, index.column()); + case Qt::DisplayRole: + return getText(mod, index.column()); + case Qt::TextAlignmentRole: + return getTextAlign(index.column()); + case ModRoles::ValueRole: + return getValue(mod, index.column()); + case ModRoles::ModNameRole: + return mod.getID(); + } + } + return QVariant(); +} + +int ModStateItemModel::rowCount(const QModelIndex & index) const +{ + if(index.isValid()) + return modIndex[modIndexToName(index)].size(); + return modIndex[""].size(); +} + +int ModStateItemModel::columnCount(const QModelIndex &) const +{ + return ModFields::COUNT; +} + +Qt::ItemFlags ModStateItemModel::flags(const QModelIndex &) const +{ + return Qt::ItemIsSelectable | Qt::ItemIsEnabled; +} + +QVariant ModStateItemModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + static const std::array header = + { + QT_TRANSLATE_NOOP("ModFields", "Name"), + QT_TRANSLATE_NOOP("ModFields", ""), // status icon + QT_TRANSLATE_NOOP("ModFields", ""), // status icon + QT_TRANSLATE_NOOP("ModFields", "Type"), + }; + + if(role == Qt::DisplayRole && orientation == Qt::Horizontal) + return QCoreApplication::translate("ModFields", header[section]); + return QVariant(); +} + +void ModStateItemModel::reloadViewModel() +{ + beginResetModel(); + endResetModel(); +} + +void ModStateItemModel::modChanged(QString modID) +{ + int index = modNameToID.indexOf(modID); + QModelIndex parent = this->parent(createIndex(0, 0, index)); + int row = modIndex[modIndexToName(parent)].indexOf(modID); + emit dataChanged(createIndex(row, 0, index), createIndex(row, 4, index)); +} + +void ModStateItemModel::endResetModel() +{ + modNameToID = model->getAllMods(); + modIndex.clear(); + for(const QString & str : modNameToID) + { + if(str.contains('.')) + { + modIndex[str.section('.', 0, -2)].append(str); + } + else + { + modIndex[""].append(str); + } + } + QAbstractItemModel::endResetModel(); +} + +QModelIndex ModStateItemModel::index(int row, int column, const QModelIndex & parent) const +{ + if(parent.isValid()) + { + if(modIndex[modIndexToName(parent)].size() > row) + return createIndex(row, column, modNameToID.indexOf(modIndex[modIndexToName(parent)][row])); + } + else + { + if(modIndex[""].size() > row) + return createIndex(row, column, modNameToID.indexOf(modIndex[""][row])); + } + return QModelIndex(); +} + +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 + { + if(entry.key() != "" && entry.value().indexOf(modID) != -1) + { + return createIndex(entry.value().indexOf(modID), child.column(), modNameToID.indexOf(entry.key())); + } + } + return QModelIndex(); +} + +void CModFilterModel::setTypeFilter(ModFilterMask newFilterMask) +{ + 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 +{ + 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); + QString modID = index.data(ModRoles::ModNameRole).toString(); + if (base->model->getMod(modID).isHidden()) + return false; + + if(filterMatchesThis(index)) + { + return true; + } + + for(size_t i = 0; i < base->rowCount(index); i++) + { + if(filterMatchesThis(base->index(i, 0, index))) + return true; + } + + QModelIndex parent = source_parent; + while(parent.isValid()) + { + if(filterMatchesThis(parent)) + return true; + parent = parent.parent(); + } + return false; +} + +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..6b0a64656 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 reloadViewModel(); + 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..aaaa037ce --- /dev/null +++ b/launcher/modManager/modstatemodel.cpp @@ -0,0 +1,159 @@ +/* + * 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/modding/ModManager.h" + +ModStateModel::ModStateModel() + : repositoryData(std::make_unique()) + , modManager(std::make_unique()) +{ +} + +ModStateModel::~ModStateModel() = default; + +void ModStateModel::setRepositoryData(const JsonNode & repositoriesList) +{ + *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 ""; +} + +void ModStateModel::createNewPreset(const QString & presetName) +{ + modManager->createNewPreset(presetName.toStdString()); +} + +void ModStateModel::deletePreset(const QString & presetName) +{ + modManager->deletePreset(presetName.toStdString()); +} + +void ModStateModel::activatePreset(const QString & presetName) +{ + modManager->activatePreset(presetName.toStdString()); +} + +void ModStateModel::renamePreset(const QString & oldPresetName, const QString & newPresetName) +{ + modManager->renamePreset(oldPresetName.toStdString(), newPresetName.toStdString()); +} + +QStringList ModStateModel::getAllPresets() const +{ + auto result = modManager->getAllPresets(); + return stringListStdToQt(result); +} + +QString ModStateModel::getActivePreset() const +{ + return QString::fromStdString(modManager->getActivePreset()); +} diff --git a/launcher/modManager/modstatemodel.h b/launcher/modManager/modstatemodel.h new file mode 100644 index 000000000..e15433dc7 --- /dev/null +++ b/launcher/modManager/modstatemodel.h @@ -0,0 +1,60 @@ +/* + * 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 setRepositoryData(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; + + void createNewPreset(const QString & presetName); + void deletePreset(const QString & presetName); + void activatePreset(const QString & presetName); + void renamePreset(const QString & oldPresetName, const QString & newPresetName); + + QStringList getAllPresets() const; + QString getActivePreset() 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 2e7642553..09d160a36 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 @@ -27,9 +27,7 @@ #include #endif -namespace -{ -QString resolutionToString(const QSize & resolution) +static QString resolutionToString(const QSize & resolution) { return QString{"%1x%2"}.arg(resolution.width()).arg(resolution.height()); } @@ -41,12 +39,27 @@ static constexpr std::array cursorTypesList = }; static constexpr std::array upscalingFilterTypes = +{ + "auto", + "none", + "xbrz2", + "xbrz3", + "xbrz4" +}; + +static constexpr std::array downscalingFilterTypes = { "nearest", "linear", "best" }; +MainWindow * CSettingsView::getMainWindow() +{ + foreach(QWidget *w, qApp->allWidgets()) + if(QMainWindow* mainWin = qobject_cast(w)) + return dynamic_cast(mainWin); + return nullptr; } void CSettingsView::setDisplayList() @@ -114,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)); @@ -142,10 +160,14 @@ void CSettingsView::loadSettings() Languages::fillLanguages(ui->comboBoxLanguage, false); fillValidRenderers(); - std::string upscalingFilter = settings["video"]["scalingMode"].String(); + std::string upscalingFilter = settings["video"]["upscalingFilter"].String(); int upscalingFilterIndex = vstd::find_pos(upscalingFilterTypes, upscalingFilter); ui->comboBoxUpscalingFilter->setCurrentIndex(upscalingFilterIndex); + std::string downscalingFilter = settings["video"]["downscalingFilter"].String(); + int downscalingFilterIndex = vstd::find_pos(downscalingFilterTypes, downscalingFilter); + ui->comboBoxDownscalingFilter->setCurrentIndex(downscalingFilterIndex); + ui->sliderMusicVolume->setValue(settings["general"]["music"].Integer()); ui->sliderSoundVolume->setValue(settings["general"]["sound"].Integer()); ui->sliderRelativeCursorSpeed->setValue(settings["general"]["relativePointerSpeedMultiplier"].Integer()); @@ -157,6 +179,21 @@ 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); + else if (settings["video"]["fontsType"].String() == "original") + ui->buttonFontOriginal->setChecked(true); + 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(); } @@ -164,7 +201,6 @@ void CSettingsView::loadSettings() 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()); @@ -181,6 +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() @@ -433,8 +478,7 @@ void CSettingsView::on_comboBoxLanguage_currentIndexChanged(int index) QString selectedLanguage = ui->comboBoxLanguage->itemData(index).toString(); node->String() = selectedLanguage.toStdString(); - if(auto * mainWindow = dynamic_cast(qApp->activeWindow())) - mainWindow->updateTranslation(); + getMainWindow()->updateTranslation(); } void CSettingsView::changeEvent(QEvent *event) @@ -460,47 +504,39 @@ 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() { QString baseLanguage = Languages::getHeroesDataLanguage(); - auto * mainWindow = dynamic_cast(qApp->activeWindow()); + auto * mainWindow = getMainWindow(); if (!mainWindow) return; - QString languageName = QString::fromStdString(settings["general"]["language"].String()); - QString modName = mainWindow->getModView()->getTranslationModName(languageName); - bool translationExists = !modName.isEmpty(); - bool translationNeeded = languageName != baseLanguage; - bool showTranslation = translationNeeded && translationExists; + auto translationStatus = mainWindow->getTranslationStatus(); + bool showTranslation = translationStatus == ETranslationStatus::DISABLED || translationStatus == ETranslationStatus::NOT_INSTALLLED; ui->labelTranslation->setVisible(showTranslation); ui->labelTranslationStatus->setVisible(showTranslation); ui->pushButtonTranslation->setVisible(showTranslation); + ui->pushButtonTranslation->setVisible(translationStatus != ETranslationStatus::ACTIVE); - if (!translationExists || !translationNeeded) - return; - - bool translationAvailable = mainWindow->getModView()->isModAvailable(modName); - bool translationEnabled = mainWindow->getModView()->isModEnabled(modName); - - ui->pushButtonTranslation->setVisible(!translationEnabled); - - if (translationEnabled) + if (translationStatus == ETranslationStatus::ACTIVE) { ui->labelTranslationStatus->setText(tr("Active")); } - if (!translationEnabled && !translationAvailable) + if (translationStatus == ETranslationStatus::DISABLED) { ui->labelTranslationStatus->setText(tr("Disabled")); ui->pushButtonTranslation->setText(tr("Enable")); } - if (translationAvailable) + if (translationStatus == ETranslationStatus::NOT_INSTALLLED) { ui->labelTranslationStatus->setText(tr("Not Installed")); ui->pushButtonTranslation->setText(tr("Install")); @@ -509,7 +545,7 @@ void CSettingsView::loadTranslation() void CSettingsView::on_pushButtonTranslation_clicked() { - auto * mainWindow = dynamic_cast(qApp->activeWindow()); + auto * mainWindow = getMainWindow(); assert(mainWindow); if (!mainWindow) @@ -568,12 +604,12 @@ void CSettingsView::on_lineEditRepositoryExtra_textEdited(const QString &arg1) void CSettingsView::on_spinBoxInterfaceScaling_valueChanged(int arg1) { Settings node = settings.write["video"]["resolution"]["scaling"]; - node->Float() = arg1; + node->Float() = ui->buttonScalingAuto->isChecked() ? 0 : arg1; } void CSettingsView::on_refreshRepositoriesButton_clicked() { - auto * mainWindow = dynamic_cast(qApp->activeWindow()); + auto * mainWindow = getMainWindow(); assert(mainWindow); if (!mainWindow) @@ -593,7 +629,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) @@ -649,10 +684,16 @@ void CSettingsView::on_buttonIgnoreSslErrors_clicked(bool checked) void CSettingsView::on_comboBoxUpscalingFilter_currentIndexChanged(int index) { - Settings node = settings.write["video"]["scalingMode"]; + Settings node = settings.write["video"]["upscalingFilter"]; node->String() = upscalingFilterTypes[index]; } +void CSettingsView::on_comboBoxDownscalingFilter_currentIndexChanged(int index) +{ + Settings node = settings.write["video"]["downscalingFilter"]; + node->String() = downscalingFilterTypes[index]; +} + void CSettingsView::on_sliderMusicVolume_valueChanged(int value) { Settings node = settings.write["general"]["music"]; @@ -732,3 +773,72 @@ void CSettingsView::on_sliderControllerSticksSensitivity_valueChanged(int value) Settings node = settings.write["input"]["controllerAxisSpeed"]; node->Integer() = value; } + +void CSettingsView::on_sliderScalingFont_valueChanged(int value) +{ + int actualValuePercentage = value * 5; + ui->labelScalingFontValue->setText(QString("%1%").arg(actualValuePercentage)); + Settings node = settings.write["video"]["fontScalingFactor"]; + node->Float() = actualValuePercentage / 100.0; +} + +void CSettingsView::on_buttonFontAuto_clicked(bool checked) +{ + Settings node = settings.write["video"]["fontsType"]; + node->String() = "auto"; +} + +void CSettingsView::on_buttonFontScalable_clicked(bool checked) +{ + Settings node = settings.write["video"]["fontsType"]; + node->String() = "scalable"; +} + +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 406a926d2..d05e7eb1e 100644 --- a/launcher/settingsView/csettingsview_moc.h +++ b/launcher/settingsView/csettingsview_moc.h @@ -14,10 +14,14 @@ namespace Ui class CSettingsView; } +class MainWindow; + class CSettingsView : public QWidget { Q_OBJECT + MainWindow * getMainWindow(); + public: explicit CSettingsView(QWidget * parent = nullptr); ~CSettingsView(); @@ -67,6 +71,7 @@ private slots: void on_buttonIgnoreSslErrors_clicked(bool checked); void on_comboBoxUpscalingFilter_currentIndexChanged(int index); + void on_comboBoxDownscalingFilter_currentIndexChanged(int index); void on_sliderMusicVolume_valueChanged(int value); void on_sliderSoundVolume_valueChanged(int value); void on_buttonRelativeCursorMode_toggled(bool value); @@ -78,10 +83,23 @@ 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_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 cdb69905f..678358af5 100644 --- a/launcher/settingsView/csettingsview_moc.ui +++ b/launcher/settingsView/csettingsview_moc.ui @@ -48,452 +48,12 @@ 0 0 - 730 - 1691 + 729 + 1503 - - - - - - 75 - true - - - - General - - - 5 - - - - - - - false - - - BattleAI - - - - BattleAI - - - - - StupidAI - - - - - - - - Sticks Acceleration - - - - - - - Refresh now - - - - - - - - - - - - - - % - - - 50 - - - 400 - - - 10 - - - - - - - Reserved screen area - - - - - - - Online Lobby address - - - - - - - VCAI - - - - VCAI - - - - - Nullkiller - - - - - - - - - 0 - 0 - - - - - - - true - - - - - - - 100 - - - Qt::Horizontal - - - QSlider::TicksAbove - - - 10 - - - - - - - - 0 - 0 - - - - - - - true - - - - - - - VCMI Language - - - - - - - 25 - - - Qt::Horizontal - - - QSlider::TicksAbove - - - 5 - - - - - - - - 0 - 0 - - - - - - - true - - - - - - - Default repository - - - - - - - 1024 - - - 65535 - - - 3030 - - - - - - - Heroes III Translation - - - - - - - 100 - - - 300 - - - Qt::Horizontal - - - QSlider::TicksAbove - - - 25 - - - - - - - Renderer - - - - - - - - 75 - true - - - - Artificial Intelligence - - - 5 - - - - - - - Use Relative Pointer Mode - - - - - - - Fullscreen - - - - - - - Autocombat AI in battles - - - - - - - Display index - - - - - - - Long Touch Duration - - - - - - - Sticks Sensitivity - - - - - - - Touch Tap Tolerance - - - - - - - - - - - - - - 100 - - - Qt::Horizontal - - - QSlider::TicksAbove - - - 10 - - - - - - - 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 - - - - - - - - true - - - - 0 - 0 - - - - - - - true - - - false - - - - - - - - - - true - - - - - - - Show intro - - - - - - - - - - 0 - - - 50 - - - 1 - - - 10 - - - 0 - - - Qt::Horizontal - - - QSlider::TicksAbove - - - 10 - - - - - - - Online Lobby port - - - - + + @@ -509,61 +69,43 @@ Fullscreen Exclusive Mode - game will cover entirety of your screen and will use - - - - - - - - 75 - true - - + + - Video - - - 5 + Mods Validation - - - - - 75 - true - - - - Audio - - - 5 - + + + + + Automatic + + + + + None + + + + + xBRZ x2 + + + + + xBRZ x3 + + + + + xBRZ x4 + + - - - - - 0 - 0 - - - - - - - true - - - false - - - - + 500 @@ -591,22 +133,766 @@ Fullscreen Exclusive Mode - game will cover entirety of your screen and will use - - + + - VSync + Reset - + - - + + + + 500 + + + 2000 + + + 250 + + + 250 + + + Qt::Horizontal + + + QSlider::TicksAbove + + + 250 + + + + + + + true + + + + 0 + 0 + + + + + + + true + + + false + + + + + + + Reserved screen area + + + + + + + + true + + + + General + + + 5 + + + + + + + Autosave + + + + + + + 100 + + + Qt::Horizontal + + + QSlider::TicksAbove + + + 10 + + + + + + + true + + + + 0 + 0 + + + + + + + true + + + false + + + + + + + Sound Volume + + + + + + + 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 + + + + + + + + + + % + + + 50 + + + 400 + + + 10 + + + + + + + Relative Pointer Speed + + + + + + + Downscaling Filter + + + + + + + true + + + + 0 + 0 + + + + + + + true + + + false + + + + + + + 1024 + + + 65535 + + + 3030 + + + + + + + Show intro + + + + + + + + 0 + 0 + + + + Automatic + + + true + + + true + + + buttonGroupFonts + + + + + + + 100 + + + Qt::Horizontal + + + QSlider::TicksAbove + + + 10 + + + + + + + + 0 + 0 + + + + + + + true + + + false + + + + + + + Refresh now + + + + + + + Adventure Map Allies + + + + + + + Neutral AI in battles + + + + + + + 10 + + + 30 + + + 1 + + + 2 + + + 20 + + + 20 + + + Qt::Horizontal + + + QSlider::TicksAbove + + + 2 + + + + + + + + + + + + + + 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 @@ -619,13 +905,52 @@ Fullscreen Exclusive Mode - game will cover entirety of your screen and will use - Best (Linear) + Automatic (Linear) - - + + + + Ignore SSL errors + + + + + + + + + + Show Tutorial again + + + + + + + Sticks Acceleration + + + + + + + + true + + + + Video + + + 5 + + + + + false @@ -644,15 +969,43 @@ Fullscreen Exclusive Mode - game will cover entirety of your screen and will use - - + + - Check on startup + - - + + + + Fullscreen + + + + + + + Mouse Click Tolerance + + + + + + + Enemy AI in battles + + + + + + + Autosave limit (0 = off) + + + + + 0 @@ -679,236 +1032,25 @@ Fullscreen Exclusive Mode - game will cover entirety of your screen and will use - - - - - 0 - 0 - - + + - - - - true + Interface Scaling - - - - 500 - - - 2000 - - - 250 - - - 250 - - - Qt::Horizontal - - - QSlider::TicksAbove - - - 250 - - - - - - - - - - 1024 - - - 65535 - - - 3030 - - - - - + + - Enemy AI in battles + Music Volume - - - - - 75 - true - - - - Input - Touchscreen - - - 5 - - - - - - - Autosave prefix - - - - - - - Ignore SSL errors - - - - - - - - - - Autosave limit (0 = off) - - - - - - - - - - - - - - Autosave - - - - - - - Haptic Feedback - - - - - - - - 0 - 0 - - - - - - - true - - - - - - - Additional repository - - - - - - - Mouse Click Tolerance - - - - - - - Software Cursor - - - - - - - Relative Pointer Speed - - - - - - - - 75 - true - - - - Network - - - 5 - - - - - - - Network port - - - - - - - true - - - - 0 - 0 - - - - - - - true - - + + + false - - - - - - Upscaling Filter - - - - - - - Adventure Map Enemies - - - - - BattleAI @@ -924,41 +1066,22 @@ Fullscreen Exclusive Mode - game will cover entirety of your screen and will use - - + + - Neutral AI in battles + - - + + - Adventure Map Allies + Cursor Scaling - - - - - 75 - true - - - - Input - Mouse - - - 5 - - - - - - - true - + + 0 @@ -971,82 +1094,61 @@ Fullscreen Exclusive Mode - game will cover entirety of your screen and will use true - - false - - - - - - - - 20 + + + + + 0 + 0 + - - 1000 + + Scalable - - 10 + + true + + true + + + buttonGroupFonts + - - - - VCAI - - - - VCAI - - - - - Nullkiller - - - - - - - - empty = map name prefix - - - - - + + - 75 true - Input - Controller + Network 5 - - + + + + + true + + - Framerate Limit + Miscellaneous + + + 5 - - - - Controller Click Tolerance - - - - + 100 @@ -1074,28 +1176,65 @@ Fullscreen Exclusive Mode - game will cover entirety of your screen and will use - - - - Interface Scaling + + + + Select a display mode for the game + +Windowed - the game will run inside a window that covers part of your screen. + +Borderless Windowed Mode - the game will run in a full-screen window, matching your screen's resolution. + +Fullscreen Exclusive Mode - the game will cover the entirety of your screen and will use selected resolution. + + + 0 + + + + Windowed + + + + + Borderless fullscreen + + + + + Exclusive fullscreen + + + + + + + + 20 + + + 1000 + + + 10 - - + + - Resolution + Long Touch Duration - - + + - Sound Volume + Check on startup - + 0 @@ -1123,8 +1262,8 @@ Fullscreen Exclusive Mode - game will cover entirety of your screen and will use - - + + 0 @@ -1139,25 +1278,175 @@ Fullscreen Exclusive Mode - game will cover entirety of your screen and will use - - - - Music Volume + + + + 100 + + + 300 + + + Qt::Horizontal + + + QSlider::TicksAbove + + + 25 - - + + - Show Tutorial again + Font Scaling (experimental) - - - - Reset + + + + + 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 + @@ -1168,4 +1457,8 @@ Fullscreen Exclusive Mode - game will cover entirety of your screen and will use + + + + diff --git a/launcher/startGame/StartGameTab.cpp b/launcher/startGame/StartGameTab.cpp new file mode 100644 index 000000000..238acdf4a --- /dev/null +++ b/launcher/startGame/StartGameTab.cpp @@ -0,0 +1,431 @@ +/* + * StartGameTab.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 "StartGameTab.h" +#include "ui_StartGameTab.h" + +#include "../mainwindow_moc.h" +#include "../main.h" +#include "../updatedialog_moc.h" + +#include "../modManager/cmodlistview_moc.h" + +#include "../../lib/filesystem/Filesystem.h" +#include "../../lib/VCMIDirs.h" + +void StartGameTab::changeEvent(QEvent *event) +{ + if(event->type() == QEvent::LanguageChange) + { + ui->retranslateUi(this); + refreshState(); + } + + QWidget::changeEvent(event); +} + +StartGameTab::StartGameTab(QWidget * parent) + : QWidget(parent) + , ui(new Ui::StartGameTab) +{ + ui->setupUi(this); + + ui->buttonGameResume->setIcon(QIcon{":/icons/menu-game.png"}); //TODO: different icon? + ui->buttonGameStart->setIcon(QIcon{":/icons/menu-game.png"}); + ui->buttonGameEditor->setIcon(QIcon{":/icons/menu-editor.png"}); + + refreshState(); + + ui->buttonGameResume->setVisible(false); // TODO: implement + ui->buttonPresetExport->setVisible(false); // TODO: implement + ui->buttonPresetImport->setVisible(false); // TODO: implement + +#ifndef ENABLE_EDITOR + ui->buttonGameEditor->hide(); +#endif +} + +StartGameTab::~StartGameTab() +{ + delete ui; +} + +MainWindow * StartGameTab::getMainWindow() +{ + foreach(QWidget *w, qApp->allWidgets()) + if(QMainWindow* mainWin = qobject_cast(w)) + return dynamic_cast(mainWin); + return nullptr; +} + +void StartGameTab::refreshState() +{ + refreshGameData(); + refreshUpdateStatus(EGameUpdateStatus::NOT_CHECKED);//TODO - follow automatic check on startup setting + refreshTranslation(getMainWindow()->getTranslationStatus()); + refreshPresets(); + refreshMods(); +} + +void StartGameTab::refreshPresets() +{ + QSignalBlocker blocker(ui->comboBoxModPresets); + + QStringList allPresets = getMainWindow()->getModView()->getAllPresets(); + ui->comboBoxModPresets->clear(); + ui->comboBoxModPresets->addItems(allPresets); + ui->comboBoxModPresets->setCurrentText(getMainWindow()->getModView()->getActivePreset()); + ui->buttonPresetDelete->setVisible(allPresets.size() > 1); +} + +void StartGameTab::refreshGameData() +{ + // Some players are using pirated version of the game with some of the files missing + // leading to broken town hall menu (and possibly other dialogs) + // Provide diagnostics to indicate problem with chair-monitor adaptor layer and not with VCMI + static constexpr std::array potentiallyMissingFiles = { + "Data/TpThBkDg.bmp", + "Data/TpThBkFr.bmp", + "Data/TpThBkIn.bmp", + "Data/TpThBkNc.bmp", + "Data/TpThBkSt.bmp", + "Data/TpThBRrm.bmp", + "Data/TpThBkCs.bmp", + "Data/TpThBkRm.bmp", + "Data/TpThBkTw.bmp", + }; + + // Some players for some reason don't have AB expansion campaign files + static constexpr std::array armaggedonBladeCampaigns = { + "DATA/AB", + "DATA/BLOOD", + "DATA/SLAYER", + "DATA/FESTIVAL", + "DATA/FIRE", + "DATA/FOOL", + }; + + bool missingSoundtrack = !CResourceHandler::get()->existsResource(AudioPath::builtin("Music/MainMenu")); + bool missingVideoFiles = !CResourceHandler::get()->existsResource(VideoPath::builtin("Video/H3Intro")) && !CResourceHandler::get()->existsResource(ResourcePath("Video/H3Intro", EResType::VIDEO_LOW_QUALITY)); + bool missingGameFiles = false; + bool missingCampaings = false; + + for (const auto & filename : potentiallyMissingFiles) + missingGameFiles &= !CResourceHandler::get()->existsResource(ImagePath::builtin(filename)); + + for (const auto & filename : armaggedonBladeCampaigns) + missingCampaings &= !CResourceHandler::get()->existsResource(ResourcePath(filename, EResType::CAMPAIGN)); + + ui->labelMissingCampaigns->setVisible(missingCampaings); + ui->labelMissingFiles->setVisible(missingGameFiles); + ui->labelMissingVideo->setVisible(missingVideoFiles); + ui->labelMissingSoundtrack->setVisible(missingSoundtrack); + + ui->buttonMissingCampaignsHelp->setVisible(missingCampaings); + ui->buttonMissingFilesHelp->setVisible(missingGameFiles); + ui->buttonMissingVideoHelp->setVisible(missingVideoFiles); + ui->buttonMissingSoundtrackHelp->setVisible(missingSoundtrack); +} + +void StartGameTab::refreshTranslation(ETranslationStatus status) +{ + ui->buttonInstallTranslation->setVisible(status == ETranslationStatus::NOT_INSTALLLED); + ui->buttonInstallTranslationHelp->setVisible(status == ETranslationStatus::NOT_INSTALLLED); + + ui->buttonActivateTranslation->setVisible(status == ETranslationStatus::NOT_INSTALLLED); + ui->buttonActivateTranslationHelp->setVisible(status == ETranslationStatus::NOT_INSTALLLED); +} + +void StartGameTab::refreshMods() +{ + constexpr int chroniclesCount = 8; + QStringList updateableMods = getMainWindow()->getModView()->getUpdateableMods(); + QStringList chroniclesMods = getMainWindow()->getModView()->getInstalledChronicles(); + + ui->buttonUpdateMods->setText(tr("Update %n mods", "", updateableMods.size())); + ui->buttonUpdateMods->setVisible(!updateableMods.empty()); + ui->buttonUpdateModsHelp->setVisible(!updateableMods.empty()); + + ui->labelChronicles->setText(tr("Heroes Chronicles:\n%n/%1 installed", "", chroniclesMods.size()).arg(chroniclesCount)); + ui->labelChronicles->setVisible(chroniclesMods.size() != chroniclesCount); + ui->buttonChroniclesHelp->setVisible(chroniclesMods.size() != chroniclesCount); +} + +void StartGameTab::refreshUpdateStatus(EGameUpdateStatus status) +{ + QString availableVersion; // TODO + + ui->labelTitleEngine->setText("VCMI " VCMI_VERSION_STRING); + ui->buttonUpdateCheck->setVisible(status == EGameUpdateStatus::NOT_CHECKED); + ui->labelUpdateNotFound->setVisible(status == EGameUpdateStatus::NO_UPDATE); + ui->labelUpdateAvailable->setVisible(status == EGameUpdateStatus::UPDATE_AVAILABLE); + ui->buttonOpenChangelog->setVisible(status == EGameUpdateStatus::UPDATE_AVAILABLE); + ui->buttonOpenDownloads->setVisible(status == EGameUpdateStatus::UPDATE_AVAILABLE); + + if (status == EGameUpdateStatus::UPDATE_AVAILABLE) + ui->labelUpdateAvailable->setText(tr("Update to %1 available").arg(availableVersion)); +} + +void StartGameTab::on_buttonGameStart_clicked() +{ + getMainWindow()->hide(); + startGame({}); +} + +void StartGameTab::on_buttonOpenChangelog_clicked() +{ + QDesktopServices::openUrl(QUrl("https://vcmi.eu/ChangeLog/")); +} + +void StartGameTab::on_buttonOpenDownloads_clicked() +{ + QDesktopServices::openUrl(QUrl("https://vcmi.eu/download/")); +} + +void StartGameTab::on_buttonUpdateCheck_clicked() +{ + UpdateDialog::showUpdateDialog(true); +} + +void StartGameTab::on_buttonGameEditor_clicked() +{ + getMainWindow()->hide(); + startEditor({}); +} + +void StartGameTab::on_buttonImportFiles_clicked() +{ + const auto & importFunctor = [this] + { +#ifndef VCMI_MOBILE + QString filter = + tr("All supported files") + " (*.h3m *.vmap *.h3c *.vcmp *.zip *.json *.exe);;" + + tr("Maps") + " (*.h3m *.vmap);;" + + tr("Campaigns") + " (*.h3c *.vcmp);;" + + tr("Configs") + " (*.json);;" + + tr("Mods") + " (*.zip);;" + + tr("Gog files") + " (*.exe)"; +#else + //Workaround for sometimes incorrect mime for some extensions (e.g. for exe) + QString filter = tr("All files (*.*)"); +#endif + QStringList files = QFileDialog::getOpenFileNames(this, tr("Select files (configs, mods, maps, campaigns, gog files) to install..."), QDir::homePath(), filter); + + for(const auto & file : files) + { + logGlobal->info("Importing file %s", file.toStdString()); + getMainWindow()->manualInstallFile(file); + } + }; + + // iOS can't display modal dialogs when called directly on button press + // https://bugreports.qt.io/browse/QTBUG-98651 + QTimer::singleShot(0, this, importFunctor); +} + +void StartGameTab::on_buttonInstallTranslation_clicked() +{ + if (getMainWindow()->getTranslationStatus() == ETranslationStatus::NOT_INSTALLLED) + { + QString preferredlanguage = QString::fromStdString(settings["general"]["language"].String()); + QString modName = getMainWindow()->getModView()->getTranslationModName(preferredlanguage); + getMainWindow()->getModView()->doInstallMod(modName); + } +} + +void StartGameTab::on_buttonActivateTranslation_clicked() +{ + QString preferredlanguage = QString::fromStdString(settings["general"]["language"].String()); + QString modName = getMainWindow()->getModView()->getTranslationModName(preferredlanguage); + getMainWindow()->getModView()->enableModByName(modName); +} + +void StartGameTab::on_buttonUpdateMods_clicked() +{ + QStringList updateableMods = getMainWindow()->getModView()->getUpdateableMods(); + + getMainWindow()->switchToModsTab(); + + for (const auto & modName : updateableMods) + getMainWindow()->getModView()->doUpdateMod(modName); +} + +void StartGameTab::on_buttonHelpImportFiles_clicked() +{ + QString message = tr( + "This option allows you to import additional data files into your VCMI installation. " + "At the moment, following options are supported:\n\n" + " - Heroes III Maps (.h3m or .vmap).\n" + " - Heroes III Campaigns (.h3c or .vcmp).\n" + " - Heroes III Chronicles using offline backup installer from GOG.com (.exe).\n" + " - VCMI mods in zip format (.zip)\n" + " - VCMI configuration files (.json)\n" + ); + + QMessageBox::information(this, ui->buttonImportFiles->text(), message); +} + +void StartGameTab::on_buttonInstallTranslationHelp_clicked() +{ + QString message = tr( + "Your Heroes III version uses different language. " + "VCMI provides translations of the game into various languages that you can use. " + "Use this option to automatically install such translation to your language." + ); + QMessageBox::information(this, ui->buttonInstallTranslation->text(), message); +} + +void StartGameTab::on_buttonActivateTranslationHelp_clicked() +{ + QString message = tr( + "Translation of Heroes III into your language is installed, but has been turned off. " + "Use this option to enable it." + ); + + QMessageBox::information(this, ui->buttonActivateTranslation->text(), message); +} + +void StartGameTab::on_buttonUpdateModsHelp_clicked() +{ + QString message = tr( + "A new version of some of the mods that you have installed is now available in mod repository. " + "Use this option to automatically update all your mods to latest version.\n\n" + "WARNING: In some cases, updated versions of mods may not be compatible with your existing saves. " + "You many want to postpone mod update until you finish any of your ongoing games." + ); + + QMessageBox::information(this, ui->buttonUpdateMods->text(), message); +} + +void StartGameTab::on_buttonChroniclesHelp_clicked() +{ + QString message = tr( + "If you own Heroes Chronicles on gog.com, you can use offline backup installers provided by gog " + "to import Heroes Chronicles data into VCMI as custom campaigns.\n" + "To import Heroes Chronicles, download offline backup installer of each chronicle that you wish to install, " + "select 'Import files' option and select downloaded file. " + "This will generate and install mod for VCMI that contains imported chronicles" + ); + + QMessageBox::information(this, ui->labelChronicles->text(), message); +} + +void StartGameTab::on_buttonMissingSoundtrackHelp_clicked() +{ + QString message = tr( + "VCMI has detected that Heroes III music files are missing from your installation. " + "VCMI will run, but in-game music will not be available.\n\n" + "To resolve this problem, please copy missing mp3 files from Heroes III to VCMI data files directory manually " + "or reinstall VCMI and re-import Heroes III data files" + ); + QMessageBox::information(this, ui->labelMissingSoundtrack->text(), message); +} + +void StartGameTab::on_buttonMissingVideoHelp_clicked() +{ + QString message = tr( + "VCMI has detected that Heroes III video files are missing from your installation. " + "VCMI will run, but in-game cutscenes will not be available.\n\n" + "To resolve this problem, please copy VIDEO.VID file from Heroes III to VCMI data files directory manually " + "or reinstall VCMI and re-import Heroes III data files" + ); + QMessageBox::information(this, ui->labelMissingVideo->text(), message); +} + +void StartGameTab::on_buttonMissingFilesHelp_clicked() +{ + QString message = tr( + "VCMI has detected that some of Heroes III data files are missing from your installation. " + "You may attempt to run VCMI, but game may not work as expected or crash.\n\n" + "To resolve this problem, please reinstall game and reimport data files using supported version of Heroes III. " + "VCMI requires Heroes III: Shadow of Death or Complete Edition to run, which you can get (for example) from gog.com" + ); + QMessageBox::information(this, ui->labelMissingFiles->text(), message); +} + +void StartGameTab::on_buttonMissingCampaignsHelp_clicked() +{ + QString message = tr( + "VCMI has detected that some of Heroes III: Armageddon's Blade data files are missing from your installation. " + "VCMI will work, but Armageddon's Blade campaigns will not be available.\n\n" + "To resolve this problem, please copy missing data files from Heroes III to VCMI data files directory manually " + "or reinstall VCMI and re-import Heroes III data files" + ); + QMessageBox::information(this, ui->labelMissingCampaigns->text(), message); +} + +void StartGameTab::on_buttonPresetExport_clicked() +{ + // TODO +} + +void StartGameTab::on_buttonPresetImport_clicked() +{ + // TODO +} + +void StartGameTab::on_buttonPresetNew_clicked() +{ + bool ok; + QString presetName = QInputDialog::getText( + this, + ui->buttonPresetNew->text(), + tr("Enter preset name:"), + QLineEdit::Normal, + QString(), + &ok); + + if (ok && !presetName.isEmpty()) + { + getMainWindow()->getModView()->createNewPreset(presetName); + getMainWindow()->getModView()->activatePreset(presetName); + refreshPresets(); + } +} + +void StartGameTab::on_buttonPresetDelete_clicked() +{ + QString activePresetBefore = getMainWindow()->getModView()->getActivePreset(); + QStringList allPresets = getMainWindow()->getModView()->getAllPresets(); + + allPresets.removeAll(activePresetBefore); + if (!allPresets.empty()) + { + getMainWindow()->getModView()->activatePreset(allPresets.front()); + getMainWindow()->getModView()->deletePreset(activePresetBefore); + refreshPresets(); + } +} + +void StartGameTab::on_comboBoxModPresets_currentTextChanged(const QString &presetName) +{ + getMainWindow()->getModView()->activatePreset(presetName); +} + +void StartGameTab::on_buttonPresetRename_clicked() +{ + QString currentName = getMainWindow()->getModView()->getActivePreset(); + + bool ok; + QString newName = QInputDialog::getText( + this, + ui->buttonPresetNew->text(), + tr("Rename preset '%1' to:").arg(currentName), + QLineEdit::Normal, + currentName, + &ok); + + if (ok && !newName.isEmpty()) + { + getMainWindow()->getModView()->renamePreset(currentName, newName); + refreshPresets(); + } +} + diff --git a/launcher/startGame/StartGameTab.h b/launcher/startGame/StartGameTab.h new file mode 100644 index 000000000..d66e449b5 --- /dev/null +++ b/launcher/startGame/StartGameTab.h @@ -0,0 +1,83 @@ +/* + * StartGameTab.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 + * + */ +#pragma once + +#include + +namespace Ui +{ +class StartGameTab; +} + +enum class EGameUpdateStatus : int8_t +{ + NOT_CHECKED, + NO_UPDATE, + UPDATE_AVAILABLE +}; + +enum class ETranslationStatus : int8_t; + +class MainWindow; + +class StartGameTab : public QWidget +{ + Q_OBJECT + + MainWindow * getMainWindow(); + + void refreshUpdateStatus(EGameUpdateStatus status); + void refreshTranslation(ETranslationStatus status); + void refreshMods(); + void refreshPresets(); + void refreshGameData(); + + void changeEvent(QEvent *event) override; +public: + explicit StartGameTab(QWidget * parent = nullptr); + ~StartGameTab(); + + void refreshState(); + +private slots: + void on_buttonGameStart_clicked(); + void on_buttonOpenChangelog_clicked(); + void on_buttonOpenDownloads_clicked(); + void on_buttonUpdateCheck_clicked(); + void on_buttonGameEditor_clicked(); + void on_buttonImportFiles_clicked(); + void on_buttonInstallTranslation_clicked(); + void on_buttonActivateTranslation_clicked(); + void on_buttonUpdateMods_clicked(); + void on_buttonHelpImportFiles_clicked(); + void on_buttonInstallTranslationHelp_clicked(); + void on_buttonActivateTranslationHelp_clicked(); + void on_buttonUpdateModsHelp_clicked(); + void on_buttonChroniclesHelp_clicked(); + void on_buttonMissingSoundtrackHelp_clicked(); + void on_buttonMissingVideoHelp_clicked(); + void on_buttonMissingFilesHelp_clicked(); + void on_buttonMissingCampaignsHelp_clicked(); + + void on_buttonPresetExport_clicked(); + + void on_buttonPresetImport_clicked(); + + void on_buttonPresetNew_clicked(); + + void on_buttonPresetDelete_clicked(); + + void on_comboBoxModPresets_currentTextChanged(const QString &arg1); + + void on_buttonPresetRename_clicked(); + +private: + Ui::StartGameTab * ui; +}; diff --git a/launcher/startGame/StartGameTab.ui b/launcher/startGame/StartGameTab.ui new file mode 100644 index 000000000..d17dd69fb --- /dev/null +++ b/launcher/startGame/StartGameTab.ui @@ -0,0 +1,857 @@ + + + StartGameTab + + + + 0 + 0 + 757 + 372 + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + true + + + + Game Data Files + + + Qt::AlignCenter + + + true + + + + + + + + true + + + + Mod Preset + + + Qt::AlignCenter + + + true + + + + + + + + true + + + + + + + Qt::AlignCenter + + + true + + + + + + + QFrame::Sunken + + + Qt::ScrollBarAlwaysOff + + + true + + + + + 0 + 0 + 246 + 350 + + + + + + + + 0 + 30 + + + + + 16777215 + 30 + + + + Unsupported or corrupted game data detected! + + + true + + + + + + + + 40 + 30 + + + + + 40 + 30 + + + + ? + + + + + + + + 40 + 30 + + + + + 40 + 30 + + + + ? + + + + + + + + 0 + 30 + + + + + 16777215 + 30 + + + + + + + true + + + + + + + + 40 + 30 + + + + + 40 + 30 + + + + ? + + + + + + + + 40 + 30 + + + + + 40 + 30 + + + + ? + + + + + + + + 0 + 30 + + + + + 16777215 + 30 + + + + Install Translation + + + + + + + + 0 + 30 + + + + + 16777215 + 30 + + + + Armaggedon's Blade campaigns are missing! + + + true + + + + + + + Qt::Vertical + + + + 0 + 0 + + + + + + + + + 0 + 30 + + + + + 16777215 + 30 + + + + No video files detected! + + + true + + + + + + + + 0 + 30 + + + + + 16777215 + 30 + + + + Import files + + + + + + + + 0 + 30 + + + + + 16777215 + 30 + + + + + + + + + + + + 40 + 30 + + + + + 40 + 30 + + + + ? + + + + + + + + 40 + 30 + + + + + 40 + 30 + + + + ? + + + + + + + + 0 + 30 + + + + + 16777215 + 30 + + + + No soundtrack detected! + + + true + + + + + + + + 40 + 30 + + + + + 40 + 30 + + + + ? + + + + + + + + 0 + 30 + + + + + 16777215 + 30 + + + + Activate Translation + + + + + + + + 40 + 30 + + + + + 40 + 30 + + + + ? + + + + + + + + 40 + 30 + + + + + 40 + 30 + + + + ? + + + + + + + + + + + Qt::ScrollBarAlwaysOff + + + true + + + + + 0 + 0 + 247 + 350 + + + + + + + + 0 + 30 + + + + + 16777215 + 30 + + + + Export to Clipboard + + + + + + + + 0 + 30 + + + + + 16777215 + 30 + + + + Create New Preset + + + + + + + + 0 + 30 + + + + + 16777215 + 30 + + + + + + + + + 0 + 30 + + + + + 16777215 + 30 + + + + Delete Current Preset + + + + + + + Qt::Vertical + + + + 0 + 0 + + + + + + + + + 0 + 30 + + + + + 16777215 + 30 + + + + Import from Clipboard + + + + + + + + 0 + 30 + + + + + 16777215 + 30 + + + + Rename Current Preset + + + + + + + + + + + Qt::ScrollBarAlwaysOff + + + true + + + + + 0 + 0 + 272 + 350 + + + + + + + + 0 + 30 + + + + + 16777215 + 30 + + + + You are using the latest version + + + true + + + + + + + + 0 + 30 + + + + + 16777215 + 30 + + + + + + + true + + + + + + + + 0 + 30 + + + + + 16777215 + 30 + + + + Check For Updates + + + + + + + + 0 + 30 + + + + + 16777215 + 30 + + + + Go to Downloads Page + + + + + + + + 0 + 30 + + + + + 16777215 + 30 + + + + Go to Changelog Page + + + + + + + Qt::Vertical + + + + 0 + 0 + + + + + + + + + + + 0 + 0 + + + + + 80 + 80 + + + + Resume + + + + 64 + 64 + + + + Qt::ToolButtonTextUnderIcon + + + + + + + + 0 + 0 + + + + + 80 + 80 + + + + Editor + + + + 64 + 64 + + + + Qt::ToolButtonTextUnderIcon + + + + + + + + 0 + 0 + + + + + 80 + 80 + + + + + true + + + + Play + + + + 64 + 64 + + + + Qt::ToolButtonTextUnderIcon + + + + + + + + + + + + + + diff --git a/launcher/translation/chinese.ts b/launcher/translation/chinese.ts index 26518f982..473c6929a 100644 --- a/launcher/translation/chinese.ts +++ b/launcher/translation/chinese.ts @@ -24,65 +24,65 @@ 联系社区 - + Build Information 编译信息 - + User data directory 用户数据目录 + - - - + + Open 打开 - + Check for updates 检查更新 - + Game version 游戏版本 - + Log files directory 日志文件目录 - + Data Directories 数据目录 - + Game data directory 游戏数据目录 - + Operating System 操作系统 - + Configuration files directory 配置文件目录 - + Project homepage 项目主页 - + Report a bug 反馈bug @@ -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 @@ -233,58 +209,53 @@ Inactive 未激活 - - Download && refresh repositories - 下载并刷新仓库 - - + Description 详细介绍 - + Changelog 修改日志 - + Screenshots 截图 - Install from file - 从文件中安装 + 从文件中安装 - + Uninstall 卸载 - + Enable 激活 - + Disable 禁用 - + Update 更新 - + Install 安装 - + %p% (%v KB out of %m KB) %p% (%v KB 完成,总共 %m KB) @@ -294,190 +265,199 @@ 重载源 - + Abort 终止 - + 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 - 模组 + 模组 - - Select files (configs, mods, maps, campaigns) to install... - 选择文件(配置,模组,地图,战役)来安装 + 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吗? + 您想要替换%1吗? - + Downloading %1. %p% (%v MB out of %m MB) finished 正在下载 %1. %p% (%v MB 共 %m MB) 已完成 - Downloading %s%. %p% (%v MB out of %m MB) finished - 正在下载 %s%. %p% (%v MB 共 %m MB) 已完成 - - - + Download failed 下载失败 - + Unable to download all files. Encountered errors: @@ -490,7 +470,7 @@ Encountered errors: - + Install successfully downloaded? @@ -499,34 +479,39 @@ Install successfully downloaded? 安装下载成功的部分? - + + Installing Heroes Chronicles + 安装历代记 + + + Installing mod %1 正在安装模组 %1 - + Operation failed 操作失败 - + Encountered errors: 遇到问题: - + screenshots 截图 - + Screenshot %1 截图 %1 - + Mod is incompatible Mod统一翻译为模组 模组不兼容 @@ -535,355 +520,413 @@ 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 人工智能 - Mod Repositories - 模组仓库 - - - + 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 + 杂项 + + + + Select a display mode for the game + +Windowed - the game will run inside a window that covers part of your screen. + +Borderless Windowed Mode - the game will run in a full-screen window, matching your screen's resolution. + +Fullscreen Exclusive Mode - the game will cover the entirety of your screen and will use selected resolution. + 选择游戏的显示模式: + +窗口化 - 游戏将在一个窗口中运行,占用屏幕的一部分。 + +无边框全屏模式 - 游戏将在一个全屏窗口中运行,分辨率与屏幕匹配。 + +独占全屏模式 - 游戏将覆盖整个屏幕,并使用选定的分辨率。 + + + + Font Scaling (experimental) + 字体缩放(测试中) + + + + Original + 原始字体 + + + Upscaling Filter 图像放大过滤器 - + + Basic + 基本 + + + Use Relative Pointer Mode 使用相对指针模式 - + Nearest 最邻近 - + Linear 线性 - - Best (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 = 不限制) - Friendly AI in battles - 战场友方单位AI - - - + Framerate Limit 帧率限制 - + Autosave prefix 自动保存文件名前缀 - + Mouse Click Tolerance 鼠标点击灵敏度 - + Sticks Acceleration 摇杆加速度 - + empty = map name prefix 空 = 地图名称前缀 - + Refresh now 立即刷新 - + Default repository 默认仓库 - + Renderer 渲染器 - + On 开启 - Cursor - 鼠标指针 - - - Heroes III Data Language - 英雄无敌3数据语言 - - - Select display mode for game Windowed - game will run inside a window that covers part of your screen @@ -891,7 +934,7 @@ 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. - 选择游戏的显示方式 + 选择游戏的显示方式 窗口化 -游戏会运行在一个窗口内,该窗口只占据部分屏幕。 @@ -900,131 +943,178 @@ Fullscreen Exclusive Mode - game will cover entirety of your screen and will use 独占全屏模式 - 游戏会运行在一个覆盖全部屏幕的窗口,使用和你选择的分辨率。 - + Reserved screen area 保留屏幕区域 - Hardware - 硬件 - - - Software - 软件 - - - + 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 安装 + + ChroniclesExtractor + + File cannot opened + 无法打开文件 + + + + + Invalid file selected + 所选的文件无效 + + + You have to select an gog installer file! + 您必须选择一个gog安装文件! + + + You have to select an chronicle installer file! + 您必须选择一个历代记安装文件! + + + + The file cannot be opened + 此文件无法被打开 + + + + You have to select a gog installer file! + 您必须选择一个gog安装器文件! + + + + You have to select a Heroes Chronicles installer file! + 您必须选择一个历代记安装文件! + + + + Extracting error! + 提取错误! + + + + Hash error! + 哈希错误! + + + + + + Heroes Chronicles + 英雄无敌历代记 + + + + Heroes Chronicles %1 - %2 + 英雄无敌历代记 %1 - %2 + + 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 @@ -1050,18 +1140,128 @@ Fullscreen Exclusive Mode - game will cover entirety of your screen and will use 选择您的语言 - + Have a question? Found a bug? Want to help? Join us! 有疑问?找到BUG?需要帮助?加入我们! - Thank you for installing VCMI! Before you can start playing, there are a few more steps that need to be completed. 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! + 谢谢您安装VCMI! + +在您开始游戏之前,还有几个步骤需要完成。 + +请记住,为了使用VCMI,您必须拥有《魔法门之英雄无敌3完整版》或《死亡阴影》的原始数据文件。 + +目前不支持《英雄无敌3高清版》! + + + + Locate Heroes III data files + 定位英雄无敌3数据文件 + + + + Use offline installer from gog.com + 使用gog.com的离线安装器 + + + 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 + 你可以从原始游戏里手动拷贝Maps, Data 和 Mp3目录到VCMI的数据目录下,数据目录可以从页面顶部找到 + + + + Install gog.com files + 安装 gog.com 文件 + + + + Your Heroes III data files have been successfully found. + 成功的找到英雄无敌3数据文件。 + + + + Interface Improvements + 界面改进 + + + + Install a translation of Heroes III in your preferred language + 安装与您英雄无敌3语言相符的翻译 + + + + Installing... %p% + 安装中... %p% + + + + If you already have Heroes III files on your device, you can select this directory and VCMI will copy the existing data automatically. + 如果你已经在你的设备里准备好英雄无敌3文件,你可以选择这个目录,VCMI会自动复制已存在的数据。 + + + + Copy existing files + 复制已存在的文件 + + + 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. + 如果你已经在gog.com拥有了英雄无敌3,你可以从gog.com下载离线安装器。VCMI将会通过离线安装器导入英雄无敌3数据 +离线安装器包含两部分:exe文件和bin文件。请确保这两部分都已下载。 + + + + Optionally, you can install additional mods either now, or at any point later, using the VCMI Launcher + 你可以现在选择安装额外模组或是以后使用VCMI启动器安装模组 + + + Install mod that provides various interface improvements, such as better interface for random maps and selectable actions in battles + 安装提供各种各样界面改进的模组,例如美化随机地图界面或添加战场行动选项 + + + + Install compatible version of "Horn of the Abyss", a fan-made Heroes III expansion ported by the VCMI team + 安装兼容版本的“深渊的号角”,一个由爱好者制作的英雄无敌3扩展包,由VCMI团队移植 + + + + Install compatible version of "In The Wake of Gods", a fan-made Heroes III expansion + 安装兼容版本的“追随神迹”,一个由爱好者制作的英雄无敌3扩展包 + + + + Install mod that provides various interface improvements, such as a better interface for random maps and selectable actions in battles + 安装提供各种各样界面改进的模组,例如美化随机地图界面或添加战场行动选项 + + + + Finish + 完成 + + + + VCMI on Github + 访问VCMI的Github + + + + VCMI on Discord + 访问VCMI的Discord + + + + Thank you for installing VCMI! + +Before you can start playing, there are a few more steps to complete. + +Please remember that 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! 谢谢您安装VCMI! @@ -1072,289 +1272,196 @@ Heroes® of Might and Magic® III HD is currently not supported! 目前不支持《英雄无敌3高清版》! - - Locate Heroes III data files - 定位英雄无敌3数据文件 - - - - Use offline installer from gog.com - 使用gog.com的离线安装器 - - - - 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 - 你可以从原始游戏里手动拷贝Maps, Data 和 Mp3目录到VCMI的数据目录下,数据目录可以从页面顶部找到 - - - - Install gog.com files - 安装 gog.com 文件 - - - To run VCMI, Heroes III data files need to be present in one of the specified locations. Please copy the Heroes III data to one of these directories. - 要运行VCMI,英雄无敌3的数据文件需要存在于指定位置之一。请将英雄III的数据复制到这些目录之一。 - - - Alternatively, you can provide the directory where Heroes III data is installed and VCMI will copy the existing data automatically. - 除此之外,您也可以提供安装英雄无敌3数据的目录,VCMI将自动复制现有数据。 - - - - Your Heroes III data files have been successfully found. - 成功的找到英雄无敌3数据文件。 - - - The automatic detection of the Heroes III language has failed. Please select the language of your Heroes III manually - 自动检测英雄无敌3语言失败。请手动选择英雄无敌3语言 - - - - Interface Improvements - 界面改进 - - - - Install a translation of Heroes III in your preferred language - 安装与您英雄无敌3语言相符的翻译 - - - - Installing... %p% - 安装中... %p% - - - - If you already have Heroes III files on your device, you can select this directory and VCMI will copy the existing data automatically. - 如果你已经在你的设备里准备好英雄无敌3文件,你可以选择这个目录,VCMI会自动复制已存在的数据。 - - - - Copy existing files - 复制已存在的文件 - - - - 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. - 如果你已经在gog.com拥有了英雄无敌3,你可以从gog.com下载离线安装器。VCMI将会通过离线安装器导入英雄无敌3数据 -离线安装器包含两部分:exe文件和bin文件。请确保这两部分都已下载。 - - - - Optionally, you can install additional mods either now, or at any point later, using the VCMI Launcher - 你可以现在选择安装额外模组或是以后使用VCMI启动器安装模组 - - - - Install mod that provides various interface improvements, such as better interface for random maps and selectable actions in battles - 安装提供各种各样界面改进的模组,例如美化随机地图界面或添加战场行动选项 - - - - Install compatible version of "Horn of the Abyss", a fan-made Heroes III expansion ported by the VCMI team - 安装兼容版本的“深渊的号角”,一个由爱好者制作的英雄无敌3扩展包,由VCMI团队移植 - - - - Install compatible version of "In The Wake of Gods", a fan-made Heroes III expansion - 安装兼容版本的“追随神迹”,一个由爱好者制作的英雄无敌3扩展包 - - - - Finish - 完成 - - - - VCMI on Github - 访问VCMI的Github - - - - VCMI on Discord - 访问VCMI的Discord - - - - + + Next 下一步 - + + You can manually copy directories Maps, Data, and Mp3 from the original game directory to the VCMI data directory that you can see on top of this page + 你可以从原始游戏里手动拷贝Maps, Data 和 Mp3目录到VCMI的数据目录下,数据目录可以从页面顶部找到 + + + Manual Installation 手动安装 - + Search again 再次搜索 - If you don't have a copy of Heroes III installed, VCMI can import your Heroes III data using the offline installer from gog.com. - 如果你没有一份英雄无敌3的安装拷贝,VCMI可以从 gog.com的离线安装包导入你的英雄无敌3数据。 - - - + Heroes III data files 英雄无敌3数据文件 - + Copy existing data 复制已存在的数据 - Your Heroes III language has been successfully detected. - 已经成功检测英雄无敌3语言。 + + If you own Heroes III on gog.com, you can download a backup offline installer from gog.com. VCMI will then import Heroes III data using the offline installer. +Offline installer consists of two files: ".exe" and ".bin" - you must download both. + 如果你已经在gog.com拥有了英雄无敌3,你可以从gog.com下载离线安装器。VCMI将会通过离线安装器导入英雄无敌3数据 +离线安装器包含两部分:exe文件和bin文件。请确保这两部分都已下载。 - Heroes III language - 英雄无敌3语言 - - - - + + Back 返回 - + Install VCMI Mod Preset Mod统一翻译为模组 安装VCMI预设模组 - + Horn of the Abyss 深渊的号角 - + Heroes III Translation 英雄无敌3翻译 - + In The Wake of Gods 追随神迹 - + Heroes III installation found! 英雄无敌3安装目录已找到! - + Copy data to VCMI folder? 复制数据到VCMI文件夹吗? - + Select %1 file... param is file extension 选择%1文件... - + You have to select %1 file! param is file extension 你必须选择%1文件! - + GOG file (*.*) GOG文件 (*.*) - + File selection 选择文件 - - File cannot opened + + File cannot be opened 打开文件失败 - + Invalid file selected 所选的文件无效 - + GOG installer GOG安装包 - - GOG data - GOC数据 - - - Installing... Please wait! - 安装中...请等待! - - - - No Heroes III data! - 没有英雄无敌3数据! - - - - Selected files do not contain Heroes III data! - 所选的文件不包含英雄无敌3数据! - - - - - - - Heroes III data not found! - 未找到英雄无敌3数据! - - - - Failed to detect valid Heroes III data in chosen directory. -Please select directory with installed Heroes III data. - 从所选目录检测有效的英雄无敌3数据失败。 -请选择已安装英雄无敌3的数据目录。 - - - - You've provided GOG Galaxy installer! This file doesn't contain the game. Please download the offline backup game installer! - 您提供的是GOG Galaxy安装器!这个文件不包含游戏内容,请下载离线游戏安装器! - - - - Stream error while extracting files! -error reason: - 提取文件时遭遇文件流错误! -错误原因: - - - - Not a supported Inno Setup installer! - 这不是一个支持的Inno Setup安装器! - - - - Extracting error! - 提取错误! - - - + Heroes III: HD Edition files are not supported by VCMI. -Please select directory with Heroes III: Complete Edition or Heroes III: Shadow of Death. +Please select the directory with Heroes III: Complete Edition or Heroes III: Shadow of Death. VCMI不支持英雄无敌3高清版文件。 请选择包含《英雄无敌3:完全版》或《英雄无敌3:死亡阴影》的目录。 - + + Unknown or unsupported Heroes III version found. +Please select the directory with Heroes III: Complete Edition or Heroes III: Shadow of Death. + 检测到未知或不支持的英雄无敌3版本。 +请选择包含《英雄无敌3:完全版》或《英雄无敌3:死亡阴影》的目录。 + + + + GOG data + GOG数据 + + + + You've provided a GOG Galaxy installer! This file doesn't contain the game. Please download the offline backup game installer! + 您提供的是GOG Galaxy安装器!这个文件不包含游戏内容,请下载离线游戏安装器! + + + + Hash error! + 哈希错误! + + + + No Heroes III data! + 没有英雄无敌3数据! + + + + Selected files do not contain Heroes III data! + 所选的文件不包含英雄无敌3数据! + + + + Failed to detect valid Heroes III data in chosen directory. +Please select the directory with installed Heroes III data. + 从所选目录检测有效的英雄无敌3数据失败。 +请选择已安装英雄无敌3的数据目录。 + + + + + + + Heroes III data not found! + 未找到英雄无敌3数据! + + + Failed to detect valid Heroes III data in chosen directory. +Please select directory with installed Heroes III data. + 从所选目录检测有效的英雄无敌3数据失败。 +请选择已安装英雄无敌3的数据目录。 + + + You've provided GOG Galaxy installer! This file doesn't contain the game. Please download the offline backup game installer! + 您提供的是GOG Galaxy安装器!这个文件不包含游戏内容,请下载离线游戏安装器! + + + + Extracting error! + 提取错误! + + + Heroes III: HD Edition files are not supported by VCMI. +Please select directory with Heroes III: Complete Edition or Heroes III: Shadow of Death. + VCMI不支持英雄无敌3高清版文件。 +请选择包含《英雄无敌3:完全版》或《英雄无敌3:死亡阴影》的目录。 + + Unknown or unsupported Heroes III version found. Please select directory with Heroes III: Complete Edition or Heroes III: Shadow of Death. - 检测到未知或不支持的英雄无敌3版本。 + 检测到未知或不支持的英雄无敌3版本。 请选择包含《英雄无敌3:完全版》或《英雄无敌3:死亡阴影》的目录。 @@ -1366,6 +1473,94 @@ Please select directory with Heroes III: Complete Edition or Heroes III: Shadow 图片查看器 + + Innoextract + + + Stream error while extracting files! +error reason: + 提取文件时遭遇文件流错误! +错误原因: + + + + Not a supported Inno Setup installer! + 这不是一个支持的Inno Setup安装器! + + + + VCMI was compiled without innoextract support, which is needed to extract exe files! + VCMI编译时没有启用innoextract支持,启用了才可以从exe文件中提取数据! + + + + SHA1 hash of provided files: +Exe (%1 bytes): +%2 + 提供文件的SHA1哈希值: +Exe(%1字节): +%2 + + + + +Bin (%1 bytes): +%2 + +Bin (%1字节): +%2 + + + + Internal copy process failed. Enough space on device? + +%1 + 内部错误:复制失败。设备上是否有足够的空间? + +%1 + + + + Exe + Exe + + + + Bin + Bin + + + + Language mismatch! +%1 + +%2 + 语言不匹配! +%1 + +%2 + + + + Only one file known! Maybe files are corrupted? Please download again. +%1 + +%2 + 仅检测到一个文件!文件可能已损坏?请重新下载。 +%1 + +%2 + + + + Unknown files! Maybe files are corrupted? Please download again. + +%1 + 未知文件!文件可能已损坏?请重新下载。 + +%1 + + Language @@ -1477,53 +1672,545 @@ Please select directory with Heroes III: Complete Edition or Heroes III: Shadow 帮助 - - Map Editor - 地图编辑器 + + Game + 游戏 + + + Map Editor + 地图编辑器 - Start game - 开始游戏 + 开始游戏 Mods 模组 + + + Replace config file? + 替换配置文件? + + + + Do you want to replace %1? + 您想要替换%1吗? + ModFields - + Name 名称 - + Type 类型 + + + ModStateController - Version - 版本 + + 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并获取模组最新版本 + + + + Mod is not compatible, please update VCMI and check the 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 a protected directory, please remove it manually: + + 模组位于受保护的目录,请手动删除它: + + + + 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 - + Error starting executable 启动可执行文件时出错 - + Failed to start %1 Reason: %2 启动%1失败 原因:%2 + + StartGameTab + + + Form + 表单 + + + + Import from Clipboard + 从剪切板导入 + + + + Rename Current Preset + 重命名当前预设 + + + + Current Preset + 当前预设 + + + + Create New Preset + 创建新预设 + + + + Export to Clipboard + 导出到剪切板 + + + + Delete Current Preset + 删除当前预设 + + + + Unsupported or corrupted game data detected! + 检测到不支持或损坏的游戏数据! + + + + + + + + + + + + ? + ? + + + + Install Translation + 安装翻译 + + + + No soundtrack detected! + 未检测到音轨! + + + + Armaggedon's Blade campaigns are missing! + 末日之刃战役缺失! + + + + No video files detected! + 未检测到视频文件! + + + + Activate Translation + 已启用的翻译 + + + + Import files + 导入文件 + + + + Check For Updates + 检查更新 + + + + Go to Downloads Page + 跳转到下载页面 + + + + Go to Changelog Page + 跳转到更新日志页面 + + + + You are using the latest version + 您已使用最新版 + + + + Game Data Files + 游戏数据文件 + + + + Mod Preset + 模组预设 + + + + Resume + 恢复 + + + + Play + 进行游戏 + + + + Editor + 编辑器 + + + + Update %n mods + + 更新%n模组 + + + + + Heroes Chronicles: +%n/%1 installed + + 英雄无敌历代记: +%n/%1 已安装 + + + + + Update to %1 available + 可以更新到%1 + + + + All supported files + 所有支持的文件格式 + + + + Maps + 地图 + + + + Campaigns + 战役 + + + + Configs + 配置 + + + + Mods + 模组 + + + + Gog files + Gog文件 + + + + All files (*.*) + 所有文件 (*.*) + + + + Select files (configs, mods, maps, campaigns, gog files) to install... + 选择需要安装的文件(配置,模组,地图,战役,gog文件)... + + + + This option allows you to import additional data files into your VCMI installation. At the moment, following options are supported: + + - Heroes III Maps (.h3m or .vmap). + - Heroes III Campaigns (.h3c or .vcmp). + - Heroes III Chronicles using offline backup installer from GOG.com (.exe). + - VCMI mods in zip format (.zip) + - VCMI configuration files (.json) + + 此选项允许您将额外的数据文件导入到您的VCMI安装中。目前支持以下选项: + + - 英雄无敌3地图文件(.h3m 或 .vmap) + - 英雄无敌3战役文件(.h3c 或 .vcmp) + - 英雄无敌3历代记文件,使用来自 GOG.com的离线备份安装程序(.exe) + - VCMI zip格式模组文件(.zip) + - VCMI配置文件(.json) + + + + + Your Heroes III version uses different language. VCMI provides translations of the game into various languages that you can use. Use this option to automatically install such translation to your language. + 您的英雄无敌3版本使用的是不同的语言。VCMI提供了多种语言的翻译,您可以使用这些翻译。使用此选项可以自动安装适合您语言的翻译。 + + + + Translation of Heroes III into your language is installed, but has been turned off. Use this option to enable it. + 您语言对应的英雄无敌3翻译已安装,但是被关闭了。使用这个选项来启用它。 + + + + A new version of some of the mods that you have installed is now available in mod repository. Use this option to automatically update all your mods to latest version. + +WARNING: In some cases, updated versions of mods may not be compatible with your existing saves. You many want to postpone mod update until you finish any of your ongoing games. + 您已安装的一部分模组的现在可以从模组源里获取更新。使用此选项可以自动将您的所有模组更新到最新版本。 + +警告:在某些情况下,模组的更新版本可能与您现有的存档不兼容。建议您在完成当前的游戏后再更新模组。 + + + + If you own Heroes Chronicles on gog.com, you can use offline backup installers provided by gog to import Heroes Chronicles data into VCMI as custom campaigns. +To import Heroes Chronicles, download offline backup installer of each chronicle that you wish to install, select 'Import files' option and select downloaded file. This will generate and install mod for VCMI that contains imported chronicles + 如果您在gog.com上拥有英雄无敌历代记,您可以使用gog提供的离线备份安装程序将 英雄无敌历代记数据作为自定义战役导入到 VCMI中。 +要导入英雄无敌历代记,请下载您希望安装的每个历代记的离线备份安装程序,选择“导入文件”选项并选择已下载的文件。这将生成并安装一个包含已导入的历代记的VCMI模组 + + + + VCMI has detected that Heroes III music files are missing from your installation. VCMI will run, but in-game music will not be available. + +To resolve this problem, please copy missing mp3 files from Heroes III to VCMI data files directory manually or reinstall VCMI and re-import Heroes III data files + VCMI检测到您的安装中缺少英雄无敌3的音乐文件。VCMI可以运行,但游戏内音乐将不可用。 + +要解决此问题,请手动将缺失的MP3文件从英雄无敌3复制到VCM 的数据文件目录,或重新安装VCMI 并重新导入英雄无敌3数据文件 + + + + VCMI has detected that Heroes III video files are missing from your installation. VCMI will run, but in-game cutscenes will not be available. + +To resolve this problem, please copy VIDEO.VID file from Heroes III to VCMI data files directory manually or reinstall VCMI and re-import Heroes III data files + VCMI检测到您的安装中缺少英雄无敌3的视频文件。VCMI可以运行,但游戏内过场动画将不可用。 + +要解决此问题,请手动将英雄无敌3的VIDEO.VID文件复制到VCMI的数据文件目录,或重新安装VCMI并重新导入英雄无敌3的数据文件 + + + + VCMI has detected that some of Heroes III data files are missing from your installation. You may attempt to run VCMI, but game may not work as expected or crash. + +To resolve this problem, please reinstall game and reimport data files using supported version of Heroes III. VCMI requires Heroes III: Shadow of Death or Complete Edition to run, which you can get (for example) from gog.com + VCMI检测到您的安装中缺少部分英雄无敌3的数据文件。您可以尝试运行VCMI,但游戏可能无法正常工作或崩溃。 + +要解决此问题,请重新安装游戏并使用支持的英雄无敌3版本重新导入数据文件。VCMI需要英雄无敌3:死亡阴影或完全版才能运行,您可以(例如)从gog.com获取 + + + + VCMI has detected that some of Heroes III: Armageddon's Blade data files are missing from your installation. VCMI will work, but Armageddon's Blade campaigns will not be available. + +To resolve this problem, please copy missing data files from Heroes III to VCMI data files directory manually or reinstall VCMI and re-import Heroes III data files + VCMI检测到您的安装中缺少部分英雄无敌3:末日之刃的数据文件。VCMI可以运行,但末日之刃的战役将不可用。 + +要解决此问题,请手动将缺失的英雄无敌3数据文件复制到VCMI的数据文件目录,或重新安装VCMI并重新导入英雄无敌3的数据文件 + + + + Enter preset name: + 输入预设名字: + + + + Rename preset '%1' to: + 重命名预设'%1'为: + + UpdateDialog @@ -1548,8 +2235,12 @@ Reason: %2 - Cannot read JSON from url or incorrect JSON data + Cannot read JSON from URL or incorrect JSON data 无法从url中读取JSON或JSON数据不正确 + + Cannot read JSON from url or incorrect JSON data + 无法从url中读取JSON或JSON数据不正确 + diff --git a/launcher/translation/czech.ts b/launcher/translation/czech.ts index e02143603..19c74c9ae 100644 --- a/launcher/translation/czech.ts +++ b/launcher/translation/czech.ts @@ -24,176 +24,69 @@ Naše komunita - + Build Information Informace o sestavení - + User data directory Složka uživatelských dat + - - - + + Open Otevřít - + Check for updates Zkontrolovat aktualizace - + Game version Verze hry - + Log files directory - Složka záznamů hry + Adresář souborů s logy - + Data Directories Složky dat - + Game data directory Složka herních dat - + Operating System Operační systém - + Configuration files directory Složka nastavení hry - + Project homepage Domovská stránka projektu - + Report a bug Nahlásit chybu - - CModListModel - - - 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 - Bojovníci - - - - Compatibility - Kompabilita - - - - Artifacts - Artefakty - - - - AI - AI - - CModListView @@ -209,7 +102,7 @@ Downloadable - Stahovatelné + Ke stažení @@ -219,7 +112,7 @@ Updatable - Aktualizovatelné + K aktualizaci @@ -231,58 +124,49 @@ Inactive Neaktivní - - Download && refresh repositories - Stáhnout a aktualizovat repozitáře - - + Description Popis - + Changelog Seznam změn - + Screenshots Snímky obrazovky - - Install from file - Instalovat ze souboru - - - + Uninstall Odinstalovat - + Enable Povolit - + Disable Zakázat - + Update Aktualizovat - + Install Instalovat - + %p% (%v KB out of %m KB) %p% (%v KB z %m KB) @@ -292,188 +176,141 @@ Načíst repozitáře - + Abort 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 + + 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 mods are incompatible with it - Tato modifikace nemůže být povolena, protože následující modifikace s ní nejsou kompatibilní + + 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 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 + + 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 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 - - - + 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 - - - - Select files (configs, mods, maps, campaigns) to install... - Vyberte soubory (nastavení, modifikace, mapy anebo kampaně) pro 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 - Downloading %s%. %p% (%v MB out of %m MB) finished - Stahování %s%. %p% (%v MB z %m MB) dokončeno - - - + Download failed Stahování selhalo - + Unable to download all files. Encountered errors: @@ -486,7 +323,7 @@ Vyskytly se chyby: - + Install successfully downloaded? @@ -495,531 +332,520 @@ Install successfully downloaded? Nainstalovat úspěšně stažené? - + + Installing Heroes Chronicles + Instalování Heroes Chronicles + + + 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í - - CModManager - - - 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 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 - - - - Required mod %1 is missing - Vyžadovaná modifkace %1 chybí - - - - Required mod %1 is not enabled - Vyžadovaná modifikace %1 není povolena - - - - - This mod conflicts with %1 - Tato modifikace koliduje s %1 - - - - Mod is already disabled - Modifikace je již povolena - - - - This mod is needed to run %1 - Modifikace %1 je vyžadována pro běh - - - - 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 dat 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íme odstraňte ji ručně: - - - CSettingsView - + + Off Vypnuto - + Artificial Intelligence Umělá inteligence - Mod Repositories - Repozitáře modifikací - - - + 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 - + 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 - - Online Lobby address - Adresa online předsíně + + + + 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 lobby + + + + Cursor Scaling + Škálování kurzoru + + + + Scalable + Škálovatelné + + + + Miscellaneous + Ostatní + + + + Select a display mode for the game + +Windowed - the game will run inside a window that covers part of your screen. + +Borderless Windowed Mode - the game will run in a full-screen window, matching your screen's resolution. + +Fullscreen Exclusive Mode - the game will cover the entirety of your screen and will use selected resolution. + Vyberte režim zobrazení hry + +Okno - hra poběží v okně, které pokryje část vaší obrazovky. + +Celá obrazovka bez okrajů - hra poběží v okně na celou obrazovku, přizpůsobeném rozlišení vaší obrazovky." + +Režim celé obrazovky - hra pokryje celou vaši obrazovku a použije vybrané rozlišení. + + + + 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í - - Best (Linear) - Nejlepší (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) - Friendly AI in battles - Přátelské AI v bitvách + + Downscaling Filter + Filtr pro zmenšování - + 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č - + On Zapnuto - Cursor - Kurzor - - - Heroes III Data Language - Jazyk dat Heroes III - - - - 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. - Vyberte režim zobrazení pro hru - -V okně - hra bude běžet v okně zakrývajícím část vaší obrazovky - -Celá obrazovka bez okrajů- hra poběží v okně, které zakryje vaši celou obrazovku se stejným rozlišením. - -Exkluzivní celá obrazovka - hra zakryje vaši celou obrazovku a použije vybrané rozlišení. - - - + Reserved screen area Vyhrazená část obrazovky - Hardware - Hardware - - - Software - Software - - - + 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 + + ChroniclesExtractor + + + + Invalid file selected + Vybrán neplatný soubor + + + + The file cannot be opened + Soubor nelze otevřít + + + + You have to select a gog installer file! + Musíte vybrat instalační soubor GOG! + + + + You have to select a Heroes Chronicles installer file! + Musíte vybrat instalační soubor Heroes Chronicles! + + + + Extracting error! + Chyb při rozbalování! + + + + Hash error! + Nesouhlasí kontrolní součet! + + + + + + Heroes Chronicles + Heroes Chronicles + + + + Heroes Chronicles %1 - %2 + Heroes Chronicles %1 - %2 + + File size - - %1 B - %1 B - - - - %1 KiB - %1 KiB - - - + + %1 MiB %1 MiB - - - %1 GiB - %1 GiB - - - - %1 TiB - %1 TiB - FirstLaunchView @@ -1044,118 +870,77 @@ Exkluzivní celá obrazovka - hra zakryje vaši celou obrazovku a použije vybra Vyberte váš jazyk - + Have a question? Found a bug? Want to help? Join us! Máte otázku? Našli jste chybu? Chcete pomoct? Připojte se k nám! - - Thank you for installing VCMI! - -Before you can start playing, there are a few more steps that need to be completed. - -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! - -Před začátkem hraní musíte ještě dokončit pár 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. - -Heroes® of Might and Magic® III HD není v současnosti podporovaný! - - - + Locate Heroes III data files Najít soubory dat Heroes III - + Use offline installer from gog.com Použít offline instalátor z gog.com - - 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. - - - + Install gog.com files Nainstalovat soubory gog.com - To run VCMI, Heroes III data files need to be present in one of the specified locations. Please copy the Heroes III data to one of these directories. - Pro běh VCMI, datové soubory Heroes III musí být přítomny v jednom z určených umístění. Prosíme, zkopírujte data Heroes do jedné z těchto složek. - - - Alternatively, you can provide the directory where Heroes III data is installed and VCMI will copy the existing data automatically. - Nebo můžete poskytnout složku s instalací Heroes III a VCMI zkopíruje existující data automaticky. - - - + Your Heroes III data files have been successfully found. Vaše soubory dat Heroes III byly úspěšně nalezeny. - The automatic detection of the Heroes III language has failed. Please select the language of your Heroes III manually - Automatické rozpoznání jazyka Heroes III selhalo. Prosíme, vyberte jazyk vašich Heroes III ručně - - - + Interface Improvements Vylepšení rozhraní - + 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 - + Installing... %p% Instalování... %p% - + If you already have Heroes III files on your device, you can select this directory and VCMI will copy the existing data automatically. Pokud již máte soubory Heroes III na vašem zařízení, můžete vybrat jejich složku a VCMI existující data zkopíruje automaticky. - + Copy existing files Kopírovat existující data - - 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. - Pokud vlastníte Heroes III na gog.com, můžete odsud stáhnout záložní offline instalátor a VCMI z něj naimportuje data. -Offline instalátor obsahuje dvě části, .exe a .bin. Ujistěte se, že stahujete obě. - - - + 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í - - Install mod that provides various interface improvements, such as better interface for random maps and selectable actions in battles - Instalovat modifikaci, která poskytuje různá vylepšení rozhraní, například lepší rozhraní pro náhodné mapy a volitelné akce v bitvách - - - + 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" - + + Install mod that provides various interface improvements, such as a better interface for random maps and selectable actions in battles + Nainstalujte modifikaci, která přináší různá vylepšení, jako je lepší rozhraní pro náhodné mapy a možnost výběru akcí v bitvách + + + Finish Dokončit @@ -1165,182 +950,196 @@ Offline instalátor obsahuje dvě části, .exe a .bin. Ujistěte se, že stahuj VCMI na GitHubu - + VCMI on Discord VCMI na Discordu - - + + Thank you for installing VCMI! + +Before you can start playing, there are a few more steps to complete. + +Please remember that 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, že jste nainstalovali VCMI! + +Než začnete hrát, je třeba dokončit několik kroků. + +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í podporována! + + + + Next Další - + + You can manually copy directories Maps, Data, and Mp3 from the original game directory to the VCMI data directory that you can see on top of this page + Můžete ručně zkopírovat složky Maps, Data a Mp3 z instalačního adresáře hry do adresáře VCMI, který vidíte výše + + + Manual Installation Ruční instalace - + Search again Hledat znovu - + Heroes III data files Soubory dat Heroes III - + Copy existing data Kopírovat existující data - Your Heroes III language has been successfully detected. - Váš jazyk Heroes III byl úspěšně zjištěn. + + If you own Heroes III on gog.com, you can download a backup offline installer from gog.com. VCMI will then import Heroes III data using the offline installer. +Offline installer consists of two files: ".exe" and ".bin" - you must download both. + Pokud vlastníte Heroes III na gog.com, můžete si stáhnout záložní offline instalační program z gog.com. VCMI poté importuje data Heroes III pomocí tohoto offline instalačního programu. Offline instalační program se skládá ze dvou souborů: \".exe\" a \".bin\" – musíte stáhnout oba. - Heroes III language - Jazyk Heroes III - - - - + + Back Zpět - + Install VCMI Mod Preset - Instalovat předvybrané modifiakce VCMI + Instalovat předvybrané VCMI modifikace - + Horn of the Abyss - + Horn of the Abyss - + Heroes III Translation Překlady Heroes III - + In The Wake of Gods - + In The Wake of Gods - + Heroes III installation found! Instalace Heroes III nalezena! - + Copy data to VCMI folder? Zkopírovat data do složky VCMI? - + Select %1 file... param is file extension Vyberte soubor %1... - + You have to select %1 file! param is file extension Musíte vybrat soubor %1! - + GOG file (*.*) GOG soubor (*.*) - + File selection Výběr souboru - - File cannot opened - + + File cannot be opened + Soubor nelze otevřít - + Invalid file selected Vybrán neplatný soubor - + GOG installer Instalátor GOG - + + You've provided a 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! + + + + Heroes III: HD Edition files are not supported by VCMI. +Please select the directory with Heroes III: Complete Edition or Heroes III: Shadow of Death. + Soubory z Heroes III: HD Edition nejsou ve VCMI podporovány. +Vyberte prosím adresář s Heroes III: Complete Edition nebo Heroes III: Shadow of Death. + + + + Unknown or unsupported Heroes III version found. +Please select the directory with Heroes III: Complete Edition or Heroes III: Shadow of Death. + Byla nalezena neznámá nebo nepodporovaná verze Heroes III. +Vyberte prosím adresář s Heroes III: Complete Edition nebo Heroes III: Shadow of Death. + + + GOG data Data GOG - - You've provided GOG Galaxy installer! This file doesn't contain the game. Please download the offline backup game installer! - - - - - Stream error while extracting files! -error reason: - - - - - Not a supported Inno Setup installer! - - - - + Extracting error! - + Chyba při rozbalování! - + + Hash error! + Nesouhlasí kontrolní součet! + + + No Heroes III data! - Žádná data Heroes III! + Chybí data Heroes III! - + Selected files do not contain Heroes III data! Vybrané soubory neobsahují data Heroes III! - - - - + + Failed to detect valid Heroes III data in chosen directory. +Please select the directory with installed Heroes III data. + Nepodařilo se detekovat platná data Heroes III ve vybraném adresáři. +Vyberte prosím adresář s nainstalovanými daty Heroes III. + + + + + + Heroes III data not found! Data Heroes III nenalezena! - - - Failed to detect valid Heroes III data in chosen directory. -Please select directory with installed Heroes III data. - Detekce platných dat Heroes III ve vybrané složce selhala. -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. - - - - 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. - ImageViewer @@ -1350,6 +1149,94 @@ Prosíme vyberte složku s Heroes III: Complete Edition nebo Heroes III: Shadow Prohlížeč obrázků + + Innoextract + + + 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ů! + + + + SHA1 hash of provided files: +Exe (%1 bytes): +%2 + Kontrolní součet SHA1 zadaných souborů: +Exe (%1 bajtů): +%2 + + + + +Bin (%1 bytes): +%2 + +Bin (%1 bajtů): +%2 + + + + Internal copy process failed. Enough space on device? + +%1 + Interní proces kopírování selhal. Máte na zařízení dostatek místa? + +%1 + + + + Exe + Exe + + + + Bin + Bin + + + + Language mismatch! +%1 + +%2 + Jazyk se neshoduje! +%1 + +%2 + + + + Only one file known! Maybe files are corrupted? Please download again. +%1 + +%2 + V databázi byl nalezen pouze jeden soubor! Možná jsou soubory poškozené? Stáhněte je prosím znovu. +%1 + +%2 + + + + Unknown files! Maybe files are corrupted? Please download again. + +%1 + Neznámé soubory! Možná jsou soubory poškozené? Stáhněte je prosím znovu. + +%1 + + Language @@ -1437,18 +1324,6 @@ Prosíme vyberte složku s Heroes III: Complete Edition nebo Heroes III: Shadow Vietnamese Vietnamština - - Other (East European) - Ostatní (východní Evropa) - - - Other (Cyrillic Script) - Ostatní (azbuka) - - - Other (West European) - Ostatní (západní Evropa) - Auto (%1) @@ -1460,7 +1335,7 @@ Prosíme vyberte složku s Heroes III: Complete Edition nebo Heroes III: Shadow VCMI Launcher - Spouštěč VCMI + VCMI Launcher @@ -1473,53 +1348,533 @@ Prosíme vyberte složku s Heroes III: Complete Edition nebo Heroes III: Shadow Nápověda - - Map Editor - Editor map - - - - Start game - Spustit hru + + Game + Hra Mods Modifikace + + + Replace config file? + Chcete nahradit konfigurační soubor? + + + + Do you want to replace %1? + Chcete nahradit %1? + ModFields - + Name Název - + Type Druh + + + ModStateController - Version - Verze + + 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 check the 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 a protected directory, please remove it manually: + + Modifikace se nachází v chráněném adresáři, odstraňte ji prosím 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 - + Error starting executable Chyba při spouštění souboru - + Failed to start %1 Reason: %2 Selhal start %1 Důvod: %2 + + StartGameTab + + + Form + Forma + + + + Import from Clipboard + Importovat ze schránky + + + + Rename Current Preset + Přejmenovat aktuální konfiguraci + + + + Current Preset + Aktuální konfigurace + + + + Create New Preset + Vytvořit novou konfiguraci + + + + Export to Clipboard + Exportovat do schránky + + + + Delete Current Preset + Odstranit aktuální konfiguraci + + + + Unsupported or corrupted game data detected! + Byla zjištěna nepodporovaná nebo poškozená herní data! + + + + + + + + + + + + ? + ? + + + + Install Translation + Instalovat překlad + + + + No soundtrack detected! + Nebyl detekován žádný soundtrack! + + + + Armaggedon's Blade campaigns are missing! + Kampaně z Armaggedon's Blade chybí! + + + + No video files detected! + Nebyl nalezen žádný videosoubor! + + + + Activate Translation + Aktivovat překlad + + + + Import files + Importovat soubory + + + + Check For Updates + Zkontrolovat aktualizace + + + + Go to Downloads Page + Otevřít stránku ke stažení + + + + Go to Changelog Page + Otevřít stránku se změnami + + + + You are using the latest version + Používáte nejnovější verzi + + + + Game Data Files + Herní soubory + + + + Mod Preset + Přednastavení modifikací + + + + Resume + Pokračovat + + + + Play + Hrát + + + + Editor + Editor + + + + Update %n mods + + Aktualizovat %n modifikaci + Aktualizovat %n modifikace + Aktualizovat %n modifikací + + + + + Heroes Chronicles: +%n/%1 installed + + Heroes Chronicles: +nainstalováno %n/%1 + Heroes Chronicles: +nainstalováno %n/%1 + Heroes Chronicles: +nainstalováno %n/%1 + + + + + Update to %1 available + Dostupná aktualizace na %1 + + + + All supported files + Všechny podporované soubory + + + + Maps + Mapy + + + + Campaigns + Kampaně + + + + Configs + Konfigurace + + + + Mods + Modifikace + + + + Gog files + Soubory GOG + + + + 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... + + + + This option allows you to import additional data files into your VCMI installation. At the moment, following options are supported: + + - Heroes III Maps (.h3m or .vmap). + - Heroes III Campaigns (.h3c or .vcmp). + - Heroes III Chronicles using offline backup installer from GOG.com (.exe). + - VCMI mods in zip format (.zip) + - VCMI configuration files (.json) + + Tato možnost vám umožňuje importovat další datové soubory do vaší instalace VCMI. V současnosti jsou podporovány následující možnosti: + +Mapy pro Heroes III (.h3m nebo .vmap). +Kampaně pro Heroes III (.h3c nebo .vcmp). +Heroes III Chronicles pomocí offline instalačního programu z GOG.com (.exe). +Modifikace pro VCMI ve formátu zip (.zip). +Konfigurační soubory pro VCMI (.json) + + + + + Your Heroes III version uses different language. VCMI provides translations of the game into various languages that you can use. Use this option to automatically install such translation to your language. + Vaše verze Heroes III používá jiný jazyk. VCMI poskytuje překlady hry do různých jazyků, které můžete použít. Použijte tuto možnost pro automatickou instalaci překladu do vašeho jazyka. + + + + Translation of Heroes III into your language is installed, but has been turned off. Use this option to enable it. + Překlad Heroes III do vašeho jazyka je nainstalován, ale byl vypnut. Použijte tuto možnost k jeho aktivaci. + + + + A new version of some of the mods that you have installed is now available in mod repository. Use this option to automatically update all your mods to latest version. + +WARNING: In some cases, updated versions of mods may not be compatible with your existing saves. You many want to postpone mod update until you finish any of your ongoing games. + Je dostupná nová verze některých modifikací, které máte nainstalované. Použijte tuto možnost k automatické aktualizaci všech modifikací na nejnovější verzi. + +UPOZORNĚNÍ: Aktualizované verze modifikací nemusí být v některých případech kompatibilní s vašimi uloženými hrami. Doporučujeme odložit aktualizaci, dokud nedokončíte rozehrané hry. + + + + If you own Heroes Chronicles on gog.com, you can use offline backup installers provided by gog to import Heroes Chronicles data into VCMI as custom campaigns. +To import Heroes Chronicles, download offline backup installer of each chronicle that you wish to install, select 'Import files' option and select downloaded file. This will generate and install mod for VCMI that contains imported chronicles + Pokud vlastníte Heroes Chronicles na gog.com, můžete pomocí poskytnutých offline instalačních souborů GOG importovat data Heroes Chronicles do VCMI jako vlastní kampaně. +Pro import Heroes Chronicles si stáhněte offline instalační soubory každé kroniky, kterou chcete nainstalovat, zvolte možnost 'Importovat soubory' a vyberte stažený soubor. Tím se vytvoří a nainstaluje modifikace pro VCMI obsahující importované data Heroes Chronicles + + + + VCMI has detected that Heroes III music files are missing from your installation. VCMI will run, but in-game music will not be available. + +To resolve this problem, please copy missing mp3 files from Heroes III to VCMI data files directory manually or reinstall VCMI and re-import Heroes III data files + Ve vaší VCMI instalaci chybí zvukové soubory z Heroes III. VCMI bude fungovat, ale hudba a zvuky ve hře nebude dostupná. + +Pro vyřešení tohoto problému ručně zkopírujte chybějící MP3 soubory z Heroes III do složky s daty VCMI nebo znovu nainstalujte VCMI a reimportujte herní data z Heroes III + + + + VCMI has detected that Heroes III video files are missing from your installation. VCMI will run, but in-game cutscenes will not be available. + +To resolve this problem, please copy VIDEO.VID file from Heroes III to VCMI data files directory manually or reinstall VCMI and re-import Heroes III data files + Ve vaší VCMI instalaci chybí video soubory z Heroes III. VCMI bude fungovat, ale herní videa nebudou dostupná. + +Pro vyřešení tohoto problému ručně zkopírujte soubor VIDEO.VID z Heroes III do složky s daty VCMI nebo znovu nainstalujte VCMI a reimportujte herní data z Heroes III + + + + VCMI has detected that some of Heroes III data files are missing from your installation. You may attempt to run VCMI, but game may not work as expected or crash. + +To resolve this problem, please reinstall game and reimport data files using supported version of Heroes III. VCMI requires Heroes III: Shadow of Death or Complete Edition to run, which you can get (for example) from gog.com + Ve vaší VCMI instalaci chybí některé datové soubory z Heroes III. VCMI můžete spustit, ale hra nemusí fungovat správně nebo může docházet k pádům. + +Pro vyřešení tohoto problému znovu nainstalujte hru a reimportujte podporované datové soubory Heroes III. VCMI vyžaduje Heroes III: Complete Edition nebo Shadow of Death, které můžete získat například na gog.com + + + + VCMI has detected that some of Heroes III: Armageddon's Blade data files are missing from your installation. VCMI will work, but Armageddon's Blade campaigns will not be available. + +To resolve this problem, please copy missing data files from Heroes III to VCMI data files directory manually or reinstall VCMI and re-import Heroes III data files + Ve vaší VCMI instalaci chybí některé datové soubory z Heroes III: Armageddon's Blade. VCMI bude fungovat, ale kampaně z Armageddon's Blade nebudou dostupné. + +Pro vyřešení tohoto problému ručně zkopírujte chybějící datové soubory z Heroes III do složky s daty VCMI nebo znovu nainstalujte VCMI a reimportujte herní data z Heroes III + + + + Enter preset name: + Zadejte název konfigurace: + + + + Rename preset '%1' to: + Přejmenovat konfiguraci '%1' na: + + UpdateDialog @@ -1544,8 +1899,8 @@ Důvod: %2 - Cannot read JSON from url or incorrect JSON data - Nelze přečíst JSON z URL nebo nesprávná data JSON + Cannot read JSON from URL or incorrect JSON data + Nelze načíst JSON z URL nebo data JSON nejsou správná diff --git a/launcher/translation/english.ts b/launcher/translation/english.ts index dea3e3e1a..5391eb3ff 100644 --- a/launcher/translation/english.ts +++ b/launcher/translation/english.ts @@ -24,176 +24,69 @@ - + Build Information - + User data directory + - - - + + Open - + Check for updates - + Game version - + Log files directory - + Data Directories - + Game data directory - + Operating System - + Configuration files directory - + Project homepage - + Report a bug - - CModListModel - - - Translation - - - - - Town - - - - - Test - - - - - Templates - - - - - Spells - - - - - Music - - - - - Maps - - - - - Sounds - - - - - Skills - - - - - - Other - - - - - Objects - - - - - - Mechanics - - - - - - Interface - - - - - Heroes - - - - - - Graphical - - - - - Expansion - - - - - Creatures - - - - - Compatibility - - - - - Artifacts - - - - - AI - - - CModListView @@ -233,52 +126,47 @@ - + Description - + Changelog - + Screenshots - - Install from file - - - - + Uninstall - + Enable - + Disable - + Update - + Install - + %p% (%v KB out of %m KB) @@ -288,184 +176,141 @@ - + Abort - + 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 - - - - - Select files (configs, mods, maps, campaigns) 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: @@ -474,506 +319,515 @@ Encountered errors: - + Install successfully downloaded? - + + Installing Heroes 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 - + + Cursor Scaling + + + + + Scalable + + + + + Miscellaneous + + + + + Select a display mode for the game + +Windowed - the game will run inside a window that covers part of your screen. + +Borderless Windowed Mode - the game will run in a full-screen window, matching your screen's resolution. + +Fullscreen Exclusive Mode - the game will cover the entirety of your screen and will use selected resolution. + + + + + Font Scaling (experimental) + + + + + Original + + + + Upscaling Filter - + + Basic + + + + Use Relative Pointer Mode - + Nearest - + Linear - - Best (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 - + On - - 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. - - - - + 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 + + ChroniclesExtractor + + + + Invalid file selected + + + + + The file cannot be opened + + + + + You have to select a gog installer file! + + + + + You have to select a Heroes Chronicles installer file! + + + + + Extracting error! + + + + + Hash error! + + + + + + + Heroes Chronicles + + + File size - - %1 B - - - - - %1 KiB - - - - + + %1 MiB - - - %1 GiB - - - - - %1 TiB - - FirstLaunchView @@ -998,99 +852,77 @@ Fullscreen Exclusive Mode - game will cover entirety of your screen and will use - + Have a question? Found a bug? Want to help? Join us! - - Thank you for installing VCMI! - -Before you can start playing, there are a few more steps that need to be completed. - -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! - - - - + Locate Heroes III data files - + Use offline installer from gog.com - - 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 - - - - + Install gog.com files - + Your Heroes III data files have been successfully found. - + Interface Improvements - + Install a translation of Heroes III in your preferred language - + Installing... %p% - + If you already have Heroes III files on your device, you can select this directory and VCMI will copy the existing data automatically. - + Copy existing files - - 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. - - - - + Optionally, you can install additional mods either now, or at any point later, using the VCMI Launcher - - Install mod that provides various interface improvements, such as better interface for random maps and selectable actions in battles - - - - + Install compatible version of "Horn of the Abyss", a fan-made Heroes III expansion ported by the VCMI team - + Install compatible version of "In The Wake of Gods", a fan-made Heroes III expansion - + + Install mod that provides various interface improvements, such as a better interface for random maps and selectable actions in battles + + + + Finish @@ -1100,169 +932,185 @@ Offline installer consists of two parts, .exe and .bin. Make sure you download b - + VCMI on Discord - - + + Thank you for installing VCMI! + +Before you can start playing, there are a few more steps to complete. + +Please remember that 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! + + + + + Next - + + You can manually copy directories Maps, Data, and Mp3 from the original game directory to the VCMI data directory that you can see on top of this page + + + + Manual Installation - + Search again - + Heroes III data files - + Copy existing data - - + + If you own Heroes III on gog.com, you can download a backup offline installer from gog.com. VCMI will then import Heroes III data using the offline installer. +Offline installer consists of two files: ".exe" and ".bin" - you must download both. + + + + + Back - + Install VCMI Mod Preset - + Horn of the Abyss - + Heroes III Translation - + In The Wake of Gods - + Heroes III installation found! - + Copy data to VCMI folder? - + Select %1 file... param is file extension - + You have to select %1 file! param is file extension - + GOG file (*.*) - + File selection - - File cannot opened + + File cannot be opened - + Invalid file selected - + GOG installer - + + You've provided a GOG Galaxy installer! This file doesn't contain the game. Please download the offline backup game installer! + + + + + Heroes III: HD Edition files are not supported by VCMI. +Please select the directory with Heroes III: Complete Edition or Heroes III: Shadow of Death. + + + + + Unknown or unsupported Heroes III version found. +Please select the directory with Heroes III: Complete Edition or Heroes III: Shadow of Death. + + + + GOG data - - You've provided GOG Galaxy installer! This file doesn't contain the game. Please download the offline backup game installer! - - - - - Stream error while extracting files! -error reason: - - - - - Not a supported Inno Setup installer! - - - - + Extracting error! - + + Hash error! + + + + No Heroes III data! - + Selected files do not contain Heroes III data! - - - - - Heroes III data not found! - - - - + Failed to detect valid Heroes III data in chosen directory. -Please select directory with installed Heroes III data. +Please select the directory with installed Heroes III data. - - Heroes III: HD Edition files are not supported by VCMI. -Please select directory with Heroes III: Complete Edition or 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. + + + + + Heroes III data not found! @@ -1274,6 +1122,79 @@ Please select directory with Heroes III: Complete Edition or Heroes III: Shadow + + Innoextract + + + Stream error while extracting files! +error reason: + + + + + Not a supported Inno Setup installer! + + + + + VCMI was compiled without innoextract support, which is needed to extract exe files! + + + + + SHA1 hash of provided files: +Exe (%1 bytes): +%2 + + + + + +Bin (%1 bytes): +%2 + + + + + Internal copy process failed. Enough space on device? + +%1 + + + + + Exe + + + + + Bin + + + + + Language mismatch! +%1 + +%2 + + + + + Only one file known! Maybe files are corrupted? Please download again. +%1 + +%2 + + + + + Unknown files! Maybe files are corrupted? Please download again. + +%1 + + + Language @@ -1385,13 +1306,8 @@ Please select directory with Heroes III: Complete Edition or Heroes III: Shadow - - Map Editor - - - - - Start game + + Game @@ -1399,34 +1315,499 @@ Please select directory with Heroes III: Complete Edition or Heroes III: Shadow Mods + + + Replace config file? + + + + + Do you want to replace %1? + + 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 check the 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 a 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 - + Error starting executable - + Failed to start %1 Reason: %2 + + StartGameTab + + + Form + + + + + Import from Clipboard + + + + + Rename Current Preset + + + + + Current Preset + + + + + Create New Preset + + + + + Export to Clipboard + + + + + Delete Current Preset + + + + + Unsupported or corrupted game data detected! + + + + + + + + + + + + + ? + + + + + Install Translation + + + + + No soundtrack detected! + + + + + Armaggedon's Blade campaigns are missing! + + + + + No video files detected! + + + + + Activate Translation + + + + + Import files + + + + + Check For Updates + + + + + Go to Downloads Page + + + + + Go to Changelog Page + + + + + You are using the latest version + + + + + Game Data Files + + + + + Mod Preset + + + + + Resume + + + + + Play + + + + + Editor + + + + + Update %n mods + + + + + + + + Heroes Chronicles: +%n/%1 installed + + + + + + + + Update to %1 available + + + + + All supported files + + + + + Maps + + + + + Campaigns + + + + + Configs + + + + + Mods + + + + + Gog files + + + + + All files (*.*) + + + + + Select files (configs, mods, maps, campaigns, gog files) to install... + + + + + This option allows you to import additional data files into your VCMI installation. At the moment, following options are supported: + + - Heroes III Maps (.h3m or .vmap). + - Heroes III Campaigns (.h3c or .vcmp). + - Heroes III Chronicles using offline backup installer from GOG.com (.exe). + - VCMI mods in zip format (.zip) + - VCMI configuration files (.json) + + + + + + Your Heroes III version uses different language. VCMI provides translations of the game into various languages that you can use. Use this option to automatically install such translation to your language. + + + + + Translation of Heroes III into your language is installed, but has been turned off. Use this option to enable it. + + + + + A new version of some of the mods that you have installed is now available in mod repository. Use this option to automatically update all your mods to latest version. + +WARNING: In some cases, updated versions of mods may not be compatible with your existing saves. You many want to postpone mod update until you finish any of your ongoing games. + + + + + If you own Heroes Chronicles on gog.com, you can use offline backup installers provided by gog to import Heroes Chronicles data into VCMI as custom campaigns. +To import Heroes Chronicles, download offline backup installer of each chronicle that you wish to install, select 'Import files' option and select downloaded file. This will generate and install mod for VCMI that contains imported chronicles + + + + + VCMI has detected that Heroes III music files are missing from your installation. VCMI will run, but in-game music will not be available. + +To resolve this problem, please copy missing mp3 files from Heroes III to VCMI data files directory manually or reinstall VCMI and re-import Heroes III data files + + + + + VCMI has detected that Heroes III video files are missing from your installation. VCMI will run, but in-game cutscenes will not be available. + +To resolve this problem, please copy VIDEO.VID file from Heroes III to VCMI data files directory manually or reinstall VCMI and re-import Heroes III data files + + + + + VCMI has detected that some of Heroes III data files are missing from your installation. You may attempt to run VCMI, but game may not work as expected or crash. + +To resolve this problem, please reinstall game and reimport data files using supported version of Heroes III. VCMI requires Heroes III: Shadow of Death or Complete Edition to run, which you can get (for example) from gog.com + + + + + VCMI has detected that some of Heroes III: Armageddon's Blade data files are missing from your installation. VCMI will work, but Armageddon's Blade campaigns will not be available. + +To resolve this problem, please copy missing data files from Heroes III to VCMI data files directory manually or reinstall VCMI and re-import Heroes III data files + + + + + Enter preset name: + + + + + Rename preset '%1' to: + + + UpdateDialog @@ -1451,7 +1832,7 @@ Reason: %2 - Cannot read JSON from url or incorrect JSON data + Cannot read JSON from URL or incorrect JSON data diff --git a/launcher/translation/french.ts b/launcher/translation/french.ts index d0b1739af..a2f0fb890 100644 --- a/launcher/translation/french.ts +++ b/launcher/translation/french.ts @@ -24,65 +24,65 @@ Notre communauté - + Build Information Information de construction - + User data directory Dossier de donnée utilisateur + - - - + + Open Ouvrir - + Check for updates Rechercher des mises à jour - + Game version Version de jeu - + Log files directory Dossier de journalisation - + Data Directories Dossier de Données - + Game data directory Dossier de données du jeu - + Operating System Système d"exploitation - + Configuration files directory - + Dossier de fichiers de configuration - + Project homepage Page d"accueil du projet - + Report a bug Signaler un bug @@ -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 @@ -231,254 +207,246 @@ Inactive Inactifs - - Download && refresh repositories - Télécharger et rafraîchir les dépôts - Reload repositories - + Recharger les dossiers - + Description Description - + Changelog Journal - + Screenshots Impressions écran - + %p% (%v KB out of %m KB) %p% (%v Ko sur %m Ko) - Install from file - Installer depuis un fichier + Installer depuis un fichier - + Uninstall Désinstaller - + Enable Activer - + Disable Désactiver - + Update Mettre à jour - + Install Installer - + Abort 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 + Tous les fichiers supportés - Maps - Cartes + Cartes - Campaigns - Campagnes + Campagnes - Configs - Configurations + Configurations - Mods - Mods + Mods - - Select files (configs, mods, maps, campaigns) to install... - Sélectionner les fichiers à installer (configurations, mods, cartes, campagnes)... - - - Replace config file? - Remplacer le fichier de configuration? + Remplacer le fichier de configuration ? - Do you want to replace %1? - Voulez vous remplacer %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é - Downloading %s%. %p% (%v MB out of %m MB) finished - Téléchargement %s%. %p% (%v MB sur %m MB) terminé - - - + Download failed Téléchargement échoué - + Unable to download all files. Encountered errors: @@ -491,7 +459,7 @@ Erreur rencontrées: - + Install successfully downloaded? @@ -500,34 +468,39 @@ Install successfully downloaded? Installer les téchargements réussis? - + + Installing Heroes 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 @@ -535,268 +508,332 @@ 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 - Mod Repositories - Dépôts de Mod - - - + On 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 - - Software Cursor + + 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 + + + + + Select a display mode for the game + +Windowed - the game will run inside a window that covers part of your screen. + +Borderless Windowed Mode - the game will run in a full-screen window, matching your screen's resolution. + +Fullscreen Exclusive Mode - the game will cover the entirety of your screen and will use selected resolution. + + + + + 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 + - Online Lobby address - - - - - Upscaling Filter - - - - - Use Relative Pointer Mode - - - - - Nearest - - - - - Linear - - - - - Best (Linear) - - - - - Input - Touchscreen - - - - - Network - - - - - Show Tutorial again - - - - Reset - + Réinitialiser - + Audio - + Audio - + Relative Pointer Speed - - - - - Music Volume - - - - - Ignore SSL errors - - - - - Input - Mouse - - - - - Long Touch Duration - - - - - % - + 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 @@ -804,7 +841,7 @@ 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. - Sélectionnez le mode d"affichage pour le jeu + Sélectionnez le mode d"affichage pour le jeu Fenêtré - le jeu s"exécutera à l"intérieur d"une fenêtre qui couvre une partie de votre écran @@ -813,217 +850,240 @@ 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 - Cursor - Curseur - - - Heroes III Data Language - Langue des Données de Heroes III - - - + Framerate Limit Limite de fréquence d"images - Hardware - Matériel - - - Software - Logiciel - - - + 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 - Friendly AI in battles - IA amicale dans les batailles - - - + 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 + + ChroniclesExtractor + + + + Invalid file selected + Fichier sélectionné non valide + + + + The file cannot be opened + + + + + You have to select a gog installer file! + + + + + You have to select a Heroes Chronicles installer file! + + + + + Extracting error! + Erreur d'extraction ! + + + + Hash error! + + + + + + + Heroes Chronicles + + + + + Heroes Chronicles %1 - %2 + + + 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 @@ -1049,68 +1109,78 @@ Mode exclusif plein écran - le jeu couvrira l"intégralité de votre écra Sélectionner votre langue - + Have a question? Found a bug? Want to help? Join us! Avez-vous une question ? Avez-vous trouvé un bogue ? Besoin d'aide ? Rejoignez-nous ! - + + Thank you for installing VCMI! + +Before you can start playing, there are a few more steps to complete. + +Please remember that 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! + + + + Locate Heroes III data files Localiser les fichiers de données de Heroes III - + Use offline installer from gog.com - + Utiliser l'installeur hors ligne depuis gog.com - 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 - + Vous pouvez copier manuellement les dossiers de Maps, Data et Mp3 depuis le dossier de jeu d'origine vers le dossier data VCMI que vous pouvez voir en haut de cette page - + Install gog.com files Installer les fichier de GOG.com - + Manual Installation - + Installation Manuelle - + Installing... %p% - + Installation... %p% - + If you already have Heroes III files on your device, you can select this directory and VCMI will copy the existing data automatically. - + Si vous avez déjà les fichiers Heroes III sur votre appareil, vous pouvez sélectionner ce dossier et VCMI copiera automatiquement les données existantes. - + Copy existing files - + Copier les fichiers existants - + Your Heroes III data files have been successfully found. Vos fichiers de données de Heroes III ont été trouvés. - 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. - + Si vous possédez Heroes III sur gog.com, vous pouvez télécharger le programme d'installation hors ligne de sauvegarde depuis gog.com, et VCMI importera les données de Heroes III à l'aide du programme d'installation hors ligne. +Le programme d'installation hors ligne se compose de deux parties, .exe et .bin. Assurez-vous de les télécharger tous les deux. - + Install a translation of Heroes III in your preferred language Installer une traduction de Heroes III dans la langue de votre choix - + Finish Terminer @@ -1120,12 +1190,11 @@ Offline installer consists of two parts, .exe and .bin. Make sure you download b VCMI sur Github - + VCMI on Discord VCMI sur Discord - Thank you for installing VCMI! Before you can start playing, there are a few more steps that need to be completed. @@ -1133,7 +1202,7 @@ 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! - Merci d"avoir installé VCMI ! + Merci d"avoir installé VCMI ! Avant de pouvoir commencer à jouer, il reste quelques étapes à franchir. @@ -1142,215 +1211,215 @@ Veuillez garder à l"esprit que pour utiliser VCMI, vous devez posséder le Heroes® of Might and Magic® III HD n"est actuellement pas pris en charge ! - - + + Next Suivant - + Search again Chercher de nouveau - If you don't have a copy of Heroes III installed, VCMI can import your Heroes III data using the offline installer from gog.com. - Si vous n'avez pas de copie de Heroes III installée, VCMI peut importer vos données Heroes III en utilisant l'installeur hors-ligne de gog.com. - - - To run VCMI, Heroes III data files need to be present in one of the specified locations. Please copy the Heroes III data to one of these directories. - Pour exécuter VCMI, les fichiers de données Heroes III doivent être présents dans l"un des emplacements spécifiés. Veuillez copier les données de Heroes III dans l"un de ces dossiers. - - - + Heroes III data files Fichiers de données de Heroes III - Alternatively, you can provide the directory where Heroes III data is installed and VCMI will copy the existing data automatically. - Alternativement, vous pouvez fournir le répertoire où les données Heroes III sont installées et VCMI copiera automatiquement les données existantes. - - - + Copy existing data Copier les données existantes - Your Heroes III language has been successfully detected. - Votre langue de Heroes III a été détectée avec succès. + + You can manually copy directories Maps, Data, and Mp3 from the original game directory to the VCMI data directory that you can see on top of this page + - The automatic detection of the Heroes III language has failed. Please select the language of your Heroes III manually - La détection automatique de la langue de Heroes III a échoué. Veuillez sélectionner la langue de votre Heroes III manuellement + + If you own Heroes III on gog.com, you can download a backup offline installer from gog.com. VCMI will then import Heroes III data using the offline installer. +Offline installer consists of two files: ".exe" and ".bin" - you must download both. + - Heroes III language - Langue de Heroes III - - - - + + Back Retour - + Install VCMI Mod Preset Installer le Préréglage du Mod VCMI - + Horn of the Abyss Horn of the Abyss - + Heroes III Translation Traduction de Heroes III - + Interface Improvements Améliorations de l'interface - + + Install mod that provides various interface improvements, such as a better interface for random maps and selectable actions in battles + + + + In The Wake of Gods In The Wake of Gods - + Optionally, you can install additional mods either now, or at any point later, using the VCMI Launcher En option, vous pouvez installer des mods supplémentaires soit maintenant, soit à tout moment plus tard, à l"aide du lanceur VCMI - Install mod that provides various interface improvements, such as better interface for random maps and selectable actions in battles - Installer le mod qui fournit diverses améliorations d'interface, telles qu'une meilleure interface pour les cartes aléatoires et des actions sélectionnables dans les batailles + Installer le mod qui fournit diverses améliorations d'interface, telles qu'une meilleure interface pour les cartes aléatoires et des actions sélectionnables dans les batailles - + Install compatible version of "Horn of the Abyss", a fan-made Heroes III expansion ported by the VCMI team Installer une version compatible de "Horn of the Abyss", une extension Heroes III conçue par des fans et portée par l"équipe VCMI - + Install compatible version of "In The Wake of Gods", a fan-made Heroes III expansion Installer une version compatible de "In The Wake of Gods", une extension Heroes III créée par des fans - + Heroes III installation found! Installation de Heroes III trouvée! - + Copy data to VCMI folder? Copier le dossier data dans le dossier VCMI ? - + Select %1 file... param is file extension Sélectionner le fichier %1... - + You have to select %1 file! param is file extension Vous avez sélectionné le fichier %1 ! - + GOG file (*.*) Fichier GOG (*.*) - + File selection Sélection de fichier - - File cannot opened - + + File cannot be opened + Le fichier ne peut pas être ouvert - + Invalid file selected Fichier sélectionné non valide - + GOG installer Installateur GOG - + + Heroes III: HD Edition files are not supported by VCMI. +Please select the directory with Heroes III: Complete Edition or Heroes III: Shadow of Death. + + + + + Unknown or unsupported Heroes III version found. +Please select the directory with Heroes III: Complete Edition or Heroes III: Shadow of Death. + + + + GOG data Données GOG - Installing... Please wait! - Installation... Veuillez patienter! + + You've provided a GOG Galaxy installer! This file doesn't contain the game. Please download the offline backup game installer! + - + + Hash error! + + + + No Heroes III data! Pas de données Heroes III! - + Selected files do not contain Heroes III data! Les fichiers sélectionnés ne contiennent pas les données de Heroes III ! - - - - + + Failed to detect valid Heroes III data in chosen directory. +Please select the directory with installed Heroes III data. + + + + + + + Heroes III data not found! Données Heroes III introuvables ! - Failed to detect valid Heroes III data in chosen directory. Please select directory with installed Heroes III data. - Impossible de détecter des données Heroes III valides dans le répertoire choisi, + Impossible de détecter des données Heroes III valides dans le répertoire choisi, Veuillez selectionner un dossier ou les données de Heroes III sont présentes. - You've provided GOG Galaxy installer! This file doesn't contain the game. Please download the offline backup game installer! - + Vous avez fourni le programme d'installation de GOG Galaxy ! Ce fichier ne contient pas le jeu. Veuillez télécharger le programme d'installation de sauvegarde hors ligne du jeu ! - - Stream error while extracting files! -error reason: - - - - - Not a supported Inno Setup installer! - - - - + Extracting error! - + Erreur d'extraction ! - Heroes III: HD Edition files are not supported by VCMI. Please select directory with Heroes III: Complete Edition or Heroes III: Shadow of Death. - Les fichiers de Heroes III HD Edition ne sont pas supportés par VCMI. + Les fichiers de Heroes III HD Edition ne sont pas supportés par VCMI. Veuillez sélectionner un dossier contenant les données de Heroes III: Complete Edition ou 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. - Version inconnue ou non supportée de Heroes III. + Version inconnue ou non supportée de Heroes III. Veuillez sélectionner un dossier contenant les données de Heroes III: Complete Edition ou Heroes III: Shadow of Death. @@ -1362,6 +1431,80 @@ Veuillez sélectionner un dossier contenant les données de Heroes III: Complete Afficheur d'Image + + Innoextract + + + Stream error while extracting files! +error reason: + Erreur de flux lors de l'extraction des fichiers ! +Raison de l'erreur : + + + + Not a supported Inno Setup installer! + Programme d’installation Inno Setup non pris en charge ! + + + + VCMI was compiled without innoextract support, which is needed to extract exe files! + + + + + SHA1 hash of provided files: +Exe (%1 bytes): +%2 + + + + + +Bin (%1 bytes): +%2 + + + + + Internal copy process failed. Enough space on device? + +%1 + + + + + Exe + + + + + Bin + + + + + Language mismatch! +%1 + +%2 + + + + + Only one file known! Maybe files are corrupted? Please download again. +%1 + +%2 + + + + + Unknown files! Maybe files are corrupted? Please download again. + +%1 + + + Language @@ -1449,18 +1592,6 @@ Veuillez sélectionner un dossier contenant les données de Heroes III: Complete Vietnamese Vietnamien - - Other (East European) - Autre (Europe de l'Est) - - - Other (Cyrillic Script) - Autre (Alphabet Cyrillique) - - - Other (West European) - Autre (Europe de l'Ouest) - Auto (%1) @@ -1490,44 +1621,519 @@ Veuillez sélectionner un dossier contenant les données de Heroes III: Complete Aide - - Map Editor - Éditeur de carte + + Game + + + + Map Editor + Éditeur de carte - Start game - Démarrer une partie + Démarrer une partie + + + + Replace config file? + Remplacer le fichier de configuration ? + + + + Do you want to replace %1? + Voulez vous remplacer %1 ? ModFields - + Name Nom - + Type Type + + + ModStateController - Version - Version + + 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 + + + + Mod is not compatible, please update VCMI and check the latest mod revisions + + + + + 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 a protected directory, please remove it manually: + + + + + 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 - + Error starting executable + Erreur lors du démarrage de l'exécutable + + + + Failed to start %1 +Reason: %2 + Échec de démarrage %1 +Raison : %2 + + + + StartGameTab + + + Form - - Failed to start %1 -Reason: %2 + + Import from Clipboard + + + + + Rename Current Preset + + + + + Current Preset + + + + + Create New Preset + + + + + Export to Clipboard + + + + + Delete Current Preset + + + + + Unsupported or corrupted game data detected! + + + + + + + + + + + + + ? + + + + + Install Translation + + + + + No soundtrack detected! + + + + + Armaggedon's Blade campaigns are missing! + + + + + No video files detected! + + + + + Activate Translation + + + + + Import files + + + + + Check For Updates + + + + + Go to Downloads Page + + + + + Go to Changelog Page + + + + + You are using the latest version + + + + + Game Data Files + + + + + Mod Preset + + + + + Resume + + + + + Play + + + + + Editor + + + + + Update %n mods + + + + + + + + Heroes Chronicles: +%n/%1 installed + + + + + + + + Update to %1 available + + + + + 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... + + + + + This option allows you to import additional data files into your VCMI installation. At the moment, following options are supported: + + - Heroes III Maps (.h3m or .vmap). + - Heroes III Campaigns (.h3c or .vcmp). + - Heroes III Chronicles using offline backup installer from GOG.com (.exe). + - VCMI mods in zip format (.zip) + - VCMI configuration files (.json) + + + + + + Your Heroes III version uses different language. VCMI provides translations of the game into various languages that you can use. Use this option to automatically install such translation to your language. + + + + + Translation of Heroes III into your language is installed, but has been turned off. Use this option to enable it. + + + + + A new version of some of the mods that you have installed is now available in mod repository. Use this option to automatically update all your mods to latest version. + +WARNING: In some cases, updated versions of mods may not be compatible with your existing saves. You many want to postpone mod update until you finish any of your ongoing games. + + + + + If you own Heroes Chronicles on gog.com, you can use offline backup installers provided by gog to import Heroes Chronicles data into VCMI as custom campaigns. +To import Heroes Chronicles, download offline backup installer of each chronicle that you wish to install, select 'Import files' option and select downloaded file. This will generate and install mod for VCMI that contains imported chronicles + + + + + VCMI has detected that Heroes III music files are missing from your installation. VCMI will run, but in-game music will not be available. + +To resolve this problem, please copy missing mp3 files from Heroes III to VCMI data files directory manually or reinstall VCMI and re-import Heroes III data files + + + + + VCMI has detected that Heroes III video files are missing from your installation. VCMI will run, but in-game cutscenes will not be available. + +To resolve this problem, please copy VIDEO.VID file from Heroes III to VCMI data files directory manually or reinstall VCMI and re-import Heroes III data files + + + + + VCMI has detected that some of Heroes III data files are missing from your installation. You may attempt to run VCMI, but game may not work as expected or crash. + +To resolve this problem, please reinstall game and reimport data files using supported version of Heroes III. VCMI requires Heroes III: Shadow of Death or Complete Edition to run, which you can get (for example) from gog.com + + + + + VCMI has detected that some of Heroes III: Armageddon's Blade data files are missing from your installation. VCMI will work, but Armageddon's Blade campaigns will not be available. + +To resolve this problem, please copy missing data files from Heroes III to VCMI data files directory manually or reinstall VCMI and re-import Heroes III data files + + + + + Enter preset name: + + + + + Rename preset '%1' to: @@ -1555,8 +2161,12 @@ Reason: %2 + Cannot read JSON from URL or incorrect JSON data + + + Cannot read JSON from url or incorrect JSON data - Impossible de lire les données JSON depuis l'URL ou données JSON érronées + Impossible de lire les données JSON depuis l'URL ou données JSON érronées diff --git a/launcher/translation/german.ts b/launcher/translation/german.ts index cefaff5ba..89bcf032a 100644 --- a/launcher/translation/german.ts +++ b/launcher/translation/german.ts @@ -24,65 +24,65 @@ Unsere Gemeinschaft - + Build Information Build-Informationen - + User data directory Verzeichnis der Benutzerdaten + - - - + + Open Öffnen - + Check for updates Nach Updates suchen - + Game version Spielversion - + Log files directory Verzeichnis der Log-Dateien - + Data Directories Daten-Verzeichnisse - + Game data directory Verzeichnis der Spiel-Dateien - + Operating System Betriebssystem - + Configuration files directory Verzeichnis der Konfiguarions-Dateien - + Project homepage Projekt-Homepage - + Report a bug Melde einen Fehler @@ -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 @@ -231,58 +207,53 @@ Inactive Inaktiv - - Download && refresh repositories - Repositories herunterladen && aktualisieren - - + Description Beschreibung - + Changelog Änderungslog - + Screenshots Screenshots - Install from file - Datei installieren + Datei installieren - + Uninstall Deinstallieren - + Enable Aktivieren - + Disable Deaktivieren - + Update Aktualisieren - + Install Installieren - + %p% (%v KB out of %m KB) %p% (%v КB von %m КB) @@ -292,188 +263,197 @@ Verzeichnis aktualisieren - + Abort 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. + Diese Mod kann nicht aktiviert werden, da sie in eine andere Sprache übersetzt wird. + + + + This mod can not be enabled because the following dependencies are not present + Diese Mod kann nicht aktiviert werden, da die folgenden Abhängigkeiten nicht vorhanden sind + + + + This mod can not be installed because the following dependencies are not present + Diese Mod kann nicht installiert werden, da die folgenden Abhängigkeiten nicht vorhanden sind + + + 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 + Alle unterstützten Dateien - Maps - Karten + Karten - Campaigns - Kampagnen + Kampagnen - Configs - Konfigurationen + Konfigurationen - Mods - Mods + Mods - - Select files (configs, mods, maps, campaigns) to install... - Wähle Dateien (Konfigurationen, Mods, Karten, Kampagnen) zum installieren... + Gog files + Gog-Dateien + + + 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? + Konfigurationsdatei ersetzen? - Do you want to replace %1? - Soll %1 ersetzt werden? + Soll %1 ersetzt werden? - + Downloading %1. %p% (%v MB out of %m MB) finished Downloade %1. %p% (%v MB von %m MB) abgeschlossen - Downloading %s%. %p% (%v MB out of %m MB) finished - Herunterladen von %s%. %p% (%v MB von %m MB) beendet - - - + Download failed Download fehlgeschlagen - + Unable to download all files. Encountered errors: @@ -486,7 +466,7 @@ Es sind Fehler aufgetreten: - + Install successfully downloaded? @@ -495,34 +475,39 @@ Install successfully downloaded? Installation erfolgreich heruntergeladen? - + + Installing Heroes Chronicles + Installation der Heroes 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 @@ -530,355 +515,413 @@ 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 - Mod Repositories - Mod-Repositorien - - - + 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 + + + + Select a display mode for the game + +Windowed - the game will run inside a window that covers part of your screen. + +Borderless Windowed Mode - the game will run in a full-screen window, matching your screen's resolution. + +Fullscreen Exclusive Mode - the game will cover the entirety of your screen and will use selected resolution. + Wähle einen Anzeigemodus für das Spiel + +Fenstermodus - das Spiel läuft in einem Fenster, das einen Teil des Bildschirms bedeckt. + +Randloser Fenstermodus - das Spiel läuft in einem Vollbildfenster, das der Auflösung Ihres Bildschirms entspricht. + +Exklusiver Vollbildmodus - das Spiel nimmt den gesamten Bildschirm ein und verwendet die gewählte Auflösung. + + + + Font Scaling (experimental) + Schriftskalierung (experimentell) + + + + Original + Original + + + Upscaling Filter Hochskalierungsfilter - + + Basic + Grundlegend + + + Use Relative Pointer Mode Relativen Zeigermodus verwenden - + Nearest Nearest - + Linear Linear - - Best (Linear) - Bester (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) - Friendly AI in battles - Freundliche KI in Kämpfen - - - + 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 - + On An - Cursor - Zeiger - - - Heroes III Data Language - Sprache der Heroes III Daten - - - Select display mode for game Windowed - game will run inside a window that covers part of your screen @@ -886,7 +929,7 @@ 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. - Anzeigemodus für das Spiel wählen + Anzeigemodus für das Spiel wählen Fenstermodus - das Spiel läuft in einem Fenster, das einen Teil des Bildschirms bedeckt @@ -895,130 +938,177 @@ 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 - Hardware - Hardware - - - Software - Software - - - + 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 + + ChroniclesExtractor + + File cannot opened + Datei kann nicht geöffnet werden + + + + + Invalid file selected + 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! + + + + The file cannot be opened + Die Datei kann nicht geöffnet werden + + + + You have to select a gog installer file! + Es muss eine Gog-Installer-Datei ausgewählt werden! + + + + You have to select a Heroes Chronicles installer file! + Es muss eine Heroes Chronicles-Installationsdatei ausgewählt werden! + + + + Extracting error! + Fehler beim Extrahieren! + + + + Hash error! + Hash-Fehler! + + + + + + Heroes Chronicles + Heroes Chronicles + + + + Heroes Chronicles %1 - %2 + Heroes Chronicles %1 - %2 + + 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 @@ -1044,12 +1134,11 @@ Exklusiver Vollbildmodus - das Spiel bedeckt den gesamten Bildschirm und verwend Sprache auswählen - + Have a question? Found a bug? Want to help? Join us! Haben Sie eine Frage? Einen Fehler gefunden? Möchten Sie helfen? Machen Sie mit! - Thank you for installing VCMI! Before you can start playing, there are a few more steps that need to be completed. @@ -1057,7 +1146,7 @@ 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! - Vielen Dank für die Installation von VCMI! + Vielen Dank für die Installation von VCMI! Es sind noch ein paar Schritte notwendig, bevor Sie mit dem Spielen beginnen können. @@ -1066,96 +1155,86 @@ Denken Sie daran, dass Sie die Originaldateien, Heroes III: Complete Edition ode Heroes III: HD Edition wird derzeit nicht unterstützt! - + Locate Heroes III data files Heroes III Daten suchen - + Use offline installer from gog.com Offline-Installer von gog.com verwenden - 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 - Es können die Verzeichnisse Maps, Data und Mp3 manuell aus dem ursprünglichen Spielverzeichnis in das VCMI-Datenverzeichnis kopiert werden, das oben auf dieser Seite gesehen werden kann + Es können die Verzeichnisse Maps, Data und Mp3 manuell aus dem ursprünglichen Spielverzeichnis in das VCMI-Datenverzeichnis kopiert werden, das oben auf dieser Seite gesehen werden kann - + Install gog.com files gog.com Dateien installieren - To run VCMI, Heroes III data files need to be present in one of the specified locations. Please copy the Heroes III data to one of these directories. - VCMI benötigt Heroes III Daten in einem der oben aufgeführten Verzeichnisse. Bitte kopieren Sie die Heroes III-Daten in eines dieser Verzeichnisse. - - - Alternatively, you can provide the directory where Heroes III data is installed and VCMI will copy the existing data automatically. - Alternativ können Sie ein Verzeichnis mit installierten Heroes III-Daten auswählen, und VCMI wird die vorhandenen Daten automatisch kopieren. - - - + Your Heroes III data files have been successfully found. Ihre Heroes III-Datendateien wurden erfolgreich gefunden. - The automatic detection of the Heroes III language has failed. Please select the language of your Heroes III manually - Automatische Erkennung der Sprache fehlgeschlagen. Bitte wählen Sie die Sprache Ihrer Heroes III Kopie - - - + Interface Improvements Interface-Verbesserungen - + Install a translation of Heroes III in your preferred language Übersetzung von Heroes III für Ihre Sprache installieren - + Installing... %p% Installation... %p% - + If you already have Heroes III files on your device, you can select this directory and VCMI will copy the existing data automatically. Wenn bereits Heroes III-Dateien auf Ihrem Gerät sind, kann dieses Verzeichnis auswählt werden und VCMI wird die vorhandenen Daten automatisch kopieren. - + Copy existing files Vorhandene Dateien kopieren - 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. - Wenn Sie Heroes III auf gog.com besitzen, können Sie den Backup-Offline-Installer von gog.com herunterladen, und VCMI wird die Daten von Heroes III mit dem Offline-Installer importieren. + Wenn Sie Heroes III auf gog.com besitzen, können Sie den Backup-Offline-Installer von gog.com herunterladen, und VCMI wird die Daten von Heroes III mit dem Offline-Installer importieren. Der Offline-Installer besteht aus zwei Teilen, .exe und .bin. Stellen Sie sicher, dass Sie beide Teile herunterladen. - + Optionally, you can install additional mods either now, or at any point later, using the VCMI Launcher Optional können Sie jetzt oder zu einem beliebigen späteren Zeitpunkt zusätzliche Mods installieren - Install mod that provides various interface improvements, such as better interface for random maps and selectable actions in battles - Installiere Mod, die verschiedene Interface-Verbesserungen bietet, wie z.B. ein besseres Interface für zufällige Karten und wählbare Aktionen in Kämpfen + Installiere Mod, die verschiedene Interface-Verbesserungen bietet, wie z.B. ein besseres Interface für zufällige Karten und wählbare Aktionen in Kämpfen - + Install compatible version of "Horn of the Abyss", a fan-made Heroes III expansion ported by the VCMI team Installieren Sie die kompatible Version des Addons Horn of the Abyss: eine von Fans entwickelte Heroes III-Erweiterung, portiert vom VCMI-Team - + Install compatible version of "In The Wake of Gods", a fan-made Heroes III expansion Installieren Sie die kompatible Version des Addons "In The Wake of Gods": von Fans entwickelte Heroes III-Erweiterung - + + Install mod that provides various interface improvements, such as a better interface for random maps and selectable actions in battles + Installiere Mod, die verschiedene Interface-Verbesserungen bietet, wie z.B. ein besseres Interface für zufällige Karten und wählbare Aktionen in Kämpfen + + + Finish Fertigstellen @@ -1165,189 +1244,217 @@ Der Offline-Installer besteht aus zwei Teilen, .exe und .bin. Stellen Sie sicher VCMI auf Github - + VCMI on Discord VCMI auf Discord - - + + Thank you for installing VCMI! + +Before you can start playing, there are a few more steps to complete. + +Please remember that 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! + Vielen Dank für die Installation von VCMI! + +Bevor Ihr mit dem Spielen beginnen könnt, sind noch ein paar Schritte zu erledigen. + +Bitte denkt daran, dass ihr die Originaldateien von Heroes® of Might and Magic® III: Complete oder The Shadow of Death besitzen müsst, um VCMI nutzen zu können. + +Heroes® of Might and Magic® III HD wird derzeit nicht unterstützt! + + + + Next Weiter - + + You can manually copy directories Maps, Data, and Mp3 from the original game directory to the VCMI data directory that you can see on top of this page + Es ist möglich, die Verzeichnisse Maps, Data und Mp3 manuell aus dem ursprünglichen Spielverzeichnis in das VCMI-Datenverzeichnis zu kopieren, das oben auf dieser Seite zu sehen ist + + + Manual Installation Manuelle Installation - + Search again Erneut suchen - If you don't have a copy of Heroes III installed, VCMI can import your Heroes III data using the offline installer from gog.com. - Wenn Sie kein Exemplar von Heroes III installiert haben, kann VCMI Ihre Heroes III-Daten mit dem Offline-Installationsprogramm von gog.com importieren. - - - + Heroes III data files Heroes III Dateien - + Copy existing data Vorhandene Daten kopieren - Your Heroes III language has been successfully detected. - Ihre Heroes III-Sprache wurde erfolgreich erkannt. + + If you own Heroes III on gog.com, you can download a backup offline installer from gog.com. VCMI will then import Heroes III data using the offline installer. +Offline installer consists of two files: ".exe" and ".bin" - you must download both. + Wenn Sie Heroes III auf gog.com besitzen, können Sie einen Backup-Offline-Installer von gog.com herunterladen. VCMI importiert dann die Daten von Heroes III mit Hilfe des Offline-Installers. +Der Offline-Installer besteht aus zwei Dateien: „.exe“ und ‚.bin‘ - Sie müssen beide herunterladen. - Heroes III language - Heroes III Sprache - - - - + + Back Zurück - + Install VCMI Mod Preset VCMI Mod Voreinstellung installieren - + Horn of the Abyss Horn of the Abyss - + Heroes III Translation Heroes III Übersetzung - + In The Wake of Gods In The Wake of Gods - + Heroes III installation found! Heroes III-Installation gefunden! - + Copy data to VCMI folder? Daten in den VCMI-Ordner kopieren? - + Select %1 file... param is file extension %1 Datei auswählen... - + You have to select %1 file! param is file extension Sie müssen %1 Datei auswählen! - + GOG file (*.*) GOG Datei (*.*) - + File selection Dateiauswahl - - File cannot opened + + File cannot be opened Datei kann nicht geöffnet werden - + Invalid file selected Ungültige Datei ausgewählt - + GOG installer GOG-Installationsprogramm - + + Heroes III: HD Edition files are not supported by VCMI. +Please select the directory with Heroes III: Complete Edition or Heroes III: Shadow of Death. + Heroes III: HD Edition Dateien werden von VCMI nicht unterstützt. +Bitte wählt das Verzeichnis mit Heroes III: Complete Edition oder Heroes III: Shadow of Death. + + + + Unknown or unsupported Heroes III version found. +Please select the directory with Heroes III: Complete Edition or Heroes III: Shadow of Death. + Unbekannte oder nicht unterstützte Heroes III-Version gefunden. +Bitte wählt das Verzeichnis mit Heroes III: Complete Edition oder Heroes III: Shadow of Death. + + + GOG data GOG-Datendatei - Installing... Please wait! - Installiert... Bitte warten! + + You've provided a GOG Galaxy installer! This file doesn't contain the game. Please download the offline backup game installer! + Ein GOG Galaxy-Installationsprogramm wurde bereitgestellt! Diese Datei enthält nicht das Spiel. Bitte ladet den Offline-Backup-Installer für das Spiel herunter! - + + Hash error! + Hash-Fehler! + + + No Heroes III data! Keine Heroes III-Daten! - + Selected files do not contain Heroes III data! Die ausgewählten Dateien enthalten keine Heroes III-Daten! - - - - + + Failed to detect valid Heroes III data in chosen directory. +Please select the directory with installed Heroes III data. + Es konnten keine gültigen Heroes III-Daten im gewählten Verzeichnis gefunden werden. +Bitte wählt das Verzeichnis mit installierten Heroes III-Daten. + + + + + + Heroes III data not found! Heroes III Daten nicht gefunden! - Failed to detect valid Heroes III data in chosen directory. Please select directory with installed Heroes III data. - Es konnten keine gültigen Heroes III-Daten im gewählten Verzeichnis gefunden werden. + Es konnten keine gültigen Heroes III-Daten im gewählten Verzeichnis gefunden werden. Bitte wählen Sie ein Verzeichnis mit installierten Heroes III-Daten. - You've provided GOG Galaxy installer! This file doesn't contain the game. Please download the offline backup game installer! - Es wurde der GOG Galaxy-Installer ausgewählt! Das Spiel ist in dieser Datei nicht enthalten. Lade den Offline-Backup-Installer für das Spiel herunter! + Es wurde der GOG Galaxy-Installer ausgewählt! Das Spiel ist in dieser Datei nicht enthalten. Lade den Offline-Backup-Installer für das Spiel herunter! - - Stream error while extracting files! -error reason: - Stream-Fehler beim Extrahieren von Dateien! -Fehlerursache: - - - - Not a supported Inno Setup installer! - Kein unterstütztes Inno Setup Installationsprogramm! - - - + Extracting error! Fehler beim Extrahieren! - Heroes III: HD Edition files are not supported by VCMI. Please select directory with Heroes III: Complete Edition or Heroes III: Shadow of Death. - Heroes III: HD Edition Dateien werden von VCMI nicht unterstützt. + Heroes III: HD Edition Dateien werden von VCMI nicht unterstützt. Bitte wählen Sie ein Verzeichnis mit Heroes III: Complete Edition oder 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. - Unbekannte oder nicht unterstützte Heroes III-Version gefunden. + Unbekannte oder nicht unterstützte Heroes III-Version gefunden. Bitte wählen Sie ein Verzeichnis mit Heroes III: Complete Edition oder Heroes III: Shadow of Death. @@ -1359,6 +1466,94 @@ Bitte wählen Sie ein Verzeichnis mit Heroes III: Complete Edition oder Heroes I Bildbetrachter + + Innoextract + + + Stream error while extracting files! +error reason: + Stream-Fehler beim Extrahieren von Dateien! +Fehlerursache: + + + + Not a supported Inno Setup installer! + 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! + + + + SHA1 hash of provided files: +Exe (%1 bytes): +%2 + SHA1-Hash der bereitgestellten Dateien: +Exe (%1 Bytes): +%2 + + + + +Bin (%1 bytes): +%2 + +Bin (%1 Bytes): +%2 + + + + Internal copy process failed. Enough space on device? + +%1 + Interner Kopiervorgang fehlgeschlagen. Genügend Platz auf dem Gerät? + +%1 + + + + Exe + Exe + + + + Bin + Bin + + + + Language mismatch! +%1 + +%2 + Die Sprache stimmt nicht überein! +%1 + +%2 + + + + Only one file known! Maybe files are corrupted? Please download again. +%1 + +%2 + Nur eine Datei bekannt! Vielleicht sind die Dateien beschädigt? Bitte erneut herunterladen. +%1 + +%2 + + + + Unknown files! Maybe files are corrupted? Please download again. + +%1 + Unbekannte Dateien! Vielleicht sind die Dateien beschädigt? Bitte erneut herunterladen. + +%1 + + Language @@ -1446,18 +1641,6 @@ Bitte wählen Sie ein Verzeichnis mit Heroes III: Complete Edition oder Heroes I Vietnamese Vietnamesisch - - Other (East European) - Sonstige (osteuropäisch) - - - Other (Cyrillic Script) - Sonstige (kyrillische Schrift) - - - Other (West European) - Sonstige (westeuropäisch) - Auto (%1) @@ -1487,48 +1670,543 @@ Bitte wählen Sie ein Verzeichnis mit Heroes III: Complete Edition oder Heroes I Hilfe - - Map Editor - Karteneditor + + Game + Spiel + + + Map Editor + Karteneditor - Start game - Spiel starten + Spiel starten + + + + Replace config file? + Konfigurationsdatei ersetzen? + + + + Do you want to replace %1? + Soll %1 ersetzt werden? ModFields - + Name Name - + Type Typ + + + ModStateController - Version - Version + + 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 + + + + Mod is not compatible, please update VCMI and check the latest mod revisions + Mod ist nicht kompatibel, bitte VCMI aktualisieren und die letzten Mod-Versionen überprüfen + + + + Can not enable translation mod for a different language! + Der Übersetzungsmod kann nicht für eine andere Sprache aktiviert werden! + + + + 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 a protected directory, please remove it manually: + + Mod befindet sich in einem geschützten Verzeichnis, bitte manuell entfernen: + + + + 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 - + Error starting executable Fehler beim Starten der ausführbaren Datei - + Failed to start %1 Reason: %2 Start von %1 fehlgeschlagen Grund: %2 + + StartGameTab + + + Form + Formular + + + + Import from Clipboard + Aus Zwischenablage + + + + Rename Current Preset + Voreinstellung umbenennen + + + + Current Preset + Voreinstellung + + + + Create New Preset + Voreinstellung erstellen + + + + Export to Clipboard + In Zwischenablage + + + + Delete Current Preset + Voreinstellung löschen + + + + Unsupported or corrupted game data detected! + Nicht unterstützte oder beschädigte Spieldaten entdeckt! + + + + + + + + + + + + ? + ? + + + + Install Translation + Übersetzung installieren + + + + No soundtrack detected! + Keine Tonspur entdeckt! + + + + Armaggedon's Blade campaigns are missing! + Die Armaggedon's Blade-Kampagnen fehlen! + + + + No video files detected! + Keine Videodateien gefunden! + + + + Activate Translation + Übersetzung aktivieren + + + + Import files + Dateien importieren + + + + Check For Updates + Nach Updates suchen + + + + Go to Downloads Page + Zur Download-Seite gehen + + + + Go to Changelog Page + Zur Changelog-Seite gehen + + + + You are using the latest version + Sie verwenden die neueste Version + + + + Game Data Files + Spieldateien + + + + Mod Preset + Mod-Voreinstellung + + + + Resume + Fortsetzen + + + + Play + Spielen + + + + Editor + Editor + + + + Update %n mods + + %n Mod aktualisieren + %n Mods aktualisieren + + + + + Heroes Chronicles: +%n/%1 installed + + Heroes Chronicles: +%n/%1 installiert + Heroes Chronicles: +%n/%1 installiert + + + + + Update to %1 available + Update auf %1 verfügbar + + + + All supported files + Alle unterstützten Dateien + + + + Maps + Karten + + + + Campaigns + Kampagnen + + + + Configs + Konfigurationen + + + + Mods + Mods + + + + Gog files + Gog-Dateien + + + + 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)... + + + + This option allows you to import additional data files into your VCMI installation. At the moment, following options are supported: + + - Heroes III Maps (.h3m or .vmap). + - Heroes III Campaigns (.h3c or .vcmp). + - Heroes III Chronicles using offline backup installer from GOG.com (.exe). + - VCMI mods in zip format (.zip) + - VCMI configuration files (.json) + + Diese Option ermöglicht es zusätzliche Dateien in Ihre VCMI-Installation zu importieren. Zur Zeit werden folgende Optionen unterstützt: + + - Heroes III Karten (.h3m oder .vmap). + - Heroes III Kampagnen (.h3c oder .vcmp). + - Heroes III Chronicles mit dem Offline-Backup-Installer von GOG.com (.exe). + - VCMI-Mods im Zip-Format (.zip) + - VCMI-Konfigurationsdateien (.json) + + + + + Your Heroes III version uses different language. VCMI provides translations of the game into various languages that you can use. Use this option to automatically install such translation to your language. + Ihre Heroes III-Version verwendet eine andere Sprache. VCMI bietet Übersetzungen des Spiels in verschiedene Sprachen an, die Sie verwenden können. Verwenden Sie diese Option, um diese Übersetzungen automatisch in Ihrer Sprache zu installieren. + + + + Translation of Heroes III into your language is installed, but has been turned off. Use this option to enable it. + Die Übersetzung von Heroes III in Ihre Sprache ist installiert, aber ausgeschaltet. Verwenden Sie diese Option, um sie zu aktivieren. + + + + A new version of some of the mods that you have installed is now available in mod repository. Use this option to automatically update all your mods to latest version. + +WARNING: In some cases, updated versions of mods may not be compatible with your existing saves. You many want to postpone mod update until you finish any of your ongoing games. + Eine neue Version von einigen der Mods, die Sie installiert haben, ist jetzt im Mod-Repository verfügbar. Verwenden Sie diese Option, um alle Ihre Mods automatisch auf die neueste Version zu aktualisieren. + +WARNUNG: In einigen Fällen sind die aktualisierten Versionen der Mods nicht mit Ihren bestehenden Spielständen kompatibel. Es kann sein, dass Sie das Mod-Update verschieben möchten, bis Sie eines Ihrer laufenden Spiele beendet haben. + + + + If you own Heroes Chronicles on gog.com, you can use offline backup installers provided by gog to import Heroes Chronicles data into VCMI as custom campaigns. +To import Heroes Chronicles, download offline backup installer of each chronicle that you wish to install, select 'Import files' option and select downloaded file. This will generate and install mod for VCMI that contains imported chronicles + Wenn Sie Heroes Chronicles auf gog.com besitzen, können Sie die von gog bereitgestellten Offline-Backup-Installer verwenden, um Heroes Chronicles-Daten als benutzerdefinierte Kampagnen in VCMI zu importieren. +Um Heroes Chronicles zu importieren, laden Sie den Offline-Backup-Installer für jede Chronicle herunter, die Sie installieren möchten, wählen Sie die Option „Dateien importieren“ und wählen Sie die heruntergeladene Datei. Dadurch wird ein Mod für VCMI generiert und installiert, der importierte Chronicle enthält + + + + VCMI has detected that Heroes III music files are missing from your installation. VCMI will run, but in-game music will not be available. + +To resolve this problem, please copy missing mp3 files from Heroes III to VCMI data files directory manually or reinstall VCMI and re-import Heroes III data files + VCMI hat festgestellt, dass Heroes III-Musikdateien in Ihrer Installation fehlen. VCMI wird laufen, aber die Musik im Spiel wird nicht verfügbar sein. + +Um dieses Problem zu beheben, kopieren Sie bitte die fehlenden mp3-Dateien von Heroes III manuell in das VCMI-Datenverzeichnis oder installieren Sie VCMI neu und importieren Sie die Heroes III-Dateien erneut + + + + VCMI has detected that Heroes III video files are missing from your installation. VCMI will run, but in-game cutscenes will not be available. + +To resolve this problem, please copy VIDEO.VID file from Heroes III to VCMI data files directory manually or reinstall VCMI and re-import Heroes III data files + VCMI hat festgestellt, dass Heroes III Videodateien in Ihrer Installation fehlen. VCMI läuft zwar, aber die Zwischensequenzen im Spiel sind nicht verfügbar. + +Um dieses Problem zu beheben, kopieren Sie bitte die Datei VIDEO.VID von Heroes III manuell in das VCMI-Datendateiverzeichnis oder installieren Sie VCMI neu und importieren Sie die Heroes III-Dateien erneut + + + + VCMI has detected that some of Heroes III data files are missing from your installation. You may attempt to run VCMI, but game may not work as expected or crash. + +To resolve this problem, please reinstall game and reimport data files using supported version of Heroes III. VCMI requires Heroes III: Shadow of Death or Complete Edition to run, which you can get (for example) from gog.com + VCMI hat festgestellt, dass einige der Heroes III Datendateien in Ihrer Installation fehlen. Sie können versuchen, VCMI zu starten, aber das Spiel funktioniert möglicherweise nicht wie erwartet oder stürzt ab. + +Um dieses Problem zu beheben, installieren Sie bitte das Spiel neu und importieren Sie die Dateien erneut mit einer unterstützten Version von Heroes III. VCMI benötigt Heroes III: Shadow of Death oder die Complete Edition, die Sie z.B. von gog.com beziehen können + + + + VCMI has detected that some of Heroes III: Armageddon's Blade data files are missing from your installation. VCMI will work, but Armageddon's Blade campaigns will not be available. + +To resolve this problem, please copy missing data files from Heroes III to VCMI data files directory manually or reinstall VCMI and re-import Heroes III data files + VCMI hat festgestellt, dass einige der Heroes III: Armageddon's Blade Datendateien in Ihrer Installation fehlen. VCMI wird funktionieren, aber Armageddon's Blade Kampagnen werden nicht verfügbar sein. + +Um dieses Problem zu beheben, kopieren Sie bitte die fehlenden Dateien aus Heroes III manuell in das VCMI-Datenverzeichnis oder installieren Sie VCMI neu und importieren Sie die Heroes III-Dateien erneut + + + + Enter preset name: + Namen der Voreinstellung eingeben: + + + + Rename preset '%1' to: + Voreinstellung '%1' umbenennen in: + + UpdateDialog @@ -1553,8 +2231,12 @@ Grund: %2 - Cannot read JSON from url or incorrect JSON data + Cannot read JSON from URL or incorrect JSON data JSON kann nicht von der URL gelesen werden oder die JSON-Daten sind falsch + + Cannot read JSON from url or incorrect JSON data + JSON kann nicht von der URL gelesen werden oder die JSON-Daten sind falsch + diff --git a/launcher/translation/polish.ts b/launcher/translation/polish.ts index d31ad97e8..83112dcce 100644 --- a/launcher/translation/polish.ts +++ b/launcher/translation/polish.ts @@ -24,65 +24,65 @@ Nasza społeczność - + Build Information Informacje o wersji - + User data directory Katalog danych użytkownika + - - - + + Open Otwórz - + Check for updates Sprawdź aktualizacje - + Game version Wersja gry - + Log files directory Katalog logów - + Data Directories Katalogi z danymi - + Game data directory Katalog danych gry - + Operating System System operacyjny - + Configuration files directory Katalog plików konfiguracji - + Project homepage Witryna projektu - + Report a bug Zgłoś błąd @@ -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 @@ -231,58 +207,53 @@ Inactive Nieaktywny - - Download && refresh repositories - Pobierz i odśwież repozytoria - - + Description Opis - + Changelog Lista zmian - + Screenshots Zrzuty ekranu - Install from file - Zainstaluj z pliku + Zainstaluj z pliku - + Uninstall Odinstaluj - + Enable Włącz - + Disable Wyłącz - + Update Zaktualizuj - + Install Zainstaluj - + %p% (%v KB out of %m KB) %p% (%v KB z %m KB) @@ -292,188 +263,197 @@ Odśwież repozytoria - + Abort 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. + Ten mod nie może zostać aktywowany, ponieważ wykorzystuje tłumaczenie na inny język + + + + This mod can not be enabled because the following dependencies are not present + Ten mod nie może zostać aktywowany, ponieważ następujące zależności nie są włączone + + + + This mod can not be installed because the following dependencies are not present + Ten mod nie może zostać zainstalowany, ponieważ następujące zależności nie są włączone + + + 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 + Wszystkie wspierane pliki - Maps - Mapy + Mapy - Campaigns - Kampanie + Kampanie - Configs - Konfiguracje + Konfiguracje - Mods - Mody + Mody - - Select files (configs, mods, maps, campaigns) to install... - Wybierz pliki (konfiguracyjne, mody, mapy, kampanie) do zainstalowania... + Gog files + Pliki Gog + + + All files (*.*) + Wszystkie pliki (*.*) + + + Select files (configs, mods, maps, campaigns, gog files) to install... + Zaznacz pliki (konfiguracje, mody, mapy, kampanie, pliki gog) do zainstalowania... - Replace config file? - Zastąpić plik konfiguracji? + Zastąpić plik konfiguracji? - Do you want to replace %1? - Czy chcesz zastąpić %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 - Downloading %s%. %p% (%v MB out of %m MB) finished - Pobieranie %s%. %p% (%v MB z %m MB) ukończono - - - + Download failed Pobieranie nieudane - + Unable to download all files. Encountered errors: @@ -486,7 +466,7 @@ Napotkane błędy: - + Install successfully downloaded? @@ -495,34 +475,39 @@ Install successfully downloaded? Zainstalować pomyślnie pobrane? - + + Installing Heroes Chronicles + Instalowanie Kronik + + + 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 @@ -530,355 +515,413 @@ 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 - Mod Repositories - Repozytoria modów - - - + 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 + Filtr wygładzający + + + 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) + Automatyczny (liniowy) + + + Haptic Feedback Wibracje - + Software Cursor Kursor programowy - + + + + Automatic + Automatyczny + + + + Mods Validation + Walidacja modów + + + + None + Żadne + + + + xBRZ x2 + xBRZ x2 + + + + xBRZ x3 + xBRZ x3 + + + + xBRZ x4 + xBRZ x4 + + + + Full + Maksymalny + + + + Use scalable fonts + Użyj skalowalnych czcionek + + + Online Lobby address Adres lobby online - + + Cursor Scaling + Skalowanie kursora + + + + Scalable + Skalowalne + + + + Miscellaneous + Różne + + + + Select a display mode for the game + +Windowed - the game will run inside a window that covers part of your screen. + +Borderless Windowed Mode - the game will run in a full-screen window, matching your screen's resolution. + +Fullscreen Exclusive Mode - the game will cover the entirety of your screen and will use selected resolution. + Wybierz tryb wyświetlania gry + + Okno - gra będzie działać w oknie, które zajmie część ekranu. + + Tryb okna bez ramek - gra będzie działać w pełnoekranowym oknie, dopasowanym do rozdzielczości ekranu. + + Tryb pełnoekranowy - gra zajmie cały ekran i będzie korzystać z wybranej rozdzielczości. + + + + Font Scaling (experimental) + Skalowanie czcionki (wersja testowa) + + + + Original + Oryginalne + + + Upscaling Filter Filtr wyostrzający - + + Basic + Podstawowy + + + Use Relative Pointer Mode Użyj relatywnego trybu kursora - + Nearest Najbliższych - + Linear Liniowy - - Best (Linear) - Najlepszy (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) - Friendly AI in battles - AI bitewne sojuszników - - - + 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 - + On Włączony - Cursor - Kursor - - - Heroes III Data Language - Język plików Heroes III - - - Select display mode for game Windowed - game will run inside a window that covers part of your screen @@ -886,7 +929,7 @@ 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. - Wybierz tryb wyświetlania dla gry + Wybierz tryb wyświetlania dla gry Okno - gra będzie funkcjonować w oknie przysłaniającym część ekranu @@ -895,130 +938,177 @@ 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 - Hardware - Sprzętowy - - - Software - Programowy - - - + 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 + + ChroniclesExtractor + + File cannot opened + Błąd otwarcia pliku + + + + + Invalid file selected + Wybrano nieprawidłowy plik + + + You have to select an gog installer file! + Należy wybrać instalator Gog + + + You have to select an chronicle installer file! + Należy wybrać instalator Kronik! + + + + The file cannot be opened + Plik nie może zostać otwarty + + + + You have to select a gog installer file! + Należy wybrać plik instalatora Gog! + + + + You have to select a Heroes Chronicles installer file! + Należy wybrać plik instalatora Kronik! + + + + Extracting error! + Błąd wypakowywania! + + + + Hash error! + Błąd sumy kontrolnej! + + + + + + Heroes Chronicles + Heroes Kroniki + + + + Heroes Chronicles %1 - %2 + Heroes Kroniki %1 - %2 + + 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 @@ -1044,12 +1134,11 @@ Pełny ekran klasyczny - gra przysłoni cały ekran uruchamiając się w wybrane Wybierz język - + Have a question? Found a bug? Want to help? Join us! Masz pytanie? Znalazłeś błąd? Chcesz pomóc? Dołącz do nas! - Thank you for installing VCMI! Before you can start playing, there are a few more steps that need to be completed. @@ -1057,7 +1146,7 @@ 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! - Dziękujemy za zainstalowanie VCMI. + Dziękujemy za zainstalowanie VCMI. Jest jeszcze kilka kroków, które trzeba wykonać żeby móc zagrać. @@ -1066,96 +1155,86 @@ Miej na uwadze, że aby używać VCMI potrzebne są pliki oryginalnej gry zawier Heroes III: HD Edition nie jest obecnie wspierane! - + Locate Heroes III data files Znajdź pliki Heroes III - + Use offline installer from gog.com Użyj instalatora offline z gog.com - 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 - Możesz ręcznie skopiować katalogi Maps, Data i Mp3 z oryginalnej gry do folderu danych VCMI pokazanego na górze tego widoku + Możesz ręcznie skopiować katalogi Maps, Data i Mp3 z oryginalnej gry do folderu danych VCMI pokazanego na górze tego widoku - + Install gog.com files Użyj instalatora z gog.com - To run VCMI, Heroes III data files need to be present in one of the specified locations. Please copy the Heroes III data to one of these directories. - VCMI wymaga plików Heroes III w jednej z wymienionych wyżej lokalizacji. Proszę, skopiuj pliki Heroes III do jednego z tych katalogów. - - - Alternatively, you can provide the directory where Heroes III data is installed and VCMI will copy the existing data automatically. - Możesz też wybrać folder z zainstalowanym Heroes III i VCMI automatycznie skopiuje istniejące dane. - - - + Your Heroes III data files have been successfully found. Twoje pliki Heroes III zostały pomyślnie znalezione. - The automatic detection of the Heroes III language has failed. Please select the language of your Heroes III manually - Automatyczna detekcja języka nie powiodła się. Proszę wybrać język twojego Heroes III - - - + Interface Improvements Ulepszenia interfejsu - + Install a translation of Heroes III in your preferred language Zainstaluj tłumaczenie Heroes III dla twojego preferowanego języka - + Installing... %p% Instalowanie... %p% - + If you already have Heroes III files on your device, you can select this directory and VCMI will copy the existing data automatically. Jeśli już posiadasz pliki Heroes III na swoim urządzeniu to możesz wybrać ich lokalizację i VCMI automatycznie skopiuje istniejące dane. - + Copy existing files Skopiuj istniejące pliki - 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. - Jeżeli posiadasz Heroes III na gog.com to możesz pobrać zapasowy instalator gry offline z gog.com i VCMI zaimportuje z niego dane Heroes III. + Jeżeli posiadasz Heroes III na gog.com to możesz pobrać zapasowy instalator gry offline z gog.com i VCMI zaimportuje z niego dane Heroes III. Instalator offline składa się z dwóch części, .exe i .bin. Upewnij się, że obydwie zostały pobrane. - + Optionally, you can install additional mods either now, or at any point later, using the VCMI Launcher Opcjonalnie możesz zainstalować dodatkowe modyfikacje teraz lub później - Install mod that provides various interface improvements, such as better interface for random maps and selectable actions in battles - Zainstaluj modyfikację, która dostarcza różne ulepszenia interfejsu takie jak lepszy ekran ustawień mapy losowej lub wybieralne akcje w bitwach + Zainstaluj modyfikację, która dostarcza różne ulepszenia interfejsu takie jak lepszy ekran ustawień mapy losowej lub wybieralne akcje w bitwach - + Install compatible version of "Horn of the Abyss", a fan-made Heroes III expansion ported by the VCMI team Zainstaluj kompatybilną wersję fanowskiego dodatku Horn of the Abyss odtworzoną przez zespół VCMI - + Install compatible version of "In The Wake of Gods", a fan-made Heroes III expansion Zainstaluj kompatybilną wersję fanowskiego dodatku "In The Wake Of Gods" - + + Install mod that provides various interface improvements, such as a better interface for random maps and selectable actions in battles + Zainstaluj mod, który wprowadza różne ulepszenia interfejsu, takie jak lepszy interfejs dla losowych map i możliwość wyboru akcji w bitwach + + + Finish Zakończ @@ -1165,189 +1244,218 @@ Instalator offline składa się z dwóch części, .exe i .bin. Upewnij się, ż VCMI na Github - + VCMI on Discord VCMI na Discordzie - - + + Thank you for installing VCMI! + +Before you can start playing, there are a few more steps to complete. + +Please remember that 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! + Dziękujemy za zainstalowanie VCMI! + + Zanim będziesz mógł rozpocząć grę, musisz wykonać jeszcze kilka kroków. + + Pamiętaj, że aby korzystać z VCMI, musisz posiadać oryginalne pliki gry Heroes® of Might and Magic® III: Complete lub The Shadow of Death. + + Heroes® of Might and Magic® III HD nie jest obecnie obsługiwane! + + + + Next Dalej - + + You can manually copy directories Maps, Data, and Mp3 from the original game directory to the VCMI data directory that you can see on top of this page + Możesz ręcznie skopiować katalogi Maps, Data i Mp3 z oryginalnego folderu gry do katalogu danych VCMI, który widzisz na górze tej strony. + + + + Manual Installation Ręczna instalacja - + Search again Szukaj ponownie - If you don't have a copy of Heroes III installed, VCMI can import your Heroes III data using the offline installer from gog.com. - Jeśli nie masz zainstalowanego Heroes III, VCMI może użyć danych z instalatora offline gog.com. - - - + Heroes III data files Pliki Heroes III - + Copy existing data Skopiuj istniejące dane - Your Heroes III language has been successfully detected. - Twój język Heroes III został pomyślnie wykryty. + + If you own Heroes III on gog.com, you can download a backup offline installer from gog.com. VCMI will then import Heroes III data using the offline installer. +Offline installer consists of two files: ".exe" and ".bin" - you must download both. + Jeśli posiadasz Heroes III na gog.com, możesz pobrać kopię zapasową instalatora offline z gog.com. VCMI zaimportuje dane z Heroes III, korzystając z tego instalatora offline. +Instalator offline składa się z dwóch plików: ".exe" i ".bin" - musisz pobrać oba. - Heroes III language - Język Heroes III - - - - + + Back Wstecz - + Install VCMI Mod Preset Zainstaluj zestaw modyfikacji - + Horn of the Abyss Horn of the Abyss - + Heroes III Translation Tłumaczenie Heroes III - + In The Wake of Gods In The Wake of Gods - + Heroes III installation found! Znaleziono zainstalowane Heroes III! - + Copy data to VCMI folder? Skopiować dane do folderu VCMI? - + Select %1 file... param is file extension Wybierz plik %1... - + You have to select %1 file! param is file extension Musisz wybrać plik %1! - + GOG file (*.*) Instalator GOG (*.*) - + File selection Wybór pliku - - File cannot opened + + File cannot be opened Nieudane otwarcie pliku - + Invalid file selected Wybrano nieprawidłowy plik - + GOG installer Instalator GOG - + + Heroes III: HD Edition files are not supported by VCMI. +Please select the directory with Heroes III: Complete Edition or Heroes III: Shadow of Death. + Pliki Heroes III: HD Edition nie są obsługiwane przez VCMI. +Wybierz katalog z Heroes III: Complete Edition lub Heroes III: Shadow of Death. + + + + Unknown or unsupported Heroes III version found. +Please select the directory with Heroes III: Complete Edition or Heroes III: Shadow of Death. + Nieznana lub nieobsługiwana wersja Heroes III została wykryta. +Wybierz katalog z Heroes III: Complete Edition lub Heroes III: Shadow of Death. + + + GOG data Dane GOG - Installing... Please wait! - Instalowanie... Proszę czekać! + + You've provided a GOG Galaxy installer! This file doesn't contain the game. Please download the offline backup game installer! + Podany plik jest instalatorem GOG Galaxy! Ten plik nie zawiera gry. Proszę pobrać zapasowy instalator offline gry! - + + Hash error! + Błąd sumy kontrolnej! + + + No Heroes III data! Brak danych Heroes III! - + Selected files do not contain Heroes III data! Wybrane pliki nie zawierają danych Heroes III! - - - - - Heroes III data not found! - Dane Heroes III nie znalezione! - - - + Failed to detect valid Heroes III data in chosen directory. -Please select directory with installed Heroes III data. +Please select the directory with installed Heroes III data. Nieudane znalezienie poprawnych plików Heroes III w podanej lokalizacji. Proszę wybrać folder z zainstalowanymi danymi Heroes III. - + + + + + Heroes III data not found! + Nie odnaleziono danych Heroes III! + + + Failed to detect valid Heroes III data in chosen directory. +Please select directory with installed Heroes III data. + Nieudane znalezienie poprawnych plików Heroes III w podanej lokalizacji. +Proszę wybrać folder z zainstalowanymi danymi Heroes III. + + You've provided GOG Galaxy installer! This file doesn't contain the game. Please download the offline backup game installer! - Podany plik jest instalatorem GOG Galaxy! Ten plik nie zawiera gry. Proszę pobrać zapasowy instalator offline gry! + Podany plik jest instalatorem GOG Galaxy! Ten plik nie zawiera gry. Proszę pobrać zapasowy instalator offline gry! - - Stream error while extracting files! -error reason: - Błąd strumienia podczas rozpakowywania plików! -powód błędu: - - - - Not a supported Inno Setup installer! - To nie jest wspierany instalator Inno Setup! - - - + Extracting error! Błąd wypakowywania! - Heroes III: HD Edition files are not supported by VCMI. Please select directory with Heroes III: Complete Edition or Heroes III: Shadow of Death. - Pliki Heroes III HD Edition nie są wspierane przez VCMI. + Pliki Heroes III HD Edition nie są wspierane przez VCMI. Proszę wybrać folder z Heroes III: Complete Edition lub 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. - Znaleziono nieznaną lub niewspieraną wersję Heroes III. + Znaleziono nieznaną lub niewspieraną wersję Heroes III. Proszę wybrać folder z Heroes III: Complete Edition lub Heroes III: Shadow of Death. @@ -1359,6 +1467,94 @@ Proszę wybrać folder z Heroes III: Complete Edition lub Heroes III: Shadow of Wyświetlacz obrazków + + Innoextract + + + Stream error while extracting files! +error reason: + Błąd strumienia podczas rozpakowywania plików! +powód błędu: + + + + Not a supported Inno Setup installer! + To nie jest wspierany instalator Inno Setup! + + + + VCMI was compiled without innoextract support, which is needed to extract exe files! + VCMI zostało skompilowane bez wsparcia innoextract, który jest niezbędny do rozpakowania plików exe + + + + SHA1 hash of provided files: +Exe (%1 bytes): +%2 + SHA1 suma kontrolna dostarczonych plików: +Exe (%1 bajtów): +%2 + + + + +Bin (%1 bytes): +%2 + +Bin (%1 bajtów): +%2 + + + + Internal copy process failed. Enough space on device? + +%1 + Wewnętrzny proces kopiowania nie powiódł się. Czy na urządzeniu jest wystarczająco dużo miejsca? + +%1 + + + + Exe + Exe + + + + Bin + Bin + + + + Language mismatch! +%1 + +%2 + Niezgodność języka! +%1 + +%2 + + + + Only one file known! Maybe files are corrupted? Please download again. +%1 + +%2 + Rozpoznano tylko jeden plik! Być może pliki są uszkodzone? Proszę pobrać je ponownie. +%1 + +%2 + + + + Unknown files! Maybe files are corrupted? Please download again. + +%1 + Nieznane pliki! Być może pliki są uszkodzone? Proszę pobrać je ponownie. + +%1 + + Language @@ -1446,18 +1642,6 @@ Proszę wybrać folder z Heroes III: Complete Edition lub Heroes III: Shadow of Vietnamese Wietnamski - - Other (East European) - Inne (Wschodnioeuropejski) - - - Other (Cyrillic Script) - Inne (Cyrylica) - - - Other (West European) - Inne (Zachodnioeuropejski) - Auto (%1) @@ -1482,53 +1666,547 @@ Proszę wybrać folder z Heroes III: Complete Edition lub Heroes III: Shadow of Pomoc - - Map Editor - Edytor map + + Game + Gra + + + Map Editor + Edytor map - Start game - Uruchom grę + Uruchom grę Mods Mody + + + Replace config file? + Zastąpić plik konfiguracji? + + + + Do you want to replace %1? + Czy chcesz zastąpić %1? + ModFields - + Name Nazwa - + Type Typ + + + ModStateController - Version - Wersja + + 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 + + + + Mod is not compatible, please update VCMI and check the 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! + Nie można aktywować moda translacyjnego dla innego języka + + + + 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 a protected directory, please remove it manually: + + Mod jest umiejscowiony w chronionym folderze, proszę go usunąć ręcznie: + + + 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 + Mechanika + + + + Interface + Interfejs + + + + Heroes + Bohaterowie + + + + Graphical + Graficzny + + + + Expansion + Dodatek + + + + Creatures + Stworzenia + + + + Compatibility + Kompatybilność + + + + Artifacts + Artefakty + + + + AI + AI QObject - + Error starting executable Błąd podczas uruchamiania pliku wykonywalnego - + Failed to start %1 Reason: %2 Nie udało się uruchomić %1 Powód: %2 + + StartGameTab + + + Form + Forma + + + + Import from Clipboard + Importuj ze schowka + + + + Rename Current Preset + Zmień nazwę bieżącej listy + + + + Current Preset + Bieżąca lista + + + + Create New Preset + Utwórz nową listę + + + + Export to Clipboard + Wyeksportuj do schowka + + + + Delete Current Preset + Usuń bieżącą listę + + + + Unsupported or corrupted game data detected! + Wykryto nieobsługiwane lub uszkodzone dane gry! + + + + + + + + + + + + ? + ? + + + + Install Translation + Zainstaluj tłumaczenie + + + + No soundtrack detected! + Nie wykryto ścieżki dźwiękowej! + + + + Armaggedon's Blade campaigns are missing! + + + + + No video files detected! + + + + + Activate Translation + Aktywne tłumaczenie + + + + Import files + Importuj pliki + + + + Check For Updates + Sprawdź aktualizacje + + + + Go to Downloads Page + Przejdź do strony pobierania + + + + Go to Changelog Page + Przejdź do listy zmian + + + + You are using the latest version + Używasz aktualnej wersji + + + + Game Data Files + Pliki gry + + + + Mod Preset + Lista modów użytkownika + + + + Resume + Wznów + + + + Play + Graj + + + + Editor + Edytor + + + + Update %n mods + + Zaktualizuj %n mod + Zaktualizuj %n mody + Zaktualizuj %n modów + + + + + Heroes Chronicles: +%n/%1 installed + + Kroniki Heroes: +%n/%1 zainstalowana + Kroniki Heroes: +%n/%1 zainstalowane + Kroniki Heroes: +%n/%1 zainstalowanych + + + + + Update to %1 available + Aktualizacja do %1 jest dostępna + + + + All supported files + Wszystkie wspierane pliki + + + + Maps + Mapy + + + + Campaigns + Kampanie + + + + Configs + Konfiguracje + + + + Mods + Mody + + + + Gog files + Pliki Gog + + + + All files (*.*) + Wszystkie pliki (*.*) + + + + Select files (configs, mods, maps, campaigns, gog files) to install... + Zaznacz pliki (konfiguracje, mody, mapy, kampanie, pliki gog) do zainstalowania... + + + + This option allows you to import additional data files into your VCMI installation. At the moment, following options are supported: + + - Heroes III Maps (.h3m or .vmap). + - Heroes III Campaigns (.h3c or .vcmp). + - Heroes III Chronicles using offline backup installer from GOG.com (.exe). + - VCMI mods in zip format (.zip) + - VCMI configuration files (.json) + + Ta opcja pozwala na zaimportowanie dodatkowych plików danych do Twojej instalacji VCMI. Obecnie obsługiwane są następujące opcje: + +- Mapy z Heroes III (.h3m lub .vmap). +- Kampanie z Heroes III (.h3c lub .vcmp). +- Heroes III Chronicles przy użyciu instalatora offline z GOG.com (.exe). +- Mody VCMI w formacie zip (.zip). +- Pliki konfiguracyjne VCMI (.json). + + + + Your Heroes III version uses different language. VCMI provides translations of the game into various languages that you can use. Use this option to automatically install such translation to your language. + Twoja wersja Heroes III używa innego języka. VCMI oferuje tłumaczenia gry na różne języki, które możesz wykorzystać. Skorzystaj z tej opcji, aby automatycznie zainstalować tłumaczenie na Twój język. + + + + Translation of Heroes III into your language is installed, but has been turned off. Use this option to enable it. + Tłumaczenie Heroes III na Twój język jest zainstalowane, ale zostało wyłączone. Skorzystaj z tej opcji, aby je włączyć. + + + + A new version of some of the mods that you have installed is now available in mod repository. Use this option to automatically update all your mods to latest version. + +WARNING: In some cases, updated versions of mods may not be compatible with your existing saves. You many want to postpone mod update until you finish any of your ongoing games. + Nowa wersja niektórych modów, które masz zainstalowane, jest już dostępna w repozytorium modów. Skorzystaj z tej opcji, aby automatycznie zaktualizować wszystkie mody do najnowszej wersji. + +UWAGA: W niektórych przypadkach zaktualizowane wersje modów mogą być niekompatybilne z Twoimi zapisami gry. Możesz chcieć odłożyć aktualizację modów do czasu ukończenia trwających rozgrywek. + + + + If you own Heroes Chronicles on gog.com, you can use offline backup installers provided by gog to import Heroes Chronicles data into VCMI as custom campaigns. +To import Heroes Chronicles, download offline backup installer of each chronicle that you wish to install, select 'Import files' option and select downloaded file. This will generate and install mod for VCMI that contains imported chronicles + Jeśli posiadasz Heroes Chronicles na gog.com, możesz użyć instalatorów offline dostarczanych przez GOG, aby zaimportować dane Heroes Chronicles do VCMI jako niestandardowe kampanie. +Aby zaimportować Heroes Chronicles, pobierz instalator offline każdej kroniki, którą chcesz zainstalować, wybierz opcję 'Importuj pliki' i wskaż pobrany plik. To spowoduje wygenerowanie i zainstalowanie moda dla VCMI zawierającego zaimportowane Kroniki. + + + + VCMI has detected that Heroes III music files are missing from your installation. VCMI will run, but in-game music will not be available. + +To resolve this problem, please copy missing mp3 files from Heroes III to VCMI data files directory manually or reinstall VCMI and re-import Heroes III data files + VCMI wykryło brak plików muzycznych Heroes III w Twojej instalacji. VCMI będzie działać, ale muzyka w grze nie będzie dostępna. + +Aby rozwiązać ten problem, skopiuj brakujące pliki mp3 z Heroes III do katalogu danych VCMI ręcznie lub ponownie zainstaluj VCMI i ponownie zaimportuj pliki danych Heroes III. + + + + VCMI has detected that Heroes III video files are missing from your installation. VCMI will run, but in-game cutscenes will not be available. + +To resolve this problem, please copy VIDEO.VID file from Heroes III to VCMI data files directory manually or reinstall VCMI and re-import Heroes III data files + VCMI wykryło brak plików wideo Heroes III w Twojej instalacji. VCMI będzie działać, ale scenki przerywnikowe w grze nie będą dostępne. + +Aby rozwiązać ten problem, skopiuj plik VIDEO.VID z Heroes III do katalogu danych VCMI ręcznie lub ponownie zainstaluj VCMI i ponownie zaimportuj pliki danych Heroes III. + + + + VCMI has detected that some of Heroes III data files are missing from your installation. You may attempt to run VCMI, but game may not work as expected or crash. + +To resolve this problem, please reinstall game and reimport data files using supported version of Heroes III. VCMI requires Heroes III: Shadow of Death or Complete Edition to run, which you can get (for example) from gog.com + VCMI wykryło brak niektórych plików danych Heroes III w Twojej instalacji. Możesz spróbować uruchomić VCMI, ale gra może nie działać poprawnie lub się zawiesić. + +Aby rozwiązać ten problem, zainstaluj grę ponownie i ponownie zaimportuj pliki danych, używając obsługiwanej wersji Heroes III. VCMI wymaga wersji Heroes III: Shadow of Death lub Complete Edition, którą możesz nabyć (na przykład) na gog.com. + + + + VCMI has detected that some of Heroes III: Armageddon's Blade data files are missing from your installation. VCMI will work, but Armageddon's Blade campaigns will not be available. + +To resolve this problem, please copy missing data files from Heroes III to VCMI data files directory manually or reinstall VCMI and re-import Heroes III data files + VCMI wykryło brak niektórych plików danych Heroes III w Twojej instalacji. Możesz spróbować uruchomić VCMI, ale gra może nie działać poprawnie lub się zawiesić. + +Aby rozwiązać ten problem, zainstaluj grę ponownie i ponownie zaimportuj pliki danych, używając obsługiwanej wersji Heroes III. VCMI wymaga wersji Heroes III: Shadow of Death lub Complete Edition, którą możesz nabyć (na przykład) na gog.com. + + + + Enter preset name: + Wpisz nazwę listy modów: + + + + Rename preset '%1' to: + Zmień nazwę listy modów '%1' na: + + UpdateDialog @@ -1553,8 +2231,12 @@ Powód: %2 - Cannot read JSON from url or incorrect JSON data + Cannot read JSON from URL or incorrect JSON data Nie można odczytać JSON z url lub JSON ma błędną zawartość + + Cannot read JSON from url or incorrect JSON data + Nie można odczytać JSON z url lub JSON ma błędną zawartość + diff --git a/launcher/translation/portuguese.ts b/launcher/translation/portuguese.ts index 364c674e7..e0935ff74 100644 --- a/launcher/translation/portuguese.ts +++ b/launcher/translation/portuguese.ts @@ -11,7 +11,7 @@ Have a question? Found a bug? Want to help? Join us! - Têm uma pergunta? Encontrou algum erro? Quer ajudar? Junte-se a nós + Tem uma pergunta? Encontrou algum erro? Quer ajudar? Junte-se a nós! @@ -24,176 +24,69 @@ Nossa comunidade - + Build Information Informações sobre a versão - + User data directory Diretório de dados do usuário + - - - + + Open Abrir - + Check for updates - Verificar por atualizações + Verificar atualizações - + Game version Versão do jogo - + Log files directory - Diretório do arquivo de registro + Diretório de arquivos de registro - + Data Directories Diretórios de dados - + Game data directory Diretório dos dados do jogo - + Operating System Sistema operacional - + Configuration files directory Diretório de arquivos de configuração - + Project homepage - Página da web do projeto + Página do projeto - + Report a bug 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 @@ -231,58 +124,53 @@ Inactive Inativo - - Download && refresh repositories - Baixar e atualizar repositórios - - + Description Descrição - + Changelog Registro de alterações - + Screenshots Capturas de tela - Install from file - Instalar a partir de arquivo + Instalar a partir de arquivo - + Uninstall Desinstalar - + Enable Ativar - + Disable Desativar - + Update Atualizar - + Install Instalar - + %p% (%v KB out of %m KB) %p% (%v KB de %m KB) @@ -292,188 +180,181 @@ Recarregar repositórios - + Abort 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 requerida + 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 + Todos os arquivos suportados - Maps - Mapas + Mapas - Campaigns - Campanhas + Campanhas - Configs - Configurações + Configurações - Mods - Mods + Mods - - Select files (configs, mods, maps, campaigns) to install... - Selecione arquivos (configurações, mods, mapas, campanhas) para instalar... + Gog files + Arquivos GOG + + + 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? + Substituir arquivo de configuração? - Do you want to replace %1? - Você deseja substituir %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 - Downloading %s%. %p% (%v MB out of %m MB) finished - Baixando %s%. %p% (%v MB de %m MB) completado - - - + Download failed Falha no download - + Unable to download all files. Encountered errors: @@ -481,404 +362,384 @@ 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 Heroes 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 - Mod Repositories - Repositórios de Mods - - - + 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) + + + 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 + + + + Select a display mode for the game + +Windowed - the game will run inside a window that covers part of your screen. + +Borderless Windowed Mode - the game will run in a full-screen window, matching your screen's resolution. + +Fullscreen Exclusive Mode - the game will cover the entirety of your screen and will use selected resolution. + + + + + 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 - - Best (Linear) - Melhor (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 - + 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) - Friendly AI in battles - IA amigável nas batalhas + + Downscaling Filter + 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 - + On Ativado - Cursor - Cursor - - - Heroes III Data Language - Idioma dos Dados do Heroes III - - - Select display mode for game Windowed - game will run inside a window that covers part of your screen @@ -886,7 +747,7 @@ 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. - Selecione o modo de exibição para o jogo + Selecione o modo de exibição para o jogo Modo de janela - o jogo será executado dentro de uma janela que cobre parte da sua tela @@ -895,131 +756,162 @@ 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 - Hardware - Hardware - - - Software - Software - - - + 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 + + ChroniclesExtractor + + File cannot opened + Não foi possível abrir o arquivo + + + + + Invalid file selected + 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! + + + + The file cannot be opened + + + + + You have to select a gog installer file! + + + + + You have to select a Heroes Chronicles installer file! + + + + + Extracting error! + Erro ao extrair! + + + + Hash error! + + + + + + + Heroes Chronicles + Heroes Chronicles + + + + Heroes Chronicles %1 - %2 + + + File size - - %1 B - %1 B - - - - %1 KiB - %1 KiB - - - + + %1 MiB %1 MiB - - - %1 GiB - %1 GiB - - - - %1 TiB - %1 TiB - FirstLaunchView @@ -1036,7 +928,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 @@ -1044,12 +936,11 @@ Modo de tela cheia exclusivo - o jogo cobrirá toda a sua tela e usará a resolu Selecione seu idioma - + Have a question? Found a bug? Want to help? Join us! Tem uma pergunta? Encontrou algum erro? Quer ajudar? Junte-se a nós! - Thank you for installing VCMI! Before you can start playing, there are a few more steps that need to be completed. @@ -1057,7 +948,7 @@ 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! - Obrigado por instalar o VCMI! + Obrigado por instalar o VCMI! Antes de começar a jogar, algumas etapas adicionais precisam ser concluídas. @@ -1066,96 +957,86 @@ Por favor, tenha em mente que para usar o VCMI, você deve possuir os arquivos d Heroes® of Might and Magic® III HD atualmente não é suportado! - + Locate Heroes III data files Localizar arquivos de dados do Heroes III - + Use offline installer from gog.com Usar instalador offline do gog.com - 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 - Você pode copiar manualmente os diretórios Mapas, Dados e Mp3 do diretório do jogo original para o diretório de dados do VCMI que você pode ver no topo desta página + Você pode copiar manualmente os diretórios Mapas, Dados e Mp3 do diretório do jogo original para o diretório de dados do VCMI que você pode ver no topo desta página - + Install gog.com files Instalar arquivos do gog.com - To run VCMI, Heroes III data files need to be present in one of the specified locations. Please copy the Heroes III data to one of these directories. - Para executar o VCMI, os arquivos de dados do Heroes III precisam estar presentes em um dos locais especificados. Por favor, copie os dados do Heroes III para um desses diretórios. - - - Alternatively, you can provide the directory where Heroes III data is installed and VCMI will copy the existing data automatically. - Alternativamente, você pode fornecer o diretório onde os dados do Heroes III estão instalados e o VCMI irá copiar os dados existentes automaticamente. - - - + Your Heroes III data files have been successfully found. Seus arquivos de dados do Heroes III foram encontrados com sucesso. - The automatic detection of the Heroes III language has failed. Please select the language of your Heroes III manually - A detecção automática do idioma do Heroes III falhou. Por favor, selecione o idioma do seu Heroes III manualmente - - - + Interface Improvements Melhorias na interface - + Install a translation of Heroes III in your preferred language Instale uma tradução do Heroes III no seu idioma preferido - + Installing... %p% Instalando... %p% - + If you already have Heroes III files on your device, you can select this directory and VCMI will copy the existing data automatically. Se você já tem arquivos do Heroes III no seu dispositivo, você pode selecionar este diretório e o VCMI irá copiar os dados existentes automaticamente. - + Copy existing files Copiar arquivos existentes - 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. - Se você possui o Heroes III no gog.com, você pode baixar o instalador offline de backup do gog.com, e o VCMI irá importar os dados do Heroes III usando o instalador offline. + Se você possui o Heroes III no gog.com, você pode baixar o instalador offline de backup do gog.com, e o VCMI irá importar os dados do Heroes III usando o instalador offline. O instalador offline consiste em duas partes, .exe e .bin. Certifique-se de baixar ambas. - + Optionally, you can install additional mods either now, or at any point later, using the VCMI Launcher Opcionalmente, você pode instalar mods adicionais agora, ou a qualquer momento depois, usando o Inicializador do VCMI - Install mod that provides various interface improvements, such as better interface for random maps and selectable actions in battles - Instale um mod que forneça várias melhorias na interface, como uma interface melhor para mapas aleatórios e ações selecionáveis em batalhas + Instale um mod que forneça várias melhorias na interface, como uma interface melhor para mapas aleatórios e ações selecionáveis em batalhas - + Install compatible version of "Horn of the Abyss", a fan-made Heroes III expansion ported by the VCMI team Instale uma versão compatível de "Horn of the Abyss", uma expansão do Heroes III feita por fãs, portada pela equipe do VCMI - + Install compatible version of "In The Wake of Gods", a fan-made Heroes III expansion Instalar uma versão compatível de "In The Wake of Gods", uma expansão feita por fãs do Heroes III - + + Install mod that provides various interface improvements, such as a better interface for random maps and selectable actions in battles + + + + Finish Concluir @@ -1165,189 +1046,207 @@ O instalador offline consiste em duas partes, .exe e .bin. Certifique-se de baix VCMI no Github - + VCMI on Discord VCMI no Discord - - + + Thank you for installing VCMI! + +Before you can start playing, there are a few more steps to complete. + +Please remember that 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! + + + + + Next Próximo - - Manual Installation - Instalação Manual + + You can manually copy directories Maps, Data, and Mp3 from the original game directory to the VCMI data directory that you can see on top of this page + - + + Manual Installation + Instalação manual + + + Search again Buscar novamente - If you don't have a copy of Heroes III installed, VCMI can import your Heroes III data using the offline installer from gog.com. - Se você não tiver uma cópia do Heroes III instalada, o VCMI pode importar seus dados do Heroes III usando o instalador offline do gog.com. - - - + Heroes III data files Arquivos de dados do Heroes III - + Copy existing data Copiar dados existentes - Your Heroes III language has been successfully detected. - Seu idioma do Heroes III foi detectado com sucesso. + + If you own Heroes III on gog.com, you can download a backup offline installer from gog.com. VCMI will then import Heroes III data using the offline installer. +Offline installer consists of two files: ".exe" and ".bin" - you must download both. + - Heroes III language - Idioma do Heroes III - - - - + + Back Voltar - + Install VCMI Mod Preset - Instalar Predefinição de Mod do VCMI + Instalar predefinição de mod do VCMI - + Horn of the Abyss Horn of the Abyss - + Heroes III Translation Tradução do Heroes III - + In The Wake of Gods In The Wake of Gods - + Heroes III installation found! Instalação do Heroes III encontrada! - + Copy data to VCMI folder? Copiar dados para a pasta do VCMI? - + Select %1 file... param is file extension Selecionar arquivo %1... - + You have to select %1 file! param is file extension Você precisa selecionar o arquivo %1! - + GOG file (*.*) Arquivo GOG (*.*) - + File selection Seleção de arquivo - - File cannot opened + + File cannot be opened O arquivo não pode ser aberto - + Invalid file selected Arquivo selecionado inválido - + GOG installer Instalador GOG - + + You've provided a GOG Galaxy installer! This file doesn't contain the game. Please download the offline backup game installer! + + + + + Heroes III: HD Edition files are not supported by VCMI. +Please select the directory with Heroes III: Complete Edition or Heroes III: Shadow of Death. + + + + + Unknown or unsupported Heroes III version found. +Please select the directory with Heroes III: Complete Edition or Heroes III: Shadow of Death. + + + + GOG data Dados do GOG - Installing... Please wait! - Instalando... Por favor, aguarde! - - - - No Heroes III data! - Nenhum dado do Heroes III! - - - - Selected files do not contain Heroes III data! - Os arquivos selecionados não contêm dados do Heroes III! - - - - - - - Heroes III data not found! - Dados do Heroes III não encontrados! - - - - Failed to detect valid Heroes III data in chosen directory. -Please select directory with installed Heroes III data. - Falha ao detectar dados válidos do Heroes III no diretório escolhido. -Por favor, selecione o diretório com os dados do Heroes III instalados. - - - You've provided GOG Galaxy installer! This file doesn't contain the game. Please download the offline backup game installer! - Você forneceu o instalador do GOG Galaxy! Este arquivo não contém o jogo. Por favor, faça o download do instalador offline de backup do jogo! + Você forneceu o instalador do GOG Galaxy! Este arquivo não contém o jogo. Por favor, faça o download do instalador offline de backup do jogo! - - Stream error while extracting files! -error reason: - Erro de fluxo ao extrair arquivos! -Motivo do erro: - - - - Not a supported Inno Setup installer! - Instalador do Inno Setup não suportado! - - - + Extracting error! Erro ao extrair! - + + Hash error! + + + + + No Heroes III data! + Nenhum dado do Heroes III! + + + + Selected files do not contain Heroes III data! + Os arquivos selecionados não contêm dados do Heroes III! + + + + Failed to detect valid Heroes III data in chosen directory. +Please select the directory with installed Heroes III data. + + + + + + + + Heroes III data not found! + Dados do Heroes III não encontrados! + + + Failed to detect valid Heroes III data in chosen directory. +Please select directory with installed Heroes III data. + Falha ao detectar dados válidos do Heroes III no diretório escolhido. +Por favor, selecione o diretório com os dados do Heroes III instalados. + + Heroes III: HD Edition files are not supported by VCMI. Please select directory with Heroes III: Complete Edition or Heroes III: Shadow of Death. - Arquivos do Heroes III: HD Edition não são suportados pelo VCMI. + Arquivos do Heroes III: HD Edition não são suportados pelo VCMI. Por favor, selecione o diretório com Heroes III: Complete Edition ou 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. - Versão desconhecida ou não suportada do Heroes III encontrada. + Versão desconhecida ou não suportada do Heroes III encontrada. Por favor, selecione o diretório com Heroes III: Complete Edition ou Heroes III: Shadow of Death. @@ -1356,7 +1255,81 @@ Por favor, selecione o diretório com Heroes III: Complete Edition ou Heroes III Image Viewer - Visualizador de Imagens + Visualizador de imagens + + + + Innoextract + + + Stream error while extracting files! +error reason: + Erro de fluxo ao extrair arquivos! +Motivo do erro: + + + + Not a supported Inno Setup installer! + 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! + + + + SHA1 hash of provided files: +Exe (%1 bytes): +%2 + + + + + +Bin (%1 bytes): +%2 + + + + + Internal copy process failed. Enough space on device? + +%1 + + + + + Exe + + + + + Bin + + + + + Language mismatch! +%1 + +%2 + + + + + Only one file known! Maybe files are corrupted? Please download again. +%1 + +%2 + + + + + Unknown files! Maybe files are corrupted? Please download again. + +%1 + @@ -1446,18 +1419,6 @@ Por favor, selecione o diretório com Heroes III: Complete Edition ou Heroes III Vietnamese Vietnamita - - Other (East European) - Outros (Leste Europeu) - - - Other (Cyrillic Script) - Outros (Escrita Cirílica) - - - Other (West European) - Outros (Oeste Europeu) - Auto (%1) @@ -1482,59 +1443,533 @@ Por favor, selecione o diretório com Heroes III: Complete Edition ou Heroes III Ajuda - - Map Editor - Editor de Mapas + + Game + + + + Map Editor + Editor de mapas - Start game - Iniciar jogo + Iniciar jogo Mods Mods + + + Replace config file? + Substituir arquivo de configuração? + + + + Do you want to replace %1? + Você deseja substituir %1? + ModFields - + Name Nome - + Type Tipo + + + ModStateController - Version - Versão + + 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 + + + + Mod is not compatible, please update VCMI and check the latest mod revisions + + + + + 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 a protected directory, please remove it manually: + + + + + 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 - + Error starting executable Erro ao iniciar o executável - + Failed to start %1 Reason: %2 Falha ao iniciar %1 Motivo: %2 + + StartGameTab + + + Form + + + + + Import from Clipboard + + + + + Rename Current Preset + + + + + Current Preset + + + + + Create New Preset + + + + + Export to Clipboard + + + + + Delete Current Preset + + + + + Unsupported or corrupted game data detected! + + + + + + + + + + + + + ? + + + + + Install Translation + + + + + No soundtrack detected! + + + + + Armaggedon's Blade campaigns are missing! + + + + + No video files detected! + + + + + Activate Translation + + + + + Import files + + + + + Check For Updates + + + + + Go to Downloads Page + + + + + Go to Changelog Page + + + + + You are using the latest version + + + + + Game Data Files + + + + + Mod Preset + + + + + Resume + + + + + Play + + + + + Editor + + + + + Update %n mods + + + + + + + + Heroes Chronicles: +%n/%1 installed + + + + + + + + Update to %1 available + + + + + All supported files + Todos os arquivos suportados + + + + Maps + Mapas + + + + Campaigns + Campanhas + + + + Configs + Configurações + + + + Mods + Mods + + + + Gog files + Arquivos GOG + + + + 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... + + + + This option allows you to import additional data files into your VCMI installation. At the moment, following options are supported: + + - Heroes III Maps (.h3m or .vmap). + - Heroes III Campaigns (.h3c or .vcmp). + - Heroes III Chronicles using offline backup installer from GOG.com (.exe). + - VCMI mods in zip format (.zip) + - VCMI configuration files (.json) + + + + + + Your Heroes III version uses different language. VCMI provides translations of the game into various languages that you can use. Use this option to automatically install such translation to your language. + + + + + Translation of Heroes III into your language is installed, but has been turned off. Use this option to enable it. + + + + + A new version of some of the mods that you have installed is now available in mod repository. Use this option to automatically update all your mods to latest version. + +WARNING: In some cases, updated versions of mods may not be compatible with your existing saves. You many want to postpone mod update until you finish any of your ongoing games. + + + + + If you own Heroes Chronicles on gog.com, you can use offline backup installers provided by gog to import Heroes Chronicles data into VCMI as custom campaigns. +To import Heroes Chronicles, download offline backup installer of each chronicle that you wish to install, select 'Import files' option and select downloaded file. This will generate and install mod for VCMI that contains imported chronicles + + + + + VCMI has detected that Heroes III music files are missing from your installation. VCMI will run, but in-game music will not be available. + +To resolve this problem, please copy missing mp3 files from Heroes III to VCMI data files directory manually or reinstall VCMI and re-import Heroes III data files + + + + + VCMI has detected that Heroes III video files are missing from your installation. VCMI will run, but in-game cutscenes will not be available. + +To resolve this problem, please copy VIDEO.VID file from Heroes III to VCMI data files directory manually or reinstall VCMI and re-import Heroes III data files + + + + + VCMI has detected that some of Heroes III data files are missing from your installation. You may attempt to run VCMI, but game may not work as expected or crash. + +To resolve this problem, please reinstall game and reimport data files using supported version of Heroes III. VCMI requires Heroes III: Shadow of Death or Complete Edition to run, which you can get (for example) from gog.com + + + + + VCMI has detected that some of Heroes III: Armageddon's Blade data files are missing from your installation. VCMI will work, but Armageddon's Blade campaigns will not be available. + +To resolve this problem, please copy missing data files from Heroes III to VCMI data files directory manually or reinstall VCMI and re-import Heroes III data files + + + + + Enter preset name: + + + + + Rename preset '%1' to: + + + UpdateDialog You have the latest version - Já possui a versão mais recente + Já tem a versão mais recente @@ -1553,8 +1988,12 @@ Motivo: %2 + Cannot read JSON from URL or incorrect JSON data + + + Cannot read JSON from url or incorrect JSON data - Não é possível ler JSON a partir do URL ou os dados JSON estão incorretos + Não é possível ler JSON a partir do URL ou os dados JSON estão incorretos diff --git a/launcher/translation/russian.ts b/launcher/translation/russian.ts index e2a89fbed..7b835d383 100644 --- a/launcher/translation/russian.ts +++ b/launcher/translation/russian.ts @@ -24,65 +24,65 @@ - + Build Information - + User data directory Данные пользователя + - - - + + Open Открыть - + Check for updates - + Game version - + Log files directory Журналы - + Data Directories Директории данных - + Game data directory - + Operating System - + Configuration files directory - + Project homepage - + Report a bug @@ -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 - ИИ + ИИ @@ -231,58 +203,49 @@ Inactive Неактивны - - Download && refresh repositories - Обновить репозиторий - - + Description Описание - + Changelog Изменения - + Screenshots Скриншоты - - Install from file - - - - + Uninstall Удалить - + Enable Включить - + Disable Отключить - + Update Обновить - + Install Установить - + %p% (%v KB out of %m KB) %p% (%v КБ з %m КБ) @@ -292,184 +255,161 @@ - + Abort Отмена - + 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 - Моды + Моды - - Select files (configs, mods, maps, campaigns) 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: @@ -478,526 +418,520 @@ Encountered errors: - + Install successfully downloaded? - + + Installing Heroes 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 Отключено - + On Включено - + Neutral AI in battles - + Enemy AI in battles - + Additional repository - + Check on startup Проверять при запуске - + Fullscreen Полноэкранный режим - + General Общее - + VCMI Language Язык VCMI - Cursor - Курсор - - - + Artificial Intelligence Искусственный интеллект - Mod Repositories - Репозитории модов - - - + 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 - + + Cursor Scaling + + + + + Scalable + + + + + Miscellaneous + + + + + Select a display mode for the game + +Windowed - the game will run inside a window that covers part of your screen. + +Borderless Windowed Mode - the game will run in a full-screen window, matching your screen's resolution. + +Fullscreen Exclusive Mode - the game will cover the entirety of your screen and will use selected resolution. + + + + + Font Scaling (experimental) + + + + + Original + + + + Upscaling Filter - + + Basic + + + + Use Relative Pointer Mode - + VSync - + Nearest - + Linear - - Best (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 - Heroes III Data Language - Язык данных Героев III - - - - 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. - - - - Hardware - Аппаратный - - - Software - Программный - - - + Heroes III Translation Перевод Героев III - + Resolution Разрешение экрана - + Autosave Автосохранение - + Display index Дисплей - + Network port Сетевой порт - + Video Графика - + Show intro Вступление - + Active Активен - + Disabled Отключен - + Enable Включить - + Not Installed Не установлен - + Install Установить + + ChroniclesExtractor + + + + Invalid file selected + + + + + The file cannot be opened + + + + + You have to select a gog installer file! + + + + + You have to select a Heroes Chronicles installer file! + + + + + Extracting error! + + + + + Hash error! + + + + + + + Heroes Chronicles + + + + + Heroes Chronicles %1 - %2 + + + File size - - %1 B - - - - - %1 KiB - - - - + + %1 MiB - - - %1 GiB - - - - - %1 TiB - - FirstLaunchView @@ -1022,12 +956,11 @@ Fullscreen Exclusive Mode - game will cover entirety of your screen and will use Выберите ваш язык - + Have a question? Found a bug? Want to help? Join us! Есть вопрос? Нашли ошибку? Хотите помочь? Присоединяйтесь! - Thank you for installing VCMI! Before you can start playing, there are a few more steps that need to be completed. @@ -1035,7 +968,7 @@ 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! - Спасибо за установку VCMI! + Спасибо за установку VCMI! Перед тем, как начать игру, есть еще несколько шагов, которые необходимо выполнить. @@ -1044,95 +977,72 @@ Heroes® of Might and Magic® III HD is currently not supported! Герои® Меча и Магии® III HD в данное время не поддерживаются! - + Locate Heroes III data files Поиск файлов данных Героев 3 - + Use offline installer from gog.com - - 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 - - - - + Install gog.com files - To run VCMI, Heroes III data files need to be present in one of the specified locations. Please copy the Heroes III data to one of these directories. - Для VCMI необходимы файлы данных Героев III в одной из перечисленных выше папок. Пожалуйста, скопируйте данные Героев III в одну из них. - - - Alternatively, you can provide the directory where Heroes III data is installed and VCMI will copy the existing data automatically. - Либо, вы можете выбрать директорию с установленными Героями III, и VCMI скопирует необходимые файлы автоматически. - - - + Your Heroes III data files have been successfully found. Данные Героев III были успешно найдены. - The automatic detection of the Heroes III language has failed. Please select the language of your Heroes III manually - Язык Героев III не был определен. Пожалуйста, выберите язык вашей копии Героев III - - - + Interface Improvements - + Install a translation of Heroes III in your preferred language Установить перевод Героев III на выбранный вами язык - + Installing... %p% - + If you already have Heroes III files on your device, you can select this directory and VCMI will copy the existing data automatically. - + Copy existing files - - 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. - - - - + Optionally, you can install additional mods either now, or at any point later, using the VCMI Launcher Вы можете установить дополнительные моды по вашему выбору сейчас или в любой момент позже (с использованием VCMI Launcher) - - Install mod that provides various interface improvements, such as better interface for random maps and selectable actions in battles - - - - + Install compatible version of "Horn of the Abyss", a fan-made Heroes III expansion ported by the VCMI team Установить совместимую версию Horn of the Abyss: фанатского дополнения к Героям III (портированную командой VCMI) - + Install compatible version of "In The Wake of Gods", a fan-made Heroes III expansion Установить совместимую версию In The Wake of Gods: фанатского дополнения к Героям III (портированную командой VCMI) - + + Install mod that provides various interface improvements, such as a better interface for random maps and selectable actions in battles + + + + Finish Завершить @@ -1142,177 +1052,185 @@ Offline installer consists of two parts, .exe and .bin. Make sure you download b VCMI в Github - + VCMI on Discord VCMI в Discord - - + + Thank you for installing VCMI! + +Before you can start playing, there are a few more steps to complete. + +Please remember that 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! + + + + + Next Далее - + + You can manually copy directories Maps, Data, and Mp3 from the original game directory to the VCMI data directory that you can see on top of this page + + + + Manual Installation - + Search again Повторить поиск - + Heroes III data files Файлы данных Героев III - + Copy existing data Скопировать данные - Your Heroes III language has been successfully detected. - Язык Героев III был определен успешно. + + If you own Heroes III on gog.com, you can download a backup offline installer from gog.com. VCMI will then import Heroes III data using the offline installer. +Offline installer consists of two files: ".exe" and ".bin" - you must download both. + - Heroes III language - Язык Героев III - - - - + + Back Назад - + Install VCMI Mod Preset Установить дополнительные моды - + Horn of the Abyss - + Heroes III Translation Перевод Героев III - + In The Wake of Gods - + Heroes III installation found! - + Copy data to VCMI folder? - + Select %1 file... param is file extension - + You have to select %1 file! param is file extension - + GOG file (*.*) - + File selection - - File cannot opened + + File cannot be opened - + Invalid file selected - + GOG installer - + + You've provided a GOG Galaxy installer! This file doesn't contain the game. Please download the offline backup game installer! + + + + + Heroes III: HD Edition files are not supported by VCMI. +Please select the directory with Heroes III: Complete Edition or Heroes III: Shadow of Death. + + + + + Unknown or unsupported Heroes III version found. +Please select the directory with Heroes III: Complete Edition or Heroes III: Shadow of Death. + + + + GOG data - - You've provided GOG Galaxy installer! This file doesn't contain the game. Please download the offline backup game installer! - - - - - Stream error while extracting files! -error reason: - - - - - Not a supported Inno Setup installer! - - - - + Extracting error! - + + Hash error! + + + + No Heroes III data! - + Selected files do not contain Heroes III data! - - - - - Heroes III data not found! - - - - + Failed to detect valid Heroes III data in chosen directory. -Please select directory with installed Heroes III data. +Please select the directory with installed Heroes III data. - - Heroes III: HD Edition files are not supported by VCMI. -Please select directory with Heroes III: Complete Edition or 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. + + + + + Heroes III data not found! @@ -1324,6 +1242,79 @@ Please select directory with Heroes III: Complete Edition or Heroes III: Shadow Просмотр изображений + + Innoextract + + + Stream error while extracting files! +error reason: + + + + + Not a supported Inno Setup installer! + + + + + VCMI was compiled without innoextract support, which is needed to extract exe files! + + + + + SHA1 hash of provided files: +Exe (%1 bytes): +%2 + + + + + +Bin (%1 bytes): +%2 + + + + + Internal copy process failed. Enough space on device? + +%1 + + + + + Exe + + + + + Bin + + + + + Language mismatch! +%1 + +%2 + + + + + Only one file known! Maybe files are corrupted? Please download again. +%1 + +%2 + + + + + Unknown files! Maybe files are corrupted? Please download again. + +%1 + + + Language @@ -1411,18 +1402,6 @@ Please select directory with Heroes III: Complete Edition or Heroes III: Shadow Vietnamese - - Other (East European) - Другой (восточноевропейский) - - - Other (Cyrillic Script) - Другой (кириллический) - - - Other (West European) - Другой (западноевропейский) - Auto (%1) @@ -1447,52 +1426,518 @@ Please select directory with Heroes III: Complete Edition or Heroes III: Shadow - - Map Editor - Редактор карт + + Game + + + + Map Editor + Редактор карт - Start game - Играть + Играть Mods Моды + + + Replace config file? + + + + + Do you want to replace %1? + + ModFields - + Name Название - + Type Тип + + + ModStateController - Version - Версия + + 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 check the 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 a 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 - + Error starting executable - + Failed to start %1 Reason: %2 + + StartGameTab + + + Form + + + + + Import from Clipboard + + + + + Rename Current Preset + + + + + Current Preset + + + + + Create New Preset + + + + + Export to Clipboard + + + + + Delete Current Preset + + + + + Unsupported or corrupted game data detected! + + + + + + + + + + + + + ? + + + + + Install Translation + + + + + No soundtrack detected! + + + + + Armaggedon's Blade campaigns are missing! + + + + + No video files detected! + + + + + Activate Translation + + + + + Import files + + + + + Check For Updates + + + + + Go to Downloads Page + + + + + Go to Changelog Page + + + + + You are using the latest version + + + + + Game Data Files + + + + + Mod Preset + + + + + Resume + + + + + Play + + + + + Editor + + + + + Update %n mods + + + + + + + + + Heroes Chronicles: +%n/%1 installed + + + + + + + + + Update to %1 available + + + + + All supported files + + + + + Maps + + + + + Campaigns + + + + + Configs + + + + + Mods + Моды + + + + Gog files + + + + + All files (*.*) + + + + + Select files (configs, mods, maps, campaigns, gog files) to install... + + + + + This option allows you to import additional data files into your VCMI installation. At the moment, following options are supported: + + - Heroes III Maps (.h3m or .vmap). + - Heroes III Campaigns (.h3c or .vcmp). + - Heroes III Chronicles using offline backup installer from GOG.com (.exe). + - VCMI mods in zip format (.zip) + - VCMI configuration files (.json) + + + + + + Your Heroes III version uses different language. VCMI provides translations of the game into various languages that you can use. Use this option to automatically install such translation to your language. + + + + + Translation of Heroes III into your language is installed, but has been turned off. Use this option to enable it. + + + + + A new version of some of the mods that you have installed is now available in mod repository. Use this option to automatically update all your mods to latest version. + +WARNING: In some cases, updated versions of mods may not be compatible with your existing saves. You many want to postpone mod update until you finish any of your ongoing games. + + + + + If you own Heroes Chronicles on gog.com, you can use offline backup installers provided by gog to import Heroes Chronicles data into VCMI as custom campaigns. +To import Heroes Chronicles, download offline backup installer of each chronicle that you wish to install, select 'Import files' option and select downloaded file. This will generate and install mod for VCMI that contains imported chronicles + + + + + VCMI has detected that Heroes III music files are missing from your installation. VCMI will run, but in-game music will not be available. + +To resolve this problem, please copy missing mp3 files from Heroes III to VCMI data files directory manually or reinstall VCMI and re-import Heroes III data files + + + + + VCMI has detected that Heroes III video files are missing from your installation. VCMI will run, but in-game cutscenes will not be available. + +To resolve this problem, please copy VIDEO.VID file from Heroes III to VCMI data files directory manually or reinstall VCMI and re-import Heroes III data files + + + + + VCMI has detected that some of Heroes III data files are missing from your installation. You may attempt to run VCMI, but game may not work as expected or crash. + +To resolve this problem, please reinstall game and reimport data files using supported version of Heroes III. VCMI requires Heroes III: Shadow of Death or Complete Edition to run, which you can get (for example) from gog.com + + + + + VCMI has detected that some of Heroes III: Armageddon's Blade data files are missing from your installation. VCMI will work, but Armageddon's Blade campaigns will not be available. + +To resolve this problem, please copy missing data files from Heroes III to VCMI data files directory manually or reinstall VCMI and re-import Heroes III data files + + + + + Enter preset name: + + + + + Rename preset '%1' to: + + + UpdateDialog @@ -1517,7 +1962,7 @@ Reason: %2 - Cannot read JSON from url or incorrect JSON data + Cannot read JSON from URL or incorrect JSON data diff --git a/launcher/translation/spanish.ts b/launcher/translation/spanish.ts index 46d1676d8..8d39c658b 100644 --- a/launcher/translation/spanish.ts +++ b/launcher/translation/spanish.ts @@ -24,65 +24,65 @@ Nuestra comunidad - + Build Information Información de la versión - + User data directory Directorio de datos del usuario + - - - + + Open Abrir - + Check for updates Comprobar actualizaciones - + Game version Versión del juego - + Log files directory Directorio de archivos de registro - + Data Directories Directorios de datos - + Game data directory Directorio de los datos del juego - + Operating System Sistema operativo - + Configuration files directory - + Project homepage Página web del proyecto - + Report a bug Informar de un error @@ -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 @@ -231,58 +207,49 @@ Inactive Inactivo - - Download && refresh repositories - Descargar y actualizar repositorios - - + Description Descripción - + Changelog Registro de cambios - + Screenshots Capturas de pantalla - - Install from file - - - - + Uninstall Desinstalar - + Enable Activar - + Disable Desactivar - + Update Actualizar - + Install Instalar - + %p% (%v KB out of %m KB) %p% (%v KB de %m KB) @@ -292,188 +259,165 @@ - + Abort 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 + Mapas - - Campaigns - - - - - Configs - - - - Mods - Mods + Mods - - Select files (configs, mods, maps, campaigns) to install... - - - - - Replace config file? - - - - - Do you want to replace %1? - - - - + Downloading %1. %p% (%v MB out of %m MB) finished - Downloading %s%. %p% (%v MB out of %m MB) finished - Descargando %s%. %p% (%v MB de %m MB) completado - - - + Download failed Descarga fallida - + Unable to download all files. Encountered errors: @@ -486,7 +430,7 @@ Errores encontrados: - + Install successfully downloaded? @@ -495,440 +439,425 @@ Install successfully downloaded? Instalar lo correctamente descargado? - + + Installing Heroes 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 - Mod Repositories - Repositorios de Mods - - - + 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 + + + + + Select a display mode for the game + +Windowed - the game will run inside a window that covers part of your screen. + +Borderless Windowed Mode - the game will run in a full-screen window, matching your screen's resolution. + +Fullscreen Exclusive Mode - the game will cover the entirety of your screen and will use selected resolution. + + + + + Font Scaling (experimental) + + + + + Original + + + + Upscaling Filter - + + Basic + + + + Use Relative Pointer Mode - + Nearest - + Linear - - Best (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) - Friendly AI in battles - IA amistosa en batallas - - - + 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 - + On Activado - Cursor - Cursor - - - + 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 @@ -936,7 +865,7 @@ 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. - Selecciona el modo de visualización del juego + Selecciona el modo de visualización del juego En ventana - el juego se ejecutará dentro de una ventana que forma parte de tu pantalla. @@ -945,80 +874,95 @@ 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. - Hardware - Hardware - - - Software - Software - - - + Show intro Mostrar introducción - + Check on startup Comprovar al inicio - Heroes III Data Language - Idioma de los datos de Heroes III. - - - + Active Activado - + Disabled Desactivado - + Enable Activar - + Not Installed No Instalado - + Install Instalar + + ChroniclesExtractor + + + + Invalid file selected + + + + + The file cannot be opened + + + + + You have to select a gog installer file! + + + + + You have to select a Heroes Chronicles installer file! + + + + + Extracting error! + + + + + Hash error! + + + + + + + Heroes Chronicles + + + + + Heroes Chronicles %1 - %2 + + + File size - - %1 B - - - - - %1 KiB - - - - + + %1 MiB - - - %1 GiB - - - - - %1 TiB - - FirstLaunchView @@ -1043,64 +987,65 @@ Pantalla completa - el juego cubrirá la totalidad de la pantalla y utilizará l VCMI en Github - + VCMI on Discord VCMI en Discord - - + + Thank you for installing VCMI! + +Before you can start playing, there are a few more steps to complete. + +Please remember that 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! + + + + + Next Siguiente - + Use offline installer from gog.com - - 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 - - - - + Manual Installation - + Search again Buscar de nuevo - + Heroes III data files ficheros de datos de Heroes III - + Copy existing data Copiar datos existentes - + Your Heroes III data files have been successfully found. Se han encontrado con éxito tus archivos de datos de Heroes III. - Your Heroes III language has been successfully detected. - Se ha detectado con éxito el idioma de tu Heroes III. - - - + Interface Improvements Mejora de la interfaz - Install mod that provides various interface improvements, such as better interface for random maps and selectable actions in battles - Instalar mod que proporciona varias mejoras en la interfaz, como mejor interacción en los mapas aleatorios y más opciones en las batallas + Instalar mod que proporciona varias mejoras en la interfaz, como mejor interacción en los mapas aleatorios y más opciones en las batallas @@ -1108,12 +1053,11 @@ Pantalla completa - el juego cubrirá la totalidad de la pantalla y utilizará l Selecciona el Idioma - + Have a question? Found a bug? Want to help? Join us! ¿Tienes alguna pregunta? ¿Encontraste algún error? ¿Quieres ayudar? ¡Únete a nosotros! - Thank you for installing VCMI! Before you can start playing, there are a few more steps that need to be completed. @@ -1121,7 +1065,7 @@ 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! - ¡Gracias por instalar VCMI! + ¡Gracias por instalar VCMI! Antes de poder jugar, hay algunos pasos más que deben completarse. @@ -1130,210 +1074,198 @@ Ten en cuenta que para usar VCMI debes ser dueño de los archivos de datos origi ¡Heroes® of Might and Magic® III HD actualmente no es compatible! - + Locate Heroes III data files Localizar los archivos de datos de Heroes III. - + + You can manually copy directories Maps, Data, and Mp3 from the original game directory to the VCMI data directory that you can see on top of this page + + + + Install gog.com files - To run VCMI, Heroes III data files need to be present in one of the specified locations. Please copy the Heroes III data to one of these directories. - Para ejecutar VCMI, los archivos de datos de Heroes III deben estar presentes en una de las ubicaciones especificadas. Por favor, copia los datos de Heroes III en uno de estos directorios. + + If you own Heroes III on gog.com, you can download a backup offline installer from gog.com. VCMI will then import Heroes III data using the offline installer. +Offline installer consists of two files: ".exe" and ".bin" - you must download both. + - Alternatively, you can provide the directory where Heroes III data is installed and VCMI will copy the existing data automatically. - Alternativamente, puedes proporcionar el directorio donde se instaló Heroes III y VCMI copiará automáticamente los datos existentes. - - - The automatic detection of the Heroes III language has failed. Please select the language of your Heroes III manually - La detección automática del idioma de Heroes III ha fallado. Por favor, selecciona manualmente el idioma de tu Heroes III. - - - Heroes III language - Idioma de Heroes III. - - - - + + Back Volver - + Install VCMI Mod Preset Instalar ajuste preestablecido de mod VCMI - + Horn of the Abyss Horn of the Abyss - + Heroes III Translation Traducción de Heroes III. - + + Install mod that provides various interface improvements, such as a better interface for random maps and selectable actions in battles + + + + In The Wake of Gods In The Wake of Gods - + Install a translation of Heroes III in your preferred language Instalar una traducción de Heroes III en tu idioma preferido. - + Optionally, you can install additional mods either now, or at any point later, using the VCMI Launcher Opcionalmente, puedes instalar mods adicionales ya sea ahora o en cualquier momento posterior, utilizando el lanzador de VCMI. - + Install compatible version of "Horn of the Abyss", a fan-made Heroes III expansion ported by the VCMI team Instalar la versión compatible de "Horn of the Abyss", una expansión de Heroes III hecha por fans y adaptada por el equipo de VCMI. - + Installing... %p% - + If you already have Heroes III files on your device, you can select this directory and VCMI will copy the existing data automatically. - + Copy existing files - - 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. - - - - + Install compatible version of "In The Wake of Gods", a fan-made Heroes III expansion Instalar la versión compatible de "In The Wake of Gods", una expansión de Heroes III hecha por fans. - + Finish Finalizar - + Heroes III installation found! Instalación de Heroes III encontrada! - + Copy data to VCMI folder? Copiar datos a la carpeta VCMI? - + Select %1 file... param is file extension - + You have to select %1 file! param is file extension - + GOG file (*.*) - + File selection - - File cannot opened + + File cannot be opened - + Invalid file selected - + GOG installer - + + You've provided a GOG Galaxy installer! This file doesn't contain the game. Please download the offline backup game installer! + + + + + Heroes III: HD Edition files are not supported by VCMI. +Please select the directory with Heroes III: Complete Edition or Heroes III: Shadow of Death. + + + + + Unknown or unsupported Heroes III version found. +Please select the directory with Heroes III: Complete Edition or Heroes III: Shadow of Death. + + + + GOG data - - You've provided GOG Galaxy installer! This file doesn't contain the game. Please download the offline backup game installer! - - - - - Stream error while extracting files! -error reason: - - - - - Not a supported Inno Setup installer! - - - - + Extracting error! - + + Hash error! + + + + No Heroes III data! - + Selected files do not contain Heroes III data! - - - - - Heroes III data not found! - - - - + Failed to detect valid Heroes III data in chosen directory. -Please select directory with installed Heroes III data. +Please select the directory with installed Heroes III data. - - Heroes III: HD Edition files are not supported by VCMI. -Please select directory with Heroes III: Complete Edition or 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. + + + + + Heroes III data not found! @@ -1345,6 +1277,79 @@ Please select directory with Heroes III: Complete Edition or Heroes III: Shadow Visor de imágenes + + Innoextract + + + Stream error while extracting files! +error reason: + + + + + Not a supported Inno Setup installer! + + + + + VCMI was compiled without innoextract support, which is needed to extract exe files! + + + + + SHA1 hash of provided files: +Exe (%1 bytes): +%2 + + + + + +Bin (%1 bytes): +%2 + + + + + Internal copy process failed. Enough space on device? + +%1 + + + + + Exe + + + + + Bin + + + + + Language mismatch! +%1 + +%2 + + + + + Only one file known! Maybe files are corrupted? Please download again. +%1 + +%2 + + + + + Unknown files! Maybe files are corrupted? Please download again. + +%1 + + + Language @@ -1432,18 +1437,6 @@ Please select directory with Heroes III: Complete Edition or Heroes III: Shadow Vietnamese Vietnamese (Vietnamita) - - Other (East European) - Otro (Europa del Este) - - - Other (Cyrillic Script) - Otro (Escritura cirílica) - - - Other (West European) - Otro (Europa del Este) - Auto (%1) @@ -1468,52 +1461,516 @@ Please select directory with Heroes III: Complete Edition or Heroes III: Shadow Ayuda - - Map Editor - Editor de Mapas + + Game + + + + Map Editor + Editor de Mapas - Start game - Iniciar juego + Iniciar juego Mods Mods + + + Replace config file? + + + + + Do you want to replace %1? + + ModFields - + Name Nombre - + Type Tipo + + + ModStateController - Version - Versión + + 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 check the 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 a 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 - + Error starting executable - + Failed to start %1 Reason: %2 + + StartGameTab + + + Form + + + + + Import from Clipboard + + + + + Rename Current Preset + + + + + Current Preset + + + + + Create New Preset + + + + + Export to Clipboard + + + + + Delete Current Preset + + + + + Unsupported or corrupted game data detected! + + + + + + + + + + + + + ? + + + + + Install Translation + + + + + No soundtrack detected! + + + + + Armaggedon's Blade campaigns are missing! + + + + + No video files detected! + + + + + Activate Translation + + + + + Import files + + + + + Check For Updates + + + + + Go to Downloads Page + + + + + Go to Changelog Page + + + + + You are using the latest version + + + + + Game Data Files + + + + + Mod Preset + + + + + Resume + + + + + Play + + + + + Editor + + + + + Update %n mods + + + + + + + + Heroes Chronicles: +%n/%1 installed + + + + + + + + Update to %1 available + + + + + All supported files + + + + + Maps + Mapas + + + + Campaigns + + + + + Configs + + + + + Mods + Mods + + + + Gog files + + + + + All files (*.*) + + + + + Select files (configs, mods, maps, campaigns, gog files) to install... + + + + + This option allows you to import additional data files into your VCMI installation. At the moment, following options are supported: + + - Heroes III Maps (.h3m or .vmap). + - Heroes III Campaigns (.h3c or .vcmp). + - Heroes III Chronicles using offline backup installer from GOG.com (.exe). + - VCMI mods in zip format (.zip) + - VCMI configuration files (.json) + + + + + + Your Heroes III version uses different language. VCMI provides translations of the game into various languages that you can use. Use this option to automatically install such translation to your language. + + + + + Translation of Heroes III into your language is installed, but has been turned off. Use this option to enable it. + + + + + A new version of some of the mods that you have installed is now available in mod repository. Use this option to automatically update all your mods to latest version. + +WARNING: In some cases, updated versions of mods may not be compatible with your existing saves. You many want to postpone mod update until you finish any of your ongoing games. + + + + + If you own Heroes Chronicles on gog.com, you can use offline backup installers provided by gog to import Heroes Chronicles data into VCMI as custom campaigns. +To import Heroes Chronicles, download offline backup installer of each chronicle that you wish to install, select 'Import files' option and select downloaded file. This will generate and install mod for VCMI that contains imported chronicles + + + + + VCMI has detected that Heroes III music files are missing from your installation. VCMI will run, but in-game music will not be available. + +To resolve this problem, please copy missing mp3 files from Heroes III to VCMI data files directory manually or reinstall VCMI and re-import Heroes III data files + + + + + VCMI has detected that Heroes III video files are missing from your installation. VCMI will run, but in-game cutscenes will not be available. + +To resolve this problem, please copy VIDEO.VID file from Heroes III to VCMI data files directory manually or reinstall VCMI and re-import Heroes III data files + + + + + VCMI has detected that some of Heroes III data files are missing from your installation. You may attempt to run VCMI, but game may not work as expected or crash. + +To resolve this problem, please reinstall game and reimport data files using supported version of Heroes III. VCMI requires Heroes III: Shadow of Death or Complete Edition to run, which you can get (for example) from gog.com + + + + + VCMI has detected that some of Heroes III: Armageddon's Blade data files are missing from your installation. VCMI will work, but Armageddon's Blade campaigns will not be available. + +To resolve this problem, please copy missing data files from Heroes III to VCMI data files directory manually or reinstall VCMI and re-import Heroes III data files + + + + + Enter preset name: + + + + + Rename preset '%1' to: + + + UpdateDialog @@ -1538,7 +1995,7 @@ Reason: %2 - Cannot read JSON from url or incorrect JSON data + Cannot read JSON from URL or incorrect JSON data diff --git a/launcher/translation/swedish.ts b/launcher/translation/swedish.ts new file mode 100644 index 000000000..5c0bb329a --- /dev/null +++ b/launcher/translation/swedish.ts @@ -0,0 +1,2189 @@ + + + + + AboutProjectView + + + VCMI on Discord + VCMI på Discord + + + + Have a question? Found a bug? Want to help? Join us! + Har du en fråga? Hittat en bugg? Vill du hjälpa till? Anslut dig till oss! + + + + VCMI on Github + VCMI på Github + + + + Our Community + Vår gemenskap + + + + Build Information + Konstruktionsinformation + + + + User data directory + Användardata-mapp + + + + + + + Open + Öppna + + + + Check for updates + Sök efter uppdateringar + + + + Game version + Spelversion + + + + Log files directory + Loggfils-mapp + + + + Data Directories + Datafilsregister + + + + Game data directory + Speldata-mapp + + + + Operating System + Operativsystem + + + + Configuration files directory + Konfigurationsfils-mapp + + + + Project homepage + Projektets hemsida + + + + Report a bug + Rapportera ett fel + + + + CModListModel + + Translation + Översättning + + + Town + Stad + + + Test + Test + + + Templates + Modeller + + + Spells + Trollformler + + + Music + Musik + + + Maps + Kartor + + + Sounds + Ljud + + + Skills + Färdigheter + + + Other + Annan + + + Objects + Objekt + + + Mechanics + Mekanik + + + Interface + Gränssnitt + + + Heroes + Hjälte + + + Graphical + Grafik + + + Expansion + Expansion/Tillägg + + + Creatures + Varelser + + + Compatibility + Kompatibilitet + + + Artifacts + Artefakter + + + AI + AI + + + + CModListView + + + Filter + Filter + + + + All mods + Alla moddar + + + + Downloadable + Nedladdningsbar + + + + Installed + Installerad + + + + Updatable + Uppdaterbar + + + + Active + Aktiv + + + + Inactive + Inaktiv + + + + Reload repositories + Ladda om repositorier + + + + + Description + Beskrivning + + + + Changelog + Förändringshistorik + + + + Screenshots + Skärmbilder + + + + %p% (%v KB out of %m KB) + %p% (%v KB av %m KB) + + + Install from file + Installera från fil + + + + Uninstall + Avinstallera + + + + Enable + Aktivera + + + + Disable + Inaktivera + + + + Update + Uppdatera + + + + Install + Installera + + + + Abort + 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 + + + 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 + + + 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 + + + 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 + + + + 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 + GOG-filer + + + 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: + + + Det går inte att ladda ner alla filer. + +Fel påträffat: + + + + + + + +Install successfully downloaded? + + +Installation framgångsrikt nedladdad? + + + + Installing Heroes 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 + + + + CModManager + + 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 + + + Required mod %1 is missing + Den obligatorisk modden %1 saknas + + + Required mod %1 is not enabled + Den obligatoriska modden %1 är inte aktiverad + + + This mod conflicts with %1 + Denna modd är i konflikt med %1 + + + Mod is already disabled + Modden är redan inaktiverad + + + This mod is needed to run %1 + Denna modden krävs för att köra %1 + + + 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: + + + + + CSettingsView + + + + Off + Inaktiverad + + + + Artificial Intelligence + Artificiell intelligens + + + + On + 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 + + + + 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 + + + + Select a display mode for the game + +Windowed - the game will run inside a window that covers part of your screen. + +Borderless Windowed Mode - the game will run in a full-screen window, matching your screen's resolution. + +Fullscreen Exclusive Mode - the game will cover the entirety of your screen and will use selected resolution. + + + + + 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 + +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. + Välj visningsläge för spelet + +Fönsterläge - spelet kommer att köras i ett fönster som täcker en viss del av din skärm (kan även täcka hela skärmen). + +Kantlöst fönsterläge - spelet körs i ett fönster som täcker hela din skärm och som använder samma upplösning som ditt operativsystem. + +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 + + + + ChroniclesExtractor + + File cannot opened + Filen kan inte öppnas + + + + + Invalid file selected + 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! + + + + The file cannot be opened + + + + + You have to select a gog installer file! + + + + + You have to select a Heroes Chronicles installer file! + + + + + Extracting error! + Extraheringsfel! + + + + Hash error! + + + + + + + Heroes Chronicles + Heroes Chronicles + + + + Heroes Chronicles %1 - %2 + Heroes Chronicles %1 - %2 + + + + File size + + %1 B + %1 B + + + %1 KiB + %1 KiB + + + + + %1 MiB + %1 MiB + + + %1 GiB + %1 GiB + + + %1 TiB + %1 TiB + + + + FirstLaunchView + + + Language + Språk + + + + Heroes III Data + Heroes III-data + + + + Mods Preset + Modd-förinställningar + + + + Select your language + Välj ditt språk + + + + Have a question? Found a bug? Want to help? Join us! + Har du en fråga? Hittat en bugg? Vill du hjälpa till? Anslut dig till oss! + + + + Thank you for installing VCMI! + +Before you can start playing, there are a few more steps to complete. + +Please remember that 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! + + + + + Locate Heroes III data files + Hitta Heroes III-datafiler + + + + Use offline installer from gog.com + Använd offline-installationsprogrammet från GOG.com + + + 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 + Du kan manuellt kopiera mapparna 'Maps', 'Data' och 'Mp3' från den ursprungliga spel-mappen till VCMI-datamappen som du kan se överst på den här sidan + + + + Install gog.com files + Installera filer från GOG.com + + + + Manual Installation + Manuell installation + + + + Installing... %p% + Installerar... %p% + + + + If you already have Heroes III files on your device, you can select this directory and VCMI will copy the existing data automatically. + Om du redan har Heroes III-filerna på din enhet kan du välja den här mappen och VCMI kommer automatiskt att kopiera befintliga data. + + + + Copy existing files + Kopiera befintliga filer + + + + Your Heroes III data files have been successfully found. + Dina Heroes III-datafiler har hittats. + + + 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. + + + + Install a translation of Heroes III in your preferred language + Installera en översättning av Heroes III på det språk du föredrar + + + + Finish + Slutför + + + + VCMI on Github + VCMI på 'Github' + + + + VCMI on Discord + VCMI på 'Discord' + + + Thank you for installing VCMI! + +Before you can start playing, there are a few more steps that need to be completed. + +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! + Tack för att du installerade VCMI! + +Innan du kan börja spela finns det några steg kvar att slutföra. + +Observera: för att använda VCMI måste du ha originaldatafilerna för Heroes® of Might and Magic® III: Complete eller The Shadow of Death. + +Heroes® of Might and Magic® III HD stöds för närvarande inte! + + + + + Next + Nästa + + + + Search again + Sök igen + + + + Heroes III data files + Heroes III-datafiler + + + + Copy existing data + Kopiera befintliga data + + + + You can manually copy directories Maps, Data, and Mp3 from the original game directory to the VCMI data directory that you can see on top of this page + + + + + If you own Heroes III on gog.com, you can download a backup offline installer from gog.com. VCMI will then import Heroes III data using the offline installer. +Offline installer consists of two files: ".exe" and ".bin" - you must download both. + + + + + + Back + Tillbaka + + + + Install VCMI Mod Preset + Installera VCMI Modd-förinställningar + + + + Horn of the Abyss + Avgrundens horn (Horn of the Abyss) + + + + Heroes III Translation + Heroes III - Översättning + + + + Interface Improvements + Gränssnitts-förbättringar + + + + Install mod that provides various interface improvements, such as a better interface for random maps and selectable actions in battles + + + + + In The Wake of Gods + I gudars kölvatten (In The Wake of Gods) + + + + 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' + + + Install mod that provides various interface improvements, such as better interface for random maps and selectable actions in battles + Installera en modd som förbättrar olika användargränssnitt i spelet, såsom ett bättre användargränssnitt för slumpmässiga kartor och valbara åtgärder i strider + + + + Install compatible version of "Horn of the Abyss", a fan-made Heroes III expansion ported by the VCMI team + Installera en kompatibel version av "Horn of the Abyss" (en fantillverkad Heroes III-expansion som blivit portad av VCMI-teamet) + + + + Install compatible version of "In The Wake of Gods", a fan-made Heroes III expansion + Installera en kompatibel version av "In The Wake of Gods" (en fantillverkad Heroes III-expansion) + + + + Heroes III installation found! + Heroes III-installationen hittades! + + + + Copy data to VCMI folder? + Kopiera data till VCMI-mappen? + + + + Select %1 file... + param is file extension + Välj filen %1 ... + + + + You have to select %1 file! + param is file extension + Du behöver välja filen %1! + + + + GOG file (*.*) + GOG-fil (*.*) + + + + File selection + Filval + + + + File cannot be opened + Filen kan inte öppnas + + + + Invalid file selected + Ogiltig fil vald + + + + GOG installer + GOG-Installationsprogram + + + + Heroes III: HD Edition files are not supported by VCMI. +Please select the directory with Heroes III: Complete Edition or Heroes III: Shadow of Death. + + + + + Unknown or unsupported Heroes III version found. +Please select the directory with Heroes III: Complete Edition or Heroes III: Shadow of Death. + + + + + GOG data + GOG-data + + + + You've provided a GOG Galaxy installer! This file doesn't contain the game. Please download the offline backup game installer! + + + + + Hash error! + + + + + No Heroes III data! + Inga Heroes III-data! + + + + Selected files do not contain Heroes III data! + De valda filerna innehåller inte Heroes III-data! + + + + Failed to detect valid Heroes III data in chosen directory. +Please select the directory with installed Heroes III data. + + + + + + + + Heroes III data not found! + Heroes III-data hittades inte! + + + Failed to detect valid Heroes III data in chosen directory. +Please select directory with installed Heroes III data. + Misslyckades med att upptäcka giltiga Heroes III-data i den valda mappen. Vänligen välj en mapp där Heroes III-data finns installerat. + + + You've provided GOG Galaxy installer! This file doesn't contain the game. Please download the offline backup game installer! + Du har tillhandahållit installationsprogrammet för GOG Galaxy! Den här filen innehåller inte spelet. Ladda ner offline-installationsprogrammet för spelet! + + + + Extracting error! + Fel vid extrahering! + + + Heroes III: HD Edition files are not supported by VCMI. +Please select directory with Heroes III: Complete Edition or Heroes III: Shadow of Death. + Heroes III HD Edition-filer stöds inte av VCMI. +Vänligen välj en mapp som innehåller data från Heroes III: Complete Edition eller 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. + Okänd eller ostödd version av Heroes III. +Vänligen välj en mapp som innehåller data från Heroes III: Complete Edition eller Heroes III: Shadow of Death. + + + + ImageViewer + + + Image Viewer + Bildvisare + + + + Innoextract + + + Stream error while extracting files! +error reason: + Strömningsfel vid extrahering av filer! +Orsak till fel: + + + + Not a supported Inno Setup installer! + 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! + + + + SHA1 hash of provided files: +Exe (%1 bytes): +%2 + + + + + +Bin (%1 bytes): +%2 + + + + + Internal copy process failed. Enough space on device? + +%1 + + + + + Exe + + + + + Bin + + + + + Language mismatch! +%1 + +%2 + + + + + Only one file known! Maybe files are corrupted? Please download again. +%1 + +%2 + + + + + Unknown files! Maybe files are corrupted? Please download again. + +%1 + + + + + Language + + + Czech + Tjeckiska + + + + Chinese + Kinesiska + + + + English + Engelska + + + + Finnish + Finska + + + + French + Franska + + + + German + Tyska + + + + Hungarian + Ungerska + + + + Italian + Italienska + + + + Korean + Koreanska + + + + Polish + Polska + + + + Portuguese + Portugisiska + + + + Russian + Ryska + + + + Spanish + Spanska + + + + Swedish + Svenska + + + + Turkish + Turkiska + + + + Ukrainian + Ukrainska + + + + Vietnamese + Vietnamesiska + + + + Auto (%1) + Auto (%1) + + + + MainWindow + + + VCMI Launcher + VCMI-startprogram (VCMI Launcher) + + + + Mods + Moddar + + + + Settings + Inställningar + + + + Help + Hjälp + + + + Game + + + + Map Editor + Kartredigerare + + + Start game + Starta spelet + + + + Replace config file? + Byt ut konfigurationsfilen? + + + + Do you want to replace %1? + Vill du ersätta %1? + + + + 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 + + + + Mod is not compatible, please update VCMI and check the latest mod revisions + + + + + 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 a protected directory, please remove it manually: + + + + + 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 + + + Error starting executable + Fel vid start av körbar fil + + + + Failed to start %1 +Reason: %2 + Startfel %1 +Orsak: %2 + + + + StartGameTab + + + Form + + + + + Import from Clipboard + + + + + Rename Current Preset + + + + + Current Preset + + + + + Create New Preset + + + + + Export to Clipboard + + + + + Delete Current Preset + + + + + Unsupported or corrupted game data detected! + + + + + + + + + + + + + ? + + + + + Install Translation + + + + + No soundtrack detected! + + + + + Armaggedon's Blade campaigns are missing! + + + + + No video files detected! + + + + + Activate Translation + + + + + Import files + + + + + Check For Updates + + + + + Go to Downloads Page + + + + + Go to Changelog Page + + + + + You are using the latest version + + + + + Game Data Files + + + + + Mod Preset + + + + + Resume + + + + + Play + + + + + Editor + + + + + Update %n mods + + + + + + + + Heroes Chronicles: +%n/%1 installed + + + + + + + + Update to %1 available + + + + + All supported files + Alla filer som stöds + + + + Maps + Kartor + + + + Campaigns + Kampanjer + + + + Configs + Konfigurationer + + + + Mods + Moddar + + + + Gog files + GOG-filer + + + + 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... + + + + This option allows you to import additional data files into your VCMI installation. At the moment, following options are supported: + + - Heroes III Maps (.h3m or .vmap). + - Heroes III Campaigns (.h3c or .vcmp). + - Heroes III Chronicles using offline backup installer from GOG.com (.exe). + - VCMI mods in zip format (.zip) + - VCMI configuration files (.json) + + + + + + Your Heroes III version uses different language. VCMI provides translations of the game into various languages that you can use. Use this option to automatically install such translation to your language. + + + + + Translation of Heroes III into your language is installed, but has been turned off. Use this option to enable it. + + + + + A new version of some of the mods that you have installed is now available in mod repository. Use this option to automatically update all your mods to latest version. + +WARNING: In some cases, updated versions of mods may not be compatible with your existing saves. You many want to postpone mod update until you finish any of your ongoing games. + + + + + If you own Heroes Chronicles on gog.com, you can use offline backup installers provided by gog to import Heroes Chronicles data into VCMI as custom campaigns. +To import Heroes Chronicles, download offline backup installer of each chronicle that you wish to install, select 'Import files' option and select downloaded file. This will generate and install mod for VCMI that contains imported chronicles + + + + + VCMI has detected that Heroes III music files are missing from your installation. VCMI will run, but in-game music will not be available. + +To resolve this problem, please copy missing mp3 files from Heroes III to VCMI data files directory manually or reinstall VCMI and re-import Heroes III data files + + + + + VCMI has detected that Heroes III video files are missing from your installation. VCMI will run, but in-game cutscenes will not be available. + +To resolve this problem, please copy VIDEO.VID file from Heroes III to VCMI data files directory manually or reinstall VCMI and re-import Heroes III data files + + + + + VCMI has detected that some of Heroes III data files are missing from your installation. You may attempt to run VCMI, but game may not work as expected or crash. + +To resolve this problem, please reinstall game and reimport data files using supported version of Heroes III. VCMI requires Heroes III: Shadow of Death or Complete Edition to run, which you can get (for example) from gog.com + + + + + VCMI has detected that some of Heroes III: Armageddon's Blade data files are missing from your installation. VCMI will work, but Armageddon's Blade campaigns will not be available. + +To resolve this problem, please copy missing data files from Heroes III to VCMI data files directory manually or reinstall VCMI and re-import Heroes III data files + + + + + Enter preset name: + + + + + Rename preset '%1' to: + + + + + UpdateDialog + + + You have the latest version + Du har den senaste versionen + + + + Close + Stäng + + + + Check for updates on startup + Sök efter uppdateringar vid uppstart + + + + Network error + Nätverksfel + + + + Cannot read JSON from URL or incorrect JSON data + + + + Cannot read JSON from url or incorrect JSON data + Det går inte att läsa JSON-data från URL:en eller fel JSON-data + + + diff --git a/launcher/translation/ukrainian.ts b/launcher/translation/ukrainian.ts index 23ab2dea4..3a9bcfec0 100644 --- a/launcher/translation/ukrainian.ts +++ b/launcher/translation/ukrainian.ts @@ -24,65 +24,65 @@ Наша спільнота - + Build Information Відомості про збірку - + User data directory Тека даних користувача + - - - + + Open Відкрити - + Check for updates Перевірити на оновлення - + Game version Версія гри - + Log files directory Тека файлів журналу - + Data Directories Теки гри - + Game data directory Тека даних гри - + Operating System Операційна система - + Configuration files directory Тека файлів конфігурації - + Project homepage Сторінка проекту - + Report a bug Повідомити про проблему @@ -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 - ШІ + ШІ @@ -231,58 +207,53 @@ Inactive Неактивні - - Download && refresh repositories - Оновити репозиторії - - + Description Опис - + Changelog Зміни - + Screenshots Знімки - Install from file - Встановити з файлу + Встановити з файлу - + Uninstall Видалити - + Enable Активувати - + Disable Деактивувати - + Update Оновити - + Install Встановити - + %p% (%v KB out of %m KB) %p% (%v КБ з %m КБ) @@ -292,188 +263,185 @@ Обновити репозиторії - + Abort Відмінити - + 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 - Модифікації + Модифікації - - Select files (configs, mods, maps, campaigns) to install... - Виберіть файли ( налаштування, моди, мапи, кампанії) для встановлення... - - - Replace config file? - Замінити файл налаштувань? + Замінити файл налаштувань? - Do you want to replace %1? - Ви дійсно хочете замінити %1? + Ви дійсно хочете замінити %1? - + Downloading %1. %p% (%v MB out of %m MB) finished Завантажується %1. %p% (%v MB з %m MB) завершено - Downloading %s%. %p% (%v MB out of %m MB) finished - Завантажуємо %s%. %p% (%v МБ з %m Мб) виконано - - - + Download failed Помилка завантаження - + Unable to download all files. Encountered errors: @@ -486,7 +454,7 @@ Encountered errors: - + Install successfully downloaded? @@ -495,34 +463,39 @@ Install successfully downloaded? Встановити успішно завантажені? - + + Installing Heroes Chronicles + Встановлюємо Хроніки Героїв + + + Installing mod %1 Встановлення модифікації %1 - + Operation failed Операція завершилася невдало - + Encountered errors: Виникли помилки: - + screenshots знімки екрану - + Screenshot %1 Знімок екрану %1 - + Mod is incompatible Модифікація несумісна @@ -530,362 +503,224 @@ 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 Штучний інтелект - Mod Repositories - Репозиторії модифікацій - - - + 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 x2 + + + + xBRZ x3 + xBRZ x3 + + + + xBRZ x4 + xBRZ x4 + + + + Full + Повне + + + + Use scalable fonts + Використання векторних шрифтів + + + Online Lobby address Адреса онлайн-лобі - - Upscaling Filter - Фільтр масштабування + + Cursor Scaling + Масштабування курсору - - Use Relative Pointer Mode - Режим відносного вказівника + + Scalable + Векторні - - Nearest - Найближчий + + Miscellaneous + Інше - - Linear - Лінійний - - - - Best (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) - Кількість автозбережень - - - Friendly AI in battles - Дружній ШІ в боях - - - - Framerate Limit - Обмеження частоти кадрів - - - - Autosave prefix - Префікс назв автозбережень - - - - Mouse Click Tolerance - Допуск кліків миші - - - - Sticks Acceleration - Прискорення стиків - - - - empty = map name prefix - (використовувати назву карти) - - - - Refresh now - Оновити зараз - - - - Default repository - Стандартний репозиторій - - - - Renderer - Рендерер - - - - On - Так - - - Cursor - Курсор - - - Heroes III Data Language - Мова Heroes III - - - - Select display mode for game + + Select a display mode for the game -Windowed - game will run inside a window that covers part of your screen +Windowed - the 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. +Borderless Windowed Mode - the game will run in a full-screen window, matching your screen's resolution. -Fullscreen Exclusive Mode - game will cover entirety of your screen and will use selected resolution. +Fullscreen Exclusive Mode - the game will cover the entirety of your screen and will use selected resolution. Виберіть режим відображення гри Віконний - гра запускатиметься у вікні, що займає частину екрана @@ -895,130 +730,361 @@ Fullscreen Exclusive Mode - game will cover entirety of your screen and will use Повноекранний ексклюзивний режим - гра займатиме весь екран і використовуватиме вибрану роздільну здатність. - + + 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 + Рендерер + + + + On + Так + + + 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. + Виберіть режим відображення гри + +Віконний - гра запускатиметься у вікні, що займає частину екрана + +Безмежний віконний режим - гра запускається у вікні, яке займає весь екран, з тією ж роздільною здатністю, що і ваш екран. + +Повноекранний ексклюзивний режим - гра займатиме весь екран і використовуватиме вибрану роздільну здатність. + + + Reserved screen area Зарезервована зона екрану - Hardware - Апаратний - - - Software - Програмний - - - + 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 Встановити + + ChroniclesExtractor + + + + Invalid file selected + Обрано невірний файл + + + + The file cannot be opened + Не вдається відкрити файл + + + + You have to select a gog installer file! + Вам необхідно вибрати файл інсталятора gog! + + + + You have to select a Heroes Chronicles installer file! + Вам необхідно вибрати інсталяційний файл Heroes Chronicles! + + + + Extracting error! + Помилка видобування! + + + + Hash error! + Помилка хешу! + + + + + + Heroes Chronicles + Хроніки Героїв + + + + Heroes Chronicles %1 - %2 + Хроніки Героїв %1 - %2 + + File size - %1 B - %1 Б + %1 Б - %1 KiB - %1 КіБ + %1 КіБ - + + %1 MiB %1 МіБ - %1 GiB - %1 ГіБ + %1 ГіБ - %1 TiB - %1 ТіБ + %1 ТіБ @@ -1044,18 +1110,128 @@ Fullscreen Exclusive Mode - game will cover entirety of your screen and will use Оберіть свою мову - + Have a question? Found a bug? Want to help? Join us! Маєте питання? Виявили помилку? Хочете допомогти? Приєднуйтесь до нас! - Thank you for installing VCMI! Before you can start playing, there are a few more steps that need to be completed. 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! + Дякуємо, що встановили VCMI. + +Залишилося зробити ще кілька кроків, перш ніж ви зможете почати грати. + +Майте на увазі, що для використання VCMI вам потрібно мати оригінальні файли гри Heroes® of Might and Magic® III: Complete або The Shadow of Death. + +Heroes® of Might and Magic® III HD наразі не підтримується! + + + + Locate Heroes III data files + Пошук файлів даних Heroes III + + + + Use offline installer from gog.com + Використати офлайн-інсталятор з gog.com + + + 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 + Ви можете вручну скопіювати теки Maps, Data та Mp3 з теки оригінальної гри до теки даних VCMI, яку ви можете побачити вгорі цієї сторінки + + + + Install gog.com files + Встановити файли gog.com + + + + Your Heroes III data files have been successfully found. + Файли даних вашої гри Heroes III успішно знайдено. + + + + Interface Improvements + Удосконалення нтерфейсу + + + + Install a translation of Heroes III in your preferred language + Встановити переклад Heroes III на вашу мову + + + + Installing... %p% + Встановлюємо... %p% + + + + If you already have Heroes III files on your device, you can select this directory and VCMI will copy the existing data automatically. + Якщо на вашому пристрої вже є файли Heroes III, ви можете вибрати цю теку і VCMI автоматично скопіює наявні дані. + + + + Copy existing files + Скопіювати існуючі файли + + + 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. + Якщо у вас є Heroes III на gog.com, ви можете завантажити резервну копію офлайн-інсталятора з gog.com, і VCMI імпортує дані Heroes III за допомогою офлайн-інсталятора. +Офлайн-інсталятор складається з двох частин, .exe та .bin. Переконайтеся, що ви завантажили обидві частини. + + + + Optionally, you can install additional mods either now, or at any point later, using the VCMI Launcher + За бажанням ви можете встановити додаткові модифікації зараз або пізніше, використовуючи VCMI Launcher + + + Install mod that provides various interface improvements, such as better interface for random maps and selectable actions in battles + Встановити різноманітні покращення інтерфейсу, такі як покращений інтерфейс випадкових карт та вибір варіантів дій у боях + + + + Install compatible version of "Horn of the Abyss", a fan-made Heroes III expansion ported by the VCMI team + Встановити сумісну версію доповнення "Horn of the Abyss", фанатське доповнення Heroes III, портоване командою VCMI + + + + Install compatible version of "In The Wake of Gods", a fan-made Heroes III expansion + Встановити сумісну версію доповнення "In The Wake of Gods", фанатське доповнення до Heroes III + + + + Install mod that provides various interface improvements, such as a better interface for random maps and selectable actions in battles + Встановити різноманітні покращення інтерфейсу, такі як покращений інтерфейс випадкових карт та вибір варіантів дій у боях + + + + Finish + Завершити + + + + VCMI on Github + VCMI на Github + + + + VCMI on Discord + VCMI на Discord + + + + Thank you for installing VCMI! + +Before you can start playing, there are a few more steps to complete. + +Please remember that 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! Дякуємо, що встановили VCMI. @@ -1066,288 +1242,195 @@ Heroes® of Might and Magic® III HD is currently not supported! Heroes® of Might and Magic® III HD наразі не підтримується! - - Locate Heroes III data files - Пошук файлів даних Heroes III - - - - Use offline installer from gog.com - Використати офлайн-інсталятор з gog.com - - - - 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 - Ви можете вручну скопіювати теки Maps, Data та Mp3 з теки оригінальної гри до теки даних VCMI, яку ви можете побачити вгорі цієї сторінки - - - - Install gog.com files - Встановити файли gog.com - - - To run VCMI, Heroes III data files need to be present in one of the specified locations. Please copy the Heroes III data to one of these directories. - VCMI потребує файлів даних Heroes III в одному з перелічених вище розташувань. Будь ласка, скопіюйте дані Heroes III в одну з цих директорій. - - - Alternatively, you can provide the directory where Heroes III data is installed and VCMI will copy the existing data automatically. - Або ж ви можете вибрати директорію зі встановленими даними Heroes III, і VCMI автоматично скопіює ці дані. - - - - Your Heroes III data files have been successfully found. - Файли даних вашої гри Heroes III успішно знайдено. - - - The automatic detection of the Heroes III language has failed. Please select the language of your Heroes III manually - Не вдалося визначити мову гри. Будь ласка, виберіть мову вашої копії Heroes III - - - - Interface Improvements - Удосконалення нтерфейсу - - - - Install a translation of Heroes III in your preferred language - Встановити переклад Heroes III на вашу мову - - - - Installing... %p% - Встановлюємо... %p% - - - - If you already have Heroes III files on your device, you can select this directory and VCMI will copy the existing data automatically. - Якщо на вашому пристрої вже є файли Heroes III, ви можете вибрати цю теку і VCMI автоматично скопіює наявні дані. - - - - Copy existing files - Скопіювати існуючі файли - - - - 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. - Якщо у вас є Heroes III на gog.com, ви можете завантажити резервну копію офлайн-інсталятора з gog.com, і VCMI імпортує дані Heroes III за допомогою офлайн-інсталятора. -Офлайн-інсталятор складається з двох частин, .exe та .bin. Переконайтеся, що ви завантажили обидві частини. - - - - Optionally, you can install additional mods either now, or at any point later, using the VCMI Launcher - За бажанням ви можете встановити додаткові модифікації зараз або пізніше, використовуючи VCMI Launcher - - - - Install mod that provides various interface improvements, such as better interface for random maps and selectable actions in battles - Встановити різноманітні покращення інтерфейсу, такі як покращений інтерфейс випадкових карт та вибір варіантів дій у боях - - - - Install compatible version of "Horn of the Abyss", a fan-made Heroes III expansion ported by the VCMI team - Встановити сумісну версію доповнення "Horn of the Abyss", фанатське доповнення Heroes III, портоване командою VCMI - - - - Install compatible version of "In The Wake of Gods", a fan-made Heroes III expansion - Встановити сумісну версію доповнення "In The Wake of Gods", фанатське доповнення до Heroes III - - - - Finish - Завершити - - - - VCMI on Github - VCMI на Github - - - - VCMI on Discord - VCMI на Discord - - - - + + Next Далі - + + You can manually copy directories Maps, Data, and Mp3 from the original game directory to the VCMI data directory that you can see on top of this page + Ви можете вручну скопіювати теки Maps, Data та Mp3 з теки оригінальної гри до теки даних VCMI, яку ви можете побачити вгорі цієї сторінки + + + Manual Installation Ручне встановлення - + Search again Повторити пошук - If you don't have a copy of Heroes III installed, VCMI can import your Heroes III data using the offline installer from gog.com. - Якщо у вас немає встановленої копії Heroes III, VCMI може імпортувати ваші дані Heroes III використовуючи офлайн-інсталятор з gog.com. - - - + Heroes III data files Файли даних Heroes III - + Copy existing data Копіювати наявні дані - Your Heroes III language has been successfully detected. - Мову вашої гри Heroes III успішно визначено. + + If you own Heroes III on gog.com, you can download a backup offline installer from gog.com. VCMI will then import Heroes III data using the offline installer. +Offline installer consists of two files: ".exe" and ".bin" - you must download both. + Якщо у вас є Heroes III на gog.com, ви можете завантажити резервну копію офлайн-інсталятора з gog.com, і VCMI імпортує дані Heroes III за допомогою офлайн-інсталятора. +Офлайн-інсталятор складається з двох частин, .exe та .bin. Переконайтеся, що ви завантажили обидві частини. - Heroes III language - Мова Heroes III - - - - + + Back Назад - + Install VCMI Mod Preset Встановлення початкових модифікацій VCMI - + Horn of the Abyss Horn of the Abyss - + Heroes III Translation Переклад Heroes III - + In The Wake of Gods In The Wake of Gods - + Heroes III installation found! Інсталяцію Heroes III знайдено! - + Copy data to VCMI folder? Скопіювати дані до теки VCMI? - + Select %1 file... param is file extension Оберіть файл %1... - + You have to select %1 file! param is file extension Ви повинні обрати файл %1! - + GOG file (*.*) Файл GOG (*.*) - + File selection Вибір файлу - - File cannot opened + + File cannot be opened Не вдається відкрити файл - + Invalid file selected Обрано невірний файл - + GOG installer Інсталятор GOG - - GOG data - Дані GOG - - - Installing... Please wait! - Встановлення... Зачекайте! - - - - No Heroes III data! - Немає файлів даних Heroes III! - - - - Selected files do not contain Heroes III data! - Обрані файли не містять файлів з грою Heroes III! - - - - - - - Heroes III data not found! - Файли даних Heroes III не знайдено! - - - - Failed to detect valid Heroes III data in chosen directory. -Please select directory with installed Heroes III data. - Не вдалося виявити файли Heroes III у вибраному каталозі. -Будь ласка, виберіть теку зі встановленими даними Heroes III. - - - - You've provided GOG Galaxy installer! This file doesn't contain the game. Please download the offline backup game installer! - Ви надали інсталятор GOG Galaxy! Цей файл не містить гри. Будь ласка, завантажте резервну копію інсталятора гри! - - - - Stream error while extracting files! -error reason: - Помилка потоку під час розпакування файлів! -причина помилки: - - - - Not a supported Inno Setup installer! - Не підтримуваний інсталятор Inno Setup! - - - - Extracting error! - Помилка видобування! - - - + Heroes III: HD Edition files are not supported by VCMI. -Please select directory with Heroes III: Complete Edition or Heroes III: Shadow of Death. +Please select the directory with Heroes III: Complete Edition or Heroes III: Shadow of Death. Файли Heroes III: HD Edition не підтримуються VCMI. Будь ласка, виберіть теку з Heroes III: Complete Edition або Heroes III: Shadow of Death. - + + Unknown or unsupported Heroes III version found. +Please select the directory with Heroes III: Complete Edition or Heroes III: Shadow of Death. + Знайдено невідому або не підтримувану версію Heroes III. +Будь ласка, виберіть теку з Heroes III: Complete Edition або Heroes III: Shadow of Death. + + + + GOG data + Дані GOG + + + + You've provided a GOG Galaxy installer! This file doesn't contain the game. Please download the offline backup game installer! + Ви надали інсталятор GOG Galaxy! Цей файл не містить гри. Будь ласка, завантажте резервну копію інсталятора гри! + + + + Hash error! + Помилка хешу! + + + + No Heroes III data! + Немає файлів даних Heroes III! + + + + Selected files do not contain Heroes III data! + Обрані файли не містять файлів з грою Heroes III! + + + + Failed to detect valid Heroes III data in chosen directory. +Please select the directory with installed Heroes III data. + Не вдалося виявити файли Heroes III у вибраному каталозі. +Будь ласка, виберіть теку зі встановленими даними Heroes III. + + + + + + + Heroes III data not found! + Файли даних Heroes III не знайдено! + + + Failed to detect valid Heroes III data in chosen directory. +Please select directory with installed Heroes III data. + Не вдалося виявити файли Heroes III у вибраному каталозі. +Будь ласка, виберіть теку зі встановленими даними Heroes III. + + + You've provided GOG Galaxy installer! This file doesn't contain the game. Please download the offline backup game installer! + Ви надали інсталятор GOG Galaxy! Цей файл не містить гри. Будь ласка, завантажте резервну копію інсталятора гри! + + + + Extracting error! + Помилка видобування! + + + Heroes III: HD Edition files are not supported by VCMI. +Please select directory with Heroes III: Complete Edition or Heroes III: Shadow of Death. + Файли Heroes III: HD Edition не підтримуються VCMI. +Будь ласка, виберіть теку з Heroes III: Complete Edition або 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. - Знайдено невідому або не підтримувану версію Heroes III. + Знайдено невідому або не підтримувану версію Heroes III. Будь ласка, виберіть теку з Heroes III: Complete Edition або Heroes III: Shadow of Death. @@ -1359,6 +1442,94 @@ Please select directory with Heroes III: Complete Edition or Heroes III: Shadow Перегляд зображень + + Innoextract + + + Stream error while extracting files! +error reason: + Помилка потоку під час розпакування файлів! +причина помилки: + + + + Not a supported Inno Setup installer! + Не підтримуваний інсталятор Inno Setup! + + + + VCMI was compiled without innoextract support, which is needed to extract exe files! + VCMI було створено без підтримки innoextract, яка необхідна для розпакування exe-файлів! + + + + SHA1 hash of provided files: +Exe (%1 bytes): +%2 + SHA1 хеш наданих файлів: +Exe (%1 байтів): +%2 + + + + +Bin (%1 bytes): +%2 + +Bin (%1 байтів): +%2 + + + + Internal copy process failed. Enough space on device? + +%1 + Не вдалося здійснити копіювання. Чи достатньо місця на пристрої? + +%1 + + + + Exe + Exe + + + + Bin + Bin + + + + Language mismatch! +%1 + +%2 + Розбіжність у мові! +%1 + +%2 + + + + Only one file known! Maybe files are corrupted? Please download again. +%1 + +%2 + Тільки один файл відомий! Можливо, файли пошкоджені? Будь ласка, завантажте ще раз. +%1 + +%2 + + + + Unknown files! Maybe files are corrupted? Please download again. + +%1 + Невідомі файли! Можливо, файли пошкоджені? Будь ласка, завантажте ще раз. + +%1 + + Language @@ -1446,18 +1617,6 @@ Please select directory with Heroes III: Complete Edition or Heroes III: Shadow Vietnamese В'єтнамська - - Other (East European) - Інша (східноєвропейська) - - - Other (Cyrillic Script) - Інша (кирилиця) - - - Other (West European) - Інша (західноєвропейська) - Auto (%1) @@ -1487,48 +1646,546 @@ Please select directory with Heroes III: Complete Edition or Heroes III: Shadow Допомога - - Map Editor - Редактор мап + + Game + Гра + + + Map Editor + Редактор мап - Start game - Грати + Грати + + + + Replace config file? + Замінити файл налаштувань? + + + + Do you want to replace %1? + Ви дійсно хочете замінити %1? ModFields - + Name Назва - + Type Тип + + + ModStateController - Version - Версія + + 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 та перевірте останні версії модифікацій + + + + Mod is not compatible, please update VCMI and check the 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 a protected directory, please remove it manually: + + Модифікація знаходиться в захищеному каталозі, будь ласка, видаліть її вручну: + + + + 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 - + Error starting executable Помилка запуску виконуваного файлу - + Failed to start %1 Reason: %2 Не вдалося запустити %1 Причина: %2 + + StartGameTab + + + Form + Form + + + + Import from Clipboard + Імпортувати з буфера обміну + + + + Rename Current Preset + Перейменувати профіль + + + + Current Preset + Поточний профіль + + + + Create New Preset + Створити новий профіль + + + + Export to Clipboard + Експорт у буфер обміну + + + + Delete Current Preset + Видалити поточний профіль + + + + Unsupported or corrupted game data detected! + Виявлено несумісні або пошкоджені дані гри! + + + + + + + + + + + + ? + ? + + + + Install Translation + Встановити переклад + + + + No soundtrack detected! + Саундтрек не виявлено! + + + + Armaggedon's Blade campaigns are missing! + Відсутні кампанії Клинка Армагедону! + + + + No video files detected! + Не виявлено відео файлів! + + + + Activate Translation + Увімкнути переклад + + + + Import files + Імпортування файлів + + + + Check For Updates + Перевірити оновлення + + + + Go to Downloads Page + На сторінку завантажень + + + + Go to Changelog Page + На сторінку історії змін + + + + You are using the latest version + Ви вже використовуєте останню версію + + + + Game Data Files + Файли Даних Гри + + + + Mod Preset + Профіль Модифікацій + + + + Resume + Продовжити + + + + Play + Грати + + + + Editor + Редактор + + + + Update %n mods + + Оновити %n модифікацію + Оновити %n модифікації + Оновити %n модифікацій + + + + + Heroes Chronicles: +%n/%1 installed + + Хроніки Героїв: +%n/%1 встановлена + Хроніки Героїв: +%n/%1 встановлено + Хроніки Героїв: +%n/%1 встановлено + + + + + Update to %1 available + Доступно оновлення до %1 + + + + All supported files + Усі підтримувані файли + + + + Maps + Мапи + + + + Campaigns + Кампанії + + + + Configs + Налаштування + + + + Mods + Модифікації + + + + Gog files + Файл GOG + + + + All files (*.*) + Усі файли (*.*) + + + + Select files (configs, mods, maps, campaigns, gog files) to install... + Виберіть файли (конфіги, моди, мапи, кампанії, gog-файли) для встановлення... + + + + This option allows you to import additional data files into your VCMI installation. At the moment, following options are supported: + + - Heroes III Maps (.h3m or .vmap). + - Heroes III Campaigns (.h3c or .vcmp). + - Heroes III Chronicles using offline backup installer from GOG.com (.exe). + - VCMI mods in zip format (.zip) + - VCMI configuration files (.json) + + Ця опція дозволяє імпортувати додаткові файли даних до вашої інсталяції VCMI. Наразі підтримуються наступні варіанти: + + - Мапи Heroes III (.h3m або .vmap). + - Кампанії Heroes III (.h3c або .vcmp). + - Хроніки Героїв використовуючи оффлайн інсталятор з GOG.com (.exe). + - Моди VCMI у форматі zip (.zip) + - Конфігураційні файли VCMI (.json) + + + + + Your Heroes III version uses different language. VCMI provides translations of the game into various languages that you can use. Use this option to automatically install such translation to your language. + Ваша версія Heroes III має іншу мову. VCMI надає переклади гри на різні мови, якими ви можете скористатися. За допомогою цієї опції ви можете автоматично інсталювати такий переклад на вашу мову. + + + + Translation of Heroes III into your language is installed, but has been turned off. Use this option to enable it. + Переклад Heroes III вашою мовою інстальовано, але його вимкнено. За допомогою цієї опції ви можете його увімкнути. + + + + A new version of some of the mods that you have installed is now available in mod repository. Use this option to automatically update all your mods to latest version. + +WARNING: In some cases, updated versions of mods may not be compatible with your existing saves. You many want to postpone mod update until you finish any of your ongoing games. + Нові версії деяких модифікацій, які ви встановили, тепер доступні у репозиторії модифікацій. Скористайтеся цією опцією, щоб автоматично оновити всі ваші модифікації до останньої версії. + +УВАГА: У деяких випадках оновлені версії модифікацій можуть бути несумісними з вашими існуючими збереженнями. Можливо, ви схочете відкласти оновлення модів, поки не завершите поточні ігри. + + + + If you own Heroes Chronicles on gog.com, you can use offline backup installers provided by gog to import Heroes Chronicles data into VCMI as custom campaigns. +To import Heroes Chronicles, download offline backup installer of each chronicle that you wish to install, select 'Import files' option and select downloaded file. This will generate and install mod for VCMI that contains imported chronicles + Якщо ви придбали Heroes Chronicles на gog.com, ви можете скористатися резервними копіями інсталяторів, наданими gog, щоб імпортувати дані Heroes Chronicles до VCMI як користувацькі кампанії. +Щоб імпортувати Heroes Chronicles, завантажте резервну копію інсталятора кожної хроніки яку ви бажаєте встановити, виберіть опцію «Імпортувати файли», а потім виберіть завантажений файл. Це створить та встановить модифікацію для VCMI, яка містить імпортовану хроніку + + + + VCMI has detected that Heroes III music files are missing from your installation. VCMI will run, but in-game music will not be available. + +To resolve this problem, please copy missing mp3 files from Heroes III to VCMI data files directory manually or reinstall VCMI and re-import Heroes III data files + Виявлено, що у вашій інсталяції відсутні музичні файли Heroes III. Ви можете запустити VCMI, але ігрова музика буде недоступна. + +Щоб усунути цю проблему, скопіюйте відсутні mp3-файли з Heroes III до теки даних VCMI вручну або переінсталюйте VCMI і повторно імпортуйте файли даних Heroes III + + + + VCMI has detected that Heroes III video files are missing from your installation. VCMI will run, but in-game cutscenes will not be available. + +To resolve this problem, please copy VIDEO.VID file from Heroes III to VCMI data files directory manually or reinstall VCMI and re-import Heroes III data files + Виявлено, що у вашій інсталяції відсутні відеофайли Heroes III. Ви можете запустити VCMI, але деякі анімації у грі будуть відсутні. + +Щоб вирішити цю проблему, скопіюйте файл VIDEO.VID з Heroes III до теки з файлами даних VCMI вручну або переінсталюйте VCMI та повторно імпортуйте файли даних Heroes III + + + + VCMI has detected that some of Heroes III data files are missing from your installation. You may attempt to run VCMI, but game may not work as expected or crash. + +To resolve this problem, please reinstall game and reimport data files using supported version of Heroes III. VCMI requires Heroes III: Shadow of Death or Complete Edition to run, which you can get (for example) from gog.com + Виявлено, що у вашій інсталяції відсутні деякі з файлів даних Heroes III. Ви можете спробувати запустити VCMI, але гра може не працювати належним чином або призвести до аварійного завершення. + +Щоб вирішити цю проблему, переінсталюйте гру та повторно імпортуйте файли даних, використовуючи підтримувану версію Heroes III. Для запуску VCMI потрібна Heroes III: Подих Смерті або повне видання, яке можна отримати (наприклад) з gog.com + + + + VCMI has detected that some of Heroes III: Armageddon's Blade data files are missing from your installation. VCMI will work, but Armageddon's Blade campaigns will not be available. + +To resolve this problem, please copy missing data files from Heroes III to VCMI data files directory manually or reinstall VCMI and re-import Heroes III data files + VCMI виявив, що у вашій інсталяції відсутні деякі файли даних Heroes III: Клинок Армагеддону. VCMI працюватиме, але кампанії Клинка Армагеддону будуть недоступні. + +Щоб вирішити цю проблему, скопіюйте відсутні файли даних з Heroes III до теки з файлами даних VCMI вручну або переінсталюйте VCMI та повторно імпортуйте файли даних Heroes III + + + + Enter preset name: + Введіть назву профілю: + + + + Rename preset '%1' to: + Перейменувати профіль '%1' на: + + UpdateDialog @@ -1553,8 +2210,12 @@ Reason: %2 - Cannot read JSON from url or incorrect JSON data + Cannot read JSON from URL or incorrect JSON data Не вдається прочитати JSON з url або невірні дані JSON + + Cannot read JSON from url or incorrect JSON data + Не вдається прочитати JSON з url або невірні дані JSON + diff --git a/launcher/translation/vietnamese.ts b/launcher/translation/vietnamese.ts index 7d28ea196..de08a8eee 100644 --- a/launcher/translation/vietnamese.ts +++ b/launcher/translation/vietnamese.ts @@ -24,65 +24,65 @@ Cộng đồng - + Build Information Thông tin bản dựng - + User data directory Đường dẫn đữ liệu người dùng + - - - + + Open Mở - + Check for updates Kiểm tra cập nhật - + Game version Phiên bản trò chơi - + Log files directory Đường dẫn nhật kí - + Data Directories Đường dẫn dữ liệu - + Game data directory Đường dẫn dữ liệu trò chơi - + Operating System Hệ điều hành - + Configuration files directory - + Project homepage Trang chủ - + Report a bug Báo lỗi @@ -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 @@ -231,58 +203,49 @@ Inactive Tắt - - Download && refresh repositories - Tải lại - - + Description Mô tả - + Changelog Các thay đổi - + Screenshots Hình ảnh - - Install from file - - - - + Uninstall Gỡ bỏ - + Enable Bật - + Disable Tắt - + Update Cập nhật - + Install Cài đặt - + %p% (%v KB out of %m KB) %p% (%v KB trong số %m KB) @@ -292,184 +255,161 @@ - + Abort 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 + Bản sửa đổi - - Select files (configs, mods, maps, campaigns) 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: @@ -478,395 +418,376 @@ Encountered errors: - + Install successfully downloaded? - + + Installing Heroes 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 - Mod Repositories - Nguồn bản sửa đổi - - - + 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 + + + + + Select a display mode for the game + +Windowed - the game will run inside a window that covers part of your screen. + +Borderless Windowed Mode - the game will run in a full-screen window, matching your screen's resolution. + +Fullscreen Exclusive Mode - the game will cover the entirety of your screen and will use selected resolution. + + + + + Font Scaling (experimental) + + + + + Original + + + + Upscaling Filter - + + Basic + + + + Use Relative Pointer Mode - + Nearest - + Linear - - Best (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) - Friendly AI in battles - Máy liên minh trong trận đánh - - - + 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 - + On Bật - Cursor - Con trỏ - - - Heroes III Data Language - Ngôn ngữ dữ liệu Heroes III - - - Select display mode for game Windowed - game will run inside a window that covers part of your screen @@ -874,7 +795,7 @@ 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. - Chọn chế độ hiện thị + Chọn chế độ hiện thị Cửa sổ - Trò chơi chạy trong 1 cửa sổ @@ -883,131 +804,150 @@ 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 - Hardware - Phần cứng - - - Software - Phần mềm - - - + 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 + + ChroniclesExtractor + + + + Invalid file selected + + + + + The file cannot be opened + + + + + You have to select a gog installer file! + + + + + You have to select a Heroes Chronicles installer file! + + + + + Extracting error! + + + + + Hash error! + + + + + + + Heroes Chronicles + + + + + Heroes Chronicles %1 - %2 + + + File size - - %1 B - - - - - %1 KiB - - - - + + %1 MiB - - - %1 GiB - - - - - %1 TiB - - FirstLaunchView @@ -1032,12 +972,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 đ Chọn ngôn ngữ - + Have a question? Found a bug? Want to help? Join us! Có thắc mắc? Gặp lỗi? Cần giúp đỡ? Tham gia cùng chúng tôi! - Thank you for installing VCMI! Before you can start playing, there are a few more steps that need to be completed. @@ -1045,7 +984,7 @@ 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! - Cảm ơn bạn đã cài VCMI! + Cảm ơn bạn đã cài VCMI! Trước khi bắt đầu, còn vài bước cần hoàn thành. @@ -1054,95 +993,76 @@ Trước khi bắt đầu, còn vài bước cần hoàn thành. Hiện tại chưa hỗ trợ Heroes® of Might and Magic® III HD! - + Locate Heroes III data files Định vị tệp dữ liệu Heroes III - + Use offline installer from gog.com - - 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 - - - - + Install gog.com files - To run VCMI, Heroes III data files need to be present in one of the specified locations. Please copy the Heroes III data to one of these directories. - Để chạy VCMI, dữ liệu Heroes III cần được đặt ở 1 trong những đường dẫn cho trước. Sao chép dữ liệu Heroes III đến 1 trong những đường dẫn này. - - - Alternatively, you can provide the directory where Heroes III data is installed and VCMI will copy the existing data automatically. - Thay vào đó, bạn có thể cung cấp đường dẫn cài đặt dữ liệu Heroes III và VCMI sẽ tự sao chép dữ liệu. - - - + Your Heroes III data files have been successfully found. Dữ liệu Heroes III đã được tìm thấy. - The automatic detection of the Heroes III language has failed. Please select the language of your Heroes III manually - Tự nhận diện ngôn ngữ Heroes III thất bại. Chọn ngôn ngữ Heroes III thủ công - - - + Interface Improvements Cải thiện giao diện - + Install a translation of Heroes III in your preferred language Cài ngôn ngữ Heroes III - + Installing... %p% - + If you already have Heroes III files on your device, you can select this directory and VCMI will copy the existing data automatically. - + Copy existing files - - 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. - - - - + Optionally, you can install additional mods either now, or at any point later, using the VCMI Launcher Tùy chọn, bạn có thể cài bản sửa đổi bổ sung bây giờ, hoặc bất kì lúc nào bằng VCMI Launcher - Install mod that provides various interface improvements, such as better interface for random maps and selectable actions in battles - Cài đặt bản sửa đổi cung cấp nhiều cải tiến giao diện cho bản đồ ngẫu nhiên và thao tác trong trận đánh + Cài đặt bản sửa đổi cung cấp nhiều cải tiến giao diện cho bản đồ ngẫu nhiên và thao tác trong trận đánh - + Install compatible version of "Horn of the Abyss", a fan-made Heroes III expansion ported by the VCMI team Cài đặt phiên bản tương thích Horn of the Abyss, bản mở rộng Heroes III người hâm mộ tự làm, được nhóm VCMI chuyển qua - + Install compatible version of "In The Wake of Gods", a fan-made Heroes III expansion Cài đặt phiên bản tương thích In The Wake of Gods, bản mở rộng Heroes III người hâm mộ tự làm - + + Install mod that provides various interface improvements, such as a better interface for random maps and selectable actions in battles + + + + Finish Hoàn thành @@ -1152,177 +1072,185 @@ Offline installer consists of two parts, .exe and .bin. Make sure you download b VCMI trên Github - + VCMI on Discord VCMI trên Discord - - + + Thank you for installing VCMI! + +Before you can start playing, there are a few more steps to complete. + +Please remember that 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! + + + + + Next Tiếp theo - + + You can manually copy directories Maps, Data, and Mp3 from the original game directory to the VCMI data directory that you can see on top of this page + + + + Manual Installation - + Search again Tìm kiếm lại - + Heroes III data files Tệp dữ liệu Heroes III - + Copy existing data Sao chép dữ liệu đang có - Your Heroes III language has been successfully detected. - Ngôn ngữ Heroes III đã được nhận diện. + + If you own Heroes III on gog.com, you can download a backup offline installer from gog.com. VCMI will then import Heroes III data using the offline installer. +Offline installer consists of two files: ".exe" and ".bin" - you must download both. + - Heroes III language - Ngôn ngữ Heroes III - - - - + + Back Quay lại - + Install VCMI Mod Preset Cài đặt bản sửa đổi VCMI thiết lập trước - + Horn of the Abyss Horn of the Abyss - + Heroes III Translation Bản dịch Heroes III - + In The Wake of Gods In The Wake of Gods - + Heroes III installation found! - + Copy data to VCMI folder? - + Select %1 file... param is file extension - + You have to select %1 file! param is file extension - + GOG file (*.*) - + File selection - - File cannot opened + + File cannot be opened - + Invalid file selected - + GOG installer - + + You've provided a GOG Galaxy installer! This file doesn't contain the game. Please download the offline backup game installer! + + + + + Heroes III: HD Edition files are not supported by VCMI. +Please select the directory with Heroes III: Complete Edition or Heroes III: Shadow of Death. + + + + + Unknown or unsupported Heroes III version found. +Please select the directory with Heroes III: Complete Edition or Heroes III: Shadow of Death. + + + + GOG data - - You've provided GOG Galaxy installer! This file doesn't contain the game. Please download the offline backup game installer! - - - - - Stream error while extracting files! -error reason: - - - - - Not a supported Inno Setup installer! - - - - + Extracting error! - + + Hash error! + + + + No Heroes III data! - + Selected files do not contain Heroes III data! - - - - - Heroes III data not found! - - - - + Failed to detect valid Heroes III data in chosen directory. -Please select directory with installed Heroes III data. +Please select the directory with installed Heroes III data. - - Heroes III: HD Edition files are not supported by VCMI. -Please select directory with Heroes III: Complete Edition or 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. + + + + + Heroes III data not found! @@ -1334,6 +1262,79 @@ Please select directory with Heroes III: Complete Edition or Heroes III: Shadow Trình xem ảnh + + Innoextract + + + Stream error while extracting files! +error reason: + + + + + Not a supported Inno Setup installer! + + + + + VCMI was compiled without innoextract support, which is needed to extract exe files! + + + + + SHA1 hash of provided files: +Exe (%1 bytes): +%2 + + + + + +Bin (%1 bytes): +%2 + + + + + Internal copy process failed. Enough space on device? + +%1 + + + + + Exe + + + + + Bin + + + + + Language mismatch! +%1 + +%2 + + + + + Only one file known! Maybe files are corrupted? Please download again. +%1 + +%2 + + + + + Unknown files! Maybe files are corrupted? Please download again. + +%1 + + + Language @@ -1421,18 +1422,6 @@ Please select directory with Heroes III: Complete Edition or Heroes III: Shadow Vietnamese Tiếng Việt - - Other (East European) - Khác (Đông Âu) - - - Other (Cyrillic Script) - Khác (Chữ Kirin) - - - Other (West European) - Khác (Tây Âu) - Auto (%1) @@ -1457,52 +1446,514 @@ Please select directory with Heroes III: Complete Edition or Heroes III: Shadow - - Map Editor - Tạo bản đồ + + Game + + + + Map Editor + Tạo bản đồ - Start game - Chơi ngay + Chơi ngay Mods Bản sửa đổi + + + Replace config file? + + + + + Do you want to replace %1? + + ModFields - + Name Tên - + Type Loại + + + ModStateController - Version - Phiên bản + + 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 check the 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 a 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 - + Error starting executable - + Failed to start %1 Reason: %2 + + StartGameTab + + + Form + + + + + Import from Clipboard + + + + + Rename Current Preset + + + + + Current Preset + + + + + Create New Preset + + + + + Export to Clipboard + + + + + Delete Current Preset + + + + + Unsupported or corrupted game data detected! + + + + + + + + + + + + + ? + + + + + Install Translation + + + + + No soundtrack detected! + + + + + Armaggedon's Blade campaigns are missing! + + + + + No video files detected! + + + + + Activate Translation + + + + + Import files + + + + + Check For Updates + + + + + Go to Downloads Page + + + + + Go to Changelog Page + + + + + You are using the latest version + + + + + Game Data Files + + + + + Mod Preset + + + + + Resume + + + + + Play + + + + + Editor + + + + + Update %n mods + + + + + + + Heroes Chronicles: +%n/%1 installed + + + + + + + Update to %1 available + + + + + 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... + + + + + This option allows you to import additional data files into your VCMI installation. At the moment, following options are supported: + + - Heroes III Maps (.h3m or .vmap). + - Heroes III Campaigns (.h3c or .vcmp). + - Heroes III Chronicles using offline backup installer from GOG.com (.exe). + - VCMI mods in zip format (.zip) + - VCMI configuration files (.json) + + + + + + Your Heroes III version uses different language. VCMI provides translations of the game into various languages that you can use. Use this option to automatically install such translation to your language. + + + + + Translation of Heroes III into your language is installed, but has been turned off. Use this option to enable it. + + + + + A new version of some of the mods that you have installed is now available in mod repository. Use this option to automatically update all your mods to latest version. + +WARNING: In some cases, updated versions of mods may not be compatible with your existing saves. You many want to postpone mod update until you finish any of your ongoing games. + + + + + If you own Heroes Chronicles on gog.com, you can use offline backup installers provided by gog to import Heroes Chronicles data into VCMI as custom campaigns. +To import Heroes Chronicles, download offline backup installer of each chronicle that you wish to install, select 'Import files' option and select downloaded file. This will generate and install mod for VCMI that contains imported chronicles + + + + + VCMI has detected that Heroes III music files are missing from your installation. VCMI will run, but in-game music will not be available. + +To resolve this problem, please copy missing mp3 files from Heroes III to VCMI data files directory manually or reinstall VCMI and re-import Heroes III data files + + + + + VCMI has detected that Heroes III video files are missing from your installation. VCMI will run, but in-game cutscenes will not be available. + +To resolve this problem, please copy VIDEO.VID file from Heroes III to VCMI data files directory manually or reinstall VCMI and re-import Heroes III data files + + + + + VCMI has detected that some of Heroes III data files are missing from your installation. You may attempt to run VCMI, but game may not work as expected or crash. + +To resolve this problem, please reinstall game and reimport data files using supported version of Heroes III. VCMI requires Heroes III: Shadow of Death or Complete Edition to run, which you can get (for example) from gog.com + + + + + VCMI has detected that some of Heroes III: Armageddon's Blade data files are missing from your installation. VCMI will work, but Armageddon's Blade campaigns will not be available. + +To resolve this problem, please copy missing data files from Heroes III to VCMI data files directory manually or reinstall VCMI and re-import Heroes III data files + + + + + Enter preset name: + + + + + Rename preset '%1' to: + + + UpdateDialog @@ -1527,7 +1978,7 @@ Reason: %2 - Cannot read JSON from url or incorrect JSON data + Cannot read JSON from URL or incorrect JSON data diff --git a/launcher/updatedialog_moc.cpp b/launcher/updatedialog_moc.cpp index 6c9825868..4e95de625 100644 --- a/launcher/updatedialog_moc.cpp +++ b/launcher/updatedialog_moc.cpp @@ -67,7 +67,7 @@ UpdateDialog::UpdateDialog(bool calledManually, QWidget *parent): } auto byteArray = response->readAll(); - JsonNode node(reinterpret_cast(byteArray.constData()), byteArray.size()); + JsonNode node(reinterpret_cast(byteArray.constData()), byteArray.size(), ""); loadFromJson(node); }); } @@ -98,7 +98,7 @@ void UpdateDialog::loadFromJson(const JsonNode & node) node["changeLog"].getType() != JsonNode::JsonType::DATA_STRING || node["downloadLinks"].getType() != JsonNode::JsonType::DATA_STRUCT) //we need at least one link - other are optional { - ui->plainTextEdit->setPlainText(tr("Cannot read JSON from url or incorrect JSON data")); + ui->plainTextEdit->setPlainText(tr("Cannot read JSON from URL or incorrect JSON data")); return; } 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 c5c8e191b..3f6964c2d 100644 --- a/lib/ArtifactUtils.cpp +++ b/lib/ArtifactUtils.cpp @@ -11,15 +11,46 @@ #include "ArtifactUtils.h" #include "CArtHandler.h" -#include "GameSettings.h" +#include "IGameSettings.h" #include "spells/CSpellHandler.h" -#include "mapping/CMap.h" #include "mapObjects/CGHeroInstance.h" VCMI_LIB_NAMESPACE_BEGIN +DLL_LINKAGE bool ArtifactUtils::checkIfSlotValid(const CArtifactSet & artSet, const ArtifactPosition & slot) +{ + if(artSet.bearerType() == ArtBearer::HERO) + { + if(isSlotEquipment(slot) || isSlotBackpack(slot) || slot == ArtifactPosition::TRANSITION_POS) + return true; + } + else if(artSet.bearerType() == ArtBearer::ALTAR) + { + if(isSlotBackpack(slot)) + return true; + } + else if(artSet.bearerType() == ArtBearer::COMMANDER) + { + if(vstd::contains(commanderSlots(), slot)) + return true; + } + else if(artSet.bearerType() == ArtBearer::CREATURE) + { + if(slot == ArtifactPosition::CREATURE_SLOT) + return true; + } + return false; +} + DLL_LINKAGE ArtifactPosition ArtifactUtils::getArtAnyPosition(const CArtifactSet * target, const ArtifactID & aid) +{ + if(auto targetSlot = getArtEquippedPosition(target, aid); targetSlot != ArtifactPosition::PRE_FIRST) + return targetSlot; + return getArtBackpackPosition(target, aid); +} + +DLL_LINKAGE ArtifactPosition ArtifactUtils::getArtEquippedPosition(const CArtifactSet * target, const ArtifactID & aid) { const auto * art = aid.toArtifact(); for(const auto & slot : art->getPossibleSlots().at(target->bearerType())) @@ -27,7 +58,7 @@ DLL_LINKAGE ArtifactPosition ArtifactUtils::getArtAnyPosition(const CArtifactSet if(art->canBePutAt(target, slot)) return slot; } - return getArtBackpackPosition(target, aid); + return ArtifactPosition::PRE_FIRST; } DLL_LINKAGE ArtifactPosition ArtifactUtils::getArtBackpackPosition(const CArtifactSet * target, const ArtifactID & aid) @@ -153,7 +184,7 @@ DLL_LINKAGE bool ArtifactUtils::isBackpackFreeSlots(const CArtifactSet * target, { if(target->bearerType() == ArtBearer::HERO) { - const auto backpackCap = VLC->settings()->getInteger(EGameSettings::HEROES_BACKPACK_CAP); + const auto backpackCap = VLC->engineSettings()->getInteger(EGameSettings::HEROES_BACKPACK_CAP); if(backpackCap < 0) return true; else @@ -164,93 +195,75 @@ DLL_LINKAGE bool ArtifactUtils::isBackpackFreeSlots(const CArtifactSet * target, } DLL_LINKAGE std::vector ArtifactUtils::assemblyPossibilities( - const CArtifactSet * artSet, const ArtifactID & aid) + const CArtifactSet * artSet, const ArtifactID & aid, const bool onlyEquiped) { std::vector arts; const auto * art = aid.toArtifact(); 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(), false, 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; } -DLL_LINKAGE CArtifactInstance * ArtifactUtils::createScroll(const SpellID & sid) +DLL_LINKAGE CArtifactInstance * ArtifactUtils::createScroll(const SpellID & spellId) { - auto ret = new CArtifactInstance(ArtifactID(ArtifactID::SPELL_SCROLL).toArtifact()); - auto bonus = std::make_shared(BonusDuration::PERMANENT, BonusType::SPELL, - BonusSource::ARTIFACT_INSTANCE, -1, BonusSourceID(ArtifactID(ArtifactID::SPELL_SCROLL)), BonusSubtypeID(sid)); - ret->addNewBonus(bonus); - return ret; + return ArtifactUtils::createArtifact(ArtifactID::SPELL_SCROLL, spellId); } -DLL_LINKAGE CArtifactInstance * ArtifactUtils::createNewArtifactInstance(const CArtifact * art) +DLL_LINKAGE CArtifactInstance * ArtifactUtils::createArtifact(const ArtifactID & artId, const SpellID & spellId) { - assert(art); - - auto * artInst = new CArtifactInstance(art); - if(art->isCombined()) + const std::function createArtInst = + [&createArtInst, &spellId](const CArtifact * art) -> CArtifactInstance* { - for(const auto & part : art->getConstituents()) - artInst->addPart(ArtifactUtils::createNewArtifactInstance(part), ArtifactPosition::PRE_FIRST); - } - if(art->isGrowing()) - { - auto bonus = std::make_shared(); - bonus->type = BonusType::LEVEL_COUNTER; - bonus->val = 0; - artInst->addNewBonus(bonus); - } - return artInst; -} + assert(art); -DLL_LINKAGE CArtifactInstance * ArtifactUtils::createNewArtifactInstance(const ArtifactID & aid) -{ - return ArtifactUtils::createNewArtifactInstance(aid.toArtifact()); -} - -DLL_LINKAGE CArtifactInstance * ArtifactUtils::createArtifact(CMap * map, const ArtifactID & aid, SpellID spellID) -{ - CArtifactInstance * art = nullptr; - if(aid.getNum() >= 0) - { - if(spellID == SpellID::NONE) + auto * artInst = new CArtifactInstance(art); + if(art->isCombined() && !art->isFused()) { - art = ArtifactUtils::createNewArtifactInstance(aid); + for(const auto & part : art->getConstituents()) + artInst->addPart(createArtInst(part), ArtifactPosition::PRE_FIRST); } - else + if(art->isGrowing()) { - art = ArtifactUtils::createScroll(spellID); + auto bonus = std::make_shared(); + bonus->type = BonusType::LEVEL_COUNTER; + bonus->val = 0; + artInst->addNewBonus(bonus); } + if(art->isScroll()) + { + artInst->addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::SPELL, + BonusSource::ARTIFACT_INSTANCE, -1, BonusSourceID(ArtifactID(ArtifactID::SPELL_SCROLL)), BonusSubtypeID(spellId))); + } + return artInst; + }; + + if(artId.getNum() >= 0) + { + return createArtInst(artId.toArtifact()); } else { - art = new CArtifactInstance(); // random, empty + return new CArtifactInstance(); // random, empty } - map->addNewArtifactInstance(art); - if(art->artType && art->isCombined()) - { - for(auto & part : art->getPartsInfo()) - { - map->addNewArtifactInstance(part.art); - } - } - return art; } DLL_LINKAGE void ArtifactUtils::insertScrrollSpellName(std::string & description, const SpellID & sid) diff --git a/lib/ArtifactUtils.h b/lib/ArtifactUtils.h index de3a5a027..51a824aef 100644 --- a/lib/ArtifactUtils.h +++ b/lib/ArtifactUtils.h @@ -21,12 +21,12 @@ class CGHeroInstance; class CArtifactSet; class CArtifactInstance; struct ArtSlotInfo; -class CMap; namespace ArtifactUtils { - // Calculates where an artifact gets placed when it gets transferred from one hero to another. + DLL_LINKAGE bool checkIfSlotValid(const CArtifactSet & artSet, const ArtifactPosition & slot); DLL_LINKAGE ArtifactPosition getArtAnyPosition(const CArtifactSet * target, const ArtifactID & aid); + DLL_LINKAGE ArtifactPosition getArtEquippedPosition(const CArtifactSet * target, const ArtifactID & aid); DLL_LINKAGE ArtifactPosition getArtBackpackPosition(const CArtifactSet * target, const ArtifactID & aid); // TODO: Make this constexpr when the toolset is upgraded DLL_LINKAGE const std::vector & unmovableSlots(); @@ -38,11 +38,9 @@ namespace ArtifactUtils DLL_LINKAGE bool isSlotBackpack(const ArtifactPosition & slot); DLL_LINKAGE bool isSlotEquipment(const ArtifactPosition & slot); DLL_LINKAGE bool isBackpackFreeSlots(const CArtifactSet * target, const size_t reqSlots = 1); - DLL_LINKAGE std::vector assemblyPossibilities(const CArtifactSet * artSet, const ArtifactID & aid); - DLL_LINKAGE CArtifactInstance * createScroll(const SpellID & sid); - DLL_LINKAGE CArtifactInstance * createNewArtifactInstance(const CArtifact * art); - DLL_LINKAGE CArtifactInstance * createNewArtifactInstance(const ArtifactID & aid); - DLL_LINKAGE CArtifactInstance * createArtifact(CMap * map, const ArtifactID & aid, SpellID spellID = SpellID::NONE); + DLL_LINKAGE std::vector assemblyPossibilities(const CArtifactSet * artSet, const ArtifactID & aid, const bool onlyEquiped = false); + DLL_LINKAGE CArtifactInstance * createScroll(const SpellID & spellId); + DLL_LINKAGE CArtifactInstance * createArtifact(const ArtifactID & artId, const SpellID & spellId = SpellID::NONE); DLL_LINKAGE void insertScrrollSpellName(std::string & description, const SpellID & sid); } diff --git a/lib/BasicTypes.cpp b/lib/BasicTypes.cpp index b366bda55..dec7cbaba 100644 --- a/lib/BasicTypes.cpp +++ b/lib/BasicTypes.cpp @@ -12,7 +12,7 @@ #include "VCMI_Lib.h" #include "GameConstants.h" -#include "GameSettings.h" +#include "IGameSettings.h" #include "bonuses/BonusList.h" #include "bonuses/Bonus.h" #include "bonuses/IBonusBearer.h" @@ -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 @@ -86,14 +86,14 @@ int AFactionMember::getPrimSkillLevel(PrimarySkill id) const static const std::string keyAllSkills = "type_PRIMARY_SKILL"; auto allSkills = getBonusBearer()->getBonuses(selectorAllSkills, keyAllSkills); auto ret = allSkills->valOfBonuses(Selector::subtype()(BonusSubtypeID(id))); - auto minSkillValue = (id == PrimarySkill::SPELL_POWER || id == PrimarySkill::KNOWLEDGE) ? 1 : 0; + auto minSkillValue = VLC->engineSettings()->getVector(EGameSettings::HEROES_MINIMAL_PRIMARY_SKILLS)[id.getNum()]; return std::max(ret, minSkillValue); //otherwise, some artifacts may cause negative skill value effect, sp=0 works in old saves } int AFactionMember::moraleValAndBonusList(TConstBonusListPtr & bonusList) const { - int32_t maxGoodMorale = VLC->settings()->getVector(EGameSettings::COMBAT_GOOD_MORALE_DICE).size(); - int32_t maxBadMorale = - (int32_t) VLC->settings()->getVector(EGameSettings::COMBAT_BAD_MORALE_DICE).size(); + int32_t maxGoodMorale = VLC->engineSettings()->getVector(EGameSettings::COMBAT_GOOD_MORALE_DICE).size(); + int32_t maxBadMorale = - (int32_t) VLC->engineSettings()->getVector(EGameSettings::COMBAT_BAD_MORALE_DICE).size(); if(getBonusBearer()->hasBonusOfType(BonusType::MAX_MORALE)) { @@ -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"; @@ -123,8 +123,8 @@ int AFactionMember::moraleValAndBonusList(TConstBonusListPtr & bonusList) const int AFactionMember::luckValAndBonusList(TConstBonusListPtr & bonusList) const { - int32_t maxGoodLuck = VLC->settings()->getVector(EGameSettings::COMBAT_GOOD_LUCK_DICE).size(); - int32_t maxBadLuck = - (int32_t) VLC->settings()->getVector(EGameSettings::COMBAT_BAD_LUCK_DICE).size(); + int32_t maxGoodLuck = VLC->engineSettings()->getVector(EGameSettings::COMBAT_GOOD_LUCK_DICE).size(); + int32_t maxBadLuck = - (int32_t) VLC->engineSettings()->getVector(EGameSettings::COMBAT_BAD_LUCK_DICE).size(); if(getBonusBearer()->hasBonusOfType(BonusType::MAX_LUCK)) { @@ -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/BattleFieldHandler.cpp b/lib/BattleFieldHandler.cpp index 4d1eb5591..ff265d2e5 100644 --- a/lib/BattleFieldHandler.cpp +++ b/lib/BattleFieldHandler.cpp @@ -15,11 +15,11 @@ VCMI_LIB_NAMESPACE_BEGIN -BattleFieldInfo * BattleFieldHandler::loadFromJson(const std::string & scope, const JsonNode & json, const std::string & identifier, size_t index) +std::shared_ptr BattleFieldHandler::loadFromJson(const std::string & scope, const JsonNode & json, const std::string & identifier, size_t index) { assert(identifier.find(':') == std::string::npos); - auto * info = new BattleFieldInfo(BattleField(index), identifier); + auto info = std::make_shared(BattleField(index), identifier); info->modScope = scope; info->graphics = ImagePath::fromJson(json["graphics"]); @@ -40,6 +40,9 @@ BattleFieldInfo * BattleFieldHandler::loadFromJson(const std::string & scope, co for(auto node : json["impassableHexes"].Vector()) info->impassableHexes.emplace_back(node.Integer()); + info->openingSoundFilename = AudioPath::fromJson(json["openingSound"]); + info->musicFilename = AudioPath::fromJson(json["music"]); + return info; } @@ -70,6 +73,11 @@ std::string BattleFieldInfo::getJsonKey() const return modScope + ':' + identifier; } +std::string BattleFieldInfo::getModScope() const +{ + return modScope; +} + std::string BattleFieldInfo::getNameTextID() const { return name; diff --git a/lib/BattleFieldHandler.h b/lib/BattleFieldHandler.h index b338cd772..f95e01c89 100644 --- a/lib/BattleFieldHandler.h +++ b/lib/BattleFieldHandler.h @@ -32,6 +32,8 @@ public: std::string icon; si32 iconIndex; std::vector impassableHexes; + AudioPath openingSoundFilename; + AudioPath musicFilename; BattleFieldInfo() : BattleFieldInfo(BattleField::NONE, "") @@ -50,6 +52,7 @@ public: int32_t getIndex() const override; int32_t getIconIndex() const override; std::string getJsonKey() const override; + std::string getModScope() const override; std::string getNameTextID() const override; std::string getNameTranslated() const override; void registerIcons(const IconRegistar & cb) const override; @@ -64,7 +67,7 @@ public: class BattleFieldHandler : public CHandlerBase { public: - virtual BattleFieldInfo * loadFromJson( + std::shared_ptr loadFromJson( const std::string & scope, const JsonNode & json, const std::string & identifier, diff --git a/lib/CArtHandler.cpp b/lib/CArtHandler.cpp index 9cdd430e9..0232109c5 100644 --- a/lib/CArtHandler.cpp +++ b/lib/CArtHandler.cpp @@ -11,15 +11,16 @@ #include "StdInc.h" #include "ArtifactUtils.h" -#include "CGeneralTextHandler.h" #include "ExceptionsCommon.h" -#include "GameSettings.h" +#include "IGameSettings.h" #include "mapObjects/MapObjects.h" #include "constants/StringConstants.h" #include "json/JsonBonus.h" #include "mapObjectConstructors/AObjectTypeHandler.h" #include "mapObjectConstructors/CObjectClassesHandler.h" #include "serializer/JsonSerializeFormat.h" +#include "texts/CGeneralTextHandler.h" +#include "texts/CLegacyConfigParser.h" // Note: list must match entries in ArtTraits.txt #define ART_POS_LIST \ @@ -55,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; @@ -105,6 +121,11 @@ std::string CArtifact::getJsonKey() const return modScope + ':' + identifier; } +std::string CArtifact::getModScope() const +{ + return modScope; +} + void CArtifact::registerIcons(const IconRegistar & cb) const { cb(getIconIndex(), 0, "ARTIFACT", image); @@ -171,7 +192,6 @@ bool CArtifact::isTradable() const switch(id.toEnum()) { case ArtifactID::SPELLBOOK: - case ArtifactID::GRAIL: return false; default: return !isBig(); @@ -197,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; @@ -214,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 { @@ -321,7 +341,7 @@ CArtHandler::~CArtHandler() = default; std::vector CArtHandler::loadLegacyData() { - size_t dataSize = VLC->settings()->getInteger(EGameSettings::TEXTS_ARTIFACT); + size_t dataSize = VLC->engineSettings()->getInteger(EGameSettings::TEXTS_ARTIFACT); objects.resize(dataSize); std::vector h3Data; @@ -373,7 +393,7 @@ std::vector CArtHandler::loadLegacyData() void CArtHandler::loadObject(std::string scope, std::string name, const JsonNode & data) { - auto * object = loadFromJson(scope, data, name, objects.size()); + auto object = loadFromJson(scope, data, name, objects.size()); object->iconIndex = object->getIndex() + 5; @@ -384,7 +404,7 @@ void CArtHandler::loadObject(std::string scope, std::string name, const JsonNode void CArtHandler::loadObject(std::string scope, std::string name, const JsonNode & data, size_t index) { - auto * object = loadFromJson(scope, data, name, index); + auto object = loadFromJson(scope, data, name, index); object->iconIndex = object->getIndex(); @@ -400,12 +420,12 @@ const std::vector & CArtHandler::getTypeNames() const return typeNames; } -CArtifact * CArtHandler::loadFromJson(const std::string & scope, const JsonNode & node, const std::string & identifier, size_t index) +std::shared_ptr CArtHandler::loadFromJson(const std::string & scope, const JsonNode & node, const std::string & identifier, size_t index) { assert(identifier.find(':') == std::string::npos); assert(!scope.empty()); - CArtifact * art = new CArtifact(); + auto art = std::make_shared(); if(!node["growing"].isNull()) { for(auto bonus : node["growing"]["bonusesPerLevel"].Vector()) @@ -425,9 +445,9 @@ CArtifact * CArtHandler::loadFromJson(const std::string & scope, const JsonNode 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(); @@ -442,10 +462,10 @@ CArtifact * CArtHandler::loadFromJson(const std::string & scope, const JsonNode art->price = static_cast(node["value"].Float()); art->onlyOnWaterMap = node["onlyOnWaterMap"].Bool(); - loadSlots(art, node); - loadClass(art, node); - loadType(art, node); - loadComponents(art, node); + loadSlots(art.get(), node); + loadClass(art.get(), node); + loadType(art.get(), node); + loadComponents(art.get(), node); for(const auto & b : node["bonuses"].Vector()) { @@ -454,7 +474,7 @@ CArtifact * CArtHandler::loadFromJson(const std::string & scope, const JsonNode } const JsonNode & warMachine = node["warMachine"]; - if(warMachine.getType() == JsonNode::JsonType::DATA_STRING && !warMachine.String().empty()) + if(!warMachine.isNull()) { VLC->identifiers()->requestIdentifier("creature", warMachine, [=](si32 id) { @@ -600,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) @@ -650,10 +672,10 @@ bool CArtHandler::legalArtifact(const ArtifactID & id) const if(art->possibleSlots.count(ArtBearer::HERO) && !art->possibleSlots.at(ArtBearer::HERO).empty()) return true; - if(art->possibleSlots.count(ArtBearer::CREATURE) && !art->possibleSlots.at(ArtBearer::CREATURE).empty() && VLC->settings()->getBoolean(EGameSettings::MODULE_STACK_ARTIFACT)) + if(art->possibleSlots.count(ArtBearer::CREATURE) && !art->possibleSlots.at(ArtBearer::CREATURE).empty() && VLC->engineSettings()->getBoolean(EGameSettings::MODULE_STACK_ARTIFACT)) return true; - if(art->possibleSlots.count(ArtBearer::COMMANDER) && !art->possibleSlots.at(ArtBearer::COMMANDER).empty() && VLC->settings()->getBoolean(EGameSettings::MODULE_COMMANDERS)) + if(art->possibleSlots.count(ArtBearer::COMMANDER) && !art->possibleSlots.at(ArtBearer::COMMANDER).empty() && VLC->engineSettings()->getBoolean(EGameSettings::MODULE_COMMANDERS)) return true; return false; @@ -663,7 +685,7 @@ std::set CArtHandler::getDefaultAllowed() const { std::set allowedArtifacts; - for (auto artifact : objects) + for (const auto & artifact : objects) { if (!artifact->isCombined()) allowedArtifacts.insert(artifact->getId()); @@ -685,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)) { @@ -698,80 +718,45 @@ 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; + } + 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 result; -} - -ArtifactPosition CArtifactSet::getArtPos(const CArtifactInstance *art) const -{ - for(auto i : artifactsWorn) - if(i.second.artifact == art) - return i.first; - - for(int i = 0; i < artifactsInBackpack.size(); i++) - if(artifactsInBackpack[i].artifact == art) - return ArtifactPosition::BACKPACK_START + i; - 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; return nullptr; } -const ArtifactPosition CArtifactSet::getSlotByInstance(const CArtifactInstance * artInst) const +ArtifactPosition CArtifactSet::getArtPos(const CArtifactInstance * artInst) const { 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; @@ -786,38 +771,44 @@ const ArtifactPosition CArtifactSet::getSlotByInstance(const CArtifactInstance * 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; @@ -829,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()) @@ -865,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) { @@ -874,36 +884,18 @@ 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 { if(pos == ArtifactPosition::TRANSITION_POS) - { - // Always add to the end. Always take from the beginning. - if(artifactsTransitionPos.empty()) - return nullptr; - else - return &(*artifactsTransitionPos.begin()); - } + return &artifactsTransitionPos; if(vstd::contains(artifactsWorn, pos)) return &artifactsWorn.at(pos); if(ArtifactUtils::isSlotBackpack(pos)) @@ -918,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) @@ -929,50 +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) - { - // Always add to the end. Always take from the beginning. - artifactsTransitionPos.emplace_back(); - slotInfo = &artifactsTransitionPos.back(); - } - 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) - { - assert(!artifactsTransitionPos.empty()); - artifactsTransitionPos.erase(artifactsTransitionPos.begin()); - } - 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) @@ -980,7 +941,7 @@ void CArtifactSet::artDeserializationFix(CBonusSystemNode *node) node->attachTo(*elem.second.artifact); } -void CArtifactSet::serializeJsonArtifacts(JsonSerializeFormat & handler, const std::string & fieldName, CMap * map) +void CArtifactSet::serializeJsonArtifacts(JsonSerializeFormat & handler, const std::string & fieldName) { //todo: creature and commander artifacts if(handler.saving && artifactsInBackpack.empty() && artifactsWorn.empty()) @@ -988,7 +949,6 @@ void CArtifactSet::serializeJsonArtifacts(JsonSerializeFormat & handler, const s if(!handler.saving) { - assert(map); artifactsInBackpack.clear(); artifactsWorn.clear(); } @@ -998,13 +958,13 @@ void CArtifactSet::serializeJsonArtifacts(JsonSerializeFormat & handler, const s switch(bearerType()) { case ArtBearer::HERO: - serializeJsonHero(handler, map); + serializeJsonHero(handler); break; case ArtBearer::CREATURE: - serializeJsonCreature(handler, map); + serializeJsonCreature(handler); break; case ArtBearer::COMMANDER: - serializeJsonCommander(handler, map); + serializeJsonCommander(handler); break; default: assert(false); @@ -1012,11 +972,11 @@ void CArtifactSet::serializeJsonArtifacts(JsonSerializeFormat & handler, const s } } -void CArtifactSet::serializeJsonHero(JsonSerializeFormat & handler, CMap * map) +void CArtifactSet::serializeJsonHero(JsonSerializeFormat & handler) { for(const auto & slot : ArtifactUtils::allWornSlots()) { - serializeJsonSlot(handler, slot, map); + serializeJsonSlot(handler, slot); } std::vector backpackTemp; @@ -1032,9 +992,9 @@ void CArtifactSet::serializeJsonHero(JsonSerializeFormat & handler, CMap * map) { for(const ArtifactID & artifactID : backpackTemp) { - auto * artifact = ArtifactUtils::createArtifact(map, artifactID); + 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); @@ -1043,17 +1003,17 @@ void CArtifactSet::serializeJsonHero(JsonSerializeFormat & handler, CMap * map) } } -void CArtifactSet::serializeJsonCreature(JsonSerializeFormat & handler, CMap * map) +void CArtifactSet::serializeJsonCreature(JsonSerializeFormat & handler) { logGlobal->error("CArtifactSet::serializeJsonCreature not implemented"); } -void CArtifactSet::serializeJsonCommander(JsonSerializeFormat & handler, CMap * map) +void CArtifactSet::serializeJsonCommander(JsonSerializeFormat & handler) { logGlobal->error("CArtifactSet::serializeJsonCommander not implemented"); } -void CArtifactSet::serializeJsonSlot(JsonSerializeFormat & handler, const ArtifactPosition & slot, CMap * map) +void CArtifactSet::serializeJsonSlot(JsonSerializeFormat & handler, const ArtifactPosition & slot) { ArtifactID artifactID; @@ -1073,9 +1033,9 @@ void CArtifactSet::serializeJsonSlot(JsonSerializeFormat & handler, const Artifa if(artifactID != ArtifactID::NONE) { - auto * artifact = ArtifactUtils::createArtifact(map, artifactID.toEnum()); + 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); @@ -1088,8 +1048,8 @@ void CArtifactSet::serializeJsonSlot(JsonSerializeFormat & handler, const Artifa } } -CArtifactFittingSet::CArtifactFittingSet(ArtBearer::ArtBearer Bearer): - Bearer(Bearer) +CArtifactFittingSet::CArtifactFittingSet(ArtBearer::ArtBearer bearer) + : bearer(bearer) { } @@ -1103,7 +1063,7 @@ CArtifactFittingSet::CArtifactFittingSet(const CArtifactSet & artSet) ArtBearer::ArtBearer CArtifactFittingSet::bearerType() const { - return this->Bearer; + return this->bearer; } VCMI_LIB_NAMESPACE_END diff --git a/lib/CArtHandler.h b/lib/CArtHandler.h index c840e8d45..a902f22c8 100644 --- a/lib/CArtHandler.h +++ b/lib/CArtHandler.h @@ -14,8 +14,10 @@ #include "bonuses/Bonus.h" #include "bonuses/CBonusSystemNode.h" +#include "ConstTransitivePtr.h" #include "GameConstants.h" #include "IHandlerBase.h" +#include "serializer/Serializeable.h" VCMI_LIB_NAMESPACE_BEGIN @@ -23,8 +25,6 @@ class CArtHandler; class CGHeroInstance; class CArtifactSet; class CArtifactInstance; -class CRandomGenerator; -class CMap; class JsonSerializeFormat; #define ART_BEARER_LIST \ @@ -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 @@ -105,6 +110,7 @@ public: int32_t getIndex() const override; int32_t getIconIndex() const override; std::string getJsonKey() const override; + std::string getModScope() const override; void registerIcons(const IconRegistar & cb) const override; ArtifactID getId() const override; const IBonusBearer * getBonusBearer() const override; @@ -162,7 +168,7 @@ public: protected: const std::vector & getTypeNames() const override; - CArtifact * loadFromJson(const std::string & scope, const JsonNode & json, const std::string & identifier, size_t index) override; + std::shared_ptr loadFromJson(const std::string & scope, const JsonNode & json, const std::string & identifier, size_t index) override; private: void addSlot(CArtifact * art, const std::string & slotID) const; @@ -174,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) @@ -187,42 +193,29 @@ struct DLL_LINKAGE ArtSlotInfo } }; -class DLL_LINKAGE CArtifactSet +class DLL_LINKAGE CArtifactSet : public virtual Serializeable { public: using ArtPlacementMap = std::map; std::vector artifactsInBackpack; //hero's artifacts from bag 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 - std::vector artifactsTransitionPos; // Used as transition position for dragAndDrop artifact exchange - - void setNewArtSlot(const ArtifactPosition & slot, ConstTransitivePtr art, bool locked); - void eraseArtSlot(const ArtifactPosition & slot); + ArtSlotInfo artifactsTransitionPos; // Used as transition position for dragAndDrop artifact exchange 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; + ArtifactPosition getArtPos(const CArtifactInstance * art) const; const CArtifactInstance * getArtByInstanceId(const ArtifactInstanceID & artInstId) const; - const ArtifactPosition getSlotByInstance(const CArtifactInstance * artInst) 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) { @@ -232,16 +225,15 @@ public: void artDeserializationFix(CBonusSystemNode *node); - void serializeJsonArtifacts(JsonSerializeFormat & handler, const std::string & fieldName, CMap * map); -protected: - std::pair searchForConstituent(const ArtifactID & aid) const; + void serializeJsonArtifacts(JsonSerializeFormat & handler, const std::string & fieldName); + const CArtifactInstance * getCombinedArtWithPart(const ArtifactID & partId) const; private: - void serializeJsonHero(JsonSerializeFormat & handler, CMap * map); - void serializeJsonCreature(JsonSerializeFormat & handler, CMap * map); - void serializeJsonCommander(JsonSerializeFormat & handler, CMap * map); + void serializeJsonHero(JsonSerializeFormat & handler); + void serializeJsonCreature(JsonSerializeFormat & handler); + void serializeJsonCommander(JsonSerializeFormat & handler); - void serializeJsonSlot(JsonSerializeFormat & handler, const ArtifactPosition & slot, CMap * map);//normal slots + void serializeJsonSlot(JsonSerializeFormat & handler, const ArtifactPosition & slot);//normal slots }; // Used to try on artifacts before the claimed changes have been applied @@ -253,7 +245,7 @@ public: ArtBearer::ArtBearer bearerType() const override; protected: - ArtBearer::ArtBearer Bearer; + ArtBearer::ArtBearer bearer; }; VCMI_LIB_NAMESPACE_END 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 58bbd0b9b..3c142b063 100644 --- a/lib/CBonusTypeHandler.cpp +++ b/lib/CBonusTypeHandler.cpp @@ -15,11 +15,13 @@ #include "filesystem/Filesystem.h" -#include "GameConstants.h" #include "CCreatureHandler.h" -#include "CGeneralTextHandler.h" -#include "json/JsonUtils.h" +#include "GameConstants.h" +#include "VCMI_Lib.h" +#include "modding/ModScope.h" #include "spells/CSpellHandler.h" +#include "texts/CGeneralTextHandler.h" +#include "json/JsonUtils.h" template class std::vector; @@ -199,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); } @@ -239,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/CConsoleHandler.cpp b/lib/CConsoleHandler.cpp index 6d2fc3640..c3dbd6fce 100644 --- a/lib/CConsoleHandler.cpp +++ b/lib/CConsoleHandler.cpp @@ -13,6 +13,8 @@ #include "CThreadHelper.h" +#include + VCMI_LIB_NAMESPACE_BEGIN std::mutex CConsoleHandler::smx; @@ -138,41 +140,10 @@ static void createMemoryDump(MINIDUMP_EXCEPTION_INFORMATION * meinfo) | MiniDumpWithThreadInfo); } - MiniDumpWriteDump(GetCurrentProcess(), GetCurrentProcessId(), dfile, dumpType, meinfo, 0, 0); + MiniDumpWriteDump(GetCurrentProcess(), GetCurrentProcessId(), dfile, dumpType, meinfo, nullptr, nullptr); MessageBoxA(0, "VCMI has crashed. We are sorry. File with information about encountered problem has been created.", "VCMI Crashhandler", MB_OK | MB_ICONERROR); } -static void onTerminate() -{ - logGlobal->error("Disaster happened."); - try - { - std::exception_ptr eptr{std::current_exception()}; - if (eptr) - { - std::rethrow_exception(eptr); - } - else - { - logGlobal->error("...but no current exception found!"); - } - } - catch (const std::exception& exc) - { - logGlobal->error("Reason: %s", exc.what()); - } - catch (...) - { - logGlobal->error("Reason: unknown exception!"); - } - - const DWORD threadId = ::GetCurrentThreadId(); - logGlobal->error("Thread ID: %d", threadId); - - createMemoryDump(nullptr); - std::abort(); -} - LONG WINAPI onUnhandledException(EXCEPTION_POINTERS* exception) { logGlobal->error("Disaster happened."); @@ -194,8 +165,48 @@ LONG WINAPI onUnhandledException(EXCEPTION_POINTERS* exception) return EXCEPTION_EXECUTE_HANDLER; } + #endif +#ifdef NDEBUG +[[noreturn]] static void onTerminate() +{ + logGlobal->error("Disaster happened."); + try + { + std::exception_ptr eptr{std::current_exception()}; + if (eptr) + { + std::rethrow_exception(eptr); + } + else + { + logGlobal->error("...but no current exception found!"); + } + } + catch (const std::exception& exc) + { + logGlobal->error("Reason: %s", exc.what()); + } + catch (...) + { + logGlobal->error("Reason: unknown exception!"); + } + + logGlobal->error("Call stack information:"); + std::stringstream stream; + stream << boost::stacktrace::stacktrace(); + logGlobal->error("%s", stream.str()); + +#ifdef VCMI_WINDOWS + const DWORD threadId = ::GetCurrentThreadId(); + logGlobal->error("Thread ID: %d", threadId); + + createMemoryDump(nullptr); +#endif + std::abort(); +} +#endif void CConsoleHandler::setColor(EConsoleTextColor::EConsoleTextColor color) { @@ -287,13 +298,16 @@ CConsoleHandler::CConsoleHandler(): GetConsoleScreenBufferInfo(handleErr, &csbi); defErrColor = csbi.wAttributes; -#ifndef _DEBUG +#ifdef NDEBUG SetUnhandledExceptionFilter(onUnhandledException); - std::set_terminate(onTerminate); #endif #else defColor = "\x1b[0m"; #endif + +#ifdef NDEBUG + std::set_terminate(onTerminate); +#endif } CConsoleHandler::~CConsoleHandler() { diff --git a/lib/CConsoleHandler.h b/lib/CConsoleHandler.h index aef086329..657a76155 100644 --- a/lib/CConsoleHandler.h +++ b/lib/CConsoleHandler.h @@ -85,7 +85,7 @@ private: static void setColor(EConsoleTextColor::EConsoleTextColor color); //sets color of text appropriate for given logging level - /// FIXME: Implement CConsoleHandler as singleton, move some logic into CLogConsoleTarget, etc... needs to be disussed:) + /// FIXME: Implement CConsoleHandler as singleton, move some logic into CLogConsoleTarget, etc... needs to be discussed:) /// Without static, application will crash complaining about mutex deleted. In short: CConsoleHandler gets deleted before /// the logging system. static std::mutex smx; diff --git a/lib/CCreatureHandler.cpp b/lib/CCreatureHandler.cpp index e5591c6c2..ee445e617 100644 --- a/lib/CCreatureHandler.cpp +++ b/lib/CCreatureHandler.cpp @@ -10,24 +10,27 @@ #include "StdInc.h" #include "CCreatureHandler.h" -#include "CGeneralTextHandler.h" #include "ResourceSet.h" +#include "entities/faction/CFaction.h" +#include "entities/faction/CTownHandler.h" #include "filesystem/Filesystem.h" #include "VCMI_Lib.h" -#include "CRandomGenerator.h" -#include "CTownHandler.h" -#include "GameSettings.h" +#include "IGameSettings.h" #include "constants/StringConstants.h" #include "bonuses/Limiters.h" #include "bonuses/Updaters.h" #include "json/JsonBonus.h" #include "serializer/JsonDeserializer.h" #include "serializer/JsonUpdater.h" +#include "texts/CGeneralTextHandler.h" +#include "texts/CLegacyConfigParser.h" #include "mapObjectConstructors/AObjectTypeHandler.h" #include "mapObjectConstructors/CObjectClassesHandler.h" #include "modding/CModHandler.h" #include "ExceptionsCommon.h" +#include + VCMI_LIB_NAMESPACE_BEGIN const std::map CCreature::creatureQuantityRanges = @@ -58,6 +61,11 @@ std::string CCreature::getJsonKey() const return modScope + ':' + identifier; } +std::string CCreature::getModScope() const +{ + return modScope; +} + void CCreature::registerIcons(const IconRegistar & cb) const { cb(getIconIndex(), 0, "CPRSMALL", smallIconName); @@ -109,7 +117,7 @@ int32_t CCreature::getHorde() const return hordeGrowth; } -FactionID CCreature::getFaction() const +FactionID CCreature::getFactionID() const { return FactionID(faction); } @@ -335,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() + "\""; @@ -459,59 +462,9 @@ 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->settings()->getInteger(EGameSettings::TEXTS_CREATURE); + size_t dataSize = VLC->engineSettings()->getInteger(EGameSettings::TEXTS_CREATURE); objects.resize(dataSize); std::vector h3Data; @@ -578,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); } @@ -588,12 +541,12 @@ std::vector CCreatureHandler::loadLegacyData() return h3Data; } -CCreature * CCreatureHandler::loadFromJson(const std::string & scope, const JsonNode & node, const std::string & identifier, size_t index) +std::shared_ptr CCreatureHandler::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 * cre = new CCreature(); + auto cre = std::make_shared(); if(node["hasDoubleWeek"].Bool()) { @@ -609,9 +562,9 @@ CCreature * CCreatureHandler::loadFromJson(const std::string & scope, const Json 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); @@ -636,9 +589,9 @@ CCreature * CCreatureHandler::loadFromJson(const std::string & scope, const Json if(!node["shots"].isNull()) cre->addBonus(node["shots"].Integer(), BonusType::SHOTS); - loadStackExperience(cre, node["stackExperience"]); - loadJsonAnimation(cre, node["graphics"]); - loadCreatureJson(cre, node); + loadStackExperience(cre.get(), node["stackExperience"]); + loadJsonAnimation(cre.get(), node["graphics"]); + loadCreatureJson(cre.get(), node); for(const auto & extraName : node["extraNames"].Vector()) { @@ -696,7 +649,7 @@ const std::vector & CCreatureHandler::getTypeNames() const void CCreatureHandler::loadCrExpMod() { - if (VLC->settings()->getBoolean(EGameSettings::MODULE_STACK_EXPERIENCE)) //reading default stack experience values + if (VLC->engineSettings()->getBoolean(EGameSettings::MODULE_STACK_EXPERIENCE)) //reading default stack experience values { //Calculate rank exp values, formula appears complicated bu no parsing needed expRanks.resize(8); @@ -746,7 +699,7 @@ void CCreatureHandler::loadCrExpMod() void CCreatureHandler::loadCrExpBon(CBonusSystemNode & globalEffects) { - if (VLC->settings()->getBoolean(EGameSettings::MODULE_STACK_EXPERIENCE)) //reading default stack experience bonuses + if (VLC->engineSettings()->getBoolean(EGameSettings::MODULE_STACK_EXPERIENCE)) //reading default stack experience bonuses { logGlobal->debug("\tLoading stack experience bonuses"); auto addBonusForAllCreatures = [&](std::shared_ptr b) { @@ -757,7 +710,7 @@ void CCreatureHandler::loadCrExpBon(CBonusSystemNode & globalEffects) auto addBonusForTier = [&](int tier, std::shared_ptr b) { assert(vstd::iswithin(tier, 1, 7)); //bonuses from level 7 are given to high-level creatures too - auto max = tier == GameConstants::CREATURES_PER_TOWN ? std::numeric_limits::max() : tier + 1; + auto max = tier == 7 ? std::numeric_limits::max() : tier + 1; auto limiter = std::make_shared(tier, max); b->addLimiter(limiter); globalEffects.addNewBonus(b); @@ -825,7 +778,7 @@ void CCreatureHandler::loadAnimationInfo(std::vector &h3Data) const parser.endLine(); // header parser.endLine(); - for(int dd = 0; dd < VLC->settings()->getInteger(EGameSettings::TEXTS_CREATURE); ++dd) + for(int dd = 0; dd < VLC->engineSettings()->getInteger(EGameSettings::TEXTS_CREATURE); ++dd) { while (parser.isNextEntryEmpty() && parser.endLine()) // skip empty lines ; @@ -884,15 +837,15 @@ void CCreatureHandler::loadJsonAnimation(CCreature * cre, const JsonNode & graph const JsonNode & missile = graphics["missile"]; const JsonNode & offsets = missile["offset"]; - cre->animation.upperRightMissleOffsetX = static_cast(offsets["upperX"].Float()); - cre->animation.upperRightMissleOffsetY = static_cast(offsets["upperY"].Float()); - cre->animation.rightMissleOffsetX = static_cast(offsets["middleX"].Float()); - cre->animation.rightMissleOffsetY = static_cast(offsets["middleY"].Float()); - cre->animation.lowerRightMissleOffsetX = static_cast(offsets["lowerX"].Float()); - cre->animation.lowerRightMissleOffsetY = static_cast(offsets["lowerY"].Float()); + cre->animation.upperRightMissileOffsetX = static_cast(offsets["upperX"].Float()); + cre->animation.upperRightMissileOffsetY = static_cast(offsets["upperY"].Float()); + cre->animation.rightMissileOffsetX = static_cast(offsets["middleX"].Float()); + cre->animation.rightMissileOffsetY = static_cast(offsets["middleY"].Float()); + cre->animation.lowerRightMissileOffsetX = static_cast(offsets["lowerX"].Float()); + cre->animation.lowerRightMissileOffsetY = static_cast(offsets["lowerY"].Float()); cre->animation.attackClimaxFrame = static_cast(missile["attackClimaxFrame"].Float()); - cre->animation.missleFrameAngles = missile["frameAngles"].convertTo >(); + cre->animation.missileFrameAngles = missile["frameAngles"].convertTo >(); cre->smallIconName = graphics["iconSmall"].String(); cre->largeIconName = graphics["iconLarge"].String(); @@ -1166,7 +1119,7 @@ void CCreatureHandler::loadStackExp(Bonus & b, BonusList & bl, CLegacyConfigPars b.subtype = BonusSubtypeID(SpellID(SpellID::METEOR_SHOWER)); b.additionalInfo = 0;//normal immunity break; - case 'N': //dispell beneficial spells + case 'N': //dispel beneficial spells b.type = BonusType::SPELL_IMMUNITY; b.subtype = BonusSubtypeID(SpellID(SpellID::DISPEL_HELPFUL_SPELLS)); b.additionalInfo = 0;//normal immunity @@ -1362,7 +1315,7 @@ CCreatureHandler::~CCreatureHandler() p.first = nullptr; } -CreatureID CCreatureHandler::pickRandomMonster(CRandomGenerator & rand, int tier) const +CreatureID CCreatureHandler::pickRandomMonster(vstd::RNG & rand, int tier) const { std::vector allowed; for(const auto & creature : objects) diff --git a/lib/CCreatureHandler.h b/lib/CCreatureHandler.h index 71123cf94..16cf2ca16 100644 --- a/lib/CCreatureHandler.h +++ b/lib/CCreatureHandler.h @@ -23,11 +23,15 @@ VCMI_LIB_NAMESPACE_BEGIN +namespace vstd +{ +class RNG; +} + class CLegacyConfigParser; class CCreatureHandler; class CCreature; class JsonSerializeFormat; -class CRandomGenerator; class DLL_LINKAGE CCreature : public Creature, public CBonusSystemNode { @@ -91,10 +95,10 @@ public: double timeBetweenFidgets, idleAnimationTime, walkAnimationTime, attackAnimationTime; - int upperRightMissleOffsetX, rightMissleOffsetX, lowerRightMissleOffsetX, - upperRightMissleOffsetY, rightMissleOffsetY, lowerRightMissleOffsetY; + int upperRightMissileOffsetX, rightMissileOffsetX, lowerRightMissileOffsetX, + upperRightMissileOffsetY, rightMissileOffsetY, lowerRightMissileOffsetY; - std::vector missleFrameAngles; + std::vector missileFrameAngles; int attackClimaxFrame; AnimationPath projectileImageName; @@ -123,10 +127,11 @@ 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; + std::string getModScope() const override; void registerIcons(const IconRegistar & cb) const override; CreatureID getId() const override; const IBonusBearer * getBonusBearer() const override; @@ -161,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; @@ -192,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(); @@ -210,7 +211,7 @@ private: protected: const std::vector & getTypeNames() const override; - CCreature * loadFromJson(const std::string & scope, const JsonNode & node, const std::string & identifier, size_t index) override; + std::shared_ptr loadFromJson(const std::string & scope, const JsonNode & node, const std::string & identifier, size_t index) override; public: std::set doubledCreatures; //they get double week @@ -225,7 +226,7 @@ public: std::vector< std::vector > skillLevels; //how much of a bonus will be given to commander with every level. SPELL_POWER also gives CASTS and RESISTANCE std::vector , std::pair > > skillRequirements; // first - Bonus, second - which two skills are needed to use it - CreatureID pickRandomMonster(CRandomGenerator & rand, int tier = -1) const; //tier <1 - CREATURES_PER_TOWN> or -1 for any + CreatureID pickRandomMonster(vstd::RNG & rand, int tier = -1) const; //tier <1 - CREATURES_PER_TOWN> or -1 for any CCreatureHandler(); ~CCreatureHandler(); diff --git a/lib/CCreatureSet.cpp b/lib/CCreatureSet.cpp index 71c3fb0d5..46fa481cf 100644 --- a/lib/CCreatureSet.cpp +++ b/lib/CCreatureSet.cpp @@ -14,13 +14,13 @@ #include "CConfigHandler.h" #include "CCreatureHandler.h" #include "VCMI_Lib.h" -#include "GameSettings.h" +#include "IGameSettings.h" +#include "entities/hero/CHeroHandler.h" #include "mapObjects/CGHeroInstance.h" #include "modding/ModScope.h" #include "IGameCallback.h" -#include "CGeneralTextHandler.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; } @@ -257,15 +252,15 @@ TExpType CCreatureSet::getStackExperience(const SlotID & slot) const return 0; //TODO? consider issuing a warning } -bool CCreatureSet::mergableStacks(std::pair & out, const SlotID & preferable) const /*looks for two same stacks, returns slot positions */ +bool CCreatureSet::mergeableStacks(std::pair & out, const SlotID & preferable) const /*looks for two same stacks, returns slot positions */ { //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::mergableStacks(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(); @@ -424,7 +427,7 @@ void CCreatureSet::setStackCount(const SlotID & slot, TQuantity count) { assert(hasStackAtSlot(slot)); assert(stacks[slot]->count + count > 0); - if (VLC->settings()->getBoolean(EGameSettings::MODULE_STACK_EXPERIENCE) && count > stacks[slot]->count) + if (count > stacks[slot]->count) stacks[slot]->experience = static_cast(stacks[slot]->experience * (count / static_cast(stacks[slot]->count))); stacks[slot]->count = count; armyChanged(); @@ -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); } @@ -705,9 +708,9 @@ CCreature::CreatureQuantityId CStackInstance::getQuantityID() const int CStackInstance::getExpRank() const { - if (!VLC->settings()->getBoolean(EGameSettings::MODULE_STACK_EXPERIENCE)) + 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->settings()->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 10654b46a..c671bd547 100644 --- a/lib/CCreatureSet.h +++ b/lib/CCreatureSet.h @@ -11,10 +11,12 @@ #include "bonuses/Bonus.h" #include "bonuses/CBonusSystemNode.h" +#include "serializer/Serializeable.h" #include "GameConstants.h" #include "CArtHandler.h" #include "CArtifactInstance.h" #include "CCreatureHandler.h" +#include "VCMI_Lib.h" #include @@ -29,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(); @@ -39,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; } @@ -104,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; @@ -124,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(); @@ -208,7 +210,7 @@ namespace NArmyFormation static const std::vector names{ "wide", "tight" }; } -class DLL_LINKAGE CCreatureSet : public IArmyDescriptor //seven combined creatures +class DLL_LINKAGE CCreatureSet : public IArmyDescriptor, public virtual Serializeable //seven combined creatures { CCreatureSet(const CCreatureSet &) = delete; CCreatureSet &operator=(const CCreatureSet&); @@ -266,12 +268,13 @@ public: TMapCreatureSlot getCreatureMap() const; TCreatureQueue getCreatureQueue(const SlotID & exclude) const; - bool mergableStacks(std::pair & out, const SlotID & preferable = SlotID()) const; //looks for two same stacks, returns slot positions; + bool mergeableStacks(std::pair & out, const SlotID & preferable = SlotID()) const; //looks for two same stacks, returns slot positions; bool validTypes(bool allowUnrandomized = false) const; //checks if all types of creatures are set properly bool slotEmpty(const SlotID & slot) const; 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 d58f6dafe..b426b5418 100644 --- a/lib/CGameInfoCallback.cpp +++ b/lib/CGameInfoCallback.cpp @@ -10,6 +10,7 @@ #include "StdInc.h" #include "CGameInfoCallback.h" +#include "entities/building/CBuilding.h" #include "gameState/CGameState.h" #include "gameState/InfoAboutArmy.h" #include "gameState/SThievesGuildInfo.h" @@ -19,10 +20,10 @@ #include "mapObjects/CGTownInstance.h" #include "mapObjects/MiscObjects.h" #include "networkPacks/ArtifactLocation.h" -#include "CGeneralTextHandler.h" +#include "texts/CGeneralTextHandler.h" #include "StartInfo.h" // for StartInfo #include "battle/BattleInfo.h" // for BattleInfo -#include "GameSettings.h" +#include "IGameSettings.h" #include "TerrainHandler.h" #include "spells/CSpellHandler.h" #include "mapping/CMap.h" @@ -82,7 +83,7 @@ const Player * CGameInfoCallback::getPlayer(PlayerColor color) const const PlayerState * CGameInfoCallback::getPlayerState(PlayerColor color, bool verbose) const { - //funtion written from scratch since it's accessed A LOT by AI + //function written from scratch since it's accessed A LOT by AI if(!color.isValidPlayer()) { @@ -173,6 +174,15 @@ const CGTownInstance* CGameInfoCallback::getTown(ObjectInstanceID objid) const return nullptr; } +const IMarket * CGameInfoCallback::getMarket(ObjectInstanceID objid) const +{ + const CGObjectInstance * obj = getObj(objid, false); + if(obj) + return dynamic_cast(obj); + else + return nullptr; +} + void CGameInfoCallback::fillUpgradeInfo(const CArmedInstance *obj, SlotID stackPos, UpgradeInfo &out) const { //boost::shared_lock lock(*gs->mx); @@ -226,15 +236,7 @@ void CGameInfoCallback::getThievesGuildInfo(SThievesGuildInfo & thi, const CGObj if(obj->ID == Obj::TOWN || obj->ID == Obj::TAVERN) { - int taverns = 0; - for(auto town : gs->players[*getPlayerID()].towns) - { - if(town->hasBuilt(BuildingID::TAVERN)) - taverns++; - - if(town->hasBuilt(BuildingSubID::THIEVES_GUILD)) - taverns += 2; - } + int taverns = gs->players[*getPlayerID()].valOfBonuses(BonusType::THIEVES_GUILD_ACCESS); gs->obtainPlayersStats(thi, taverns); } else if(obj->ID == Obj::DEN_OF_THIEVES) @@ -246,7 +248,7 @@ void CGameInfoCallback::getThievesGuildInfo(SThievesGuildInfo & thi, const CGObj int CGameInfoCallback::howManyTowns(PlayerColor Player) const { ERROR_RET_VAL_IF(!hasAccess(Player), "Access forbidden!", -1); - return static_cast(gs->players[Player].towns.size()); + return static_cast(gs->players[Player].getTowns().size()); } bool CGameInfoCallback::getTownInfo(const CGObjectInstance * town, InfoAboutTown & dest, const CGObjectInstance * selectedObject) const @@ -272,6 +274,11 @@ bool CGameInfoCallback::getTownInfo(const CGObjectInstance * town, InfoAboutTown return true; } +const IGameSettings & CGameInfoCallback::getSettings() const +{ + return gs->getSettings(); +} + int3 CGameInfoCallback::guardingCreaturePosition (int3 pos) const //FIXME: redundant? { ERROR_RET_VAL_IF(!isVisible(pos), "Tile is not visible!", int3(-1,-1,-1)); @@ -338,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(); } } @@ -350,7 +357,7 @@ bool CGameInfoCallback::getHeroInfo(const CGObjectInstance * hero, InfoAboutHero else for(auto & elem : info.army) { - elem.second.type = mostStrong; + elem.second.setType(mostStrong); } }; @@ -372,9 +379,9 @@ bool CGameInfoCallback::getHeroInfo(const CGObjectInstance * hero, InfoAboutHero int maxAIValue = 0; const CCreature * mostStrong = nullptr; - for(auto creature : VLC->creh->objects) + 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(); @@ -383,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); }; @@ -472,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)); @@ -532,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(); @@ -555,7 +573,7 @@ std::shared_ptr> CGameInfoCallback::ge for(tile.x = 0; tile.x < width; tile.x++) for(tile.y = 0; tile.y < height; tile.y++) { - if ((*team->fogOfWarMap)[tile.z][tile.x][tile.y]) + if (team->fogOfWarMap[tile.z][tile.x][tile.y]) (*ptr)[tile.z][tile.x][tile.y] = &gs->map->getTile(tile); else (*ptr)[tile.z][tile.x][tile.y] = nullptr; @@ -568,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 @@ -599,7 +617,7 @@ EBuildingState CGameInfoCallback::canBuildStructure( const CGTownInstance *t, Bu const PlayerState *ps = getPlayerState(t->tempOwner, false); if(ps) { - for(const CGTownInstance *town : ps->towns) + for(const CGTownInstance *town : ps->getTowns()) { if(town->hasBuilt(BuildingID::CAPITOL)) { @@ -612,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 } @@ -624,7 +642,7 @@ EBuildingState CGameInfoCallback::canBuildStructure( const CGTownInstance *t, Bu if (!t->genBuildingRequirements(ID).test(buildTest)) return EBuildingState::PREREQUIRES; - if(t->builded >= VLC->settings()->getInteger(EGameSettings::TOWNS_BUILDINGS_PER_TURN_CAP)) + if(t->built >= getSettings().getInteger(EGameSettings::TOWNS_BUILDINGS_PER_TURN_CAP)) return EBuildingState::CANT_BUILD_TODAY; //building limit //checking resources @@ -658,11 +676,11 @@ std::string CGameInfoCallback::getTavernRumor(const CGObjectInstance * townOrTav text.appendLocalString(EMetaText::GENERAL_TXT, 216); std::string extraText; - if(gs->rumor.type == RumorState::TYPE_NONE) + if(gs->currentRumor.type == RumorState::TYPE_NONE) return text.toString(); - auto rumor = gs->rumor.last[gs->rumor.type]; - switch(gs->rumor.type) + auto rumor = gs->currentRumor.last[gs->currentRumor.type]; + switch(gs->currentRumor.type) { case RumorState::TYPE_SPECIAL: text.replaceLocalString(EMetaText::GENERAL_TXT, rumor.first); @@ -701,9 +719,9 @@ int CGameInfoCallback::getHeroCount( PlayerColor player, bool includeGarrisoned ERROR_RET_VAL_IF(!p, "No such player!", -1); if(includeGarrisoned) - return static_cast(p->heroes.size()); + return static_cast(p->getHeroes().size()); else - for(const auto & elem : p->heroes) + for(const auto & elem : p->getHeroes()) if(!elem->inTownGarrison) ret++; return ret; @@ -715,7 +733,7 @@ bool CGameInfoCallback::isOwnedOrVisited(const CGObjectInstance *obj) const return true; const TerrainTile *t = getTile(obj->visitablePos()); //get entrance tile - const CGObjectInstance *visitor = t->visitableObjects.back(); //visitong hero if present or the obejct itself at last + const CGObjectInstance *visitor = t->visitableObjects.back(); //visitong hero if present or the object itself at last return visitor->ID == Obj::HERO && canGetFullInfo(visitor); //owned or allied hero is a visitor } @@ -747,7 +765,7 @@ std::vector < const CGTownInstance *> CPlayerSpecificInfoCallback::getTownsInfo( auto ret = std::vector < const CGTownInstance *>(); for(const auto & i : gs->players) { - for(const auto & town : i.second.towns) + for(const auto & town : i.second.getTowns()) { if(i.first == getPlayerID() || (!onlyOur && isVisible(town, getPlayerID()))) { @@ -779,14 +797,14 @@ int CPlayerSpecificInfoCallback::getHeroSerial(const CGHeroInstance * hero, bool return -1; size_t index = 0; - auto & heroes = gs->players[*getPlayerID()].heroes; + const auto & heroes = gs->players[*getPlayerID()].getHeroes(); - for (auto & heroe : heroes) + for (auto & possibleHero : heroes) { - if (includeGarrisoned || !(heroe)->inTownGarrison) + if (includeGarrisoned || !(possibleHero)->inTownGarrison) index++; - if (heroe == hero) + if (possibleHero == hero) return static_cast(index); } return -1; @@ -812,34 +830,12 @@ int3 CPlayerSpecificInfoCallback::getGrailPos( double *outKnownRatio ) std::vector < const CGObjectInstance * > CPlayerSpecificInfoCallback::getMyObjects() const { - std::vector < const CGObjectInstance * > ret; - for(const CGObjectInstance * obj : gs->map->objects) - { - if(obj && obj->tempOwner == getPlayerID()) - ret.push_back(obj); - } - return ret; -} - -std::vector < const CGDwelling * > CPlayerSpecificInfoCallback::getMyDwellings() const -{ - ASSERT_IF_CALLED_WITH_PLAYER - std::vector < const CGDwelling * > ret; - for(CGDwelling * dw : gs->getPlayerState(*getPlayerID())->dwellings) - { - ret.push_back(dw); - } - return ret; + return gs->getPlayerState(*getPlayerID())->getOwnedObjects(); } std::vector CPlayerSpecificInfoCallback::getMyQuests() const { - std::vector ret; - for(const auto & quest : gs->getPlayerState(*getPlayerID())->quests) - { - ret.push_back (quest); - } - return ret; + return gs->getPlayerState(*getPlayerID())->quests; } int CPlayerSpecificInfoCallback::howManyHeroes(bool includeGarrisoned) const @@ -857,12 +853,12 @@ const CGHeroInstance* CPlayerSpecificInfoCallback::getHeroBySerial(int serialId, if (!includeGarrisoned) { - for(ui32 i = 0; i < p->heroes.size() && static_cast(i) <= serialId; i++) - if(p->heroes[i]->inTownGarrison) + for(ui32 i = 0; i < p->getHeroes().size() && static_cast(i) <= serialId; i++) + if(p->getHeroes()[i]->inTownGarrison) serialId++; } - ERROR_RET_VAL_IF(serialId < 0 || serialId >= p->heroes.size(), "No player info", nullptr); - return p->heroes[serialId]; + ERROR_RET_VAL_IF(serialId < 0 || serialId >= p->getHeroes().size(), "No player info", nullptr); + return p->getHeroes()[serialId]; } const CGTownInstance* CPlayerSpecificInfoCallback::getTownBySerial(int serialId) const @@ -870,8 +866,8 @@ const CGTownInstance* CPlayerSpecificInfoCallback::getTownBySerial(int serialId) ASSERT_IF_CALLED_WITH_PLAYER const PlayerState *p = getPlayerState(*getPlayerID()); ERROR_RET_VAL_IF(!p, "No player info", nullptr); - ERROR_RET_VAL_IF(serialId < 0 || serialId >= p->towns.size(), "No player info", nullptr); - return p->towns[serialId]; + ERROR_RET_VAL_IF(serialId < 0 || serialId >= p->getTowns().size(), "No player info", nullptr); + return p->getTowns()[serialId]; } int CPlayerSpecificInfoCallback::getResourceAmount(GameResID type) const @@ -961,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 @@ -979,12 +975,12 @@ 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; } -std::vector CGameInfoCallback::getTeleportChannelEntraces(TeleportChannelID id, PlayerColor player) const +std::vector CGameInfoCallback::getTeleportChannelEntrances(TeleportChannelID id, PlayerColor player) const { return getVisibleTeleportObjects(gs->map->teleportChannels[id]->entrances, player); } @@ -996,7 +992,7 @@ std::vector CGameInfoCallback::getTeleportChannelExits(Telepor ETeleportChannelType CGameInfoCallback::getTeleportChannelType(TeleportChannelID id, PlayerColor player) const { - std::vector entrances = getTeleportChannelEntraces(id, player); + std::vector entrances = getTeleportChannelEntrances(id, player); std::vector exits = getTeleportChannelExits(id, player); if((entrances.empty() || exits.empty()) // impassable if exits or entrances list are empty || (entrances.size() == 1 && entrances == exits)) // impassable if only entrance and only exit is same object. e.g bidirectional monolith diff --git a/lib/CGameInfoCallback.h b/lib/CGameInfoCallback.h index 04f264f59..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);} @@ -18,12 +19,13 @@ VCMI_LIB_NAMESPACE_BEGIN class Player; class Team; +class IGameSettings; struct InfoWindow; struct PlayerSettings; struct CPackForClient; struct TerrainTile; -struct PlayerState; +class PlayerState; class CTown; struct StartInfo; struct CPathsInfo; @@ -48,6 +50,7 @@ class CGHeroInstance; class CGDwelling; class CGTeleport; class CGTownInstance; +class IMarket; class DLL_LINKAGE IGameInfoCallback : boost::noncopyable { @@ -56,7 +59,7 @@ public: // //various virtual int getDate(Date mode=Date::DAY) const = 0; //mode=0 - total days in game, mode=1 - day of week, mode=2 - current week, mode=3 - current month -// const StartInfo * getStartInfo(bool beforeRandomization = false)const; + virtual const StartInfo * getStartInfo(bool beforeRandomization = false) const = 0; virtual bool isAllowed(SpellID id) const = 0; virtual bool isAllowed(ArtifactID id) const = 0; virtual bool isAllowed(SecondarySkill id) const = 0; @@ -119,7 +122,7 @@ public: //teleport // std::vector getVisibleTeleportObjects(std::vector ids, PlayerColor player) const; -// std::vector getTeleportChannelEntraces(TeleportChannelID id, PlayerColor Player = PlayerColor::UNFLAGGABLE) const; +// std::vector getTeleportChannelEntrances(TeleportChannelID id, PlayerColor Player = PlayerColor::UNFLAGGABLE) const; // std::vector getTeleportChannelExits(TeleportChannelID id, PlayerColor Player = PlayerColor::UNFLAGGABLE) const; // ETeleportChannelType getTeleportChannelType(TeleportChannelID id, PlayerColor player = PlayerColor::UNFLAGGABLE) const; // bool isTeleportChannelImpassable(TeleportChannelID id, PlayerColor player = PlayerColor::UNFLAGGABLE) const; @@ -143,10 +146,11 @@ protected: public: //various int getDate(Date mode=Date::DAY)const override; //mode=0 - total days in game, mode=1 - day of week, mode=2 - current week, mode=3 - current month - virtual const StartInfo * getStartInfo(bool beforeRandomization = false)const; + const StartInfo * getStartInfo(bool beforeRandomization = false) const override; bool isAllowed(SpellID id) const override; bool isAllowed(ArtifactID id) const override; bool isAllowed(SecondarySkill id) const override; + const IGameSettings & getSettings() const; //player std::optional getPlayerID() const override; @@ -186,9 +190,11 @@ 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; + virtual const IMarket * getMarket(ObjectInstanceID objid) const; //map virtual int3 guardingCreaturePosition (int3 pos) const; @@ -221,7 +227,7 @@ public: //teleport virtual std::vector getVisibleTeleportObjects(std::vector ids, PlayerColor player) const; - virtual std::vector getTeleportChannelEntraces(TeleportChannelID id, PlayerColor Player = PlayerColor::UNFLAGGABLE) const; + virtual std::vector getTeleportChannelEntrances(TeleportChannelID id, PlayerColor Player = PlayerColor::UNFLAGGABLE) const; virtual std::vector getTeleportChannelExits(TeleportChannelID id, PlayerColor Player = PlayerColor::UNFLAGGABLE) const; virtual ETeleportChannelType getTeleportChannelType(TeleportChannelID id, PlayerColor player = PlayerColor::UNFLAGGABLE) const; virtual bool isTeleportChannelImpassable(TeleportChannelID id, PlayerColor player = PlayerColor::UNFLAGGABLE) const; @@ -245,7 +251,6 @@ public: virtual const CGTownInstance* getTownBySerial(int serialId) const; // serial id is [0, number of towns) virtual const CGHeroInstance* getHeroBySerial(int serialId, bool includeGarrisoned=true) const; // serial id is [0, number of heroes) virtual std::vector getHeroesInfo(bool onlyOur = true) const; //true -> only owned; false -> all visible - virtual std::vector getMyDwellings() const; //returns all dwellings that belong to player virtual std::vector getMyObjects() const; //returns all objects flagged by belonging player virtual std::vector getMyQuests() const; diff --git a/lib/CGameInterface.cpp b/lib/CGameInterface.cpp index 005848079..8581a2f6d 100644 --- a/lib/CGameInterface.cpp +++ b/lib/CGameInterface.cpp @@ -13,9 +13,6 @@ #include "CStack.h" #include "VCMIDirs.h" -#include "serializer/BinaryDeserializer.h" -#include "serializer/BinarySerializer.h" - #ifdef STATIC_AI # include "AI/VCAI/VCAI.h" # include "AI/Nullkiller/AIGateway.h" @@ -168,7 +165,7 @@ void CAdventureAI::battleCatapultAttacked(const BattleID & battleID, const Catap } void CAdventureAI::battleStart(const BattleID & battleID, const CCreatureSet * army1, const CCreatureSet * army2, int3 tile, - const CGHeroInstance * hero1, const CGHeroInstance * hero2, bool side, bool replayAllowed) + const CGHeroInstance * hero1, const CGHeroInstance * hero2, BattleSide side, bool replayAllowed) { assert(!battleAI); assert(cbc); @@ -243,28 +240,4 @@ void CAdventureAI::yourTacticPhase(const BattleID & battleID, int distance) battleAI->yourTacticPhase(battleID, distance); } -void CAdventureAI::saveGame(BinarySerializer & h) /*saving */ -{ - bool hasBattleAI = static_cast(battleAI); - h & hasBattleAI; - if(hasBattleAI) - { - h & battleAI->dllName; - } -} - -void CAdventureAI::loadGame(BinaryDeserializer & h) /*loading */ -{ - bool hasBattleAI = false; - h & hasBattleAI; - if(hasBattleAI) - { - std::string dllName; - h & dllName; - battleAI = CDynLibHandler::getNewBattleAI(dllName); - assert(cbc); //it should have been set by the one who new'ed us - battleAI->initBattleInterface(env, cbc); - } -} - VCMI_LIB_NAMESPACE_END diff --git a/lib/CGameInterface.h b/lib/CGameInterface.h index 5804a2478..a0896014a 100644 --- a/lib/CGameInterface.h +++ b/lib/CGameInterface.h @@ -53,8 +53,6 @@ class CStack; class CCreature; class CLoadFile; class CSaveFile; -class BinaryDeserializer; -class BinarySerializer; class BattleStateInfo; struct ArtifactLocation; class BattleStateInfoForRetreat; @@ -110,9 +108,6 @@ public: virtual void showWorldViewEx(const std::vector & objectPositions, bool showTerrain){}; virtual std::optional makeSurrenderRetreatDecision(const BattleID & battleID, const BattleStateInfoForRetreat & battleState) = 0; - - virtual void saveGame(BinarySerializer & h) = 0; - virtual void loadGame(BinaryDeserializer & h) = 0; }; class DLL_LINKAGE CDynLibHandler @@ -149,7 +144,7 @@ public: void battleNewRound(const BattleID & battleID) override; void battleCatapultAttacked(const BattleID & battleID, const CatapultAttack & ca) override; - void battleStart(const BattleID & battleID, const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool side, bool replayAllowed) override; + void battleStart(const BattleID & battleID, const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, BattleSide side, bool replayAllowed) override; void battleStacksAttacked(const BattleID & battleID, const std::vector & bsa, bool ranged) override; void actionStarted(const BattleID & battleID, const BattleAction &action) override; void battleNewRoundFirst(const BattleID & battleID) override; @@ -161,9 +156,6 @@ public: void battleSpellCast(const BattleID & battleID, const BattleSpellCast *sc) override; void battleEnd(const BattleID & battleID, const BattleResult *br, QueryID queryID) override; void battleUnitsChanged(const BattleID & battleID, const std::vector & units) override; - - void saveGame(BinarySerializer & h) override; - void loadGame(BinaryDeserializer & h) override; }; VCMI_LIB_NAMESPACE_END diff --git a/lib/CGeneralTextHandler.h b/lib/CGeneralTextHandler.h deleted file mode 100644 index 6cc7ead26..000000000 --- a/lib/CGeneralTextHandler.h +++ /dev/null @@ -1,316 +0,0 @@ -/* - * CGeneralTextHandler.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 "filesystem/ResourcePath.h" - -VCMI_LIB_NAMESPACE_BEGIN - -class CInputStream; -class JsonNode; -class JsonSerializeFormat; - -/// Parser for any text files from H3 -class DLL_LINKAGE CLegacyConfigParser -{ - std::string fileEncoding; - - std::unique_ptr data; - char * curr; - char * end; - - /// extracts part of quoted string. - std::string extractQuotedPart(); - - /// extracts quoted string. Any end of lines are ignored, double-quote is considered as "escaping" - std::string extractQuotedString(); - - /// extracts non-quoted string - std::string extractNormalString(); - - /// reads "raw" string without encoding conversion - std::string readRawString(); - -public: - /// read one entry from current line. Return ""/0 if end of line reached - std::string readString(); - float readNumber(); - - template - std::vector readNumArray(size_t size) - { - std::vector ret; - ret.reserve(size); - while (size--) - ret.push_back((numeric)readNumber()); - return ret; - } - - /// returns true if next entry is empty - bool isNextEntryEmpty() const; - - /// end current line - bool endLine(); - - explicit CLegacyConfigParser(const TextPath & URI); -}; - -class CGeneralTextHandler; - -/// Small wrapper that provides text access API compatible with old code -class DLL_LINKAGE LegacyTextContainer -{ - CGeneralTextHandler & owner; - std::string basePath; - -public: - LegacyTextContainer(CGeneralTextHandler & owner, std::string basePath); - std::string operator [](size_t index) const; -}; - -/// Small wrapper that provides help text access API compatible with old code -class DLL_LINKAGE LegacyHelpContainer -{ - CGeneralTextHandler & owner; - std::string basePath; - -public: - LegacyHelpContainer(CGeneralTextHandler & owner, std::string basePath); - std::pair operator[](size_t index) const; -}; - -class TextIdentifier -{ - std::string identifier; -public: - const std::string & get() const - { - return identifier; - } - - TextIdentifier(const char * id): - identifier(id) - {} - - TextIdentifier(const std::string & id): - identifier(id) - {} - - template - TextIdentifier(const std::string & id, size_t index, T... rest): - TextIdentifier(id + '.' + std::to_string(index), rest...) - {} - - template - TextIdentifier(const std::string & id, const std::string & id2, T... rest): - TextIdentifier(id + '.' + id2, rest...) - {} -}; - -class DLL_LINKAGE TextLocalizationContainer -{ -protected: - static std::recursive_mutex globalTextMutex; - - 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; - - /// ID of mod that created this string - std::string modContext; - - template - void serialize(Handler & h) - { - h & baseValue; - h & baseLanguage; - h & modContext; - } - }; - - /// map identifier -> localization - std::unordered_map stringsLocalizations; - - 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); - - std::string getModLanguage(const std::string & modContext); - - // returns true if identifier with such name was registered, even if not translated to current language - 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); - - /// add selected string to internal storage - 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); - - /// 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); - } - - /// converts identifier into user-readable string - const std::string & deserialize(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; - - /// Add or override subcontainer which can store identifiers - void addSubContainer(const TextLocalizationContainer & container); - - /// Remove subcontainer with give name - void removeSubContainer(const TextLocalizationContainer & container); - - void jsonSerialize(JsonNode & dest) const; - - template - void serialize(Handler & h) - { - std::lock_guard globalLock(globalTextMutex); - - std::string key; - auto sz = stringsLocalizations.size(); - - if (h.version >= Handler::Version::REMOVE_TEXT_CONTAINER_SIZE_T) - { - int64_t size = sz; - h & size; - sz = size; - } - else - { - h & sz; - } - - if(h.saving) - { - for(auto s : stringsLocalizations) - { - key = s.first; - h & key; - h & s.second; - } - } - else - { - for(size_t i = 0; i < sz; ++i) - { - h & key; - h & stringsLocalizations[key]; - } - } - } -}; - -class DLL_LINKAGE TextContainerRegistrable : public TextLocalizationContainer -{ -public: - TextContainerRegistrable(); - ~TextContainerRegistrable(); - - TextContainerRegistrable(const TextContainerRegistrable & other); - TextContainerRegistrable(TextContainerRegistrable && other) noexcept; - - TextContainerRegistrable& operator=(const TextContainerRegistrable & b) = default; -}; - -/// Handles all text-related data in game -class DLL_LINKAGE CGeneralTextHandler: public TextLocalizationContainer -{ - void readToVector(const std::string & sourceID, const std::string & sourceName); - - /// number of scenarios in specific campaign. TODO: move to a better location - std::vector scenariosCountPerCampaign; - -public: - LegacyTextContainer allTexts; - - LegacyTextContainer arraytxt; - LegacyTextContainer primarySkillNames; - 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) - - //towns - LegacyTextContainer tcommands, hcommands, fcommands; //texts for town screen, town hall screen and fort screen - LegacyTextContainer tavernInfo; - LegacyTextContainer tavernRumors; - - LegacyTextContainer qeModCommands; - - LegacyHelpContainer zelp; - LegacyTextContainer lossCondtions; - 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); - - int32_t pluralText(int32_t textIndex, int32_t count) const; - - size_t getCampaignLength(size_t campaignID) const; - - CGeneralTextHandler(); - CGeneralTextHandler(const CGeneralTextHandler&) = delete; - CGeneralTextHandler operator=(const CGeneralTextHandler&) = delete; - - /// Attempts to detect encoding & language of H3 files - static void detectInstallParameters(); - - /// Returns name of language preferred by user - static std::string getPreferredLanguage(); - - /// Returns name of language of Heroes III text files - static std::string getInstalledLanguage(); - - /// Returns name of encoding of Heroes III text files - static std::string getInstalledEncoding(); -}; - -VCMI_LIB_NAMESPACE_END diff --git a/lib/CHeroHandler.cpp b/lib/CHeroHandler.cpp deleted file mode 100644 index 459216f28..000000000 --- a/lib/CHeroHandler.cpp +++ /dev/null @@ -1,816 +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 "CGeneralTextHandler.h" -#include "filesystem/Filesystem.h" -#include "VCMI_Lib.h" -#include "constants/StringConstants.h" -#include "battle/BattleHex.h" -#include "CCreatureHandler.h" -#include "GameSettings.h" -#include "CRandomGenerator.h" -#include "CTownHandler.h" -#include "CSkillHandler.h" -#include "BattleFieldHandler.h" -#include "bonuses/Limiters.h" -#include "bonuses/Updaters.h" -#include "json/JsonBonus.h" -#include "json/JsonUtils.h" -#include "mapObjectConstructors/AObjectTypeHandler.h" -#include "mapObjectConstructors/CObjectClassesHandler.h" -#include "modding/IdentifierStorage.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; -} - -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, CRandomGenerator & 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; -} - -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()); - //minimal value is 0 for attack and defense and 1 for spell power and knowledge - auto primarySkillLegalMinimum = (pSkill == PrimarySkill::ATTACK || pSkill == PrimarySkill::DEFENSE) ? 0 : 1; - - 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; -} - -CHeroClass * 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 = new CHeroClass(); - - 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, PrimarySkill::ATTACK); - fillPrimarySkillData(node, heroClass, PrimarySkill::DEFENSE); - fillPrimarySkillData(node, heroClass, PrimarySkill::SPELL_POWER); - fillPrimarySkillData(node, heroClass, 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->settings()->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; -} - -CHero * 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 = new CHero(); - 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, node); - loadHeroSkills(hero, node); - loadHeroSpecialty(hero, 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->settings()->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 b47aa5a33..000000000 --- a/lib/CHeroHandler.h +++ /dev/null @@ -1,215 +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 - -class CHeroClass; -class CGHeroInstance; -struct BattleHex; -class JsonNode; -class CRandomGenerator; -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; - 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; - 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, CRandomGenerator & 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; - CHeroClass * 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; - CHero * 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 57561df24..85e1c95a8 100644 --- a/lib/CMakeLists.txt +++ b/lib/CMakeLists.txt @@ -30,13 +30,14 @@ set(lib_SRCS network/NetworkHandler.cpp network/NetworkServer.cpp + texts/TextOperations.cpp + vstd/DateUtils.cpp vstd/StringUtils.cpp CConfigHandler.cpp CConsoleHandler.cpp CThreadHelper.cpp - TextOperations.cpp VCMIDirs.cpp ) @@ -47,6 +48,7 @@ set(lib_MAIN_SRCS battle/BattleAttackInfo.cpp battle/BattleHex.cpp battle/BattleInfo.cpp + battle/BattleLayout.cpp battle/BattleProxy.cpp battle/BattleStateInfoForRetreat.cpp battle/CBattleInfoCallback.cpp @@ -80,6 +82,16 @@ set(lib_MAIN_SRCS constants/EntityIdentifiers.cpp + entities/building/CBuilding.cpp + entities/building/CBuildingHandler.cpp + 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 events/ObjectVisitEnded.cpp @@ -92,8 +104,11 @@ set(lib_MAIN_SRCS gameState/CGameState.cpp gameState/CGameStateCampaign.cpp + gameState/HighScore.cpp gameState/InfoAboutArmy.cpp + gameState/RumorState.cpp gameState/TavernHeroesPool.cpp + gameState/GameStatistics.cpp mapObjectConstructors/AObjectTypeHandler.cpp mapObjectConstructors/CBankInstanceConstructor.cpp @@ -101,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 @@ -112,11 +128,12 @@ set(lib_MAIN_SRCS mapObjects/CGMarket.cpp mapObjects/CGObjectInstance.cpp mapObjects/CGPandoraBox.cpp - mapObjects/CGTownBuilding.cpp + mapObjects/TownBuildingInstance.cpp mapObjects/CGTownInstance.cpp mapObjects/CObjectHandler.cpp mapObjects/CQuest.cpp mapObjects/CRewardableObject.cpp + mapObjects/FlaggableMapObject.cpp mapObjects/IMarket.cpp mapObjects/IObjectInterface.cpp mapObjects/MiscObjects.cpp @@ -140,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 @@ -173,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 @@ -207,6 +227,7 @@ set(lib_MAIN_SRCS serializer/JsonSerializeFormat.cpp serializer/JsonSerializer.cpp serializer/JsonUpdater.cpp + serializer/SerializerReflection.cpp spells/AbilityCaster.cpp spells/AdventureSpellMechanics.cpp @@ -240,6 +261,11 @@ set(lib_MAIN_SRCS spells/effects/RemoveObstacle.cpp spells/effects/Sacrifice.cpp + texts/CGeneralTextHandler.cpp + texts/CLegacyConfigParser.cpp + texts/MetaString.cpp + texts/TextLocalizationContainer.cpp + ArtifactUtils.cpp BasicTypes.cpp BattleFieldHandler.cpp @@ -247,25 +273,20 @@ set(lib_MAIN_SRCS CArtHandler.cpp CArtifactInstance.cpp CBonusTypeHandler.cpp - CBuildingHandler.cpp CCreatureHandler.cpp CCreatureSet.cpp CGameInfoCallback.cpp CGameInterface.cpp - CGeneralTextHandler.cpp - CHeroHandler.cpp CPlayerState.cpp CRandomGenerator.cpp CScriptingModule.cpp CSkillHandler.cpp CStack.cpp - CTownHandler.cpp GameSettings.cpp IGameCallback.cpp IHandlerBase.cpp LoadProgress.cpp LogicalExpression.cpp - MetaString.cpp ObstacleHandler.cpp StartInfo.cpp ResourceSet.cpp @@ -334,10 +355,11 @@ set(lib_HEADERS network/NetworkInterface.h network/NetworkServer.h + texts/TextOperations.h + CConfigHandler.h CConsoleHandler.h CThreadHelper.h - TextOperations.h VCMIDirs.h ) @@ -391,6 +413,8 @@ set(lib_MAIN_HEADERS battle/BattleAttackInfo.h battle/BattleHex.h battle/BattleInfo.h + battle/BattleLayout.h + battle/BattleSide.h battle/BattleStateInfoForRetreat.h battle/BattleProxy.h battle/CBattleInfoCallback.h @@ -434,6 +458,18 @@ set(lib_MAIN_HEADERS constants/NumericConstants.h constants/StringConstants.h + entities/building/CBuilding.h + entities/building/CBuildingHandler.h + entities/building/TownFortifications.h + 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 events/ObjectVisitEnded.h @@ -447,9 +483,12 @@ set(lib_MAIN_HEADERS gameState/CGameState.h gameState/CGameStateCampaign.h gameState/EVictoryLossCheckResult.h + gameState/HighScore.h gameState/InfoAboutArmy.h + gameState/RumorState.h gameState/SThievesGuildInfo.h gameState/TavernHeroesPool.h + gameState/GameStatistics.h gameState/TavernSlot.h gameState/QuestInfo.h @@ -461,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 @@ -474,15 +514,18 @@ set(lib_MAIN_HEADERS mapObjects/CGMarket.h mapObjects/CGObjectInstance.h mapObjects/CGPandoraBox.h - mapObjects/CGTownBuilding.h + mapObjects/TownBuildingInstance.h mapObjects/CGTownInstance.h 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 @@ -505,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 @@ -527,7 +571,9 @@ set(lib_MAIN_HEADERS networkPacks/PacksForClientBattle.h networkPacks/PacksForLobby.h networkPacks/PacksForServer.h + networkPacks/SetRewardableConfiguration.h networkPacks/SetStackEffect.h + networkPacks/SaveLocalState.h networkPacks/StackLocation.h networkPacks/TradeItem.h @@ -540,12 +586,6 @@ set(lib_MAIN_HEADERS pathfinder/PathfindingRules.h pathfinder/TurnInfo.h - registerTypes/RegisterTypes.h - registerTypes/RegisterTypesClientPacks.h - registerTypes/RegisterTypesLobbyPacks.h - registerTypes/RegisterTypesMapObjects.h - registerTypes/RegisterTypesServerPacks.h - rewardable/Configuration.h rewardable/Info.h rewardable/Interface.h @@ -565,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 @@ -600,8 +642,10 @@ set(lib_MAIN_HEADERS serializer/JsonSerializeFormat.h serializer/JsonSerializer.h serializer/JsonUpdater.h - serializer/Cast.h serializer/ESerializationVersion.h + serializer/RegisterTypes.h + serializer/Serializeable.h + serializer/SerializerReflection.h spells/AbilityCaster.h spells/AdventureSpellMechanics.h @@ -635,6 +679,13 @@ set(lib_MAIN_HEADERS spells/effects/RemoveObstacle.h spells/effects/Sacrifice.h + texts/CGeneralTextHandler.h + texts/Languages.h + texts/CLegacyConfigParser.h + texts/MetaString.h + texts/TextIdentifier.h + texts/TextLocalizationContainer.h + AI_Base.h ArtifactUtils.h BattleFieldHandler.h @@ -642,13 +693,10 @@ set(lib_MAIN_HEADERS CArtHandler.h CArtifactInstance.h CBonusTypeHandler.h - CBuildingHandler.h CCreatureHandler.h CCreatureSet.h CGameInfoCallback.h CGameInterface.h - CGeneralTextHandler.h - CHeroHandler.h ConstTransitivePtr.h Color.h CPlayerState.h @@ -658,7 +706,6 @@ set(lib_MAIN_HEADERS CSoundBase.h CStack.h CStopWatch.h - CTownHandler.h ExceptionsCommon.h ExtraOptionsInfo.h FunctionList.h @@ -668,12 +715,11 @@ set(lib_MAIN_HEADERS IBonusTypeHandler.h IGameCallback.h IGameEventsReceiver.h + IGameSettings.h IHandlerBase.h int3.h - Languages.h LoadProgress.h LogicalExpression.h - MetaString.h ObstacleHandler.h Point.h Rect.h @@ -705,7 +751,7 @@ endif() set_target_properties(vcmi PROPERTIES COMPILE_DEFINITIONS "VCMI_DLL=1") target_link_libraries(vcmi PUBLIC - minizip::minizip ZLIB::ZLIB + minizip::minizip ZLIB::ZLIB TBB::tbb ${SYSTEM_LIBS} Boost::boost Boost::thread Boost::filesystem Boost::program_options Boost::locale Boost::date_time ) @@ -746,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) @@ -770,6 +826,8 @@ if(NOT ENABLE_STATIC_LIBS) endif() if(APPLE_IOS AND NOT USING_CONAN) + install(IMPORTED_RUNTIME_ARTIFACTS TBB::tbb LIBRARY DESTINATION ${LIB_DIR}) # CMake 3.21+ + get_target_property(LINKED_LIBS vcmi LINK_LIBRARIES) foreach(LINKED_LIB IN LISTS LINKED_LIBS) if(NOT TARGET ${LINKED_LIB}) diff --git a/lib/CPlayerState.cpp b/lib/CPlayerState.cpp index 2ac3e39db..069be91c1 100644 --- a/lib/CPlayerState.cpp +++ b/lib/CPlayerState.cpp @@ -10,15 +10,24 @@ #include "StdInc.h" #include "CPlayerState.h" +#include "json/JsonNode.h" +#include "mapObjects/CGDwelling.h" +#include "mapObjects/CGTownInstance.h" +#include "mapObjects/CGHeroInstance.h" #include "gameState/QuestInfo.h" -#include "CGeneralTextHandler.h" +#include "texts/CGeneralTextHandler.h" #include "VCMI_Lib.h" 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); } @@ -50,6 +59,11 @@ std::string PlayerState::getJsonKey() const return color.toString(); } +std::string PlayerState::getModScope() const +{ + return "core"; +} + std::string PlayerState::getNameTranslated() const { return VLC->generaltexth->translate(getNameTextID()); @@ -85,4 +99,54 @@ int PlayerState::getResourceAmount(int type) const return vstd::atOrDefault(resources, static_cast(type), 0); } +template +std::vector PlayerState::getObjectsOfType() const +{ + std::vector result; + for (auto const & object : ownedObjects) + { + auto casted = dynamic_cast(object); + if (casted) + result.push_back(casted); + } + return result; +} + +std::vector PlayerState::getHeroes() const +{ + return getObjectsOfType(); +} + +std::vector PlayerState::getTowns() const +{ + return getObjectsOfType(); +} + +std::vector PlayerState::getHeroes() +{ + return getObjectsOfType(); +} + +std::vector PlayerState::getTowns() +{ + return getObjectsOfType(); +} + +std::vector PlayerState::getOwnedObjects() const +{ + return {ownedObjects.begin(), ownedObjects.end()}; +} + +void PlayerState::addOwnedObject(CGObjectInstance * object) +{ + assert(object->asOwnable() != nullptr); + ownedObjects.push_back(object); +} + +void PlayerState::removeOwnedObject(CGObjectInstance * object) +{ + vstd::erase(ownedObjects, object); +} + + VCMI_LIB_NAMESPACE_END diff --git a/lib/CPlayerState.h b/lib/CPlayerState.h index 071e3e9c5..411b0945f 100644 --- a/lib/CPlayerState.h +++ b/lib/CPlayerState.h @@ -16,16 +16,16 @@ #include "bonuses/CBonusSystemNode.h" #include "ResourceSet.h" #include "TurnTimerInfo.h" -#include "ConstTransitivePtr.h" VCMI_LIB_NAMESPACE_BEGIN +class CGObjectInstance; class CGHeroInstance; class CGTownInstance; class CGDwelling; struct QuestInfo; -struct DLL_LINKAGE PlayerState : public CBonusSystemNode, public Player +class DLL_LINKAGE PlayerState : public CBonusSystemNode, public Player { struct VisitedObjectGlobal { @@ -47,6 +47,11 @@ struct DLL_LINKAGE PlayerState : public CBonusSystemNode, public Player } }; + std::vector ownedObjects; + + template + std::vector getObjectsOfType() const; + public: PlayerColor color; bool human; //true if human controlled player, false for AI @@ -55,15 +60,12 @@ public: /// list of objects that were "destroyed" by player, either via simple pick-up (e.g. resources) or defeated heroes or wandering monsters std::set destroyedObjects; - std::set visitedObjects; // as a std::set, since most accesses here will be from visited status checks std::set visitedObjectsGlobal; - std::vector > heroes; - std::vector > towns; - std::vector > dwellings; //used for town growth 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 @@ -85,13 +87,24 @@ public: int32_t getIndex() const override; int32_t getIconIndex() const override; std::string getJsonKey() const override; + std::string getModScope() const override; std::string getNameTranslated() const override; std::string getNameTextID() const override; void registerIcons(const IconRegistar & cb) const override; + std::vector getHeroes() const; + std::vector getTowns() const; + std::vector getHeroes(); + std::vector getTowns(); + + std::vector getOwnedObjects() const; + + void addOwnedObject(CGObjectInstance * object); + void removeOwnedObject(CGObjectInstance * object); + bool checkVanquished() const { - return heroes.empty() && towns.empty(); + return getHeroes().empty() && getTowns().empty(); } template void serialize(Handler &h) @@ -102,9 +115,24 @@ public: h & resources; h & status; h & turnTimer; - h & heroes; - h & towns; - h & dwellings; + + if (h.version >= Handler::Version::LOCAL_PLAYER_STATE_DATA) + h & *playerLocalSettings; + + if (h.version >= Handler::Version::PLAYER_STATE_OWNED_OBJECTS) + { + h & ownedObjects; + } + else + { + std::vector heroes; + std::vector towns; + std::vector dwellings; + + h & heroes; + h & towns; + h & dwellings; + } h & quests; h & visitedObjects; h & visitedObjectsGlobal; @@ -112,13 +140,11 @@ public: h & daysWithoutCastle; h & cheated; h & battleBonuses; - if (h.version >= Handler::Version::ARTIFACT_COSTUMES) - h & costumesArtifacts; + h & costumesArtifacts; h & enteredLosingCheatCode; h & enteredWinningCheatCode; h & static_cast(*this); - if (h.version >= Handler::Version::DESTROYED_OBJECTS) - h & destroyedObjects; + h & destroyedObjects; } }; @@ -128,7 +154,9 @@ public: TeamID id; //position in gameState::teams std::set players; // members of this team //TODO: boost::array, bool if possible - std::unique_ptr> fogOfWarMap; //[z][x][y] true - visible, false - hidden + boost::multi_array fogOfWarMap; //[z][x][y] true - visible, false - hidden + + std::set scoutedObjects; TeamState(); @@ -136,8 +164,23 @@ public: { h & id; h & players; + if (h.version < Handler::Version::REMOVE_FOG_OF_WAR_POINTER) + { + struct Helper : public Serializeable + { + void serialize(Handler &h) const + {} + }; + Helper helper; + auto ptrHelper = &helper; + h & ptrHelper; + } + h & fogOfWarMap; h & static_cast(*this); + + if (h.version >= Handler::Version::REWARDABLE_BANKS) + h & scoutedObjects; } }; diff --git a/lib/CRandomGenerator.cpp b/lib/CRandomGenerator.cpp index 22ff8c78e..0cec4fa31 100644 --- a/lib/CRandomGenerator.cpp +++ b/lib/CRandomGenerator.cpp @@ -15,76 +15,87 @@ VCMI_LIB_NAMESPACE_BEGIN CRandomGenerator::CRandomGenerator() { + logRng->trace("CRandomGenerator constructed"); resetSeed(); } CRandomGenerator::CRandomGenerator(int seed) { + logRng->trace("CRandomGenerator constructed (%d)", seed); setSeed(seed); } void CRandomGenerator::setSeed(int seed) { + logRng->trace("CRandomGenerator::setSeed (%d)", seed); rand.seed(seed); } void CRandomGenerator::resetSeed() { + logRng->trace("CRandomGenerator::resetSeed"); boost::hash stringHash; auto threadIdHash = stringHash(boost::lexical_cast(boost::this_thread::get_id())); setSeed(static_cast(threadIdHash * std::time(nullptr))); } -TRandI CRandomGenerator::getIntRange(int lower, int upper) -{ - if (lower <= upper) - return std::bind(TIntDist(lower, upper), std::ref(rand)); - throw std::runtime_error("Invalid range provided: " + std::to_string(lower) + " ... " + std::to_string(upper)); -} - -vstd::TRandI64 CRandomGenerator::getInt64Range(int64_t lower, int64_t upper) -{ - if(lower <= upper) - return std::bind(TInt64Dist(lower, upper), std::ref(rand)); - throw std::runtime_error("Invalid range provided: " + std::to_string(lower) + " ... " + std::to_string(upper)); -} - int CRandomGenerator::nextInt(int upper) { - return getIntRange(0, upper)(); + logRng->trace("CRandomGenerator::nextInt (%d)", upper); + return nextInt(0, upper); +} + +int64_t CRandomGenerator::nextInt64(int64_t upper) +{ + logRng->trace("CRandomGenerator::nextInt64 (%d)", upper); + return nextInt64(0, upper); } int CRandomGenerator::nextInt(int lower, int upper) { - return getIntRange(lower, upper)(); + logRng->trace("CRandomGenerator::nextInt64 (%d, %d)", lower, upper); + + if (lower > upper) + throw std::runtime_error("Invalid range provided: " + std::to_string(lower) + " ... " + std::to_string(upper)); + + return TIntDist(lower, upper)(rand); } int CRandomGenerator::nextInt() { + logRng->trace("CRandomGenerator::nextInt64"); return TIntDist()(rand); } -vstd::TRand CRandomGenerator::getDoubleRange(double lower, double upper) +int CRandomGenerator::nextBinomialInt(int coinsCount, double coinChance) { - if(lower <= upper) - return std::bind(TRealDist(lower, upper), std::ref(rand)); - throw std::runtime_error("Invalid range provided: " + std::to_string(lower) + " ... " + std::to_string(upper)); + logRng->trace("CRandomGenerator::nextBinomialInt (%d, %f)", coinsCount, coinChance); + std::binomial_distribution<> distribution(coinsCount, coinChance); + return distribution(rand); +} +int64_t CRandomGenerator::nextInt64(int64_t lower, int64_t upper) +{ + logRng->trace("CRandomGenerator::nextInt64 (%d, %d)", lower, upper); + if (lower > upper) + throw std::runtime_error("Invalid range provided: " + std::to_string(lower) + " ... " + std::to_string(upper)); + + return TInt64Dist(lower, upper)(rand); } double CRandomGenerator::nextDouble(double upper) { - return getDoubleRange(0, upper)(); + logRng->trace("CRandomGenerator::nextDouble (%f)", upper); + return nextDouble(0, upper); } double CRandomGenerator::nextDouble(double lower, double upper) { - return getDoubleRange(lower, upper)(); -} + logRng->trace("CRandomGenerator::nextDouble (%f, %f)", lower, upper); + if(lower > upper) + throw std::runtime_error("Invalid range provided: " + std::to_string(lower) + " ... " + std::to_string(upper)); -double CRandomGenerator::nextDouble() -{ - return TRealDist()(rand); + return TRealDist(lower, upper)(rand); } CRandomGenerator & CRandomGenerator::getDefault() @@ -93,9 +104,5 @@ CRandomGenerator & CRandomGenerator::getDefault() return defaultRand; } -TGenerator & CRandomGenerator::getStdGenerator() -{ - return rand; -} VCMI_LIB_NAMESPACE_END diff --git a/lib/CRandomGenerator.h b/lib/CRandomGenerator.h index 265b716a7..c24daaf14 100644 --- a/lib/CRandomGenerator.h +++ b/lib/CRandomGenerator.h @@ -11,9 +11,11 @@ #pragma once #include +#include "serializer/Serializeable.h" VCMI_LIB_NAMESPACE_BEGIN + /// Generator to use for all randomization in game /// minstd_rand is selected due to following reasons: /// 1. Its randomization quality is below mt_19937 however this is unlikely to be noticeable in game @@ -22,12 +24,11 @@ using TGenerator = std::minstd_rand; using TIntDist = std::uniform_int_distribution; using TInt64Dist = std::uniform_int_distribution; using TRealDist = std::uniform_real_distribution; -using TRandI = std::function; /// The random generator randomly generates integers and real numbers("doubles") between /// a given range. This is a header only class and mainly a wrapper for /// convenient usage of the standard random API. An instance of this RNG is not thread safe. -class DLL_LINKAGE CRandomGenerator : public vstd::RNG, boost::noncopyable +class DLL_LINKAGE CRandomGenerator final : public vstd::RNG, boost::noncopyable, public Serializeable { public: /// Seeds the generator by default with the product of the current time in milliseconds and the @@ -43,45 +44,33 @@ public: /// current thread ID. void resetSeed(); - /// Generate several integer numbers within the same range. - /// e.g.: auto a = gen.getIntRange(0,10); a(); a(); a(); - /// requires: lower <= upper - TRandI getIntRange(int lower, int upper); - - vstd::TRandI64 getInt64Range(int64_t lower, int64_t upper) override; - /// Generates an integer between 0 and upper. /// requires: 0 <= upper - int nextInt(int upper); + int nextInt(int upper) override; + int64_t nextInt64(int64_t upper) override; /// requires: lower <= upper - int nextInt(int lower, int upper); + int nextInt(int lower, int upper) override; + int64_t nextInt64(int64_t lower, int64_t upper) override; /// Generates an integer between 0 and the maximum value it can hold. - int nextInt(); + int nextInt() override; + + /// + int nextBinomialInt(int coinsCount, double coinChance) override; - /// Generate several double/real numbers within the same range. - /// e.g.: auto a = gen.getDoubleRange(4.5,10.2); a(); a(); a(); - /// requires: lower <= upper - vstd::TRand getDoubleRange(double lower, double upper) override; /// Generates a double between 0 and upper. /// requires: 0 <= upper - double nextDouble(double upper); + double nextDouble(double upper) override; /// requires: lower <= upper - double nextDouble(double lower, double upper); - - /// Generates a double between 0.0 and 1.0. - double nextDouble(); + double nextDouble(double lower, double upper) override; /// Gets a globally accessible RNG which will be constructed once per thread. For the /// seed a combination of the thread ID and current time in milliseconds will be used. static CRandomGenerator & getDefault(); - /// Provide method so that this RNG can be used with legacy std:: API - TGenerator & getStdGenerator(); - private: TGenerator rand; diff --git a/lib/CSkillHandler.cpp b/lib/CSkillHandler.cpp index 9db6f7362..e5436ba08 100644 --- a/lib/CSkillHandler.cpp +++ b/lib/CSkillHandler.cpp @@ -14,14 +14,17 @@ #include "CSkillHandler.h" -#include "CGeneralTextHandler.h" +#include "constants/StringConstants.h" #include "filesystem/Filesystem.h" #include "json/JsonBonus.h" #include "json/JsonUtils.h" #include "modding/IdentifierStorage.h" #include "modding/ModUtility.h" #include "modding/ModScope.h" -#include "constants/StringConstants.h" +#include "texts/CGeneralTextHandler.h" +#include "texts/CLegacyConfigParser.h" +#include "texts/TextOperations.h" +#include "VCMI_Lib.h" VCMI_LIB_NAMESPACE_BEGIN @@ -29,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); @@ -42,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 @@ -61,6 +71,11 @@ std::string CSkill::getJsonKey() const return modScope + ':' + identifier; } +std::string CSkill::getModScope() const +{ + return modScope; +} + std::string CSkill::getDescriptionTextID(int level) const { TextIdentifier id("skill", modScope, identifier, "description", NSecondarySkill::levels[level]); @@ -114,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 << "])"; } @@ -190,7 +205,7 @@ const std::vector & CSkillHandler::getTypeNames() const return typeNames; } -CSkill * CSkillHandler::loadFromJson(const std::string & scope, const JsonNode & json, const std::string & identifier, size_t index) +std::shared_ptr CSkillHandler::loadFromJson(const std::string & scope, const JsonNode & json, const std::string & identifier, size_t index) { assert(identifier.find(':') == std::string::npos); assert(!scope.empty()); @@ -199,12 +214,13 @@ CSkill * CSkillHandler::loadFromJson(const std::string & scope, const JsonNode & major = json["obligatoryMajor"].Bool(); minor = json["obligatoryMinor"].Bool(); - auto * skill = new CSkill(SecondarySkill((si32)index), identifier, major, minor); + auto skill = std::make_shared(SecondarySkill(index), identifier, major, minor); 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: @@ -229,7 +245,7 @@ CSkill * CSkillHandler::loadFromJson(const std::string & scope, const JsonNode & 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(); @@ -262,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 fe699edcb..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,8 +51,9 @@ 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; SecondarySkill getId() const override; @@ -73,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); @@ -94,7 +97,7 @@ public: protected: const std::vector & getTypeNames() const override; - CSkill * loadFromJson(const std::string & scope, const JsonNode & json, const std::string & identifier, size_t index) override; + std::shared_ptr loadFromJson(const std::string & scope, const JsonNode & json, const std::string & identifier, size_t index) override; }; VCMI_LIB_NAMESPACE_END diff --git a/lib/CSoundBase.h b/lib/CSoundBase.h index 881296a10..6599ec7a9 100644 --- a/lib/CSoundBase.h +++ b/lib/CSoundBase.h @@ -184,7 +184,7 @@ VCMI_SOUND_NAME(CGORDefend) VCMI_SOUND_FILE(CGORDFND.wav) \ VCMI_SOUND_NAME(CGORKill) VCMI_SOUND_FILE(CGORKILL.wav) \ VCMI_SOUND_NAME(CGORMove) VCMI_SOUND_FILE(CGORMOVE.wav) \ VCMI_SOUND_NAME(CGORWNCE) VCMI_SOUND_FILE(CGORWNCE.wav) \ -VCMI_SOUND_NAME(chainLigthning) VCMI_SOUND_FILE(CHAINLTE.wav) \ +VCMI_SOUND_NAME(chainLightning) VCMI_SOUND_FILE(CHAINLTE.wav) \ VCMI_SOUND_NAME(chat) VCMI_SOUND_FILE(CHAT.wav) \ VCMI_SOUND_NAME(chest) VCMI_SOUND_FILE(CHEST.wav) \ VCMI_SOUND_NAME(CHMPAttack) VCMI_SOUND_FILE(CHMPATTK.wav) \ @@ -259,7 +259,7 @@ VCMI_SOUND_NAME(Dig) VCMI_SOUND_FILE(DIGSOUND.wav) \ VCMI_SOUND_NAME(DIPMAGK) VCMI_SOUND_FILE(DIPMAGK.wav) \ VCMI_SOUND_NAME(DISEASE) VCMI_SOUND_FILE(DISEASE.wav) \ VCMI_SOUND_NAME(DISGUISE) VCMI_SOUND_FILE(DISGUISE.wav) \ -VCMI_SOUND_NAME(DISPELL) VCMI_SOUND_FILE(DISPELL.wav) \ +VCMI_SOUND_NAME(DISPEL) VCMI_SOUND_FILE(DISPELL.wav) \ VCMI_SOUND_NAME(DISRUPTR) VCMI_SOUND_FILE(DISRUPTR.wav) \ VCMI_SOUND_NAME(dragonHall) VCMI_SOUND_FILE(DRAGON.wav) \ VCMI_SOUND_NAME(DRAINLIF) VCMI_SOUND_FILE(DRAINLIF.wav) \ @@ -817,7 +817,7 @@ VCMI_SOUND_NAME(PLIZShot) VCMI_SOUND_FILE(PLIZSHOT.wav) \ VCMI_SOUND_NAME(PLIZWNCE) VCMI_SOUND_FILE(PLIZWNCE.wav) \ VCMI_SOUND_NAME(POISON) VCMI_SOUND_FILE(POISON.wav) \ VCMI_SOUND_NAME(PRAYER) VCMI_SOUND_FILE(PRAYER.wav) \ -VCMI_SOUND_NAME(PRECISON) VCMI_SOUND_FILE(PRECISON.wav) \ +VCMI_SOUND_NAME(PRECISION) VCMI_SOUND_FILE(PRECION.wav) \ VCMI_SOUND_NAME(PROTECTA) VCMI_SOUND_FILE(PROTECTA.wav) \ VCMI_SOUND_NAME(PROTECTE) VCMI_SOUND_FILE(PROTECTE.wav) \ VCMI_SOUND_NAME(PROTECTF) VCMI_SOUND_FILE(PROTECTF.wav) \ @@ -842,7 +842,7 @@ VCMI_SOUND_NAME(RDDRMove) VCMI_SOUND_FILE(RDDRMOVE.wav) \ VCMI_SOUND_NAME(RDDRWNCE) VCMI_SOUND_FILE(RDDRWNCE.wav) \ VCMI_SOUND_NAME(REGENER) VCMI_SOUND_FILE(REGENER.wav) \ VCMI_SOUND_NAME(REMoveOB) VCMI_SOUND_FILE(REMOVEOB.wav) \ -VCMI_SOUND_NAME(RESURECT) VCMI_SOUND_FILE(RESURECT.wav) \ +VCMI_SOUND_NAME(RESURRECT) VCMI_SOUND_FILE(RESURECT.wav) \ VCMI_SOUND_NAME(RGRFAttack) VCMI_SOUND_FILE(RGRFATTK.wav) \ VCMI_SOUND_NAME(RGRFDefend) VCMI_SOUND_FILE(RGRFDFND.wav) \ VCMI_SOUND_NAME(RGRFKill) VCMI_SOUND_FILE(RGRFKILL.wav) \ diff --git a/lib/CStack.cpp b/lib/CStack.cpp index a84f8cd0f..1819e57df 100644 --- a/lib/CStack.cpp +++ b/lib/CStack.cpp @@ -15,7 +15,7 @@ #include #include -#include "CGeneralTextHandler.h" +#include "texts/CGeneralTextHandler.h" #include "battle/BattleInfo.h" #include "spells/CSpellHandler.h" #include "networkPacks/PacksForClientBattle.h" @@ -24,11 +24,11 @@ VCMI_LIB_NAMESPACE_BEGIN ///CStack -CStack::CStack(const CStackInstance * Base, const PlayerColor & O, int I, ui8 Side, const SlotID & S): +CStack::CStack(const CStackInstance * Base, const PlayerColor & O, int I, BattleSide Side, const SlotID & S): CBonusSystemNode(STACK_BATTLE), base(Base), ID(I), - type(Base->type), + typeID(Base->getId()), baseAmount(Base->count), owner(O), slot(S), @@ -45,10 +45,10 @@ CStack::CStack(): { } -CStack::CStack(const CStackBasicDescriptor * stack, const PlayerColor & O, int I, ui8 Side, const SlotID & S): +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]"; @@ -212,11 +212,9 @@ void CStack::prepareAttacked(BattleStackAttacked & bsa, vstd::RNG & rand, const auto resurrectedAdd = static_cast(baseAmount - (resurrectedCount / resurrectFactor)); - auto rangeGen = rand.getInt64Range(0, 99); - for(int32_t i = 0; i < resurrectedAdd; i++) { - if(resurrectValue > rangeGen()) + if(resurrectValue > rand.nextInt(0, 99)) resurrectedCount += 1; } @@ -298,12 +296,15 @@ std::vector CStack::meleeAttackHexes(const battle::Unit * attacker, c bool CStack::isMeleeAttackPossible(const battle::Unit * attacker, const battle::Unit * defender, BattleHex attackerPos, BattleHex defenderPos) { + if(defender->hasBonusOfType(BonusType::INVINCIBLE)) + return false; + return !meleeAttackHexes(attacker, defender, attackerPos, defenderPos).empty(); } 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 @@ -325,7 +326,7 @@ bool CStack::isOnTerrain(TerrainId terrain) const const CCreature * CStack::unitType() const { - return type; + return typeID.toCreature(); } int32_t CStack::unitBaseAmount() const @@ -351,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; } @@ -369,7 +370,7 @@ uint32_t CStack::unitId() const return ID; } -ui8 CStack::unitSide() const +BattleSide CStack::unitSide() const { return side; } @@ -400,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 3252a722a..d339eba40 100644 --- a/lib/CStack.h +++ b/lib/CStack.h @@ -27,12 +27,12 @@ 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; PlayerColor owner; //owner - player color (255 for neutrals) - ui8 side = 1; + BattleSide side = BattleSide::NONE; SlotID slot; //slot - position in garrison (may be 255 for neutrals/called creatures) @@ -41,8 +41,8 @@ public: BattleHex initialPosition; //position on battlefield; -2 - keep, -3 - lower tower, -4 - upper tower - CStack(const CStackInstance * base, const PlayerColor & O, int I, ui8 Side, const SlotID & S); - CStack(const CStackBasicDescriptor * stack, const PlayerColor & O, int I, ui8 Side, const SlotID & S = SlotID(255)); + CStack(const CStackInstance * base, const PlayerColor & O, int I, BattleSide Side, const SlotID & S); + CStack(const CStackBasicDescriptor * stack, const PlayerColor & O, int I, BattleSide Side, const SlotID & S = SlotID(255)); CStack(); ~CStack(); @@ -65,16 +65,16 @@ public: BattleHex::EDir destShiftDir() const; - void prepareAttacked(BattleStackAttacked & bsa, vstd::RNG & rand) const; //requires bsa.damageAmout filled + void prepareAttacked(BattleStackAttacked & bsa, vstd::RNG & rand) const; //requires bsa.damageAmount filled static void prepareAttacked(BattleStackAttacked & bsa, vstd::RNG & rand, - const std::shared_ptr & customState); //requires bsa.damageAmout filled + const std::shared_ptr & customState); //requires bsa.damageAmount filled const CCreature * unitType() const override; int32_t unitBaseAmount() const override; uint32_t unitId() const override; - ui8 unitSide() const override; + BattleSide unitSide() const override; PlayerColor unitOwner() const override; SlotID unitSlot() const override; @@ -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/CTownHandler.h b/lib/CTownHandler.h deleted file mode 100644 index f755d59e4..000000000 --- a/lib/CTownHandler.h +++ /dev/null @@ -1,358 +0,0 @@ -/* - * CTownHandler.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 "ConstTransitivePtr.h" -#include "ResourceSet.h" -#include "int3.h" -#include "GameConstants.h" -#include "IHandlerBase.h" -#include "LogicalExpression.h" -#include "battle/BattleHex.h" -#include "bonuses/Bonus.h" -#include "bonuses/BonusList.h" -#include "Point.h" -#include "rewardable/Info.h" -#include "filesystem/ResourcePath.h" - -VCMI_LIB_NAMESPACE_BEGIN - -class CLegacyConfigParser; -class JsonNode; -class CTown; -class CFaction; -struct BattleHex; -class JsonSerializeFormat; - -/// a typical building encountered in every castle ;] -/// this is structure available to both client and server -/// contains all mechanics-related data about town structures -class DLL_LINKAGE CBuilding -{ - std::string modScope; - std::string identifier; - -public: - using TRequired = LogicalExpression; - - CTown * town; // town this building belongs to - TResources resources; - TResources produce; - TRequired requirements; - - BuildingID bid; //structure ID - BuildingID upgrade; /// indicates that building "upgrade" can be improved by this, -1 = empty - BuildingSubID::EBuildingSubID subId; /// subtype for special buildings, -1 = the building is not special - std::set overrideBids; /// the building which bonuses should be overridden with bonuses of the current building - BonusList buildingBonuses; - BonusList onVisitBonuses; - - Rewardable::Info rewardableObjectInfo; ///configurable rewards for special buildings - - enum EBuildMode - { - BUILD_NORMAL, // 0 - normal, default - BUILD_AUTO, // 1 - auto - building appears when all requirements are built - BUILD_SPECIAL, // 2 - special - building can not be built normally - BUILD_GRAIL // 3 - grail - building reqires grail to be built - } mode; - - enum ETowerHeight // for lookup towers and some grails - { - HEIGHT_NO_TOWER = 5, // building has not 'lookout tower' ability - HEIGHT_LOW = 10, // low lookout tower, but castle without lookout tower gives radius 5 - HEIGHT_AVERAGE = 15, - HEIGHT_HIGH = 20, // such tower is in the Tower town - HEIGHT_SKYSHIP = std::numeric_limits::max() // grail, open entire map - } height; - - static const std::map MODES; - static const std::map TOWER_TYPES; - - CBuilding() : town(nullptr), mode(BUILD_NORMAL) {}; - - const BuildingTypeUniqueID getUniqueTypeID() const; - - std::string getJsonKey() const; - - std::string getNameTranslated() const; - std::string getDescriptionTranslated() const; - - std::string getBaseTextID() const; - std::string getNameTextID() const; - std::string getDescriptionTextID() const; - - //return base of upgrade(s) or this - BuildingID getBase() const; - - // returns how many times build has to be upgraded to become build - si32 getDistance(const BuildingID & build) const; - - STRONG_INLINE - bool IsTradeBuilding() const - { - return bid == BuildingID::MARKETPLACE || subId == BuildingSubID::ARTIFACT_MERCHANT || subId == BuildingSubID::FREELANCERS_GUILD; - } - - STRONG_INLINE - bool IsWeekBonus() const - { - return subId == BuildingSubID::STABLES || subId == BuildingSubID::MANA_VORTEX; - } - - STRONG_INLINE - bool IsVisitingBonus() const - { - return subId == BuildingSubID::ATTACK_VISITING_BONUS || - subId == BuildingSubID::DEFENSE_VISITING_BONUS || - subId == BuildingSubID::SPELL_POWER_VISITING_BONUS || - subId == BuildingSubID::KNOWLEDGE_VISITING_BONUS || - subId == BuildingSubID::EXPERIENCE_VISITING_BONUS || - subId == BuildingSubID::CUSTOM_VISITING_BONUS; - } - - void addNewBonus(const std::shared_ptr & b, BonusList & bonusList) const; - - friend class CTownHandler; -}; - -/// This is structure used only by client -/// Consists of all gui-related data about town structures -/// Should be moved from lib to client -struct DLL_LINKAGE CStructure -{ - CBuilding * building; // base building. If null - this structure will be always present on screen - CBuilding * buildable; // building that will be used to determine built building and visible cost. Usually same as "building" - - int3 pos; - AnimationPath defName; - ImagePath borderName; - ImagePath areaName; - std::string identifier; - - bool hiddenUpgrade; // used only if "building" is upgrade, if true - structure on town screen will behave exactly like parent (mouse clicks, hover texts, etc) -}; - -struct DLL_LINKAGE SPuzzleInfo -{ - ui16 number; //type of puzzle - si16 x, y; //position - ui16 whenUncovered; //determines the sequnce of discovering (the lesser it is the sooner puzzle will be discovered) - ImagePath filename; //file with graphic of this puzzle -}; - -class DLL_LINKAGE CFaction : public Faction -{ - friend class CTownHandler; - friend class CBuilding; - friend class CTown; - - std::string modScope; - std::string identifier; - - FactionID index = FactionID::NEUTRAL; - - FactionID getFaction() const override; //This function should not be used - -public: - TerrainId nativeTerrain; - EAlignment alignment = EAlignment::NEUTRAL; - bool preferUndergroundPlacement = false; - bool special = false; - - /// Boat that will be used by town shipyard (if any) - /// and for placing heroes directly on boat (in map editor, water prisons & taverns) - BoatId boatType = BoatId::CASTLE; - - CTown * town = nullptr; //NOTE: can be null - - ImagePath creatureBg120; - ImagePath creatureBg130; - - std::vector puzzleMap; - - CFaction() = default; - ~CFaction(); - - int32_t getIndex() const override; - int32_t getIconIndex() const override; - std::string getJsonKey() const override; - void registerIcons(const IconRegistar & cb) const override; - FactionID getId() const override; - - std::string getNameTranslated() const override; - std::string getNameTextID() const override; - std::string getDescriptionTranslated() const; - std::string getDescriptionTextID() const; - - bool hasTown() const override; - TerrainId getNativeTerrain() const override; - EAlignment getAlignment() const override; - BoatId getBoatType() const override; - - void updateFrom(const JsonNode & data); - void serializeJson(JsonSerializeFormat & handler); -}; - -class DLL_LINKAGE CTown -{ - friend class CTownHandler; - size_t namesCount = 0; - -public: - CTown(); - ~CTown(); - - std::string getBuildingScope() const; - std::set getAllBuildings() const; - const CBuilding * getSpecialBuilding(BuildingSubID::EBuildingSubID subID) const; - std::string getGreeting(BuildingSubID::EBuildingSubID subID) const; - void setGreeting(BuildingSubID::EBuildingSubID subID, const std::string & message) const; //may affect only mutable field - BuildingID getBuildingType(BuildingSubID::EBuildingSubID subID) const; - - std::string getRandomNameTextID(size_t index) const; - size_t getRandomNamesCount() const; - - CFaction * faction; - - /// level -> list of creatures on this tier - // TODO: replace with pointers to CCreature - std::vector > creatures; - - std::map > buildings; - - std::vector dwellings; //defs for adventure map dwellings for new towns, [0] means tier 1 creatures etc. - std::vector dwellingNames; - - // should be removed at least from configs in favor of auto-detection - std::map hordeLvl; //[0] - first horde building creature level; [1] - second horde building (-1 if not present) - ui32 mageLevel; //max available mage guild level - GameResID primaryRes; - ArtifactID warMachine; - SpellID moatAbility; - - // 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; - - // Client-only data. Should be moved away from lib - struct ClientInfo - { - //icons [fort is present?][build limit reached?] -> index of icon in def files - int icons[2][2]; - std::string iconSmall[2][2]; /// icon names used during loading - std::string iconLarge[2][2]; - VideoPath tavernVideo; - AudioPath musicTheme; - ImagePath townBackground; - ImagePath guildBackground; - ImagePath guildWindow; - AnimationPath buildingsIcons; - ImagePath hallBackground; - /// vector[row][column] = list of buildings in this slot - std::vector< std::vector< std::vector > > hallSlots; - - /// list of town screen structures. - /// NOTE: index in vector is meaningless. Vector used instead of list for a bit faster access - std::vector > structures; - - std::string siegePrefix; - std::vector siegePositions; - CreatureID siegeShooter; // shooter creature ID - std::string towerIconSmall; - std::string towerIconLarge; - - } clientInfo; - -private: - ///generated bonusing buildings messages for all towns of this type. - mutable std::map specialMessages; //may be changed by CGTownBuilding::getVisitingBonusGreeting() const -}; - -class DLL_LINKAGE CTownHandler : public CHandlerBase -{ - struct BuildingRequirementsHelper - { - JsonNode json; - CBuilding * building; - CTown * town; - }; - - std::map warMachinesToLoad; - std::vector requirementsToLoad; - std::vector overriddenBidsToLoad; //list of buildings, which bonuses should be overridden. - - static const TPropagatorPtr & emptyPropagator(); - - void initializeRequirements(); - void initializeOverridden(); - void initializeWarMachines(); - - /// loads CBuilding's into town - void loadBuildingRequirements(CBuilding * building, const JsonNode & source, std::vector & bidsToLoad) const; - void loadBuilding(CTown * town, const std::string & stringID, const JsonNode & source); - void loadBuildings(CTown * town, const JsonNode & source); - - std::shared_ptr createBonus(CBuilding * build, BonusType type, int val) const; - std::shared_ptr createBonus(CBuilding * build, BonusType type, int val, BonusSubtypeID subtype) const; - std::shared_ptr createBonus(CBuilding * build, BonusType type, int val, BonusSubtypeID subtype, const TPropagatorPtr & prop) const; - - /// loads CStructure's into town - void loadStructure(CTown & town, const std::string & stringID, const JsonNode & source) const; - void loadStructures(CTown & town, const JsonNode & source) const; - - /// loads town hall vector (hallSlots) - void loadTownHall(CTown & town, const JsonNode & source) const; - void loadSiegeScreen(CTown & town, const JsonNode & source) const; - - void loadClientData(CTown & town, const JsonNode & source) const; - - void loadTown(CTown * town, const JsonNode & source); - - void loadPuzzle(CFaction & faction, const JsonNode & source) const; - - void loadRandomFaction(); - - -public: - template - static R getMappedValue(const K key, const R defval, const std::map & map, bool required = true); - template - static R getMappedValue(const JsonNode & node, const R defval, const std::map & map, bool required = true); - - CTown * randomTown; - CFaction * randomFaction; - - CTownHandler(); - ~CTownHandler(); - - std::vector loadLegacyData() 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 addBonusesForVanilaBuilding(CBuilding * building) const; - - void loadCustom() override; - void afterLoadFinalization() override; - - std::set getDefaultAllowed() const; - std::set getAllowedFactions(bool withTown = true) const; - - static void loadSpecialBuildingBonuses(const JsonNode & source, BonusList & bonusList, CBuilding * building); - -protected: - const std::vector & getTypeNames() const override; - CFaction * loadFromJson(const std::string & scope, const JsonNode & data, const std::string & identifier, size_t index) override; -}; - -VCMI_LIB_NAMESPACE_END diff --git a/lib/Color.h b/lib/Color.h index 6ad89513a..1bc03414a 100644 --- a/lib/Color.h +++ b/lib/Color.h @@ -57,6 +57,11 @@ public: h & b; h & a; } + + bool operator==(ColorRGBA const& rhs) const + { + return r == rhs.r && g == rhs.g && b == rhs.b && a == rhs.a; + } }; VCMI_LIB_NAMESPACE_END diff --git a/lib/GameSettings.cpp b/lib/GameSettings.cpp index bef48d5c4..2e177d8ef 100644 --- a/lib/GameSettings.cpp +++ b/lib/GameSettings.cpp @@ -33,86 +33,87 @@ std::vector IGameSettings::getVector(EGameSettings option) const return getValue(option).convertTo>(); } +GameSettings::GameSettings() = default; GameSettings::~GameSettings() = default; -GameSettings::GameSettings() - : gameSettings(static_cast(EGameSettings::OPTIONS_COUNT)) -{ -} - -void GameSettings::load(const JsonNode & input) -{ - struct SettingOption - { - EGameSettings setting; - std::string group; - std::string key; +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::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" }, }; - static const std::vector optionPath = { - {EGameSettings::BONUSES_GLOBAL, "bonuses", "global" }, - {EGameSettings::BONUSES_PER_HERO, "bonuses", "perHero" }, - {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_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::DWELLINGS_ACCUMULATE_WHEN_NEUTRAL, "dwellings", "accumulateWhenNeutral" }, - {EGameSettings::DWELLINGS_ACCUMULATE_WHEN_OWNED, "dwellings", "accumulateWhenOwned" }, - {EGameSettings::DWELLINGS_MERGE_ON_RECRUIT, "dwellings", "mergeOnRecruit" }, - {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_BACKPACK_CAP, "heroes", "backpackSize" }, - {EGameSettings::HEROES_TAVERN_INVITE, "heroes", "tavernInvite" }, - {EGameSettings::MAP_FORMAT_RESTORATION_OF_ERATHIA, "mapFormat", "restorationOfErathia" }, - {EGameSettings::MAP_FORMAT_ARMAGEDDONS_BLADE, "mapFormat", "armageddonsBlade" }, - {EGameSettings::MAP_FORMAT_SHADOW_OF_DEATH, "mapFormat", "shadowOfDeath" }, - {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::MARKETS_BLACK_MARKET_RESTOCK_PERIOD, "markets", "blackMarketRestockPeriod" }, - {EGameSettings::BANKS_SHOW_GUARDS_COMPOSITION, "banks", "showGuardsComposition" }, - {EGameSettings::MODULE_COMMANDERS, "modules", "commanders" }, - {EGameSettings::MODULE_STACK_ARTIFACT, "modules", "stackArtifact" }, - {EGameSettings::MODULE_STACK_EXPERIENCE, "modules", "stackExperience" }, - {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::PATHFINDER_IGNORE_GUARDS, "pathfinder", "ignoreGuards" }, - {EGameSettings::PATHFINDER_USE_BOAT, "pathfinder", "useBoat" }, - {EGameSettings::PATHFINDER_USE_MONOLITH_TWO_WAY, "pathfinder", "useMonolithTwoWay" }, - {EGameSettings::PATHFINDER_USE_MONOLITH_ONE_WAY_UNIQUE, "pathfinder", "useMonolithOneWayUnique" }, - {EGameSettings::PATHFINDER_USE_MONOLITH_ONE_WAY_RANDOM, "pathfinder", "useMonolithOneWayRandom" }, - {EGameSettings::PATHFINDER_USE_WHIRLPOOL, "pathfinder", "useWhirlpool" }, - {EGameSettings::PATHFINDER_ORIGINAL_FLY_RULES, "pathfinder", "originalFlyRules" }, - {EGameSettings::DIMENSION_DOOR_ONLY_TO_UNCOVERED_TILES, "spells", "dimensionDoorOnlyToUncoveredTiles"}, - {EGameSettings::DIMENSION_DOOR_EXPOSES_TERRAIN_TYPE, "spells", "dimensionDoorExposesTerrainType" }, - {EGameSettings::DIMENSION_DOOR_FAILURE_SPENDS_POINTS, "spells", "dimensionDoorFailureSpendsPoints" }, - {EGameSettings::DIMENSION_DOOR_TRIGGERS_GUARDS, "spells", "dimensionDoorTriggersGuards" }, - {EGameSettings::DIMENSION_DOOR_TOURNAMENT_RULES_LIMIT, "spells", "dimensionDoorTournamentRulesLimit"}, - {EGameSettings::TOWNS_BUILDINGS_PER_TURN_CAP, "towns", "buildingsPerTurnCap" }, - {EGameSettings::TOWNS_STARTING_DWELLING_CHANCES, "towns", "startingDwellingChances" }, - }; +void GameSettings::loadBase(const JsonNode & input) +{ + JsonUtils::validate(input, "vcmi:gameSettings", input.getModScope()); - for(const auto & option : optionPath) + for(const auto & option : settingProperties) { const JsonNode & optionValue = input[option.group][option.key]; size_t index = static_cast(option.setting); @@ -120,16 +121,59 @@ void GameSettings::load(const JsonNode & input) if(optionValue.isNull()) continue; - JsonUtils::mergeCopy(gameSettings[index], optionValue); + JsonUtils::mergeCopy(baseSettings[index], optionValue); } + actualSettings = baseSettings; +} + +void GameSettings::loadOverrides(const JsonNode & input) +{ + for(const auto & option : settingProperties) + { + const JsonNode & optionValue = input[option.group][option.key]; + if (!optionValue.isNull()) + addOverride(option.setting, optionValue); + } +} + +void GameSettings::addOverride(EGameSettings option, const JsonNode & input) +{ + size_t index = static_cast(option); + + overridenSettings[index] = input; + JsonNode newValue = baseSettings[index]; + JsonUtils::mergeCopy(newValue, input); + actualSettings[index] = newValue; } const JsonNode & GameSettings::getValue(EGameSettings option) const { auto index = static_cast(option); - assert(!gameSettings.at(index).isNull()); - return gameSettings.at(index); + assert(!actualSettings.at(index).isNull()); + return actualSettings.at(index); +} + +JsonNode GameSettings::getFullConfig() const +{ + JsonNode result; + for(const auto & option : settingProperties) + result[option.group][option.key] = getValue(option.setting); + + return result; +} + +JsonNode GameSettings::getAllOverrides() const +{ + JsonNode result; + for(const auto & option : settingProperties) + { + const JsonNode & value = overridenSettings[static_cast(option.setting)]; + if (!value.isNull()) + result[option.group][option.key] = value; + } + + return result; } VCMI_LIB_NAMESPACE_END diff --git a/lib/GameSettings.h b/lib/GameSettings.h index 2af03c5c9..9905fde8e 100644 --- a/lib/GameSettings.h +++ b/lib/GameSettings.h @@ -9,103 +9,65 @@ */ #pragma once +#include "IGameSettings.h" +#include "json/JsonNode.h" + VCMI_LIB_NAMESPACE_BEGIN -class JsonNode; - -enum class EGameSettings -{ - BONUSES_GLOBAL, - BONUSES_PER_HERO, - COMBAT_ATTACK_POINT_DAMAGE_FACTOR, - COMBAT_ATTACK_POINT_DAMAGE_FACTOR_CAP, - COMBAT_BAD_LUCK_DICE, - COMBAT_BAD_MORALE_DICE, - COMBAT_DEFENSE_POINT_DAMAGE_FACTOR, - COMBAT_DEFENSE_POINT_DAMAGE_FACTOR_CAP, - COMBAT_GOOD_LUCK_DICE, - COMBAT_GOOD_MORALE_DICE, - CREATURES_ALLOW_ALL_FOR_DOUBLE_MONTH, - CREATURES_ALLOW_RANDOM_SPECIAL_WEEKS, - CREATURES_DAILY_STACK_EXPERIENCE, - CREATURES_WEEKLY_GROWTH_CAP, - CREATURES_WEEKLY_GROWTH_PERCENT, - DWELLINGS_ACCUMULATE_WHEN_NEUTRAL, - DWELLINGS_ACCUMULATE_WHEN_OWNED, - DWELLINGS_MERGE_ON_RECRUIT, - HEROES_PER_PLAYER_ON_MAP_CAP, - HEROES_PER_PLAYER_TOTAL_CAP, - HEROES_RETREAT_ON_WIN_WITHOUT_TROOPS, - HEROES_STARTING_STACKS_CHANCES, - HEROES_BACKPACK_CAP, - HEROES_TAVERN_INVITE, - MARKETS_BLACK_MARKET_RESTOCK_PERIOD, - BANKS_SHOW_GUARDS_COMPOSITION, - MODULE_COMMANDERS, - MODULE_STACK_ARTIFACT, - MODULE_STACK_EXPERIENCE, - TEXTS_ARTIFACT, - TEXTS_CREATURE, - TEXTS_FACTION, - TEXTS_HERO, - TEXTS_HERO_CLASS, - TEXTS_OBJECT, - TEXTS_RIVER, - TEXTS_ROAD, - TEXTS_SPELL, - TEXTS_TERRAIN, - MAP_FORMAT_RESTORATION_OF_ERATHIA, - MAP_FORMAT_ARMAGEDDONS_BLADE, - MAP_FORMAT_SHADOW_OF_DEATH, - MAP_FORMAT_HORN_OF_THE_ABYSS, - MAP_FORMAT_JSON_VCMI, - MAP_FORMAT_IN_THE_WAKE_OF_GODS, - PATHFINDER_USE_BOAT, - PATHFINDER_IGNORE_GUARDS, - PATHFINDER_USE_MONOLITH_TWO_WAY, - PATHFINDER_USE_MONOLITH_ONE_WAY_UNIQUE, - PATHFINDER_USE_MONOLITH_ONE_WAY_RANDOM, - PATHFINDER_USE_WHIRLPOOL, - PATHFINDER_ORIGINAL_FLY_RULES, - TOWNS_BUILDINGS_PER_TURN_CAP, - TOWNS_STARTING_DWELLING_CHANCES, - COMBAT_ONE_HEX_TRIGGERS_OBSTACLES, - DIMENSION_DOOR_ONLY_TO_UNCOVERED_TILES, - DIMENSION_DOOR_EXPOSES_TERRAIN_TYPE, - DIMENSION_DOOR_FAILURE_SPENDS_POINTS, - DIMENSION_DOOR_TRIGGERS_GUARDS, - DIMENSION_DOOR_TOURNAMENT_RULES_LIMIT, - - OPTIONS_COUNT -}; - -class DLL_LINKAGE IGameSettings -{ -public: - virtual const JsonNode & getValue(EGameSettings option) const = 0; - virtual ~IGameSettings() = default; - - bool getBoolean(EGameSettings option) const; - int64_t getInteger(EGameSettings option) const; - double getDouble(EGameSettings option) const; - std::vector getVector(EGameSettings option) const; -}; - class DLL_LINKAGE GameSettings final : public IGameSettings, boost::noncopyable { - std::vector gameSettings; + struct SettingOption + { + EGameSettings setting; + std::string group; + std::string key; + }; + + static constexpr int32_t OPTIONS_COUNT = static_cast(EGameSettings::OPTIONS_COUNT); + static const std::vector settingProperties; + + // contains base settings, like those defined in base game or mods + std::array baseSettings; + // contains settings that were overriden, in map or in random map template + std::array overridenSettings; + // for convenience / performance, contains actual settings - combined version of base and override settings + std::array actualSettings; + + // converts all existing overrides into a single json node for serialization + JsonNode getAllOverrides() const; public: GameSettings(); ~GameSettings(); - void load(const JsonNode & input); + /// Loads settings as 'base settings' that can be overriden + /// For settings defined in vcmi or in mods + void loadBase(const JsonNode & input); + + /// Loads setting as an override, for use in maps or rmg templates + /// undefined behavior if setting was already overriden (TODO: decide which approach is better - replace or append) + void addOverride(EGameSettings option, const JsonNode & input); + + // loads all overrides from provided json node, for deserialization + void loadOverrides(const JsonNode &); + + JsonNode getFullConfig() const override; const JsonNode & getValue(EGameSettings option) const override; template void serialize(Handler & h) { - h & gameSettings; + if (h.saving) + { + JsonNode overrides = getAllOverrides(); + h & overrides; + } + else + { + JsonNode overrides; + h & overrides; + loadOverrides(overrides); + } } }; diff --git a/lib/IGameCallback.cpp b/lib/IGameCallback.cpp index 4dc1a1c3c..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" @@ -19,7 +18,8 @@ #include "bonuses/Limiters.h" #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" @@ -27,6 +27,7 @@ #include "mapObjectConstructors/AObjectTypeHandler.h" #include "mapObjectConstructors/CObjectClassesHandler.h" #include "mapObjects/CGMarket.h" +#include "mapObjects/TownBuildingInstance.h" #include "mapObjects/CGTownInstance.h" #include "mapObjects/CObjectHandler.h" #include "mapObjects/CQuest.h" @@ -40,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" @@ -51,6 +51,8 @@ #include "RiverHandler.h" #include "TerrainHandler.h" +#include + VCMI_LIB_NAMESPACE_BEGIN void CPrivilegedInfoCallback::getFreeTiles(std::vector & tiles) const @@ -69,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); } } @@ -103,8 +105,8 @@ void CPrivilegedInfoCallback::getTilesInRange(std::unordered_set & tiles, if(distance <= radious) { if(!player - || (mode == ETileVisibility::HIDDEN && (*team->fogOfWarMap)[pos.z][xd][yd] == 0) - || (mode == ETileVisibility::REVEALED && (*team->fogOfWarMap)[pos.z][xd][yd] == 1) + || (mode == ETileVisibility::HIDDEN && team->fogOfWarMap[pos.z][xd][yd] == 0) + || (mode == ETileVisibility::REVEALED && team->fogOfWarMap[pos.z][xd][yd] == 1) ) tiles.insert(int3(xd,yd,pos.z)); } @@ -146,14 +148,14 @@ void CPrivilegedInfoCallback::getAllTiles(std::unordered_set & tiles, std: } } -void CPrivilegedInfoCallback::pickAllowedArtsSet(std::vector & out, CRandomGenerator & 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) @@ -177,8 +179,7 @@ CGameState * CPrivilegedInfoCallback::gameState() return gs; } -template -void CPrivilegedInfoCallback::loadCommonState(Loader & in) +void CPrivilegedInfoCallback::loadCommonState(CLoadFile & in) { logGlobal->info("Loading lib part of game..."); in.checkMagicBytes(SAVEGAME_MAGIC); @@ -200,8 +201,7 @@ void CPrivilegedInfoCallback::loadCommonState(Loader & in) in.serializer & gs; } -template -void CPrivilegedInfoCallback::saveCommonState(Saver & out) const +void CPrivilegedInfoCallback::saveCommonState(CSaveFile & out) const { ActiveModsInSaveList activeMods; @@ -217,10 +217,6 @@ void CPrivilegedInfoCallback::saveCommonState(Saver & out) const out.serializer & gs; } -// hardly memory usage for `-gdwarf-4` flag -template DLL_LINKAGE void CPrivilegedInfoCallback::loadCommonState(CLoadFile &); -template DLL_LINKAGE void CPrivilegedInfoCallback::saveCommonState(CSaveFile &) const; - TerrainTile * CNonConstInfoCallback::getTile(const int3 & pos) { if(!gs->map->isInTheMap(pos)) @@ -284,14 +280,16 @@ CArtifactSet * CNonConstInfoCallback::getArtSet(const ArtifactLocation & loc) return hero; } } - else if(auto market = dynamic_cast(getObjInstance(loc.artHolder))) + else if(auto market = getMarket(loc.artHolder)) { - return market; + if(auto artSet = market->getArtifactsStorage()) + return artSet; } - else + else if(auto army = getArmyInstance(loc.artHolder)) { - return nullptr; + return army->getStackPtr(loc.creature.value()); } + return nullptr; } bool IGameCallback::isVisitCoveredByAnotherQuery(const CGObjectInstance *obj, const CGHeroInstance *hero) diff --git a/lib/IGameCallback.h b/lib/IGameCallback.h index b3d121294..c93f31d68 100644 --- a/lib/IGameCallback.h +++ b/lib/IGameCallback.h @@ -16,16 +16,25 @@ VCMI_LIB_NAMESPACE_BEGIN +namespace vstd +{ +class RNG; +} + struct SetMovePoints; struct GiveBonus; struct BlockingDialog; struct TeleportDialog; struct StackLocation; struct ArtifactLocation; -class CRandomGenerator; +struct BankConfig; +struct BattleLayout; class CCreatureSet; class CStackBasicDescriptor; class CGCreature; +class CSaveFile; +class CLoadFile; +class IObjectInterface; enum class EOpenWindowMode : uint8_t; namespace spells @@ -33,6 +42,11 @@ namespace spells class Caster; } +namespace Rewardable +{ + struct Configuration; +} + #if SCRIPTING_ENABLED namespace scripting { @@ -61,32 +75,33 @@ 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, CRandomGenerator & rand); + void pickAllowedArtsSet(std::vector & out, vstd::RNG & rand); void getAllowedSpells(std::vector &out, std::optional level = std::nullopt); - template - void saveCommonState(Saver &out) const; //stores GS and VLC - - template - void loadCommonState(Loader &in); //loads GS and VLC + void saveCommonState(CSaveFile &out) const; //stores GS and VLC + void loadCommonState(CLoadFile &in); //loads GS and VLC }; class DLL_LINKAGE IGameEventCallback { public: virtual void setObjPropertyValue(ObjectInstanceID objid, ObjProperty prop, int32_t value = 0) = 0; + virtual void setBankObjectConfiguration(ObjectInstanceID objid, const BankConfig & configuration) = 0; + virtual void setRewardableObjectConfiguration(ObjectInstanceID mapObjectID, const Rewardable::Configuration & configuration) = 0; + virtual void setRewardableObjectConfiguration(ObjectInstanceID townInstanceID, BuildingID buildingID, const Rewardable::Configuration & configuration) = 0; virtual void setObjPropertyID(ObjectInstanceID objid, ObjProperty prop, ObjPropertyID identifier) = 0; 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 createObject(const int3 & visitablePosition, const PlayerColor & initiator, MapObjectID type, MapObjectSubID subtype) = 0; + virtual void createBoat(const int3 & visitablePosition, BoatId type, PlayerColor initiator) = 0; virtual void setOwner(const CGObjectInstance * objid, PlayerColor owner)=0; virtual void giveExperience(const CGHeroInstance * hero, TExpType val) =0; virtual void changePrimSkill(const CGHeroInstance * hero, PrimarySkill which, si64 val, bool abs=false)=0; virtual void changeSecSkill(const CGHeroInstance * hero, SecondarySkill which, int val, bool abs=false)=0; - virtual void showBlockingDialog(BlockingDialog *iw) =0; + virtual void showBlockingDialog(const IObjectInterface * caller, BlockingDialog *iw) =0; virtual void showGarrisonDialog(ObjectInstanceID upobj, ObjectInstanceID hid, bool removableUnits) =0; //cb will be called when player closes garrison window virtual void showTeleportDialog(TeleportDialog *iw) =0; virtual void showObjectWindow(const CGObjectInstance * object, EOpenWindowMode window, const CGHeroInstance * visitor, bool addQuery) = 0; @@ -106,17 +121,17 @@ public: virtual void removeAfterVisit(const CGObjectInstance *object) = 0; //object will be destroyed when interaction is over. Do not call when interaction is not ongoing! - virtual bool giveHeroNewArtifact(const CGHeroInstance * h, const CArtifact * artType, ArtifactPosition pos) = 0; - virtual bool putArtifact(const ArtifactLocation & al, const CArtifactInstance * art, std::optional askAssemble = std::nullopt) = 0; - virtual void removeArtifact(const ArtifactLocation &al) = 0; + 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 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; virtual void heroVisitCastle(const CGTownInstance * obj, const CGHeroInstance * hero)=0; virtual void visitCastleObjects(const CGTownInstance * obj, const CGHeroInstance * hero)=0; virtual void stopHeroVisitCastle(const CGTownInstance * obj, const CGHeroInstance * hero)=0; - virtual void startBattlePrimary(const CArmedInstance *army1, const CArmedInstance *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool creatureBank = false, const CGTownInstance *town = nullptr)=0; //use hero=nullptr for no hero - virtual void startBattleI(const CArmedInstance *army1, const CArmedInstance *army2, int3 tile, bool creatureBank = false)=0; //if any of armies is hero, hero will be used - virtual void startBattleI(const CArmedInstance *army1, const CArmedInstance *army2, bool creatureBank = false)=0; //if any of armies is hero, hero will be used, visitable tile of second obj is place of battle + virtual void startBattle(const CArmedInstance *army1, const CArmedInstance *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, const BattleLayout & layout, const CGTownInstance *town)=0; //use hero=nullptr for no hero + virtual void startBattle(const CArmedInstance *army1, const CArmedInstance *army2)=0; //if any of armies is hero, hero will be used, visitable tile of second obj is place of battle virtual bool moveHero(ObjectInstanceID hid, int3 dst, EMovementMode moveMove, bool transit = false, PlayerColor asker = PlayerColor::NEUTRAL)=0; virtual bool swapGarrisonOnSiege(ObjectInstanceID tid)=0; virtual void giveHeroBonus(GiveBonus * bonus)=0; @@ -125,12 +140,15 @@ 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(std::unordered_set &tiles, PlayerColor player, ETileVisibility mode) = 0; + virtual void changeFogOfWar(const std::unordered_set &tiles, PlayerColor player, ETileVisibility mode) = 0; virtual void castSpell(const spells::Caster * caster, SpellID spellID, const int3 &pos) = 0; + + virtual vstd::RNG & getRandomGenerator() = 0; + }; class DLL_LINKAGE CNonConstInfoCallback : public CPrivilegedInfoCallback diff --git a/lib/IGameEventsReceiver.h b/lib/IGameEventsReceiver.h index 9ee40945a..ca7d5e86f 100644 --- a/lib/IGameEventsReceiver.h +++ b/lib/IGameEventsReceiver.h @@ -68,7 +68,7 @@ public: virtual void battleStacksEffectsSet(const BattleID & battleID, const SetStackEffect & sse){};//called when a specific effect is set to stacks virtual void battleTriggerEffect(const BattleID & battleID, const BattleTriggerEffect & bte){}; //called for various one-shot effects virtual void battleStartBefore(const BattleID & battleID, const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2) {}; //called just before battle start - virtual void battleStart(const BattleID & battleID, const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool side, bool replayAllowed){}; //called by engine when battle starts; side=0 - left, side=1 - right + virtual void battleStart(const BattleID & battleID, const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, BattleSide side, bool replayAllowed){}; //called by engine when battle starts; side=0 - left, side=1 - right virtual void battleUnitsChanged(const BattleID & battleID, const std::vector & units){}; virtual void battleObstaclesChanged(const BattleID & battleID, const std::vector & obstacles){}; virtual void battleCatapultAttacked(const BattleID & battleID, const CatapultAttack & ca){}; //called when catapult makes an attack @@ -90,7 +90,7 @@ public: virtual void artifactAssembled(const ArtifactLocation &al){}; virtual void artifactDisassembled(const ArtifactLocation &al){}; virtual void artifactMoved(const ArtifactLocation &src, const ArtifactLocation &dst){}; - virtual void bulkArtMovementStart(size_t numOfArts) {}; + virtual void bulkArtMovementStart(size_t totalNumOfArts, size_t possibleAssemblyNumOfArts) {}; virtual void askToAssembleArtifact(const ArtifactLocation & dst) {}; virtual void heroVisit(const CGHeroInstance *visitor, const CGObjectInstance *visitedObj, bool start){}; @@ -105,11 +105,11 @@ public: virtual void receivedResource(){}; virtual void showInfoDialog(EInfoWindowMode type, const std::string & text, const std::vector & components, int soundID){}; virtual void showRecruitmentDialog(const CGDwelling *dwelling, const CArmedInstance *dst, int level, QueryID queryID){} - virtual void showShipyardDialog(const IShipyard *obj){} //obj may be town or shipyard; state: 0 - can buid, 1 - lack of resources, 2 - dest tile is blocked, 3 - no water + virtual void showShipyardDialog(const IShipyard *obj){} //obj may be town or shipyard; state: 0 - can build, 1 - lack of resources, 2 - dest tile is blocked, 3 - no water virtual void showPuzzleMap(){}; virtual void viewWorldMap(){}; - virtual void showMarketWindow(const IMarket *market, const CGHeroInstance *visitor, QueryID queryID){}; + virtual void showMarketWindow(const IMarket * market, const CGHeroInstance * visitor, QueryID queryID){}; virtual void showUniversityWindow(const IMarket *market, const CGHeroInstance *visitor, QueryID queryID){}; virtual void showHillFortWindow(const CGObjectInstance *object, const CGHeroInstance *visitor){}; virtual void showThievesGuildWindow (const CGObjectInstance * obj){}; diff --git a/lib/IGameSettings.h b/lib/IGameSettings.h new file mode 100644 index 000000000..0fcae51f5 --- /dev/null +++ b/lib/IGameSettings.h @@ -0,0 +1,106 @@ +/* + * IIGameSettings.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 JsonNode; + +enum class EGameSettings +{ + BANKS_SHOW_GUARDS_COMPOSITION, + BONUSES_GLOBAL, + BONUSES_PER_HERO, + COMBAT_AREA_SHOT_CAN_TARGET_EMPTY_HEX, + COMBAT_ATTACK_POINT_DAMAGE_FACTOR, + COMBAT_ATTACK_POINT_DAMAGE_FACTOR_CAP, + COMBAT_BAD_LUCK_DICE, + COMBAT_BAD_MORALE_DICE, + COMBAT_DEFENSE_POINT_DAMAGE_FACTOR, + COMBAT_DEFENSE_POINT_DAMAGE_FACTOR_CAP, + COMBAT_GOOD_LUCK_DICE, + COMBAT_GOOD_MORALE_DICE, + COMBAT_LAYOUTS, + COMBAT_ONE_HEX_TRIGGERS_OBSTACLES, + CREATURES_ALLOW_ALL_FOR_DOUBLE_MONTH, + CREATURES_ALLOW_RANDOM_SPECIAL_WEEKS, + CREATURES_DAILY_STACK_EXPERIENCE, + CREATURES_WEEKLY_GROWTH_CAP, + CREATURES_WEEKLY_GROWTH_PERCENT, + DIMENSION_DOOR_EXPOSES_TERRAIN_TYPE, + DIMENSION_DOOR_FAILURE_SPENDS_POINTS, + DIMENSION_DOOR_ONLY_TO_UNCOVERED_TILES, + DIMENSION_DOOR_TOURNAMENT_RULES_LIMIT, + DIMENSION_DOOR_TRIGGERS_GUARDS, + DWELLINGS_ACCUMULATE_WHEN_NEUTRAL, + DWELLINGS_ACCUMULATE_WHEN_OWNED, + DWELLINGS_MERGE_ON_RECRUIT, + HEROES_BACKPACK_CAP, + HEROES_MINIMAL_PRIMARY_SKILLS, + HEROES_PER_PLAYER_ON_MAP_CAP, + HEROES_PER_PLAYER_TOTAL_CAP, + HEROES_RETREAT_ON_WIN_WITHOUT_TROOPS, + HEROES_STARTING_STACKS_CHANCES, + HEROES_TAVERN_INVITE, + MAP_FORMAT_ARMAGEDDONS_BLADE, + MAP_FORMAT_CHRONICLES, + MAP_FORMAT_HORN_OF_THE_ABYSS, + MAP_FORMAT_IN_THE_WAKE_OF_GODS, + MAP_FORMAT_JSON_VCMI, + MAP_FORMAT_RESTORATION_OF_ERATHIA, + MAP_FORMAT_SHADOW_OF_DEATH, + MARKETS_BLACK_MARKET_RESTOCK_PERIOD, + MODULE_COMMANDERS, + MODULE_STACK_ARTIFACT, + MODULE_STACK_EXPERIENCE, + PATHFINDER_IGNORE_GUARDS, + PATHFINDER_ORIGINAL_FLY_RULES, + PATHFINDER_USE_BOAT, + PATHFINDER_USE_MONOLITH_ONE_WAY_RANDOM, + PATHFINDER_USE_MONOLITH_ONE_WAY_UNIQUE, + PATHFINDER_USE_MONOLITH_TWO_WAY, + PATHFINDER_USE_WHIRLPOOL, + RESOURCES_WEEKLY_BONUSES_AI, + TEXTS_ARTIFACT, + TEXTS_CREATURE, + TEXTS_FACTION, + TEXTS_HERO, + TEXTS_HERO_CLASS, + TEXTS_OBJECT, + TEXTS_RIVER, + TEXTS_ROAD, + TEXTS_SPELL, + 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 +}; + +class DLL_LINKAGE IGameSettings +{ +public: + virtual JsonNode getFullConfig() const = 0; + virtual const JsonNode & getValue(EGameSettings option) const = 0; + virtual ~IGameSettings() = default; + + bool getBoolean(EGameSettings option) const; + int64_t getInteger(EGameSettings option) const; + double getDouble(EGameSettings option) const; + std::vector getVector(EGameSettings option) const; +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/IHandlerBase.cpp b/lib/IHandlerBase.cpp index 1512c0680..a82c2b97e 100644 --- a/lib/IHandlerBase.cpp +++ b/lib/IHandlerBase.cpp @@ -13,6 +13,7 @@ #include "modding/IdentifierStorage.h" #include "modding/ModScope.h" #include "modding/CModHandler.h" +#include "VCMI_Lib.h" VCMI_LIB_NAMESPACE_BEGIN diff --git a/lib/IHandlerBase.h b/lib/IHandlerBase.h index 1666acee1..022617de9 100644 --- a/lib/IHandlerBase.h +++ b/lib/IHandlerBase.h @@ -9,9 +9,6 @@ */ #pragma once -#include "../lib/ConstTransitivePtr.h" -#include "VCMI_Lib.h" - VCMI_LIB_NAMESPACE_BEGIN class JsonNode; @@ -60,13 +57,7 @@ template } public: - virtual ~CHandlerBase() - { - for(auto & o : objects) - { - o.dellNull(); - } - } + using ObjectPtr = std::shared_ptr<_Object>; const Entity * getBaseByIndex(const int32_t index) const override { @@ -95,23 +86,19 @@ public: void loadObject(std::string scope, std::string name, const JsonNode & data) override { - auto object = loadFromJson(scope, data, name, objects.size()); - - objects.push_back(object); + objects.push_back(loadFromJson(scope, data, name, objects.size())); for(const auto & type_name : getTypeNames()) - registerObject(scope, type_name, name, object->getIndex()); + registerObject(scope, type_name, name, objects.back()->getIndex()); } void loadObject(std::string scope, std::string name, const JsonNode & data, size_t index) override { - auto object = loadFromJson(scope, data, name, index); - assert(objects[index] == nullptr); // ensure that this id was not loaded before - objects[index] = object; + objects[index] = loadFromJson(scope, data, name, index); for(const auto & type_name : getTypeNames()) - registerObject(scope, type_name, name, object->getIndex()); + registerObject(scope, type_name, name, objects[index]->getIndex()); } const _Object * operator[] (const _ObjectID id) const @@ -124,25 +111,13 @@ public: return getObjectImpl(index); } - void updateEntity(int32_t index, const JsonNode & data) - { - if(index < 0 || index >= objects.size()) - { - logMod->error("%s id %d is invalid", getTypeNames()[0], index); - } - else - { - objects.at(index)->updateFrom(data); - } - } - size_t size() const { return objects.size(); } protected: - virtual _Object * loadFromJson(const std::string & scope, const JsonNode & json, const std::string & identifier, size_t index) = 0; + virtual ObjectPtr loadFromJson(const std::string & scope, const JsonNode & json, const std::string & identifier, size_t index) = 0; virtual const std::vector & getTypeNames() const = 0; template @@ -159,7 +134,7 @@ protected: } public: //todo: make private - std::vector> objects; + std::vector objects; }; VCMI_LIB_NAMESPACE_END diff --git a/lib/LogicalExpression.cpp b/lib/LogicalExpression.cpp index 2b1a8cb5a..1caacaef4 100644 --- a/lib/LogicalExpression.cpp +++ b/lib/LogicalExpression.cpp @@ -12,7 +12,7 @@ #include "LogicalExpression.h" #include "VCMI_Lib.h" -#include "CGeneralTextHandler.h" +#include "texts/CGeneralTextHandler.h" VCMI_LIB_NAMESPACE_BEGIN diff --git a/lib/ObstacleHandler.cpp b/lib/ObstacleHandler.cpp index d7b613959..b78f048c9 100644 --- a/lib/ObstacleHandler.cpp +++ b/lib/ObstacleHandler.cpp @@ -12,6 +12,7 @@ #include "BattleFieldHandler.h" #include "json/JsonNode.h" #include "modding/IdentifierStorage.h" +#include "VCMI_Lib.h" VCMI_LIB_NAMESPACE_BEGIN @@ -27,7 +28,12 @@ int32_t ObstacleInfo::getIconIndex() const std::string ObstacleInfo::getJsonKey() const { - return identifier; + return modScope + ':' + identifier; +} + +std::string ObstacleInfo::getModScope() const +{ + return modScope; } std::string ObstacleInfo::getNameTranslated() const @@ -84,12 +90,13 @@ bool ObstacleInfo::isAppropriate(const TerrainId terrainType, const BattleField return vstd::contains(allowedTerrains, terrainType); } -ObstacleInfo * ObstacleHandler::loadFromJson(const std::string & scope, const JsonNode & json, const std::string & identifier, size_t index) +std::shared_ptr ObstacleHandler::loadFromJson(const std::string & scope, const JsonNode & json, const std::string & identifier, size_t index) { assert(identifier.find(':') == std::string::npos); - auto * info = new ObstacleInfo(Obstacle(index), identifier); + auto info = std::make_shared(Obstacle(index), identifier); + info->modScope = scope; info->animation = AnimationPath::fromJson(json["animation"]); info->width = json["width"].Integer(); info->height = json["height"].Integer(); diff --git a/lib/ObstacleHandler.h b/lib/ObstacleHandler.h index 840016542..fe4dbdac0 100644 --- a/lib/ObstacleHandler.h +++ b/lib/ObstacleHandler.h @@ -31,6 +31,7 @@ public: Obstacle obstacle; si32 iconIndex; + std::string modScope; std::string identifier; AudioPath appearSound; AnimationPath appearAnimation; @@ -47,6 +48,7 @@ public: int32_t getIndex() const override; int32_t getIconIndex() const override; std::string getJsonKey() const override; + std::string getModScope() const override; std::string getNameTranslated() const override; std::string getNameTextID() const override; void registerIcons(const IconRegistar & cb) const override; @@ -64,8 +66,8 @@ public: class ObstacleHandler: public CHandlerBase { -public: - ObstacleInfo * loadFromJson(const std::string & scope, +public: + std::shared_ptr loadFromJson(const std::string & scope, const JsonNode & json, const std::string & identifier, size_t index) override; diff --git a/lib/Rect.h b/lib/Rect.h index 11213d2d0..9a378c8ac 100644 --- a/lib/Rect.h +++ b/lib/Rect.h @@ -119,6 +119,13 @@ public: return Rect(x-p.x,y-p.y,w,h); } + template + Rect operator*(const T &mul) const + { + return Rect(x*mul, y*mul, w*mul, h*mul); + } + + Rect& operator=(const Rect &p) { x = p.x; diff --git a/lib/ResourceSet.cpp b/lib/ResourceSet.cpp index 71db0db46..c74f21e57 100644 --- a/lib/ResourceSet.cpp +++ b/lib/ResourceSet.cpp @@ -64,6 +64,12 @@ void ResourceSet::positive() vstd::amax(elem, 0); } +void ResourceSet::applyHandicap(int percentage) +{ + for(auto & elem : *this) + elem = vstd::divideAndCeil(elem * percentage, 100); +} + static bool canAfford(const ResourceSet &res, const ResourceSet &price) { assert(res.size() == price.size() && price.size() == GameConstants::RESOURCE_QUANTITY); diff --git a/lib/ResourceSet.h b/lib/ResourceSet.h index 4d9a0e695..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; @@ -190,6 +199,7 @@ public: DLL_LINKAGE void amax(const TResourceCap &val); //performs vstd::amax on each element DLL_LINKAGE void amin(const TResourceCap &val); //performs vstd::amin on each element DLL_LINKAGE void positive(); //values below 0 are set to 0 - upgrade cost can't be negative, for example + DLL_LINKAGE void applyHandicap(int percentage); DLL_LINKAGE bool nonZero() const; //returns true if at least one value is non-zero; DLL_LINKAGE bool canAfford(const ResourceSet &price) const; DLL_LINKAGE bool canBeAfforded(const ResourceSet &res) const; diff --git a/lib/RiverHandler.cpp b/lib/RiverHandler.cpp index 7ed62a7a5..c903b18e3 100644 --- a/lib/RiverHandler.cpp +++ b/lib/RiverHandler.cpp @@ -10,20 +10,21 @@ #include "StdInc.h" #include "RiverHandler.h" -#include "CGeneralTextHandler.h" -#include "GameSettings.h" +#include "texts/CGeneralTextHandler.h" +#include "IGameSettings.h" #include "json/JsonNode.h" +#include "VCMI_Lib.h" VCMI_LIB_NAMESPACE_BEGIN RiverTypeHandler::RiverTypeHandler() { - objects.push_back(new RiverType()); + objects.emplace_back(new RiverType()); VLC->generaltexth->registerString("core", objects[0]->getNameTextID(), ""); } -RiverType * RiverTypeHandler::loadFromJson( +std::shared_ptr RiverTypeHandler::loadFromJson( const std::string & scope, const JsonNode & json, const std::string & identifier, @@ -31,7 +32,7 @@ RiverType * RiverTypeHandler::loadFromJson( { assert(identifier.find(':') == std::string::npos); - auto * info = new RiverType; + auto info = std::make_shared(); info->id = RiverId(index); info->identifier = identifier; @@ -49,7 +50,7 @@ RiverType * 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; } @@ -62,7 +63,7 @@ const std::vector & RiverTypeHandler::getTypeNames() const std::vector RiverTypeHandler::loadLegacyData() { - size_t dataSize = VLC->settings()->getInteger(EGameSettings::TEXTS_RIVER); + size_t dataSize = VLC->engineSettings()->getInteger(EGameSettings::TEXTS_RIVER); objects.resize(dataSize); return {}; @@ -73,6 +74,11 @@ std::string RiverType::getJsonKey() const return modScope + ":" + identifier; } +std::string RiverType::getModScope() const +{ + return modScope; +} + std::string RiverType::getNameTextID() const { return TextIdentifier( "river", modScope, identifier, "name" ).get(); diff --git a/lib/RiverHandler.h b/lib/RiverHandler.h index c13736e76..331058372 100644 --- a/lib/RiverHandler.h +++ b/lib/RiverHandler.h @@ -37,6 +37,7 @@ public: int32_t getIndex() const override { return id.getNum(); } int32_t getIconIndex() const override { return 0; } std::string getJsonKey() const override; + std::string getModScope() const override; void registerIcons(const IconRegistar & cb) const override {} RiverId getId() const override { return id;} void updateFrom(const JsonNode & data) {}; @@ -61,7 +62,7 @@ public: class DLL_LINKAGE RiverTypeHandler : public CHandlerBase { public: - virtual RiverType * loadFromJson( + std::shared_ptr loadFromJson( const std::string & scope, const JsonNode & json, const std::string & identifier, diff --git a/lib/RoadHandler.cpp b/lib/RoadHandler.cpp index 5ebbe54f1..0d82d9da6 100644 --- a/lib/RoadHandler.cpp +++ b/lib/RoadHandler.cpp @@ -10,20 +10,21 @@ #include "StdInc.h" #include "RoadHandler.h" -#include "CGeneralTextHandler.h" -#include "GameSettings.h" +#include "texts/CGeneralTextHandler.h" +#include "IGameSettings.h" #include "json/JsonNode.h" +#include "VCMI_Lib.h" VCMI_LIB_NAMESPACE_BEGIN RoadTypeHandler::RoadTypeHandler() { - objects.push_back(new RoadType()); + objects.emplace_back(new RoadType()); VLC->generaltexth->registerString("core", objects[0]->getNameTextID(), ""); } -RoadType * RoadTypeHandler::loadFromJson( +std::shared_ptr RoadTypeHandler::loadFromJson( const std::string & scope, const JsonNode & json, const std::string & identifier, @@ -31,7 +32,7 @@ RoadType * RoadTypeHandler::loadFromJson( { assert(identifier.find(':') == std::string::npos); - auto * info = new RoadType; + auto info = std::make_shared(); info->id = RoadId(index); info->identifier = identifier; @@ -40,7 +41,7 @@ RoadType * 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; } @@ -53,7 +54,7 @@ const std::vector & RoadTypeHandler::getTypeNames() const std::vector RoadTypeHandler::loadLegacyData() { - size_t dataSize = VLC->settings()->getInteger(EGameSettings::TEXTS_ROAD); + size_t dataSize = VLC->engineSettings()->getInteger(EGameSettings::TEXTS_ROAD); objects.resize(dataSize); return {}; @@ -64,6 +65,11 @@ std::string RoadType::getJsonKey() const return modScope + ":" + identifier; } +std::string RoadType::getModScope() const +{ + return modScope; +} + std::string RoadType::getNameTextID() const { return TextIdentifier( "road", modScope, identifier, "name" ).get(); diff --git a/lib/RoadHandler.h b/lib/RoadHandler.h index 5f31d1858..7de36cdc0 100644 --- a/lib/RoadHandler.h +++ b/lib/RoadHandler.h @@ -29,6 +29,7 @@ public: int32_t getIndex() const override { return id.getNum(); } int32_t getIconIndex() const override { return 0; } std::string getJsonKey() const override; + std::string getModScope() const override; void registerIcons(const IconRegistar & cb) const override {} RoadId getId() const override { return id;} void updateFrom(const JsonNode & data) {}; @@ -51,7 +52,7 @@ public: class DLL_LINKAGE RoadTypeHandler : public CHandlerBase { public: - virtual RoadType * loadFromJson( + std::shared_ptr loadFromJson( const std::string & scope, const JsonNode & json, const std::string & identifier, diff --git a/lib/StartInfo.cpp b/lib/StartInfo.cpp index 6acb048f8..46b365375 100644 --- a/lib/StartInfo.cpp +++ b/lib/StartInfo.cpp @@ -10,10 +10,11 @@ #include "StdInc.h" #include "StartInfo.h" -#include "CGeneralTextHandler.h" -#include "CTownHandler.h" -#include "CHeroHandler.h" +#include "texts/CGeneralTextHandler.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" @@ -24,7 +25,7 @@ VCMI_LIB_NAMESPACE_BEGIN PlayerSettings::PlayerSettings() - : bonus(PlayerStartingBonus::RANDOM), color(0), handicap(NO_HANDICAP), compOnly(false) + : bonus(PlayerStartingBonus::RANDOM), color(0), compOnly(false) { } @@ -89,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 29f2ebc5b..8a79dc344 100644 --- a/lib/StartInfo.h +++ b/lib/StartInfo.h @@ -15,6 +15,8 @@ #include "TurnTimerInfo.h" #include "ExtraOptionsInfo.h" #include "campaign/CampaignConstants.h" +#include "serializer/Serializeable.h" +#include "ResourceSet.h" VCMI_LIB_NAMESPACE_BEGIN @@ -50,9 +52,10 @@ struct DLL_LINKAGE SimturnsInfo h & optionalTurns; h & allowHumanWithAI; - static_assert(Handler::Version::RELEASE_143 < Handler::Version::CURRENT, "Please add ignoreAlliedContacts to serialization for 1.6"); - // disabled to allow multiplayer compatibility between 1.5.2 and 1.5.1 - // h & ignoreAlliedContacts + if (h.version >= Handler::Version::SAVE_COMPATIBILITY_FIXES) + h & ignoreAlliedContacts; + else + ignoreAlliedContacts = true; } }; @@ -64,6 +67,20 @@ enum class PlayerStartingBonus : int8_t RESOURCE = 2 }; +struct DLL_LINKAGE Handicap { + TResources startBonus = TResources(); + int percentIncome = 100; + int percentGrowth = 100; + + template + void serialize(Handler &h) + { + h & startBonus; + h & percentIncome; + h & percentGrowth; + } +}; + /// Struct which describes the name, the color, the starting bonus of a player struct DLL_LINKAGE PlayerSettings { @@ -76,8 +93,8 @@ struct DLL_LINKAGE PlayerSettings std::string heroNameTextId; PlayerColor color; //from 0 - - enum EHandicap {NO_HANDICAP, MILD, SEVERE}; - EHandicap handicap;//0-no, 1-mild, 2-severe + + Handicap handicap; std::string name; std::set connectedPlayerIDs; //Empty - AI, or connectrd player ids @@ -91,7 +108,14 @@ struct DLL_LINKAGE PlayerSettings h & heroNameTextId; h & bonus; h & color; - h & handicap; + if (h.version >= Handler::Version::PLAYER_HANDICAP) + h & handicap; + else + { + enum EHandicap {NO_HANDICAP, MILD, SEVERE}; + EHandicap handicapLegacy = NO_HANDICAP; + h & handicapLegacy; + } h & name; h & connectedPlayerIDs; h & compOnly; @@ -114,7 +138,7 @@ enum class EStartMode : int32_t }; /// Struct which describes the difficulty, the turn time,.. of a heroes match. -struct DLL_LINKAGE StartInfo +struct DLL_LINKAGE StartInfo : public Serializeable { EStartMode mode; ui8 difficulty; //0=easy; 4=impossible @@ -122,10 +146,7 @@ struct DLL_LINKAGE StartInfo using TPlayerInfos = std::map; TPlayerInfos playerInfos; //color indexed - ui32 seedToBeUsed; //0 if not sure (client requests server to decide, will be send in reply pack) - ui32 seedPostInit; //so we know that game is correctly synced at the start; 0 if not known yet - ui32 mapfileChecksum; //0 if not relevant - std::string startTimeIso8601; + time_t startTime; std::string fileURI; SimturnsInfo simturnsInfo; TurnTimerInfo turnTimerInfo; @@ -143,8 +164,8 @@ struct DLL_LINKAGE StartInfo // 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) @@ -152,24 +173,37 @@ struct DLL_LINKAGE StartInfo h & mode; h & difficulty; h & playerInfos; - h & seedToBeUsed; - h & seedPostInit; - h & mapfileChecksum; - h & startTimeIso8601; + if (h.version < Handler::Version::REMOVE_LIB_RNG) + { + uint32_t oldSeeds = 0; + h & oldSeeds; + h & oldSeeds; + h & oldSeeds; + } + 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; - if(h.version >= Handler::Version::HAS_EXTRA_OPTIONS) - h & extraOptionsInfo; - else - extraOptionsInfo = ExtraOptionsInfo(); + h & extraOptionsInfo; h & mapname; h & mapGenOptions; h & campState; } - StartInfo() : mode(EStartMode::INVALID), difficulty(1), seedToBeUsed(0), seedPostInit(0), - mapfileChecksum(0), startTimeIso8601(vstd::getDateTimeISO8601Basic(std::time(nullptr))), fileURI("") + StartInfo() + : mode(EStartMode::INVALID) + , difficulty(1) + , startTime(std::time(nullptr)) { } diff --git a/lib/TerrainHandler.cpp b/lib/TerrainHandler.cpp index 6980d98b8..f3431394a 100644 --- a/lib/TerrainHandler.cpp +++ b/lib/TerrainHandler.cpp @@ -10,31 +10,42 @@ #include "StdInc.h" #include "TerrainHandler.h" -#include "CGeneralTextHandler.h" -#include "GameSettings.h" +#include "IGameSettings.h" #include "json/JsonNode.h" #include "modding/IdentifierStorage.h" +#include "texts/CGeneralTextHandler.h" +#include "texts/CLegacyConfigParser.h" +#include "VCMI_Lib.h" VCMI_LIB_NAMESPACE_BEGIN -TerrainType * TerrainTypeHandler::loadFromJson( const std::string & scope, const JsonNode & json, const std::string & identifier, size_t index) +std::shared_ptr TerrainTypeHandler::loadFromJson( const std::string & scope, const JsonNode & json, const std::string & identifier, size_t index) { assert(identifier.find(':') == std::string::npos); - auto * info = new TerrainType; + auto info = std::make_shared(); info->id = TerrainId(index); info->identifier = identifier; info->modScope = scope; info->moveCost = static_cast(json["moveCost"].Integer()); - info->musicFilename = AudioPath::fromJson(json["music"]); + if (json["music"].isVector()) + { + for (auto const & entry : json["music"].Vector()) + info->musicFilename.push_back(AudioPath::fromJson(entry)); + } + else + { + info->musicFilename.push_back(AudioPath::fromJson(json["music"])); + } + info->tilesFilename = AnimationPath::fromJson(json["tiles"]); info->horseSound = AudioPath::fromJson(json["horseSound"]); info->horseSoundPenalty = AudioPath::fromJson(json["horseSoundPenalty"]); 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 = @@ -122,7 +133,7 @@ const std::vector & TerrainTypeHandler::getTypeNames() const std::vector TerrainTypeHandler::loadLegacyData() { - size_t dataSize = VLC->settings()->getInteger(EGameSettings::TEXTS_TERRAIN); + size_t dataSize = VLC->engineSettings()->getInteger(EGameSettings::TEXTS_TERRAIN); objects.resize(dataSize); @@ -180,6 +191,11 @@ std::string TerrainType::getJsonKey() const return modScope + ":" + identifier; } +std::string TerrainType::getModScope() const +{ + return modScope; +} + std::string TerrainType::getNameTextID() const { return TextIdentifier( "terrain", modScope, identifier, "name" ).get(); diff --git a/lib/TerrainHandler.h b/lib/TerrainHandler.h index 59c6ab62b..3d0715cb2 100644 --- a/lib/TerrainHandler.h +++ b/lib/TerrainHandler.h @@ -55,6 +55,7 @@ public: int32_t getIndex() const override { return id.getNum(); } int32_t getIconIndex() const override { return 0; } std::string getJsonKey() const override; + std::string getModScope() const override; void registerIcons(const IconRegistar & cb) const override {} TerrainId getId() const override { return id;} void updateFrom(const JsonNode & data) {}; @@ -67,7 +68,7 @@ public: ColorRGBA minimapBlocked; ColorRGBA minimapUnblocked; std::string shortIdentifier; - AudioPath musicFilename; + std::vector musicFilename; AnimationPath tilesFilename; std::string terrainViewPatterns; AudioPath horseSound; @@ -101,7 +102,7 @@ public: class DLL_LINKAGE TerrainTypeHandler : public CHandlerBase { public: - virtual TerrainType * loadFromJson( + std::shared_ptr loadFromJson( const std::string & scope, const JsonNode & json, const std::string & identifier, diff --git a/lib/TurnTimerInfo.cpp b/lib/TurnTimerInfo.cpp index 7bf0b63d5..a4ece9016 100644 --- a/lib/TurnTimerInfo.cpp +++ b/lib/TurnTimerInfo.cpp @@ -22,9 +22,9 @@ bool TurnTimerInfo::isBattleEnabled() const return turnTimer > 0 || baseTimer > 0 || unitTimer > 0 || battleTimer > 0; } -void TurnTimerInfo::substractTimer(int timeMs) +void TurnTimerInfo::subtractTimer(int timeMs) { - auto const & substractTimer = [&timeMs](int & targetTimer) + auto const & subtractTimer = [&timeMs](int & targetTimer) { if (targetTimer > timeMs) { @@ -38,10 +38,10 @@ void TurnTimerInfo::substractTimer(int timeMs) } }; - substractTimer(unitTimer); - substractTimer(battleTimer); - substractTimer(turnTimer); - substractTimer(baseTimer); + subtractTimer(unitTimer); + subtractTimer(battleTimer); + subtractTimer(turnTimer); + subtractTimer(baseTimer); } int TurnTimerInfo::valueMs() const diff --git a/lib/TurnTimerInfo.h b/lib/TurnTimerInfo.h index 697c24831..3627fdcf2 100644 --- a/lib/TurnTimerInfo.h +++ b/lib/TurnTimerInfo.h @@ -28,7 +28,7 @@ struct DLL_LINKAGE TurnTimerInfo bool isEnabled() const; bool isBattleEnabled() const; - void substractTimer(int timeMs); + void subtractTimer(int timeMs); int valueMs() const; bool operator == (const TurnTimerInfo & other) const; diff --git a/lib/VCMIDirs.cpp b/lib/VCMIDirs.cpp index 24b1b45eb..1eaadefc2 100644 --- a/lib/VCMIDirs.cpp +++ b/lib/VCMIDirs.cpp @@ -104,7 +104,7 @@ bool StartBatchCopyDataProgram( ":CLIENT_NOT_RUNNING" "\n" "echo %1% turned off..." "\n" - "echo Attempt to move datas." "\n" + "echo Attempt to move data." "\n" "echo From: %2%" "\n" "echo To: %4%" "\n" "echo Please resolve any conflicts..." "\n" @@ -118,7 +118,7 @@ bool StartBatchCopyDataProgram( "pause" "\n" // Press any key to continue... "goto REMOVE_OLD_DIR" "\n" ")" "\n" - "echo Game data updated succefully." "\n" + "echo Game data updated successfully." "\n" "echo Please update your shortcuts." "\n" "echo Press any key to start a game . . ." "\n" "pause > nul" "\n" @@ -138,7 +138,7 @@ bool StartBatchCopyDataProgram( bathFile << (boost::format(base) % exeName % from % (from / "*.*") % to % startGameString.str()).str(); bathFile.close(); - std::system(("start \"Updating VCMI datas\" /D \"" + to.string() + "\" \"" + bathFilename.string() + '\"').c_str()); + std::system(("start \"Updating VCMI data\" /D \"" + to.string() + "\" \"" + bathFilename.string() + '\"').c_str()); // start won't block std::system // /D start bat in other directory insteand of current directory. @@ -239,7 +239,7 @@ void VCMIDirsWIN32::init() if (bfs::current_path() == from) bfs::current_path(to); - // TODO: Log fact that we moved files succefully. + // TODO: Log fact that we moved files successfully. bfs::remove(from); return true; }; @@ -267,7 +267,7 @@ void VCMIDirsWIN32::init() { const bfs::path executablePath = getModulePath(nullptr); - // VCMI cann't determine executable path. + // VCMI can't determine executable path. // Use standard way to move directory and exit function. if (executablePath.empty()) return moveDirIfExists(from, to); @@ -473,7 +473,7 @@ void VCMIDirsOSX::init() const bfs::path& srcFilePath = file->path(); const bfs::path dstFilePath = to / srcFilePath.filename(); - // TODO: Aplication should ask user what to do when file exists: + // TODO: Application should ask user what to do when file exists: // replace/ignore/stop process/replace all/ignore all if (!bfs::exists(dstFilePath)) bfs::rename(srcFilePath, dstFilePath); diff --git a/lib/VCMI_Lib.cpp b/lib/VCMI_Lib.cpp index 885f4c4c0..b53dc06af 100644 --- a/lib/VCMI_Lib.cpp +++ b/lib/VCMI_Lib.cpp @@ -14,19 +14,18 @@ #include "CArtHandler.h" #include "CBonusTypeHandler.h" #include "CCreatureHandler.h" -#include "CHeroHandler.h" -#include "CTownHandler.h" #include "CConfigHandler.h" #include "RoadHandler.h" #include "RiverHandler.h" #include "TerrainHandler.h" -#include "CBuildingHandler.h" #include "spells/CSpellHandler.h" #include "spells/effects/Registry.h" #include "CSkillHandler.h" -#include "CGeneralTextHandler.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" @@ -136,42 +135,11 @@ const ObstacleService * LibClasses::obstacles() const return obstacleHandler.get(); } -const IGameSettings * LibClasses::settings() const +const IGameSettings * LibClasses::engineSettings() const { return settingsHandler.get(); } -void LibClasses::updateEntity(Metatype metatype, int32_t index, const JsonNode & data) -{ - switch(metatype) - { - case Metatype::ARTIFACT: - arth->updateEntity(index, data); - break; - case Metatype::CREATURE: - creh->updateEntity(index, data); - break; - case Metatype::FACTION: - townh->updateEntity(index, data); - break; - case Metatype::HERO_CLASS: - heroclassesh->updateEntity(index, data); - break; - case Metatype::HERO_TYPE: - heroh->updateEntity(index, data); - break; - case Metatype::SKILL: - skillh->updateEntity(index, data); - break; - case Metatype::SPELL: - spellh->updateEntity(index, data); - break; - default: - logGlobal->error("Invalid Metatype id %d", static_cast(metatype)); - break; - } -} - void LibClasses::loadFilesystem(bool extractArchives) { CStopWatch loadTime; @@ -188,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/VCMI_Lib.h b/lib/VCMI_Lib.h index fe5798150..9228c9356 100644 --- a/lib/VCMI_Lib.h +++ b/lib/VCMI_Lib.h @@ -20,7 +20,6 @@ class CHeroClassHandler; class CCreatureHandler; class CSpellHandler; class CSkillHandler; -class CBuildingHandler; class CObjectHandler; class CObjectClassesHandler; class ObstacleSetHandler; @@ -70,9 +69,7 @@ public: const SkillService * skills() const override; const BattleFieldService * battlefields() const override; const ObstacleService * obstacles() const override; - const IGameSettings * settings() const override; - - void updateEntity(Metatype metatype, int32_t index, const JsonNode & data) override; + const IGameSettings * engineSettings() const override; const spells::effects::Registry * spellEffects() const override; spells::effects::Registry * spellEffects() override; diff --git a/lib/VCMI_lib.cbp b/lib/VCMI_lib.cbp deleted file mode 100644 index 31c3ccba1..000000000 --- a/lib/VCMI_lib.cbp +++ /dev/null @@ -1,529 +0,0 @@ - - - - - - diff --git a/lib/VCMI_lib.vcxproj b/lib/VCMI_lib.vcxproj deleted file mode 100644 index 060a9d38b..000000000 --- a/lib/VCMI_lib.vcxproj +++ /dev/null @@ -1,504 +0,0 @@ - - - - - Debug - Win32 - - - Debug - x64 - - - RD - Win32 - - - RD - x64 - - - - {B952FFC5-3039-4DE1-9F08-90ACDA483D8F} - VCMI_lib - 10.0 - - - - DynamicLibrary - Unicode - true - v142 - - - DynamicLibrary - Unicode - true - v140_xp - - - DynamicLibrary - Unicode - v140_xp - true - - - DynamicLibrary - MultiByte - v140_xp - - - - - - - - - - - - - - - - - - - - - - - - - - - <_ProjectFileVersion>10.0.30128.1 - .. - $(VCMI_Out) - $(Configuration)\ - $(Configuration)\ - $(VCMI_Out) - $(VCMI_Out) - $(Configuration)\ - $(Configuration)\ - AllRules.ruleset - AllRules.ruleset - - - - - AllRules.ruleset - AllRules.ruleset - - - - - - - - /MP4 %(AdditionalOptions) /bigobj - /Zm150 - Disabled - $(BOOSTDIR);$(ZLIBDIR);$(SDLDIR) - false - EnableFastChecks - MultiThreadedDebugDLL - Level3 - EditAndContinue - 4251;%(DisableSpecificWarnings) - false - NoListing - false - Use - StdInc.h - $(IntDir)$(TargetName).pch - VCMI_DLL;%(PreprocessorDefinitions) - false - - - minizip.lib;zlib.lib;%(AdditionalDependencies) - ..\..\libs - - - - - /MP4 %(AdditionalOptions) /bigobj - /Zm150 - 4251;%(DisableSpecificWarnings) - false - NoListing - false - VCMI_DLL;%(PreprocessorDefinitions) - - - minizip.lib;zlib.lib;%(AdditionalDependencies) - - - - - /Oy- /bigobj - VCMI_DLL;VCMI_NO_EXTRA_VERSION;%(PreprocessorDefinitions) - StdInc.h - Use - $(BOOSTDIR);$(ZLIBDIR);$(SDLDIR) - true - - - minizip.lib;zlib.lib;%(AdditionalDependencies) - - - - - ..\..\libs - /LTCG %(AdditionalOptions) - MultiplyDefinedSymbolOnly - - - - - /Oy- %(AdditionalOptions) /bigobj - /Zm150 - VCMI_DLL;%(PreprocessorDefinitions) - Use - StdInc.h - - - minizip.lib;zlib.lib;%(AdditionalDependencies) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - VCMI_DLL;%(PreprocessorDefinitions) - Create - StdInc.h - Create - Create - Create - StdInc.h - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/lib/VCMI_lib.vcxproj.filters b/lib/VCMI_lib.vcxproj.filters deleted file mode 100644 index 08cd5df48..000000000 --- a/lib/VCMI_lib.vcxproj.filters +++ /dev/null @@ -1,924 +0,0 @@ - - - - - {93995380-89BD-4b04-88EB-625FBE52EBFB} - h;hpp;hxx;hm;inl;inc;xsd - - - {a2ca6977-50bf-4585-aa6e-779a10863922} - - - {230979a5-fbea-4d21-b2a6-cf476ebc123d} - - - {8ba33e2e-2971-4873-9c49-128c34147a38} - - - {401483eb-380a-4e36-b532-207491622703} - - - {927d9b6e-3dc5-4370-b603-1b9887095509} - - - {ee24c7f7-f4e2-4d35-b994-94a6e29ea92f} - - - {bda963b1-00e1-412a-9b44-f5cd3f8e9e33} - - - {2f582170-d8a6-42f3-8da3-8255bac28f5a} - - - {d18aad99-ef40-484a-b317-8f7a36d975fc} - - - {a3de4952-3c98-4c1a-bc4b-bd3eeaa82ba7} - - - {91722359-2231-4596-8e73-dbdcd14bd4f3} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - rmg - - - rmg - - - rmg - - - rmg - - - rmg - - - rmg - - - rmg - - - filesystem - - - filesystem - - - filesystem - - - filesystem - - - filesystem - - - filesystem - - - filesystem - - - filesystem - - - filesystem - - - logging - - - logging - - - mapping - - - mapping - - - mapping - - - mapping - - - mapping - - - filesystem - - - - mapping - - - - registerTypes - - - registerTypes - - - registerTypes - - - registerTypes - - - mapObjects - - - mapObjects - - - mapObjects - - - mapObjects - - - mapObjects - - - mapObjects - - - mapObjects - - - mapObjects - - - mapObjects - - - mapObjects - - - mapObjects - - - mapObjects - - - mapObjects - - - mapObjects - - - mapObjects - - - - spells - - - spells - - - spells - - - spells - - - spells - - - - - registerTypes - - - registerTypes - - - registerTypes - - - filesystem - - - - serializer - - - serializer - - - serializer - - - filesystem - - - filesystem - - - filesystem - - - - battle - - - battle - - - battle - - - battle - - - battle - - - battle - - - battle - - - battle - - - battle - - - battle - - - battle - - - battle - - - battle - - - serializer - - - serializer - - - serializer - - - serializer - - - serializer - - - serializer - - - serializer - - - - battle - - - battle - - - battle - - - battle - - - battle - - - spells - - - spells\effects - - - spells\effects - - - spells\effects - - - spells\effects - - - spells\effects - - - spells\effects - - - spells\effects - - - spells\effects - - - spells\effects - - - spells\effects - - - spells\effects - - - spells\effects - - - spells\effects - - - spells\effects - - - spells\effects - - - spells\effects - - - spells - - - spells - - - spells - - - spells - - - - registerTypes - - - vstd - - - vstd - - - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - rmg - - - rmg - - - rmg - - - rmg - - - rmg - - - rmg - - - rmg - - - Header Files - - - filesystem - - - filesystem - - - filesystem - - - filesystem - - - filesystem - - - filesystem - - - filesystem - - - filesystem - - - filesystem - - - filesystem - - - filesystem - - - filesystem - - - logging - - - logging - - - mapping - - - mapping - - - mapping - - - mapping - - - mapping - - - filesystem - - - Header Files - - - mapping - - - Header Files - - - rmg - - - mapObjects - - - mapObjects - - - mapObjects - - - mapObjects - - - mapObjects - - - mapObjects - - - mapObjects - - - mapObjects - - - mapObjects - - - mapObjects - - - mapObjects - - - mapObjects - - - mapObjects - - - mapObjects - - - mapObjects - - - mapObjects - - - Header Files - - - spells - - - spells - - - spells - - - spells - - - spells - - - spells - - - spells - - - Header Files - - - Header Files - - - mapping - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - mapping - - - filesystem - - - Header Files - - - serializer - - - serializer - - - serializer - - - filesystem - - - filesystem - - - filesystem - - - filesystem - - - filesystem - - - filesystem - - - Header Files - - - Header Files - - - battle - - - battle - - - battle - - - battle - - - battle - - - battle - - - battle - - - battle - - - battle - - - battle - - - battle - - - battle - - - battle - - - registerTypes - - - serializer - - - serializer - - - serializer - - - serializer - - - serializer - - - serializer - - - serializer - - - Header Files - - - Header Files - - - Header Files - - - serializer - - - battle - - - battle - - - battle - - - battle - - - battle - - - battle - - - spells - - - spells\effects - - - spells\effects - - - spells\effects - - - spells\effects - - - spells\effects - - - spells\effects - - - spells\effects - - - spells\effects - - - spells\effects - - - spells\effects - - - spells\effects - - - spells\effects - - - spells\effects - - - spells\effects - - - spells\effects - - - spells\effects - - - spells\effects - - - spells - - - spells - - - spells - - - spells - - - Header Files - - - serializer - - - \ No newline at end of file diff --git a/lib/battle/AccessibilityInfo.cpp b/lib/battle/AccessibilityInfo.cpp index a7a29dae5..22fa5196e 100644 --- a/lib/battle/AccessibilityInfo.cpp +++ b/lib/battle/AccessibilityInfo.cpp @@ -15,12 +15,22 @@ VCMI_LIB_NAMESPACE_BEGIN -bool AccessibilityInfo::tileAccessibleWithGate(BattleHex tile, ui8 side) const +bool AccessibilityInfo::tileAccessibleWithGate(BattleHex tile, BattleSide side) const { //at(otherHex) != EAccessibility::ACCESSIBLE && (at(otherHex) != EAccessibility::GATE || side != BattleSide::DEFENDER) - if(at(tile) != EAccessibility::ACCESSIBLE) - if(at(tile) != EAccessibility::GATE || side != BattleSide::DEFENDER) + auto accessibility = at(tile); + + if(accessibility == EAccessibility::ALIVE_STACK) + { + auto destructible = destructibleEnemyTurns.find(tile); + + return destructible != destructibleEnemyTurns.end(); + } + + if(accessibility != EAccessibility::ACCESSIBLE) + if(accessibility != EAccessibility::GATE || side != BattleSide::DEFENDER) return false; + return true; } @@ -29,7 +39,7 @@ bool AccessibilityInfo::accessible(BattleHex tile, const battle::Unit * stack) c return accessible(tile, stack->doubleWide(), stack->unitSide()); } -bool AccessibilityInfo::accessible(BattleHex tile, bool doubleWide, ui8 side) const +bool AccessibilityInfo::accessible(BattleHex tile, bool doubleWide, BattleSide side) const { // All hexes that stack would cover if standing on tile have to be accessible. //do not use getHexes for speed reasons diff --git a/lib/battle/AccessibilityInfo.h b/lib/battle/AccessibilityInfo.h index dc3648412..1352c4da2 100644 --- a/lib/battle/AccessibilityInfo.h +++ b/lib/battle/AccessibilityInfo.h @@ -35,11 +35,13 @@ using TAccessibilityArray = std::array destructibleEnemyTurns; + public: bool accessible(BattleHex tile, const battle::Unit * stack) const; //checks for both tiles if stack is double wide - bool accessible(BattleHex tile, bool doubleWide, ui8 side) const; //checks for both tiles if stack is double wide + bool accessible(BattleHex tile, bool doubleWide, BattleSide side) const; //checks for both tiles if stack is double wide private: - bool tileAccessibleWithGate(BattleHex tile, ui8 side) const; + bool tileAccessibleWithGate(BattleHex tile, BattleSide side) const; }; VCMI_LIB_NAMESPACE_END diff --git a/lib/battle/BattleAction.cpp b/lib/battle/BattleAction.cpp index e021cc1ba..8fe94b520 100644 --- a/lib/battle/BattleAction.cpp +++ b/lib/battle/BattleAction.cpp @@ -18,7 +18,7 @@ VCMI_LIB_NAMESPACE_BEGIN static const int32_t INVALID_UNIT_ID = -1000; BattleAction::BattleAction(): - side(-1), + side(BattleSide::NONE), stackNumber(-1), actionType(EActionType::NO_ACTION) { @@ -96,7 +96,7 @@ BattleAction BattleAction::makeMove(const battle::Unit * stack, BattleHex dest) return ba; } -BattleAction BattleAction::makeEndOFTacticPhase(ui8 side) +BattleAction BattleAction::makeEndOFTacticPhase(BattleSide side) { BattleAction ba; ba.side = side; @@ -104,7 +104,7 @@ BattleAction BattleAction::makeEndOFTacticPhase(ui8 side) return ba; } -BattleAction BattleAction::makeSurrender(ui8 side) +BattleAction BattleAction::makeSurrender(BattleSide side) { BattleAction ba; ba.side = side; @@ -112,7 +112,7 @@ BattleAction BattleAction::makeSurrender(ui8 side) return ba; } -BattleAction BattleAction::makeRetreat(ui8 side) +BattleAction BattleAction::makeRetreat(BattleSide side) { BattleAction ba; ba.side = side; diff --git a/lib/battle/BattleAction.h b/lib/battle/BattleAction.h index 40253b2a5..0c5aa533d 100644 --- a/lib/battle/BattleAction.h +++ b/lib/battle/BattleAction.h @@ -24,7 +24,7 @@ namespace battle class DLL_LINKAGE BattleAction { public: - ui8 side; //who made this action + BattleSide side; //who made this action ui32 stackNumber; //stack ID, -1 left hero, -2 right hero, EActionType actionType; //use ActionType enum for values @@ -39,9 +39,9 @@ public: static BattleAction makeShotAttack(const battle::Unit * shooter, const battle::Unit * target); static BattleAction makeCreatureSpellcast(const battle::Unit * stack, const battle::Target & target, const SpellID & spellID); static BattleAction makeMove(const battle::Unit * stack, BattleHex dest); - static BattleAction makeEndOFTacticPhase(ui8 side); - static BattleAction makeRetreat(ui8 side); - static BattleAction makeSurrender(ui8 side); + static BattleAction makeEndOFTacticPhase(BattleSide side); + static BattleAction makeRetreat(BattleSide side); + static BattleAction makeSurrender(BattleSide side); bool isTacticsAction() const; bool isUnitAction() const; diff --git a/lib/battle/BattleHex.cpp b/lib/battle/BattleHex.cpp index f6eaf319f..581cf538f 100644 --- a/lib/battle/BattleHex.cpp +++ b/lib/battle/BattleHex.cpp @@ -184,7 +184,7 @@ void BattleHex::checkAndPush(BattleHex tile, std::vector & ret) ret.push_back(tile); } -BattleHex BattleHex::getClosestTile(ui8 side, BattleHex initialPos, std::set & possibilities) +BattleHex BattleHex::getClosestTile(BattleSide side, BattleHex initialPos, std::set & possibilities) { std::vector sortedTiles (possibilities.begin(), possibilities.end()); //set can't be sorted properly :( BattleHex initialHex = BattleHex(initialPos); diff --git a/lib/battle/BattleHex.h b/lib/battle/BattleHex.h index 3b06b4828..0f1dc37e4 100644 --- a/lib/battle/BattleHex.h +++ b/lib/battle/BattleHex.h @@ -9,19 +9,12 @@ */ #pragma once +#include "BattleSide.h" + VCMI_LIB_NAMESPACE_BEGIN //TODO: change to enum class -namespace BattleSide -{ - enum Type - { - ATTACKER = 0, - DEFENDER = 1 - }; -} - namespace GameConstants { const int BFIELD_WIDTH = 17; @@ -29,8 +22,6 @@ namespace GameConstants const int BFIELD_SIZE = BFIELD_WIDTH * BFIELD_HEIGHT; } -using BattleSideOpt = std::optional; - // for battle stacks' positions struct DLL_LINKAGE BattleHex //TODO: decide if this should be changed to class for better code design { @@ -102,7 +93,7 @@ struct DLL_LINKAGE BattleHex //TODO: decide if this should be changed to class f static EDir mutualPosition(BattleHex hex1, BattleHex hex2); static uint8_t getDistance(BattleHex hex1, BattleHex hex2); static void checkAndPush(BattleHex tile, std::vector & ret); - static BattleHex getClosestTile(ui8 side, BattleHex initialPos, std::set & possibilities); //TODO: vector or set? copying one to another is bad + static BattleHex getClosestTile(BattleSide side, BattleHex initialPos, std::set & possibilities); //TODO: vector or set? copying one to another is bad template void serialize(Handler &h) diff --git a/lib/battle/BattleInfo.cpp b/lib/battle/BattleInfo.cpp index 820857eac..309f253ff 100644 --- a/lib/battle/BattleInfo.cpp +++ b/lib/battle/BattleInfo.cpp @@ -9,39 +9,51 @@ */ #include "StdInc.h" #include "BattleInfo.h" + +#include "BattleLayout.h" #include "CObstacleInstance.h" #include "bonuses/Limiters.h" #include "bonuses/Updaters.h" -#include "../CRandomGenerator.h" #include "../CStack.h" -#include "../CHeroHandler.h" +#include "../entities/building/TownFortifications.h" #include "../filesystem/Filesystem.h" #include "../mapObjects/CGTownInstance.h" -#include "../CGeneralTextHandler.h" +#include "../texts/CGeneralTextHandler.h" #include "../BattleFieldHandler.h" #include "../ObstacleHandler.h" +#include //TODO: remove #include "../IGameCallback.h" VCMI_LIB_NAMESPACE_BEGIN -///BattleInfo -CStack * BattleInfo::generateNewStack(uint32_t id, const CStackInstance & base, ui8 side, const SlotID & slot, BattleHex position) +const SideInBattle & BattleInfo::getSide(BattleSide side) const { - PlayerColor owner = sides[side].color; + return sides.at(side); +} + +SideInBattle & BattleInfo::getSide(BattleSide side) +{ + return sides.at(side); +} + +///BattleInfo +CStack * BattleInfo::generateNewStack(uint32_t id, const CStackInstance & base, BattleSide side, const SlotID & slot, BattleHex position) +{ + PlayerColor owner = getSide(side).color; assert(!owner.isValidPlayer() || (base.armyObj && base.armyObj->tempOwner == owner)); auto * ret = new CStack(&base, owner, id, side, slot); - ret->initialPosition = getAvaliableHex(base.getCreatureID(), side, position); //TODO: what if no free tile on battlefield was found? + ret->initialPosition = getAvailableHex(base.getCreatureID(), side, position); //TODO: what if no free tile on battlefield was found? stacks.push_back(ret); return ret; } -CStack * BattleInfo::generateNewStack(uint32_t id, const CStackBasicDescriptor & base, ui8 side, const SlotID & slot, BattleHex position) +CStack * BattleInfo::generateNewStack(uint32_t id, const CStackBasicDescriptor & base, BattleSide side, const SlotID & slot, BattleHex position) { - PlayerColor owner = sides[side].color; + PlayerColor owner = getSide(side).color; auto * ret = new CStack(&base, owner, id, side, slot); ret->initialPosition = position; stacks.push_back(ret); @@ -50,7 +62,7 @@ CStack * BattleInfo::generateNewStack(uint32_t id, const CStackBasicDescriptor & void BattleInfo::localInit() { - for(int i = 0; i < 2; i++) + for(BattleSide i : { BattleSide::ATTACKER, BattleSide::DEFENDER}) { auto * armyObj = battleGetArmyObject(i); armyObj->battle = this; @@ -63,22 +75,6 @@ void BattleInfo::localInit() exportBonuses(); } -namespace CGH -{ - static void readBattlePositions(const JsonNode &node, std::vector< std::vector > & dest) - { - for(const JsonNode &level : node.Vector()) - { - std::vector pom; - for(const JsonNode &value : level.Vector()) - { - pom.push_back(static_cast(value.Float())); - } - - dest.push_back(pom); - } - } -} //RNG that works like H3 one struct RandGen @@ -162,62 +158,48 @@ struct RangeGenerator std::function myRand; }; -BattleInfo * BattleInfo::setupBattle(const int3 & tile, TerrainId terrain, const BattleField & battlefieldType, const CArmedInstance * armies[2], const CGHeroInstance * heroes[2], bool creatureBank, const CGTownInstance * town) +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(); + auto * currentBattle = new BattleInfo(layout); - for(auto i = 0u; i < curB->sides.size(); i++) - curB->sides[i].init(heroes[i], armies[i]); + for(auto i : { BattleSide::LEFT_SIDE, BattleSide::RIGHT_SIDE}) + currentBattle->sides[i].init(heroes[i], armies[i]); + std::vector & stacks = (currentBattle->stacks); - std::vector & stacks = (curB->stacks); - - curB->tile = tile; - curB->battlefieldType = battlefieldType; - curB->round = -2; - curB->activeStack = -1; - curB->creatureBank = creatureBank; - 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->hasFort()) + if (town && town->fortificationsLevel().wallsHealth != 0) { - curB->si.gateState = EGateState::CLOSED; + auto fortification = town->fortificationsLevel(); - curB->si.wallState[EWallPart::GATE] = EWallState::INTACT; + currentBattle->si.gateState = EGateState::CLOSED; + + currentBattle->si.wallState[EWallPart::GATE] = EWallState::INTACT; for(const auto wall : {EWallPart::BOTTOM_WALL, EWallPart::BELOW_GATE, EWallPart::OVER_GATE, EWallPart::UPPER_WALL}) - { - if (town->hasBuilt(BuildingID::CASTLE)) - curB->si.wallState[wall] = EWallState::REINFORCED; - else - curB->si.wallState[wall] = EWallState::INTACT; - } + currentBattle->si.wallState[wall] = static_cast(fortification.wallsHealth); - if (town->hasBuilt(BuildingID::CITADEL)) - curB->si.wallState[EWallPart::KEEP] = EWallState::INTACT; + if (fortification.citadelHealth != 0) + currentBattle->si.wallState[EWallPart::KEEP] = static_cast(fortification.citadelHealth); - if (town->hasBuilt(BuildingID::CASTLE)) - { - curB->si.wallState[EWallPart::UPPER_TOWER] = EWallState::INTACT; - curB->si.wallState[EWallPart::BOTTOM_TOWER] = EWallState::INTACT; - } + if (fortification.upperTowerHealth != 0) + currentBattle->si.wallState[EWallPart::UPPER_TOWER] = static_cast(fortification.upperTowerHealth); + + if (fortification.lowerTowerHealth != 0) + currentBattle->si.wallState[EWallPart::BOTTOM_TOWER] = static_cast(fortification.lowerTowerHealth); } //randomize obstacles - if (town == nullptr && !creatureBank) //do it only when it's not siege and not creature bank + if (layout.obstaclesAllowed && (!town || !town->hasFort())) { RandGen r{}; auto ourRand = [&](){ return r.rand(); }; @@ -230,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 @@ -246,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); @@ -265,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->getAccesibility(); + auto tileAccessibility = currentBattle->getAccessibility(); const int obid = obidgen.getSuchNumber(appropriateUsualObstacle); const ObstacleInfo &obi = *Obstacle(obid).getInfo(); @@ -299,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,72 +295,45 @@ BattleInfo * BattleInfo::setupBattle(const int3 & tile, TerrainId terrain, const } } - //reading battleStartpos - add creatures AFTER random obstacles are generated - //TODO: parse once to some structure - std::vector> looseFormations[2]; - std::vector> tightFormations[2]; - std::vector> creBankFormations[2]; - std::vector commanderField; - std::vector commanderBank; - const JsonNode config(JsonPath::builtin("config/battleStartpos.json")); - const JsonVector &positions = config["battle_positions"].Vector(); - - CGH::readBattlePositions(positions[0]["levels"], looseFormations[0]); - CGH::readBattlePositions(positions[1]["levels"], looseFormations[1]); - CGH::readBattlePositions(positions[2]["levels"], tightFormations[0]); - CGH::readBattlePositions(positions[3]["levels"], tightFormations[1]); - CGH::readBattlePositions(positions[4]["levels"], creBankFormations[0]); - CGH::readBattlePositions(positions[5]["levels"], creBankFormations[1]); - - for (auto position : config["commanderPositions"]["field"].Vector()) - { - commanderField.push_back(static_cast(position.Float())); - } - for (auto position : config["commanderPositions"]["creBank"].Vector()) - { - commanderBank.push_back(static_cast(position.Float())); - } - - //adding war machines - if(!creatureBank) + //Checks if hero has artifact and create appropriate stack + auto handleWarMachine = [&](BattleSide side, const ArtifactPosition & artslot, BattleHex hex) { - //Checks if hero has artifact and create appropriate stack - auto handleWarMachine = [&](int side, const ArtifactPosition & artslot, BattleHex hex) + const CArtifactInstance * warMachineArt = heroes[side]->getArt(artslot); + + if(nullptr != warMachineArt && hex.isValid()) { - const CArtifactInstance * warMachineArt = heroes[side]->getArt(artslot); + CreatureID cre = warMachineArt->getType()->getWarMachine(); - if(nullptr != warMachineArt) - { - CreatureID cre = warMachineArt->artType->getWarMachine(); - - if(cre != CreatureID::NONE) - curB->generateNewStack(curB->nextUnitId(), CStackBasicDescriptor(cre, 1), side, SlotID::WAR_MACHINES_SLOT, hex); - } - }; - - if(heroes[0]) - { - - handleWarMachine(0, ArtifactPosition::MACH1, 52); - handleWarMachine(0, ArtifactPosition::MACH2, 18); - handleWarMachine(0, ArtifactPosition::MACH3, 154); - if(town && town->hasFort()) - handleWarMachine(0, ArtifactPosition::MACH4, 120); + if(cre != CreatureID::NONE) + currentBattle->generateNewStack(currentBattle->nextUnitId(), CStackBasicDescriptor(cre, 1), side, SlotID::WAR_MACHINES_SLOT, hex); } + }; - if(heroes[1]) - { - if(!town) //defending hero shouldn't receive ballista (bug #551) - handleWarMachine(1, ArtifactPosition::MACH1, 66); - handleWarMachine(1, ArtifactPosition::MACH2, 32); - handleWarMachine(1, ArtifactPosition::MACH3, 168); - } + if(heroes[BattleSide::ATTACKER]) + { + auto warMachineHexes = layout.warMachines.at(BattleSide::ATTACKER); + + handleWarMachine(BattleSide::ATTACKER, ArtifactPosition::MACH1, warMachineHexes.at(0)); + handleWarMachine(BattleSide::ATTACKER, ArtifactPosition::MACH2, warMachineHexes.at(1)); + handleWarMachine(BattleSide::ATTACKER, ArtifactPosition::MACH3, warMachineHexes.at(2)); + if(town && town->fortificationsLevel().wallsHealth > 0) + handleWarMachine(BattleSide::ATTACKER, ArtifactPosition::MACH4, warMachineHexes.at(3)); + } + + if(heroes[BattleSide::DEFENDER]) + { + auto warMachineHexes = layout.warMachines.at(BattleSide::DEFENDER); + + if(!town) //defending hero shouldn't receive ballista (bug #551) + handleWarMachine(BattleSide::DEFENDER, ArtifactPosition::MACH1, warMachineHexes.at(0)); + handleWarMachine(BattleSide::DEFENDER, ArtifactPosition::MACH2, warMachineHexes.at(1)); + handleWarMachine(BattleSide::DEFENDER, ArtifactPosition::MACH3, warMachineHexes.at(2)); } //war machines added //battleStartpos read - for(int side = 0; side < 2; side++) + for(BattleSide side : {BattleSide::ATTACKER, BattleSide::DEFENDER}) { int formationNo = armies[side]->stacksCount() - 1; vstd::abetween(formationNo, 0, GameConstants::ARMY_SIZE - 1); @@ -386,45 +341,32 @@ BattleInfo * BattleInfo::setupBattle(const int3 & tile, TerrainId terrain, const int k = 0; //stack serial for(auto i = armies[side]->Slots().begin(); i != armies[side]->Slots().end(); i++, k++) { - std::vector *formationVector = nullptr; - if(armies[side]->formation == EArmyFormation::TIGHT ) - formationVector = &tightFormations[side][formationNo]; - else - formationVector = &looseFormations[side][formationNo]; + const BattleHex & pos = layout.units.at(side).at(k); - if(creatureBank) - formationVector = &creBankFormations[side][formationNo]; - - BattleHex pos = (k < formationVector->size() ? formationVector->at(k) : 0); - if(creatureBank && i->second->type->isDoubleWide()) - pos += side ? BattleHex::LEFT : BattleHex::RIGHT; - - curB->generateNewStack(curB->nextUnitId(), *i->second, side, i->first, pos); + if (pos.isValid()) + currentBattle->generateNewStack(currentBattle->nextUnitId(), *i->second, side, i->first, pos); } } //adding commanders - for (int i = 0; i < 2; ++i) + for(BattleSide i : {BattleSide::ATTACKER, BattleSide::DEFENDER}) { if (heroes[i] && heroes[i]->commander && heroes[i]->commander->alive) { - curB->generateNewStack(curB->nextUnitId(), *heroes[i]->commander, i, SlotID::COMMANDER_SLOT_PLACEHOLDER, creatureBank ? commanderBank[i] : commanderField[i]); + currentBattle->generateNewStack(currentBattle->nextUnitId(), *heroes[i]->commander, i, SlotID::COMMANDER_SLOT_PLACEHOLDER, layout.commanders.at(i)); } - } - if (curB->town && curB->town->fortLevel() >= CGTownInstance::CITADEL) + if (currentBattle->town) { - // keep tower - curB->generateNewStack(curB->nextUnitId(), CStackBasicDescriptor(CreatureID::ARROW_TOWERS, 1), 1, 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->fortLevel() >= CGTownInstance::CASTLE) - { - // lower tower + upper tower - curB->generateNewStack(curB->nextUnitId(), CStackBasicDescriptor(CreatureID::ARROW_TOWERS, 1), 1, 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); - curB->generateNewStack(curB->nextUnitId(), CStackBasicDescriptor(CreatureID::ARROW_TOWERS, 1), 1, 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 } @@ -439,25 +381,21 @@ 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 - bool isTacticsAllowed = !creatureBank; //no tactics in creature banks - - constexpr int sideSize = 2; - - std::array battleRepositionHex = {}; - std::array battleRepositionHexBlock = {}; - for(int i = 0; i < sideSize; i++) + BattleSideArray battleRepositionHex = {}; + BattleSideArray battleRepositionHexBlock = {}; + for(auto i : {BattleSide::ATTACKER, BattleSide::DEFENDER}) { if(heroes[i]) { @@ -475,27 +413,27 @@ BattleInfo * BattleInfo::setupBattle(const int3 & tile, TerrainId terrain, const double tactics will be implemented. */ - if(isTacticsAllowed) + if(layout.tacticsAllowed) { if(tacticsSkillDiffAttacker > 0 && tacticsSkillDiffDefender > 0) 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 @@ -508,14 +446,14 @@ const CGHeroInstance * BattleInfo::getHero(const PlayerColor & player) const return nullptr; } -ui8 BattleInfo::whatSide(const PlayerColor & player) const +BattleSide BattleInfo::whatSide(const PlayerColor & player) const { - for(int i = 0; i < sides.size(); i++) + for(auto i : {BattleSide::ATTACKER, BattleSide::DEFENDER}) if(sides[i].color == player) return i; logGlobal->warn("BattleInfo::whatSide: Player %s is not in battle!", player.toString()); - return -1; + return BattleSide::NONE; } CStack * BattleInfo::getStack(int stackID, bool onlyAlive) @@ -523,18 +461,30 @@ CStack * BattleInfo::getStack(int stackID, bool onlyAlive) return const_cast(battleGetStackByID(stackID, onlyAlive)); } +BattleInfo::BattleInfo(const BattleLayout & layout): + BattleInfo() +{ + *this->layout = layout; +} + BattleInfo::BattleInfo(): + layout(std::make_unique()), round(-1), activeStack(-1), town(nullptr), tile(-1,-1,-1), battlefieldType(BattleField::NONE), - tacticsSide(0), + tacticsSide(BattleSide::NONE), tacticDistance(0) { setNodeType(BATTLE); } +BattleLayout BattleInfo::getLayout() const +{ + return *layout; +} + BattleID BattleInfo::getBattleID() const { return battleID; @@ -555,7 +505,7 @@ BattleInfo::~BattleInfo() for (auto & elem : stacks) delete elem; - for(int i = 0; i < 2; i++) + for(auto i : {BattleSide::ATTACKER, BattleSide::DEFENDER}) if(auto * _armyObj = battleGetArmyObject(i)) _armyObj->battle = nullptr; } @@ -600,27 +550,27 @@ IBattleInfo::ObstacleCList BattleInfo::getAllObstacles() const return ret; } -PlayerColor BattleInfo::getSidePlayer(ui8 side) const +PlayerColor BattleInfo::getSidePlayer(BattleSide side) const { - return sides.at(side).color; + return getSide(side).color; } -const CArmedInstance * BattleInfo::getSideArmy(ui8 side) const +const CArmedInstance * BattleInfo::getSideArmy(BattleSide side) const { - return sides.at(side).armyObject; + return getSide(side).armyObject; } -const CGHeroInstance * BattleInfo::getSideHero(ui8 side) const +const CGHeroInstance * BattleInfo::getSideHero(BattleSide side) const { - return sides.at(side).hero; + return getSide(side).hero; } -ui8 BattleInfo::getTacticDist() const +uint8_t BattleInfo::getTacticDist() const { return tacticDistance; } -ui8 BattleInfo::getTacticsSide() const +BattleSide BattleInfo::getTacticsSide() const { return tacticsSide; } @@ -640,14 +590,14 @@ EGateState BattleInfo::getGateState() const return si.gateState; } -uint32_t BattleInfo::getCastSpells(ui8 side) const +uint32_t BattleInfo::getCastSpells(BattleSide side) const { - return sides.at(side).castSpellsCount; + return getSide(side).castSpellsCount; } -int32_t BattleInfo::getEnchanterCounter(ui8 side) const +int32_t BattleInfo::getEnchanterCounter(BattleSide side) const { - return sides.at(side).enchanterCounter; + return getSide(side).enchanterCounter; } const IBonusBearer * BattleInfo::getBonusBearer() const @@ -662,10 +612,9 @@ int64_t BattleInfo::getActualDamage(const DamageRange & damage, int32_t attacker int64_t sum = 0; auto howManyToAv = std::min(10, attackerCount); - auto rangeGen = rng.getInt64Range(damage.min, damage.max); for(int32_t g = 0; g < howManyToAv; ++g) - sum += rangeGen(); + sum += rng.nextInt64(damage.min, damage.max); return sum / howManyToAv; } @@ -680,20 +629,14 @@ int3 BattleInfo::getLocation() const return tile; } -bool BattleInfo::isCreatureBank() const +std::vector BattleInfo::getUsedSpells(BattleSide side) const { - return creatureBank; -} - - -std::vector BattleInfo::getUsedSpells(ui8 side) const -{ - return sides.at(side).usedSpellsHistory; + return getSide(side).usedSpellsHistory; } void BattleInfo::nextRound() { - for(int i = 0; i < 2; ++i) + for(auto i : {BattleSide::ATTACKER, BattleSide::DEFENDER}) { sides.at(i).castSpellsCount = 0; vstd::amax(--sides.at(i).enchanterCounter, 0); @@ -762,7 +705,7 @@ void BattleInfo::setUnitState(uint32_t id, const JsonNode & data, int64_t health if(!changedStack->alive() && healthDelta > 0) { //checking if we resurrect a stack that is under a living stack - auto accessibility = getAccesibility(); + auto accessibility = getAccessibility(); if(!accessibility.accessible(changedStack->getPosition(), changedStack)) { @@ -933,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 { @@ -995,12 +938,12 @@ void BattleInfo::removeObstacle(uint32_t id) } } -CArmedInstance * BattleInfo::battleGetArmyObject(ui8 side) const +CArmedInstance * BattleInfo::battleGetArmyObject(BattleSide side) const { return const_cast(CBattleInfoEssentials::battleGetArmyObject(side)); } -CGHeroInstance * BattleInfo::battleGetFightingHero(ui8 side) const +CGHeroInstance * BattleInfo::battleGetFightingHero(BattleSide side) const { return const_cast(CBattleInfoEssentials::battleGetFightingHero(side)); } @@ -1010,7 +953,7 @@ scripting::Pool * BattleInfo::getContextPool() const { //this is real battle, use global scripting context pool //TODO: make this line not ugly - return battleGetFightingHero(0)->cb->getGlobalContextPool(); + return battleGetFightingHero(BattleSide::ATTACKER)->cb->getGlobalContextPool(); } #endif @@ -1046,7 +989,7 @@ bool CMP_stack::operator()(const battle::Unit * a, const battle::Unit * b) const return false; } -CMP_stack::CMP_stack(int Phase, int Turn, uint8_t Side): +CMP_stack::CMP_stack(int Phase, int Turn, BattleSide Side): phase(Phase), turn(Turn), side(Side) diff --git a/lib/battle/BattleInfo.h b/lib/battle/BattleInfo.h index 589e435f7..3c3dd4502 100644 --- a/lib/battle/BattleInfo.h +++ b/lib/battle/BattleInfo.h @@ -22,23 +22,19 @@ class CStack; class CStackInstance; class CStackBasicDescriptor; class BattleField; +struct BattleLayout; class DLL_LINKAGE BattleInfo : public CBonusSystemNode, public CBattleInfoCallback, public IBattleState { + BattleSideArray sides; //sides[0] - attacker, sides[1] - defender + std::unique_ptr layout; public: BattleID battleID = BattleID(0); - enum BattleSide - { - ATTACKER = 0, - DEFENDER - }; - std::array sides; //sides[0] - attacker, sides[1] - defender si32 round; si32 activeStack; const CGTownInstance * town; //used during town siege, nullptr if this is not a siege (note that fortless town IS also a siege) int3 tile; //for background and bonuses - bool creatureBank; //auxilary field, do not serialize bool replayAllowed; std::vector stacks; std::vector > obstacles; @@ -47,7 +43,7 @@ public: BattleField battlefieldType; //like !!BA:B TerrainId terrainType; //used for some stack nativity checks (not the bonus limiters though that have their own copy) - ui8 tacticsSide; //which side is requested to play tactics phase + BattleSide tacticsSide; //which side is requested to play tactics phase ui8 tacticDistance; //how many hexes we can go forward (1 = only hexes adjacent to margin line) template void serialize(Handler &h) @@ -70,6 +66,7 @@ public: } ////////////////////////////////////////////////////////////////////////// + BattleInfo(const BattleLayout & layout); BattleInfo(); virtual ~BattleInfo(); @@ -92,19 +89,19 @@ public: ObstacleCList getAllObstacles() const override; - PlayerColor getSidePlayer(ui8 side) const override; - const CArmedInstance * getSideArmy(ui8 side) const override; - const CGHeroInstance * getSideHero(ui8 side) const override; + PlayerColor getSidePlayer(BattleSide side) const override; + const CArmedInstance * getSideArmy(BattleSide side) const override; + const CGHeroInstance * getSideHero(BattleSide side) const override; ui8 getTacticDist() const override; - ui8 getTacticsSide() const override; + BattleSide getTacticsSide() const override; const CGTownInstance * getDefendedTown() const override; EWallState getWallState(EWallPart partOfWall) const override; EGateState getGateState() const override; - uint32_t getCastSpells(ui8 side) const override; - int32_t getEnchanterCounter(ui8 side) const override; + uint32_t getCastSpells(BattleSide side) const override; + int32_t getEnchanterCounter(BattleSide side) const override; const IBonusBearer * getBonusBearer() const override; @@ -113,9 +110,9 @@ public: int64_t getActualDamage(const DamageRange & damage, int32_t attackerCount, vstd::RNG & rng) const override; int3 getLocation() const override; - bool isCreatureBank() const override; + BattleLayout getLayout() const override; - std::vector getUsedSpells(ui8 side) const override; + std::vector getUsedSpells(BattleSide side) const override; ////////////////////////////////////////////////////////////////////////// // IBattleState @@ -144,19 +141,22 @@ public: ////////////////////////////////////////////////////////////////////////// CStack * getStack(int stackID, bool onlyAlive = true); using CBattleInfoEssentials::battleGetArmyObject; - CArmedInstance * battleGetArmyObject(ui8 side) const; + CArmedInstance * battleGetArmyObject(BattleSide side) const; using CBattleInfoEssentials::battleGetFightingHero; - CGHeroInstance * battleGetFightingHero(ui8 side) const; + CGHeroInstance * battleGetFightingHero(BattleSide side) const; - CStack * generateNewStack(uint32_t id, const CStackInstance & base, ui8 side, const SlotID & slot, BattleHex position); - CStack * generateNewStack(uint32_t id, const CStackBasicDescriptor & base, ui8 side, const SlotID & slot, BattleHex position); + CStack * generateNewStack(uint32_t id, const CStackInstance & base, BattleSide side, const SlotID & slot, BattleHex position); + CStack * generateNewStack(uint32_t id, const CStackBasicDescriptor & base, BattleSide side, const SlotID & slot, BattleHex position); + + const SideInBattle & getSide(BattleSide side) const; + SideInBattle & getSide(BattleSide side); const CGHeroInstance * getHero(const PlayerColor & player) const; //returns fighting hero that belongs to given player void localInit(); - static BattleInfo * setupBattle(const int3 & tile, TerrainId, const BattleField & battlefieldType, const CArmedInstance * armies[2], const CGHeroInstance * heroes[2], bool creatureBank, const CGTownInstance * town); + static BattleInfo * setupBattle(const int3 & tile, TerrainId, const BattleField & battlefieldType, BattleSideArray armies, BattleSideArray heroes, const BattleLayout & layout, const CGTownInstance * town); - ui8 whatSide(const PlayerColor & player) const; + BattleSide whatSide(const PlayerColor & player) const; protected: #if SCRIPTING_ENABLED @@ -169,10 +169,10 @@ class DLL_LINKAGE CMP_stack { int phase; //rules of which phase will be used int turn; - uint8_t side; + BattleSide side; public: bool operator()(const battle::Unit * a, const battle::Unit * b) const; - CMP_stack(int Phase = 1, int Turn = 0, uint8_t Side = BattleSide::ATTACKER); + CMP_stack(int Phase = 1, int Turn = 0, BattleSide Side = BattleSide::ATTACKER); }; VCMI_LIB_NAMESPACE_END diff --git a/lib/battle/BattleLayout.cpp b/lib/battle/BattleLayout.cpp new file mode 100644 index 000000000..884ae34b5 --- /dev/null +++ b/lib/battle/BattleLayout.cpp @@ -0,0 +1,81 @@ +/* + * BattleLayout.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 "BattleLayout.h" + +#include "../GameSettings.h" +#include "../IGameCallback.h" +#include "../VCMI_Lib.h" +#include "../json/JsonNode.h" +#include "../mapObjects/CArmedInstance.h" + +VCMI_LIB_NAMESPACE_BEGIN + +BattleLayout BattleLayout::createDefaultLayout(IGameCallback * cb, const CArmedInstance * attacker, const CArmedInstance * defender) +{ + return createLayout(cb, "default", attacker, defender); +} + +BattleLayout BattleLayout::createLayout(IGameCallback * cb, const std::string & layoutName, const CArmedInstance * attacker, const CArmedInstance * defender) +{ + const auto & loadHex = [](const JsonNode & node) + { + if (node.isNull()) + return BattleHex(); + else + return BattleHex(node.Integer()); + }; + + const auto & loadUnits = [](const JsonNode & node) + { + UnitsArrayType::value_type result; + for (size_t i = 0; i < GameConstants::ARMY_SIZE; ++i) + { + if (!node[i].isNull()) + result[i] = BattleHex(node[i].Integer()); + } + return result; + }; + + const JsonNode & configRoot = cb->getSettings().getValue(EGameSettings::COMBAT_LAYOUTS); + const JsonNode & config = configRoot[layoutName]; + + BattleLayout result; + + result.commanders[BattleSide::ATTACKER] = loadHex(config["attackerCommander"]); + result.commanders[BattleSide::DEFENDER] = loadHex(config["defenderCommander"]); + + for (size_t i = 0; i < 4; ++i) + result.warMachines[BattleSide::ATTACKER][i] = loadHex(config["attackerWarMachines"][i]); + + for (size_t i = 0; i < 4; ++i) + result.warMachines[BattleSide::DEFENDER][i] = loadHex(config["defenderWarMachines"][i]); + + if (attacker->formation == EArmyFormation::LOOSE && !config["attackerUnitsLoose"].isNull()) + result.units[BattleSide::ATTACKER] = loadUnits(config["attackerUnitsLoose"][attacker->stacksCount() - 1]); + else if (attacker->formation == EArmyFormation::TIGHT && !config["attackerUnitsTight"].isNull()) + result.units[BattleSide::ATTACKER] = loadUnits(config["attackerUnitsTight"][attacker->stacksCount() - 1]); + else + result.units[BattleSide::ATTACKER] = loadUnits(config["attackerUnits"]); + + if (defender->formation == EArmyFormation::LOOSE && !config["defenderUnitsLoose"].isNull()) + result.units[BattleSide::DEFENDER] = loadUnits(config["defenderUnitsLoose"][defender->stacksCount() - 1]); + else if (defender->formation == EArmyFormation::TIGHT && !config["defenderUnitsTight"].isNull()) + result.units[BattleSide::DEFENDER] = loadUnits(config["defenderUnitsTight"][defender->stacksCount() - 1]); + else + result.units[BattleSide::DEFENDER] = loadUnits(config["defenderUnits"]); + + result.obstaclesAllowed = config["obstaclesAllowed"].Bool(); + result.tacticsAllowed = config["tacticsAllowed"].Bool(); + + return result; +} + +VCMI_LIB_NAMESPACE_END diff --git a/lib/battle/BattleLayout.h b/lib/battle/BattleLayout.h new file mode 100644 index 000000000..a6a6948de --- /dev/null +++ b/lib/battle/BattleLayout.h @@ -0,0 +1,39 @@ +/* + * BattleLayout.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 "BattleHex.h" +#include "BattleSide.h" +#include "../constants/NumericConstants.h" +#include "../constants/Enumerations.h" + +VCMI_LIB_NAMESPACE_BEGIN + +class CArmedInstance; +class IGameCallback; + +struct DLL_EXPORT BattleLayout +{ + using UnitsArrayType = BattleSideArray>; + using MachinesArrayType = BattleSideArray>; + using CommanderArrayType = BattleSideArray; + + UnitsArrayType units; + MachinesArrayType warMachines; + CommanderArrayType commanders; + + bool tacticsAllowed = false; + bool obstaclesAllowed = false; + + static BattleLayout createDefaultLayout(IGameCallback * cb, const CArmedInstance * attacker, const CArmedInstance * defender); + static BattleLayout createLayout(IGameCallback * cb, const std::string & layoutName, const CArmedInstance * attacker, const CArmedInstance * defender); +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/battle/BattleProxy.cpp b/lib/battle/BattleProxy.cpp index 6bc34a77e..d9a748745 100644 --- a/lib/battle/BattleProxy.cpp +++ b/lib/battle/BattleProxy.cpp @@ -65,17 +65,17 @@ IBattleInfo::ObstacleCList BattleProxy::getAllObstacles() const return subject->battleGetAllObstacles(); } -PlayerColor BattleProxy::getSidePlayer(ui8 side) const +PlayerColor BattleProxy::getSidePlayer(BattleSide side) const { return subject->sideToPlayer(side); } -const CArmedInstance * BattleProxy::getSideArmy(ui8 side) const +const CArmedInstance * BattleProxy::getSideArmy(BattleSide side) const { return subject->battleGetArmyObject(side); } -const CGHeroInstance * BattleProxy::getSideHero(ui8 side) const +const CGHeroInstance * BattleProxy::getSideHero(BattleSide side) const { return subject->battleGetFightingHero(side); } @@ -85,7 +85,7 @@ ui8 BattleProxy::getTacticDist() const return subject->battleTacticDist(); } -ui8 BattleProxy::getTacticsSide() const +BattleSide BattleProxy::getTacticsSide() const { return subject->battleGetTacticsSide(); } @@ -105,12 +105,12 @@ EGateState BattleProxy::getGateState() const return subject->battleGetGateState(); } -uint32_t BattleProxy::getCastSpells(ui8 side) const +uint32_t BattleProxy::getCastSpells(BattleSide side) const { return subject->battleCastSpells(side); } -int32_t BattleProxy::getEnchanterCounter(ui8 side) const +int32_t BattleProxy::getEnchanterCounter(BattleSide side) const { return subject->battleGetEnchanterCounter(side); } diff --git a/lib/battle/BattleProxy.h b/lib/battle/BattleProxy.h index c43ede14d..1131d9614 100644 --- a/lib/battle/BattleProxy.h +++ b/lib/battle/BattleProxy.h @@ -38,19 +38,19 @@ public: ObstacleCList getAllObstacles() const override; - PlayerColor getSidePlayer(ui8 side) const override; - const CArmedInstance * getSideArmy(ui8 side) const override; - const CGHeroInstance * getSideHero(ui8 side) const override; + PlayerColor getSidePlayer(BattleSide side) const override; + const CArmedInstance * getSideArmy(BattleSide side) const override; + const CGHeroInstance * getSideHero(BattleSide side) const override; ui8 getTacticDist() const override; - ui8 getTacticsSide() const override; + BattleSide getTacticsSide() const override; const CGTownInstance * getDefendedTown() const override; EWallState getWallState(EWallPart partOfWall) const override; EGateState getGateState() const override; - uint32_t getCastSpells(ui8 side) const override; - int32_t getEnchanterCounter(ui8 side) const override; + uint32_t getCastSpells(BattleSide side) const override; + int32_t getEnchanterCounter(BattleSide side) const override; const IBonusBearer * getBonusBearer() const override; protected: diff --git a/lib/battle/BattleSide.h b/lib/battle/BattleSide.h new file mode 100644 index 000000000..44156d75c --- /dev/null +++ b/lib/battle/BattleSide.h @@ -0,0 +1,53 @@ +/* + * BattleSide.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 BattleSide : int8_t +{ + NONE = -1, + INVALID = -2, + ALL_KNOWING = -3, + + ATTACKER = 0, + DEFENDER = 1, + + // Aliases for convenience + LEFT_SIDE = ATTACKER, + RIGHT_SIDE = DEFENDER, +}; + +template +class BattleSideArray : public std::array +{ +public: + const T & at(BattleSide side) const + { + return std::array::at(static_cast(side)); + } + + T & at(BattleSide side) + { + return std::array::at(static_cast(side)); + } + + const T & operator[](BattleSide side) const + { + return std::array::at(static_cast(side)); + } + + T & operator[](BattleSide side) + { + return std::array::at(static_cast(side)); + } +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/battle/BattleStateInfoForRetreat.cpp b/lib/battle/BattleStateInfoForRetreat.cpp index 8eabecc12..d517f242a 100644 --- a/lib/battle/BattleStateInfoForRetreat.cpp +++ b/lib/battle/BattleStateInfoForRetreat.cpp @@ -23,7 +23,7 @@ BattleStateInfoForRetreat::BattleStateInfoForRetreat(): isLastTurnBeforeDie(false), ourHero(nullptr), enemyHero(nullptr), - ourSide(-1) + ourSide(BattleSide::NONE) { } diff --git a/lib/battle/BattleStateInfoForRetreat.h b/lib/battle/BattleStateInfoForRetreat.h index 341ed7ffe..42cd8b7a6 100644 --- a/lib/battle/BattleStateInfoForRetreat.h +++ b/lib/battle/BattleStateInfoForRetreat.h @@ -9,6 +9,8 @@ */ #pragma once +#include "BattleSide.h" + VCMI_LIB_NAMESPACE_BEGIN namespace battle @@ -24,7 +26,7 @@ public: bool canFlee; bool canSurrender; bool isLastTurnBeforeDie; - ui8 ourSide; + BattleSide ourSide; std::vector ourStacks; std::vector enemyStacks; const CGHeroInstance * ourHero; diff --git a/lib/battle/CBattleInfoCallback.cpp b/lib/battle/CBattleInfoCallback.cpp index 6dce09469..251c1f9b7 100644 --- a/lib/battle/CBattleInfoCallback.cpp +++ b/lib/battle/CBattleInfoCallback.cpp @@ -11,12 +11,15 @@ #include "CBattleInfoCallback.h" #include +#include #include "../CStack.h" #include "BattleInfo.h" #include "CObstacleInstance.h" #include "DamageCalculator.h" +#include "IGameSettings.h" #include "PossiblePlayerBattleAction.h" +#include "../entities/building/TownFortifications.h" #include "../spells/ObstacleCasterProxy.h" #include "../spells/ISpellMechanics.h" #include "../spells/Problem.h" @@ -25,7 +28,6 @@ #include "../networkPacks/PacksForClientBattle.h" #include "../BattleFieldHandler.h" #include "../Rect.h" -#include "../CRandomGenerator.h" VCMI_LIB_NAMESPACE_BEGIN @@ -50,6 +52,12 @@ static bool sameSideOfWall(BattleHex pos1, BattleHex pos2) return stackLeft == destLeft; } +static bool isInsideWalls(BattleHex pos) +{ + const int wallInStackLine = lineToWallHex(pos.getY()); + return wallInStackLine < pos; +} + // parts of wall static const std::pair wallParts[] = { @@ -104,9 +112,9 @@ ESpellCastProblem CBattleInfoCallback::battleCanCastSpell(const spells::Caster * } const PlayerColor player = caster->getCasterOwner(); const auto side = playerToSide(player); - if(!side) + if(side == BattleSide::NONE) return ESpellCastProblem::INVALID; - if(!battleDoWeKnowAbout(side.value())) + if(!battleDoWeKnowAbout(side)) { logGlobal->warn("You can't check if enemy can cast given spell!"); return ESpellCastProblem::INVALID; @@ -119,7 +127,7 @@ ESpellCastProblem CBattleInfoCallback::battleCanCastSpell(const spells::Caster * { case spells::Mode::HERO: { - if(battleCastSpells(side.value()) > 0) + if(battleCastSpells(side) > 0) return ESpellCastProblem::CASTS_PER_TURN_LIMIT; const auto * hero = dynamic_cast(caster); @@ -128,6 +136,8 @@ ESpellCastProblem CBattleInfoCallback::battleCanCastSpell(const spells::Caster * return ESpellCastProblem::NO_HERO_TO_CAST_SPELL; if(hero->hasBonusOfType(BonusType::BLOCK_ALL_MAGIC)) return ESpellCastProblem::MAGIC_IS_BLOCKED; + if(!hero->hasSpellbook()) + return ESpellCastProblem::NO_SPELLBOOK; } break; default: @@ -158,8 +168,16 @@ std::pair< std::vector, int > CBattleInfoCallback::getPath(BattleHex return std::make_pair(path, reachability.distances[dest]); } +bool CBattleInfoCallback::battleIsInsideWalls(BattleHex from) const +{ + return isInsideWalls(from); +} + bool CBattleInfoCallback::battleHasPenaltyOnLine(BattleHex from, BattleHex dest, bool checkWall, bool checkMoat) const { + if (!from.isAvailable() || !dest.isAvailable()) + throw std::runtime_error("Invalid hex (" + std::to_string(from.hex) + " and " + std::to_string(dest.hex) + ") received in battleHasPenaltyOnLine!" ); + auto isTileBlocked = [&](BattleHex tile) { EWallPart wallPart = battleHexToWallPart(tile); @@ -221,7 +239,7 @@ bool CBattleInfoCallback::battleHasPenaltyOnLine(BattleHex from, BattleHex dest, bool CBattleInfoCallback::battleHasWallPenalty(const IBonusBearer * shooter, BattleHex shooterPosition, BattleHex destHex) const { RETURN_IF_NOT_BATTLE(false); - if(!battleGetSiegeLevel()) + if(battleGetFortifications().wallsHealth == 0) return false; const std::string cachingStrNoWallPenalty = "type_NO_WALL_PENALTY"; @@ -272,7 +290,7 @@ std::vector CBattleInfoCallback::getClientActionsFor allowedActionList.push_back(PossiblePlayerBattleAction::MOVE_STACK); const auto * siegedTown = battleGetDefendedTown(); - if(siegedTown && siegedTown->hasFort() && stack->hasBonusOfType(BonusType::CATAPULT)) //TODO: check shots + if(siegedTown && siegedTown->fortificationsLevel().wallsHealth > 0 && stack->hasBonusOfType(BonusType::CATAPULT)) //TODO: check shots allowedActionList.push_back(PossiblePlayerBattleAction::CATAPULT); if(stack->hasBonusOfType(BonusType::HEALER)) allowedActionList.push_back(PossiblePlayerBattleAction::HEAL); @@ -360,7 +378,7 @@ battle::Units CBattleInfoCallback::battleAliveUnits() const }); } -battle::Units CBattleInfoCallback::battleAliveUnits(ui8 side) const +battle::Units CBattleInfoCallback::battleAliveUnits(BattleSide side) const { return battleGetUnitsIf([=](const battle::Unit * unit) { @@ -372,7 +390,7 @@ using namespace battle; //T is battle::Unit descendant template -const T * takeOneUnit(std::vector & allUnits, const int turn, int8_t & sideThatLastMoved, int phase) +const T * takeOneUnit(std::vector & allUnits, const int turn, BattleSide & sideThatLastMoved, int phase) { const T * returnedUnit = nullptr; size_t currentUnitIndex = 0; @@ -401,13 +419,13 @@ const T * takeOneUnit(std::vector & allUnits, const int turn, int8_t & } else if(currentUnitInitiative == returnedUnitInitiative) { - if(sideThatLastMoved == -1 && turn <= 0 && currentUnit->unitSide() == BattleSide::ATTACKER + if(sideThatLastMoved == BattleSide::NONE && turn <= 0 && currentUnit->unitSide() == BattleSide::ATTACKER && !(returnedUnit->unitSide() == currentUnit->unitSide() && returnedUnit->unitSlot() < currentUnit->unitSlot())) // Turn 0 attacker priority { returnedUnit = currentUnit; currentUnitIndex = i; } - else if(sideThatLastMoved != -1 && currentUnit->unitSide() != sideThatLastMoved + else if(sideThatLastMoved != BattleSide::NONE && currentUnit->unitSide() != sideThatLastMoved && !(returnedUnit->unitSide() == currentUnit->unitSide() && returnedUnit->unitSlot() < currentUnit->unitSlot())) // Alternate equal speeds units { returnedUnit = currentUnit; @@ -422,7 +440,7 @@ const T * takeOneUnit(std::vector & allUnits, const int turn, int8_t & returnedUnit = currentUnit; currentUnitIndex = i; } - else if(currentUnitInitiative == returnedUnitInitiative && sideThatLastMoved != -1 && currentUnit->unitSide() != sideThatLastMoved + else if(currentUnitInitiative == returnedUnitInitiative && sideThatLastMoved != BattleSide::NONE && currentUnit->unitSide() != sideThatLastMoved && !(returnedUnit->unitSide() == currentUnit->unitSide() && returnedUnit->unitSlot() < currentUnit->unitSlot())) // Alternate equal speeds units { returnedUnit = currentUnit; @@ -442,7 +460,7 @@ const T * takeOneUnit(std::vector & allUnits, const int turn, int8_t & return returnedUnit; } -void CBattleInfoCallback::battleGetTurnOrder(std::vector & turns, const size_t maxUnits, const int maxTurns, const int turn, int8_t sideThatLastMoved) const +void CBattleInfoCallback::battleGetTurnOrder(std::vector & turns, const size_t maxUnits, const int maxTurns, const int turn, BattleSide sideThatLastMoved) const { RETURN_IF_NOT_BATTLE(); @@ -475,7 +493,7 @@ void CBattleInfoCallback::battleGetTurnOrder(std::vector & turns, if(activeUnit) { //its first turn and active unit hasn't taken any action yet - must be placed at the beginning of queue, no matter what - if(turn == 0 && activeUnit->willMove() && !activeUnit->waited()) + if(turn == 0 && activeUnit->willMove()) { turns.back().push_back(activeUnit); if(turnsIsFull()) @@ -484,7 +502,7 @@ void CBattleInfoCallback::battleGetTurnOrder(std::vector & turns, //its first or current turn, turn priority for active stack side //TODO: what if active stack mind-controlled? - if(turn <= 0 && sideThatLastMoved < 0) + if(turn <= 0 && sideThatLastMoved == BattleSide::NONE) sideThatLastMoved = activeUnit->unitSide(); } @@ -544,7 +562,7 @@ void CBattleInfoCallback::battleGetTurnOrder(std::vector & turns, } } - if(sideThatLastMoved < 0) + if(sideThatLastMoved == BattleSide::NONE) sideThatLastMoved = BattleSide::ATTACKER; if(!turnsIsFull() && (maxTurns == 0 || turns.size() < maxTurns)) @@ -668,6 +686,9 @@ bool CBattleInfoCallback::battleCanAttack(const battle::Unit * stack, const batt if (!stack || !target) return false; + if(target->hasBonusOfType(BonusType::INVINCIBLE)) + return false; + if(!battleMatchOwner(stack, target)) return false; @@ -705,15 +726,49 @@ bool CBattleInfoCallback::battleCanShoot(const battle::Unit * attacker) const || attacker->hasBonusOfType(BonusType::FREE_SHOOTING)); } +bool CBattleInfoCallback::battleCanTargetEmptyHex(const battle::Unit * attacker) const +{ + RETURN_IF_NOT_BATTLE(false); + + if(!VLC->engineSettings()->getBoolean(EGameSettings::COMBAT_AREA_SHOT_CAN_TARGET_EMPTY_HEX)) + return false; + + if(attacker->hasBonusOfType(BonusType::SPELL_LIKE_ATTACK)) + { + auto bonus = attacker->getBonus(Selector::type()(BonusType::SPELL_LIKE_ATTACK)); + const CSpell * spell = bonus->subtype.as().toSpell(); + spells::BattleCast cast(this, attacker, spells::Mode::SPELL_LIKE_ATTACK, spell); + BattleHex dummySpellTarget = BattleHex(50); //check arbitrary hex for general spell range since currently there is no general way to access amount of hexes + + if(spell->battleMechanics(&cast)->rangeInHexes(dummySpellTarget).size() > 1) + { + return true; + } + } + + return false; +} + bool CBattleInfoCallback::battleCanShoot(const battle::Unit * attacker, BattleHex dest) const { RETURN_IF_NOT_BATTLE(false); const battle::Unit * defender = battleGetUnitByPos(dest); - if(!attacker || !defender) + if(!attacker) return false; - if(battleMatchOwner(attacker, defender) && defender->alive()) + bool emptyHexAreaAttack = battleCanTargetEmptyHex(attacker); + + if(!emptyHexAreaAttack) + { + if(!defender) + return false; + + if(defender->hasBonusOfType(BonusType::INVINCIBLE)) + return false; + } + + if(emptyHexAreaAttack || (battleMatchOwner(attacker, defender) && defender->alive())) { if(battleCanShoot(attacker)) { @@ -724,7 +779,11 @@ bool CBattleInfoCallback::battleCanShoot(const battle::Unit * attacker, BattleHe } int shootingRange = limitedRangeBonus->val; - return isEnemyUnitWithinSpecifiedRange(attacker->getPosition(), defender, shootingRange); + + if(defender) + return isEnemyUnitWithinSpecifiedRange(attacker->getPosition(), defender, shootingRange); + else + return isHexWithinSpecifiedRange(attacker->getPosition(), dest, shootingRange); } } @@ -771,7 +830,7 @@ DamageEstimation CBattleInfoCallback::battleEstimateDamage(const BattleAttackInf if (!bai.defender->ableToRetaliate()) return ret; - if (bai.attacker->hasBonusOfType(BonusType::BLOCKS_RETALIATION)) + if (bai.attacker->hasBonusOfType(BonusType::BLOCKS_RETALIATION) || bai.attacker->hasBonusOfType(BonusType::INVINCIBLE)) return ret; //TODO: rewrite using boost::numeric::interval @@ -868,10 +927,10 @@ 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, (BattlePerspective::BattlePerspective)side); + auto shouldReveal = !spellObstacle->hidden || !battleIsObstacleVisibleForSide(*obstacle, side); const auto * hero = battleGetFightingHero(spellObstacle->casterSide); auto caster = spells::ObstacleCasterProxy(getBattle()->getSidePlayer(spellObstacle->casterSide), hero, *spellObstacle); @@ -903,7 +962,7 @@ bool CBattleInfoCallback::handleObstacleTriggersForUnit(SpellCastEnvironment & s return unit.alive() && !movementStopped; } -AccessibilityInfo CBattleInfoCallback::getAccesibility() const +AccessibilityInfo CBattleInfoCallback::getAccessibility() const { AccessibilityInfo ret; ret.fill(EAccessibility::ACCESSIBLE); @@ -927,20 +986,20 @@ AccessibilityInfo CBattleInfoCallback::getAccesibility() const } //gate -> should be before stacks - if(battleGetSiegeLevel() > 0) + if(battleGetFortifications().wallsHealth > 0) { - EAccessibility accessability = EAccessibility::ACCESSIBLE; + EAccessibility accessibility = EAccessibility::ACCESSIBLE; switch(battleGetGateState()) { case EGateState::CLOSED: - accessability = EAccessibility::GATE; + accessibility = EAccessibility::GATE; break; case EGateState::BLOCKED: - accessability = EAccessibility::UNAVAILABLE; + accessibility = EAccessibility::UNAVAILABLE; break; } - ret[BattleHex::GATE_OUTER] = ret[BattleHex::GATE_INNER] = accessability; + ret[BattleHex::GATE_OUTER] = ret[BattleHex::GATE_INNER] = accessibility; } //tiles occupied by standing stacks @@ -959,7 +1018,7 @@ AccessibilityInfo CBattleInfoCallback::getAccesibility() const } //walls - if(battleGetSiegeLevel() > 0) + if(battleGetFortifications().wallsHealth > 0) { static const int permanentlyLocked[] = {12, 45, 62, 112, 147, 165}; for(auto hex : permanentlyLocked) @@ -985,14 +1044,14 @@ AccessibilityInfo CBattleInfoCallback::getAccesibility() const return ret; } -AccessibilityInfo CBattleInfoCallback::getAccesibility(const battle::Unit * stack) const +AccessibilityInfo CBattleInfoCallback::getAccessibility(const battle::Unit * stack) const { - return getAccesibility(battle::Unit::getHexes(stack->getPosition(), stack->doubleWide(), stack->unitSide())); + return getAccessibility(battle::Unit::getHexes(stack->getPosition(), stack->doubleWide(), stack->unitSide())); } -AccessibilityInfo CBattleInfoCallback::getAccesibility(const std::vector & accessibleHexes) const +AccessibilityInfo CBattleInfoCallback::getAccessibility(const std::vector & accessibleHexes) const { - auto ret = getAccesibility(); + auto ret = getAccessibility(); for(auto hex : accessibleHexes) if(hex.isValid()) ret[hex] = EAccessibility::ACCESSIBLE; @@ -1036,16 +1095,29 @@ ReachabilityInfo CBattleInfoCallback::makeBFS(const AccessibilityInfo &accessibi continue; const int costToNeighbour = ret.distances.at(curHex.hex) + 1; + for(BattleHex neighbour : BattleHex::neighbouringTilesCache[curHex.hex]) { if(neighbour.isValid()) { + auto additionalCost = 0; + + if(params.bypassEnemyStacks) + { + auto enemyToBypass = params.destructibleEnemyTurns.find(neighbour); + + if(enemyToBypass != params.destructibleEnemyTurns.end()) + { + additionalCost = enemyToBypass->second; + } + } + const int costFoundSoFar = ret.distances[neighbour.hex]; - if(accessibleCache[neighbour.hex] && costToNeighbour < costFoundSoFar) + if(accessibleCache[neighbour.hex] && costToNeighbour + additionalCost < costFoundSoFar) { hexq.push(neighbour); - ret.distances[neighbour.hex] = costToNeighbour; + ret.distances[neighbour.hex] = costToNeighbour + additionalCost; ret.predecessors[neighbour.hex] = curHex; } } @@ -1082,7 +1154,7 @@ bool CBattleInfoCallback::isInObstacle( return false; } -std::set CBattleInfoCallback::getStoppers(BattlePerspective::BattlePerspective whichSidePerspective) const +std::set CBattleInfoCallback::getStoppers(BattleSide whichSidePerspective) const { std::set ret; RETURN_IF_NOT_BATTLE(ret); @@ -1145,7 +1217,7 @@ std::pair CBattleInfoCallback::getNearestStack( return std::make_pair(nullptr, BattleHex::INVALID); } -BattleHex CBattleInfoCallback::getAvaliableHex(const CreatureID & creID, ui8 side, int initialPos) const +BattleHex CBattleInfoCallback::getAvailableHex(const CreatureID & creID, BattleSide side, int initialPos) const { bool twoHex = VLC->creatures()->getById(creID)->isDoubleWide(); @@ -1160,7 +1232,7 @@ BattleHex CBattleInfoCallback::getAvaliableHex(const CreatureID & creID, ui8 sid pos = GameConstants::BFIELD_WIDTH - 1; //top right } - auto accessibility = getAccesibility(); + auto accessibility = getAccessibility(); std::set occupyable; for(int i = 0; i < accessibility.size(); i++) @@ -1192,8 +1264,13 @@ bool CBattleInfoCallback::isInTacticRange(BattleHex dest) const auto side = battleGetTacticsSide(); auto dist = battleGetTacticDist(); - return ((!side && dest.getX() > 0 && dest.getX() <= dist) - || (side && dest.getX() < GameConstants::BFIELD_WIDTH - 1 && dest.getX() >= GameConstants::BFIELD_WIDTH - dist - 1)); + if (side == BattleSide::ATTACKER && dest.getX() > 0 && dest.getX() <= dist) + return true; + + if (side == BattleSide::DEFENDER && dest.getX() < GameConstants::BFIELD_WIDTH - 1 && dest.getX() >= GameConstants::BFIELD_WIDTH - dist - 1) + return true; + + return false; } ReachabilityInfo CBattleInfoCallback::getReachability(const battle::Unit * unit) const @@ -1215,13 +1292,19 @@ ReachabilityInfo CBattleInfoCallback::getReachability(const ReachabilityInfo::Pa if(params.flying) return getFlyingReachability(params); else - return makeBFS(getAccesibility(params.knownAccessible), params); + { + auto accessibility = getAccessibility(params.knownAccessible); + + accessibility.destructibleEnemyTurns = params.destructibleEnemyTurns; + + return makeBFS(accessibility, params); + } } ReachabilityInfo CBattleInfoCallback::getFlyingReachability(const ReachabilityInfo::Parameters ¶ms) const { ReachabilityInfo ret; - ret.accessibility = getAccesibility(params.knownAccessible); + ret.accessibility = getAccessibility(params.knownAccessible); for(int i = 0; i < GameConstants::BFIELD_SIZE; i++) { @@ -1235,19 +1318,40 @@ ReachabilityInfo CBattleInfoCallback::getFlyingReachability(const ReachabilityIn return ret; } -AttackableTiles CBattleInfoCallback::getPotentiallyAttackableHexes(const battle::Unit* attacker, BattleHex destinationTile, BattleHex attackerPos) const +AttackableTiles CBattleInfoCallback::getPotentiallyAttackableHexes( + const battle::Unit * attacker, + BattleHex destinationTile, + BattleHex attackerPos) const +{ + const auto * defender = battleGetUnitByPos(destinationTile, true); + + if(!defender) + return AttackableTiles(); // can't attack thin air + + return getPotentiallyAttackableHexes( + attacker, + defender, + destinationTile, + attackerPos, + defender->getPosition()); +} + +AttackableTiles CBattleInfoCallback::getPotentiallyAttackableHexes( + const battle::Unit* attacker, + const battle::Unit * defender, + BattleHex destinationTile, + BattleHex attackerPos, + BattleHex defenderPos) const { //does not return hex attacked directly AttackableTiles at; RETURN_IF_NOT_BATTLE(at); BattleHex attackOriginHex = (attackerPos != BattleHex::INVALID) ? attackerPos : attacker->getPosition(); //real or hypothetical (cursor) position - - const auto * defender = battleGetUnitByPos(destinationTile, true); - if (!defender) - return at; // can't attack thin air - - bool reverse = isToReverse(attacker, defender); + + defenderPos = (defenderPos != BattleHex::INVALID) ? defenderPos : defender->getPosition(); //real or hypothetical (cursor) position + + bool reverse = isToReverse(attacker, defender, attackerPos, defenderPos); if(reverse && attacker->doubleWide()) { attackOriginHex = attacker->occupiedHex(attackOriginHex); //the other hex stack stands on @@ -1288,32 +1392,51 @@ AttackableTiles CBattleInfoCallback::getPotentiallyAttackableHexes(const battle: 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); - if(direction != BattleHex::NONE) //only adjacent hexes are subject of dragon breath calculation + + if(direction == BattleHex::NONE + && defender->doubleWide() + && attacker->doubleWide() + && defenderPos == destinationTile) { - BattleHex nextHex = destinationTile.cloneInDirection(direction, false); + direction = BattleHex::mutualPosition(attackOriginHex, defender->occupiedHex(defenderPos)); + } - if ( defender->doubleWide() ) + for(int i = 0; i < 3; i++) + { + if(direction != BattleHex::NONE) //only adjacent hexes are subject of dragon breath calculation { - auto secondHex = destinationTile == defender->getPosition() ? - defender->occupiedHex(): - defender->getPosition(); + 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; @@ -1335,17 +1458,29 @@ AttackableTiles CBattleInfoCallback::getPotentiallyShootableHexes(const battle:: return at; } -std::vector CBattleInfoCallback::getAttackedBattleUnits(const battle::Unit* attacker, BattleHex destinationTile, bool rangedAttack, BattleHex attackerPos) const +std::vector CBattleInfoCallback::getAttackedBattleUnits( + const battle::Unit * attacker, + const battle::Unit * defender, + BattleHex destinationTile, + bool rangedAttack, + BattleHex attackerPos, + BattleHex defenderPos) const { std::vector units; RETURN_IF_NOT_BATTLE(units); + if(attackerPos == BattleHex::INVALID) + attackerPos = attacker->getPosition(); + + if(defenderPos == BattleHex::INVALID) + defenderPos = defender->getPosition(); + AttackableTiles at; if (rangedAttack) at = getPotentiallyShootableHexes(attacker, destinationTile, attackerPos); else - at = getPotentiallyAttackableHexes(attacker, destinationTile, attackerPos); + at = getPotentiallyAttackableHexes(attacker, defender, destinationTile, attackerPos, defenderPos); units = battleGetUnitsIf([=](const battle::Unit * unit) { @@ -1371,7 +1506,7 @@ std::set CBattleInfoCallback::getAttackedCreatures(const CStack* RETURN_IF_NOT_BATTLE(attackedCres); AttackableTiles at; - + if(rangedAttack) at = getPotentiallyShootableHexes(attacker, destinationTile, attackerPos); else @@ -1396,7 +1531,7 @@ std::set CBattleInfoCallback::getAttackedCreatures(const CStack* return attackedCres; } -static bool isHexInFront(BattleHex hex, BattleHex testHex, BattleSide::Type side ) +static bool isHexInFront(BattleHex hex, BattleHex testHex, BattleSide side ) { static const std::set rightDirs { BattleHex::BOTTOM_RIGHT, BattleHex::TOP_RIGHT, BattleHex::RIGHT }; static const std::set leftDirs { BattleHex::BOTTOM_LEFT, BattleHex::TOP_LEFT, BattleHex::LEFT }; @@ -1410,26 +1545,36 @@ static bool isHexInFront(BattleHex hex, BattleHex testHex, BattleSide::Type side } //TODO: this should apply also to mechanics and cursor interface -bool CBattleInfoCallback::isToReverse(const battle::Unit * attacker, const battle::Unit * defender) const +bool CBattleInfoCallback::isToReverse(const battle::Unit * attacker, const battle::Unit * defender, BattleHex attackerHex, BattleHex defenderHex) const { - BattleHex attackerHex = attacker->getPosition(); - BattleHex defenderHex = defender->getPosition(); + if(!defenderHex.isValid()) + defenderHex = defender->getPosition(); + + if(!attackerHex.isValid()) + attackerHex = attacker->getPosition(); if (attackerHex < 0 ) //turret return false; - if(isHexInFront(attackerHex, defenderHex, static_cast(attacker->unitSide()))) + if(isHexInFront(attackerHex, defenderHex, attacker->unitSide())) return false; + auto defenderOtherHex = defenderHex; + auto attackerOtherHex = defenderHex; + if (defender->doubleWide()) { - if(isHexInFront(attackerHex, defender->occupiedHex(), static_cast(attacker->unitSide()))) + defenderOtherHex = battle::Unit::occupiedHex(defenderHex, true, defender->unitSide()); + + if(isHexInFront(attackerHex, defenderOtherHex, attacker->unitSide())) return false; } if (attacker->doubleWide()) { - if(isHexInFront(attacker->occupiedHex(), defenderHex, static_cast(attacker->unitSide()))) + attackerOtherHex = battle::Unit::occupiedHex(attackerHex, true, attacker->unitSide()); + + if(isHexInFront(attackerOtherHex, defenderHex, attacker->unitSide())) return false; } @@ -1437,7 +1582,7 @@ bool CBattleInfoCallback::isToReverse(const battle::Unit * attacker, const battl // but this is how H3 handles it which is important, e.g. for direction of dragon breath attacks if (attacker->doubleWide() && defender->doubleWide()) { - if(isHexInFront(attacker->occupiedHex(), defender->occupiedHex(), static_cast(attacker->unitSide()))) + if(isHexInFront(attackerOtherHex, defenderOtherHex, attacker->unitSide())) return false; } return true; @@ -1496,6 +1641,14 @@ bool CBattleInfoCallback::isEnemyUnitWithinSpecifiedRange(BattleHex attackerPosi return false; } +bool CBattleInfoCallback::isHexWithinSpecifiedRange(BattleHex attackerPosition, BattleHex targetPosition, unsigned int range) const +{ + if(BattleHex::getDistance(attackerPosition, targetPosition) <= range) + return true; + + return false; +} + BattleHex CBattleInfoCallback::wallPartToBattleHex(EWallPart part) const { RETURN_IF_NOT_BATTLE(BattleHex::INVALID); @@ -1522,7 +1675,7 @@ bool CBattleInfoCallback::isWallPartAttackable(EWallPart wallPart) const if(isWallPartPotentiallyAttackable(wallPart)) { auto wallState = battleGetWallState(wallPart); - return (wallState == EWallState::REINFORCED || wallState == EWallState::INTACT || wallState == EWallState::DAMAGED); + return (wallState != EWallState::NONE && wallState != EWallState::DESTROYED); } return false; } @@ -1602,7 +1755,7 @@ std::set CBattleInfoCallback::battleAdjacentUnits(const ba return ret; } -SpellID CBattleInfoCallback::getRandomBeneficialSpell(CRandomGenerator & rand, const battle::Unit * caster, const battle::Unit * subject) const +SpellID CBattleInfoCallback::getRandomBeneficialSpell(vstd::RNG & rand, const battle::Unit * caster, const battle::Unit * subject) const { RETURN_IF_NOT_BATTLE(SpellID::NONE); //This is complete list. No spells from mods. @@ -1696,7 +1849,7 @@ SpellID CBattleInfoCallback::getRandomBeneficialSpell(CRandomGenerator & rand, c case SpellID::PROTECTION_FROM_FIRE: case SpellID::PROTECTION_FROM_WATER: { - const ui8 enemySide = 1 - subject->unitSide(); + const BattleSide enemySide = otherSide(subject->unitSide()); //todo: only if enemy has spellbook if (!battleHasHero(enemySide)) //only if there is enemy hero continue; @@ -1748,7 +1901,7 @@ SpellID CBattleInfoCallback::getRandomBeneficialSpell(CRandomGenerator & rand, c } } -SpellID CBattleInfoCallback::getRandomCastedSpell(CRandomGenerator & rand,const CStack * caster) const +SpellID CBattleInfoCallback::getRandomCastedSpell(vstd::RNG & rand,const CStack * caster) const { RETURN_IF_NOT_BATTLE(SpellID::NONE); @@ -1787,10 +1940,9 @@ int CBattleInfoCallback::battleGetSurrenderCost(const PlayerColor & Player) cons if(!battleCanSurrender(Player)) return -1; - const auto sideOpt = playerToSide(Player); - if(!sideOpt) + const BattleSide side = playerToSide(Player); + if(side == BattleSide::NONE) return -1; - const auto side = sideOpt.value(); int ret = 0; double discount = 0; @@ -1806,7 +1958,7 @@ int CBattleInfoCallback::battleGetSurrenderCost(const PlayerColor & Player) cons return ret; } -si8 CBattleInfoCallback::battleMinSpellLevel(ui8 side) const +si8 CBattleInfoCallback::battleMinSpellLevel(BattleSide side) const { const IBonusBearer * node = nullptr; if(const CGHeroInstance * h = battleGetFightingHero(side)) @@ -1824,7 +1976,7 @@ si8 CBattleInfoCallback::battleMinSpellLevel(ui8 side) const return 0; } -si8 CBattleInfoCallback::battleMaxSpellLevel(ui8 side) const +si8 CBattleInfoCallback::battleMaxSpellLevel(BattleSide side) const { const IBonusBearer *node = nullptr; if(const CGHeroInstance * h = battleGetFightingHero(side)) @@ -1843,21 +1995,21 @@ si8 CBattleInfoCallback::battleMaxSpellLevel(ui8 side) const return GameConstants::SPELL_LEVELS; } -std::optional CBattleInfoCallback::battleIsFinished() const +std::optional CBattleInfoCallback::battleIsFinished() const { auto units = battleGetUnitsIf([=](const battle::Unit * unit) { return unit->alive() && !unit->isTurret() && !unit->hasBonusOfType(BonusType::SIEGE_WEAPON); }); - std::array hasUnit = {false, false}; //index is BattleSide + BattleSideArray hasUnit = {false, false}; //index is BattleSide for(auto & unit : units) { //todo: move SIEGE_WEAPON check to Unit state hasUnit.at(unit->unitSide()) = true; - if(hasUnit[0] && hasUnit[1]) + if(hasUnit[BattleSide::ATTACKER] && hasUnit[BattleSide::DEFENDER]) return std::nullopt; } @@ -1871,12 +2023,12 @@ std::optional CBattleInfoCallback::battleIsFinished() const } } - if(!hasUnit[0] && !hasUnit[1]) - return 2; - if(!hasUnit[1]) - return 0; + if(!hasUnit[BattleSide::ATTACKER] && !hasUnit[BattleSide::DEFENDER]) + return BattleSide::NONE; + if(!hasUnit[BattleSide::DEFENDER]) + return BattleSide::ATTACKER; else - return 1; + return BattleSide::DEFENDER; } VCMI_LIB_NAMESPACE_END diff --git a/lib/battle/CBattleInfoCallback.h b/lib/battle/CBattleInfoCallback.h index b1a16ef1f..aedba1996 100644 --- a/lib/battle/CBattleInfoCallback.h +++ b/lib/battle/CBattleInfoCallback.h @@ -23,9 +23,13 @@ class SpellCastEnvironment; class CSpell; struct CObstacleInstance; class IBonusBearer; -class CRandomGenerator; class PossiblePlayerBattleAction; +namespace vstd +{ +class RNG; +} + namespace spells { class Caster; @@ -52,7 +56,7 @@ struct DLL_LINKAGE BattleClientInterfaceData class DLL_LINKAGE CBattleInfoCallback : public virtual CBattleInfoEssentials { public: - std::optional battleIsFinished() const override; //return none if battle is ongoing; otherwise the victorious side (0/1) or 2 if it is a draw + std::optional battleIsFinished() const override; //return none if battle is ongoing; otherwise the victorious side (0/1) or 2 if it is a draw std::vector> battleGetAllObstaclesOnPos(BattleHex tile, bool onlyBlocking = true) const override; std::vector> getAllAffectedObstaclesByStack(const battle::Unit * unit, const std::set & passed) const override; @@ -66,9 +70,9 @@ public: ///returns all alive units excluding turrets battle::Units battleAliveUnits() const; ///returns all alive units from particular side excluding turrets - battle::Units battleAliveUnits(ui8 side) const; + battle::Units battleAliveUnits(BattleSide side) const; - void battleGetTurnOrder(std::vector & out, const size_t maxUnits, const int maxTurns, const int turn = 0, int8_t lastMoved = -1) const; + void battleGetTurnOrder(std::vector & out, const size_t maxUnits, const int maxTurns, const int turn = 0, BattleSide lastMoved = BattleSide::NONE) const; ///returns reachable hexes (valid movement destinations), DOES contain stack current position std::vector battleGetAvailableHexes(const battle::Unit * unit, bool obtainMovementRange, bool addOccupiable, std::vector * attackable) const; @@ -82,9 +86,11 @@ public: ReachabilityInfo::TDistances battleGetDistances(const battle::Unit * unit, BattleHex assumedPosition) const; std::set battleGetAttackedHexes(const battle::Unit * attacker, BattleHex destinationTile, BattleHex attackerPos = BattleHex::INVALID) const; bool isEnemyUnitWithinSpecifiedRange(BattleHex attackerPosition, const battle::Unit * defenderUnit, unsigned int range) const; + bool isHexWithinSpecifiedRange(BattleHex attackerPosition, BattleHex targetPosition, unsigned int range) const; std::pair< std::vector, int > getPath(BattleHex start, BattleHex dest, const battle::Unit * stack) const; + bool battleCanTargetEmptyHex(const battle::Unit * attacker) const; //determines of stack with given ID can target empty hex to attack - currently used only for SPELL_LIKE_ATTACK shooting bool battleCanAttack(const battle::Unit * stack, const battle::Unit * target, BattleHex dest) const; //determines if stack with given ID can attack target at the selected destination bool battleCanShoot(const battle::Unit * attacker, BattleHex dest) const; //determines if stack with given ID shoot at the selected destination bool battleCanShoot(const battle::Unit * attacker) const; //determines if stack with given ID shoot in principle @@ -100,6 +106,7 @@ public: DamageEstimation battleEstimateDamage(const battle::Unit * attacker, const battle::Unit * defender, BattleHex attackerPosition, DamageEstimation * retaliationDmg = nullptr) const; DamageEstimation battleEstimateDamage(const battle::Unit * attacker, const battle::Unit * defender, int getMovementRange, DamageEstimation * retaliationDmg = nullptr) const; + bool battleIsInsideWalls(BattleHex from) const; bool battleHasPenaltyOnLine(BattleHex from, BattleHex dest, bool checkWall, bool checkMoat) const; bool battleHasDistancePenalty(const IBonusBearer * shooter, BattleHex shooterPosition, BattleHex destHex) const; bool battleHasWallPenalty(const IBonusBearer * shooter, BattleHex shooterPosition, BattleHex destHex) const; @@ -111,13 +118,13 @@ public: bool isWallPartAttackable(EWallPart wallPart) const; // returns true if the wall part is actually attackable, false if not std::vector getAttackableBattleHexes() const; - si8 battleMinSpellLevel(ui8 side) const; //calculates maximum spell level possible to be cast on battlefield - takes into account artifacts of both heroes; if no effects are set, 0 is returned - si8 battleMaxSpellLevel(ui8 side) const; //calculates minimum spell level possible to be cast on battlefield - takes into account artifacts of both heroes; if no effects are set, 0 is returned + si8 battleMinSpellLevel(BattleSide side) const; //calculates maximum spell level possible to be cast on battlefield - takes into account artifacts of both heroes; if no effects are set, 0 is returned + si8 battleMaxSpellLevel(BattleSide side) const; //calculates minimum spell level possible to be cast on battlefield - takes into account artifacts of both heroes; if no effects are set, 0 is returned int32_t battleGetSpellCost(const spells::Spell * sp, const CGHeroInstance * caster) const; //returns cost of given spell ESpellCastProblem battleCanCastSpell(const spells::Caster * caster, spells::Mode mode) const; //returns true if there are no general issues preventing from casting a spell - SpellID getRandomBeneficialSpell(CRandomGenerator & rand, const battle::Unit * caster, const battle::Unit * target) const; - SpellID getRandomCastedSpell(CRandomGenerator & rand, const CStack * caster) const; //called at the beginning of turn for Faerie Dragon + SpellID getRandomBeneficialSpell(vstd::RNG & rand, const battle::Unit * caster, const battle::Unit * target) const; + SpellID getRandomCastedSpell(vstd::RNG & rand, const CStack * caster) const; //called at the beginning of turn for Faerie Dragon std::vector getClientActionsForStack(const CStack * stack, const BattleClientInterfaceData & data); PossiblePlayerBattleAction getCasterAction(const CSpell * spell, const spells::Caster * caster, spells::Mode mode) const; @@ -126,25 +133,44 @@ public: bool isInTacticRange(BattleHex dest) const; si8 battleGetTacticDist() const; //returns tactic distance for calling player or 0 if this player is not in tactic phase (for ALL_KNOWING actual distance for tactic side) - AttackableTiles getPotentiallyAttackableHexes(const battle::Unit* attacker, BattleHex destinationTile, BattleHex attackerPos) const; //TODO: apply rotation to two-hex attacker + AttackableTiles getPotentiallyAttackableHexes( + const battle::Unit* attacker, + const battle::Unit* defender, + BattleHex destinationTile, + BattleHex attackerPos, + BattleHex defenderPos) const; //TODO: apply rotation to two-hex attacker + + AttackableTiles getPotentiallyAttackableHexes( + const battle::Unit * attacker, + BattleHex destinationTile, + BattleHex attackerPos) const; + AttackableTiles getPotentiallyShootableHexes(const battle::Unit* attacker, BattleHex destinationTile, BattleHex attackerPos) const; - std::vector getAttackedBattleUnits(const battle::Unit* attacker, BattleHex destinationTile, bool rangedAttack, BattleHex attackerPos = BattleHex::INVALID) const; //calculates range of multi-hex attacks + + std::vector getAttackedBattleUnits( + const battle::Unit* attacker, + const battle::Unit * defender, + BattleHex destinationTile, + bool rangedAttack, + BattleHex attackerPos = BattleHex::INVALID, + BattleHex defenderPos = BattleHex::INVALID) const; //calculates range of multi-hex attacks + std::set getAttackedCreatures(const CStack* attacker, BattleHex destinationTile, bool rangedAttack, BattleHex attackerPos = BattleHex::INVALID) const; //calculates range of multi-hex attacks - bool isToReverse(const battle::Unit * attacker, const battle::Unit * defender) const; //determines if attacker standing at attackerHex should reverse in order to attack defender + bool isToReverse(const battle::Unit * attacker, const battle::Unit * defender, BattleHex attackerHex = BattleHex::INVALID, BattleHex defenderHex = BattleHex::INVALID) const; //determines if attacker standing at attackerHex should reverse in order to attack defender ReachabilityInfo getReachability(const battle::Unit * unit) const; ReachabilityInfo getReachability(const ReachabilityInfo::Parameters & params) const; - AccessibilityInfo getAccesibility() const; - AccessibilityInfo getAccesibility(const battle::Unit * stack) const; //Hexes ocupied by stack will be marked as accessible. - AccessibilityInfo getAccesibility(const std::vector & accessibleHexes) const; //given hexes will be marked as accessible + AccessibilityInfo getAccessibility() const; + AccessibilityInfo getAccessibility(const battle::Unit * stack) const; //Hexes occupied by stack will be marked as accessible. + AccessibilityInfo getAccessibility(const std::vector & accessibleHexes) const; //given hexes will be marked as accessible std::pair getNearestStack(const battle::Unit * closest) const; - BattleHex getAvaliableHex(const CreatureID & creID, ui8 side, int initialPos = -1) const; //find place for adding new stack + BattleHex getAvailableHex(const CreatureID & creID, BattleSide side, int initialPos = -1) const; //find place for adding new stack protected: ReachabilityInfo getFlyingReachability(const ReachabilityInfo::Parameters & params) const; ReachabilityInfo makeBFS(const AccessibilityInfo & accessibility, const ReachabilityInfo::Parameters & params) const; bool isInObstacle(BattleHex hex, const std::set & obstacles, const ReachabilityInfo::Parameters & params) const; - std::set getStoppers(BattlePerspective::BattlePerspective whichSidePerspective) const; //get hexes with stopping obstacles (quicksands) + std::set getStoppers(BattleSide whichSidePerspective) const; //get hexes with stopping obstacles (quicksands) }; VCMI_LIB_NAMESPACE_END diff --git a/lib/battle/CBattleInfoEssentials.cpp b/lib/battle/CBattleInfoEssentials.cpp index f3ee409c0..638d97b5e 100644 --- a/lib/battle/CBattleInfoEssentials.cpp +++ b/lib/battle/CBattleInfoEssentials.cpp @@ -9,12 +9,15 @@ */ #include "StdInc.h" #include "CBattleInfoEssentials.h" + #include "../CStack.h" #include "BattleInfo.h" #include "CObstacleInstance.h" -#include "../mapObjects/CGTownInstance.h" -#include "../gameState/InfoAboutArmy.h" + #include "../constants/EntityIdentifiers.h" +#include "../entities/building/TownFortifications.h" +#include "../gameState/InfoAboutArmy.h" +#include "../mapObjects/CGTownInstance.h" VCMI_LIB_NAMESPACE_BEGIN @@ -35,13 +38,13 @@ BattleField CBattleInfoEssentials::battleGetBattlefieldType() const return getBattle()->getBattlefieldType(); } -int32_t CBattleInfoEssentials::battleGetEnchanterCounter(ui8 side) const +int32_t CBattleInfoEssentials::battleGetEnchanterCounter(BattleSide side) const { RETURN_IF_NOT_BATTLE(0); return getBattle()->getEnchanterCounter(side); } -std::vector> CBattleInfoEssentials::battleGetAllObstacles(std::optional perspective) const +std::vector> CBattleInfoEssentials::battleGetAllObstacles(std::optional perspective) const { std::vector > ret; RETURN_IF_NOT_BATTLE(ret); @@ -82,13 +85,13 @@ std::shared_ptr CBattleInfoEssentials::battleGetObstacl return std::shared_ptr(); } -bool CBattleInfoEssentials::battleIsObstacleVisibleForSide(const CObstacleInstance & coi, BattlePerspective::BattlePerspective side) const +bool CBattleInfoEssentials::battleIsObstacleVisibleForSide(const CObstacleInstance & coi, BattleSide side) const { RETURN_IF_NOT_BATTLE(false); - return side == BattlePerspective::ALL_KNOWING || coi.visibleForSide(side, battleHasNativeStack(side)); + return side == BattleSide::ALL_KNOWING || coi.visibleForSide(side, battleHasNativeStack(side)); } -bool CBattleInfoEssentials::battleHasNativeStack(ui8 side) const +bool CBattleInfoEssentials::battleHasNativeStack(BattleSide side) const { RETURN_IF_NOT_BATTLE(false); @@ -160,18 +163,18 @@ const CGTownInstance * CBattleInfoEssentials::battleGetDefendedTown() const return getBattle()->getDefendedTown(); } -BattlePerspective::BattlePerspective CBattleInfoEssentials::battleGetMySide() const +BattleSide CBattleInfoEssentials::battleGetMySide() const { - RETURN_IF_NOT_BATTLE(BattlePerspective::INVALID); + RETURN_IF_NOT_BATTLE(BattleSide::INVALID); if(!getPlayerID() || getPlayerID()->isSpectator()) - return BattlePerspective::ALL_KNOWING; + return BattleSide::ALL_KNOWING; if(*getPlayerID() == getBattle()->getSidePlayer(BattleSide::ATTACKER)) - return BattlePerspective::LEFT_SIDE; + return BattleSide::LEFT_SIDE; if(*getPlayerID() == getBattle()->getSidePlayer(BattleSide::DEFENDER)) - return BattlePerspective::RIGHT_SIDE; + return BattleSide::RIGHT_SIDE; logGlobal->error("Cannot find player %s in battle!", getPlayerID()->toString()); - return BattlePerspective::INVALID; + return BattleSide::INVALID; } const CStack* CBattleInfoEssentials::battleGetStackByID(int ID, bool onlyAlive) const @@ -189,11 +192,11 @@ const CStack* CBattleInfoEssentials::battleGetStackByID(int ID, bool onlyAlive) return stacks[0]; } -bool CBattleInfoEssentials::battleDoWeKnowAbout(ui8 side) const +bool CBattleInfoEssentials::battleDoWeKnowAbout(BattleSide side) const { RETURN_IF_NOT_BATTLE(false); auto p = battleGetMySide(); - return p == BattlePerspective::ALL_KNOWING || p == side; + return p == BattleSide::ALL_KNOWING || p == side; } si8 CBattleInfoEssentials::battleTacticDist() const @@ -202,16 +205,16 @@ si8 CBattleInfoEssentials::battleTacticDist() const return getBattle()->getTacticDist(); } -si8 CBattleInfoEssentials::battleGetTacticsSide() const +BattleSide CBattleInfoEssentials::battleGetTacticsSide() const { - RETURN_IF_NOT_BATTLE(-1); + RETURN_IF_NOT_BATTLE(BattleSide::NONE); return getBattle()->getTacticsSide(); } -const CGHeroInstance * CBattleInfoEssentials::battleGetFightingHero(ui8 side) const +const CGHeroInstance * CBattleInfoEssentials::battleGetFightingHero(BattleSide side) const { RETURN_IF_NOT_BATTLE(nullptr); - if(side > 1) + if(side != BattleSide::DEFENDER && side != BattleSide::ATTACKER) { logGlobal->error("FIXME: %s wrong argument!", __FUNCTION__); return nullptr; @@ -226,10 +229,10 @@ const CGHeroInstance * CBattleInfoEssentials::battleGetFightingHero(ui8 side) co return getBattle()->getSideHero(side); } -const CArmedInstance * CBattleInfoEssentials::battleGetArmyObject(ui8 side) const +const CArmedInstance * CBattleInfoEssentials::battleGetArmyObject(BattleSide side) const { RETURN_IF_NOT_BATTLE(nullptr); - if(side > 1) + if(side != BattleSide::DEFENDER && side != BattleSide::ATTACKER) { logGlobal->error("FIXME: %s wrong argument!", __FUNCTION__); return nullptr; @@ -242,7 +245,7 @@ const CArmedInstance * CBattleInfoEssentials::battleGetArmyObject(ui8 side) cons return getBattle()->getSideArmy(side); } -InfoAboutHero CBattleInfoEssentials::battleGetHeroInfo(ui8 side) const +InfoAboutHero CBattleInfoEssentials::battleGetHeroInfo(BattleSide side) const { const auto * hero = getBattle()->getSideHero(side); if(!hero) @@ -253,7 +256,7 @@ InfoAboutHero CBattleInfoEssentials::battleGetHeroInfo(ui8 side) const return InfoAboutHero(hero, infoLevel); } -uint32_t CBattleInfoEssentials::battleCastSpells(ui8 side) const +uint32_t CBattleInfoEssentials::battleCastSpells(BattleSide side) const { RETURN_IF_NOT_BATTLE(-1); return getBattle()->getCastSpells(side); @@ -268,10 +271,10 @@ bool CBattleInfoEssentials::battleCanFlee(const PlayerColor & player) const { RETURN_IF_NOT_BATTLE(false); const auto side = playerToSide(player); - if(!side) + if(side == BattleSide::NONE) return false; - const CGHeroInstance * myHero = battleGetFightingHero(side.value()); + const CGHeroInstance * myHero = battleGetFightingHero(side); //current player have no hero if(!myHero) @@ -292,28 +295,28 @@ bool CBattleInfoEssentials::battleCanFlee(const PlayerColor & player) const return true; } -BattleSideOpt CBattleInfoEssentials::playerToSide(const PlayerColor & player) const +BattleSide CBattleInfoEssentials::playerToSide(const PlayerColor & player) const { - RETURN_IF_NOT_BATTLE(std::nullopt); + RETURN_IF_NOT_BATTLE(BattleSide::NONE); if(getBattle()->getSidePlayer(BattleSide::ATTACKER) == player) - return BattleSideOpt(BattleSide::ATTACKER); + return BattleSide::ATTACKER; if(getBattle()->getSidePlayer(BattleSide::DEFENDER) == player) - return BattleSideOpt(BattleSide::DEFENDER); + return BattleSide::DEFENDER; logGlobal->warn("Cannot find side for player %s", player.toString()); - return std::nullopt; + return BattleSide::INVALID; } -PlayerColor CBattleInfoEssentials::sideToPlayer(ui8 side) const +PlayerColor CBattleInfoEssentials::sideToPlayer(BattleSide side) const { RETURN_IF_NOT_BATTLE(PlayerColor::CANNOT_DETERMINE); return getBattle()->getSidePlayer(side); } -ui8 CBattleInfoEssentials::otherSide(ui8 side) const +BattleSide CBattleInfoEssentials::otherSide(BattleSide side) { if(side == BattleSide::ATTACKER) return BattleSide::DEFENDER; @@ -326,43 +329,43 @@ PlayerColor CBattleInfoEssentials::otherPlayer(const PlayerColor & player) const RETURN_IF_NOT_BATTLE(PlayerColor::CANNOT_DETERMINE); auto side = playerToSide(player); - if(!side) + if(side == BattleSide::NONE) return PlayerColor::CANNOT_DETERMINE; - return getBattle()->getSidePlayer(otherSide(side.value())); + return getBattle()->getSidePlayer(otherSide(side)); } bool CBattleInfoEssentials::playerHasAccessToHeroInfo(const PlayerColor & player, const CGHeroInstance * h) const { RETURN_IF_NOT_BATTLE(false); const auto side = playerToSide(player); - if(side) + if(side != BattleSide::NONE) { - auto opponentSide = otherSide(side.value()); + auto opponentSide = otherSide(side); if(getBattle()->getSideHero(opponentSide) == h) return true; } return false; } -ui8 CBattleInfoEssentials::battleGetSiegeLevel() const +TownFortifications CBattleInfoEssentials::battleGetFortifications() const { - RETURN_IF_NOT_BATTLE(CGTownInstance::NONE); - return getBattle()->getDefendedTown() ? getBattle()->getDefendedTown()->fortLevel() : CGTownInstance::NONE; + RETURN_IF_NOT_BATTLE(TownFortifications()); + return getBattle()->getDefendedTown() ? getBattle()->getDefendedTown()->fortificationsLevel() : TownFortifications(); } bool CBattleInfoEssentials::battleCanSurrender(const PlayerColor & player) const { RETURN_IF_NOT_BATTLE(false); const auto side = playerToSide(player); - if(!side) + if(side == BattleSide::NONE) return false; - bool iAmSiegeDefender = (side.value() == BattleSide::DEFENDER && getBattle()->getDefendedTown() != nullptr); + bool iAmSiegeDefender = (side == BattleSide::DEFENDER && getBattle()->getDefendedTown() != nullptr); //conditions like for fleeing (except escape tunnel presence) + enemy must have a hero - return battleCanFlee(player) && !iAmSiegeDefender && battleHasHero(otherSide(side.value())); + return battleCanFlee(player) && !iAmSiegeDefender && battleHasHero(otherSide(side)); } -bool CBattleInfoEssentials::battleHasHero(ui8 side) const +bool CBattleInfoEssentials::battleHasHero(BattleSide side) const { RETURN_IF_NOT_BATTLE(false); return getBattle()->getSideHero(side) != nullptr; @@ -371,7 +374,7 @@ bool CBattleInfoEssentials::battleHasHero(ui8 side) const EWallState CBattleInfoEssentials::battleGetWallState(EWallPart partOfWall) const { RETURN_IF_NOT_BATTLE(EWallState::NONE); - if(battleGetSiegeLevel() == CGTownInstance::NONE) + if(battleGetFortifications().wallsHealth == 0) return EWallState::NONE; return getBattle()->getWallState(partOfWall); @@ -380,7 +383,7 @@ EWallState CBattleInfoEssentials::battleGetWallState(EWallPart partOfWall) const EGateState CBattleInfoEssentials::battleGetGateState() const { RETURN_IF_NOT_BATTLE(EGateState::NONE); - if(battleGetSiegeLevel() == CGTownInstance::NONE) + if(battleGetFortifications().wallsHealth == 0) return EGateState::NONE; return getBattle()->getGateState(); @@ -389,7 +392,7 @@ EGateState CBattleInfoEssentials::battleGetGateState() const bool CBattleInfoEssentials::battleIsGatePassable() const { RETURN_IF_NOT_BATTLE(true); - if(battleGetSiegeLevel() == CGTownInstance::NONE) + if(battleGetFortifications().wallsHealth == 0) return true; return battleGetGateState() == EGateState::OPENED || battleGetGateState() == EGateState::DESTROYED; @@ -413,9 +416,9 @@ const CGHeroInstance * CBattleInfoEssentials::battleGetOwnerHero(const battle::U { RETURN_IF_NOT_BATTLE(nullptr); const auto side = playerToSide(battleGetOwner(unit)); - if(!side) + if(side == BattleSide::NONE) return nullptr; - return getBattle()->getSideHero(side.value()); + return getBattle()->getSideHero(side); } bool CBattleInfoEssentials::battleMatchOwner(const battle::Unit * attacker, const battle::Unit * defender, const boost::logic::tribool positivness) const diff --git a/lib/battle/CBattleInfoEssentials.h b/lib/battle/CBattleInfoEssentials.h index 3af7c1b50..eff076295 100644 --- a/lib/battle/CBattleInfoEssentials.h +++ b/lib/battle/CBattleInfoEssentials.h @@ -9,6 +9,7 @@ */ #pragma once #include "IBattleInfoCallback.h" +#include "BattleSide.h" VCMI_LIB_NAMESPACE_BEGIN @@ -17,26 +18,16 @@ class CGHeroInstance; class CStack; class IBonusBearer; struct InfoAboutHero; +struct TownFortifications; class CArmedInstance; using TStacks = std::vector; using TStackFilter = std::function; -namespace BattlePerspective -{ - enum BattlePerspective - { - INVALID = -2, - ALL_KNOWING = -1, - LEFT_SIDE, - RIGHT_SIDE - }; -} - class DLL_LINKAGE CBattleInfoEssentials : public IBattleInfoCallback { protected: - bool battleDoWeKnowAbout(ui8 side) const; + bool battleDoWeKnowAbout(BattleSide side) const; public: enum EStackOwnership @@ -45,14 +36,14 @@ public: }; bool duringBattle() const; - BattlePerspective::BattlePerspective battleGetMySide() const; + BattleSide battleGetMySide() const; const IBonusBearer * getBonusBearer() const override; TerrainId battleTerrainType() const override; BattleField battleGetBattlefieldType() const override; - int32_t battleGetEnchanterCounter(ui8 side) const; + int32_t battleGetEnchanterCounter(BattleSide side) const; - std::vector> battleGetAllObstacles(std::optional perspective = std::nullopt) const; //returns all obstacles on the battlefield + std::vector> battleGetAllObstacles(std::optional perspective = std::nullopt) const; //returns all obstacles on the battlefield std::shared_ptr battleGetObstacleByID(uint32_t ID) const; @@ -70,27 +61,27 @@ public: uint32_t battleNextUnitId() const override; - bool battleHasNativeStack(ui8 side) const; + bool battleHasNativeStack(BattleSide side) const; const CGTownInstance * battleGetDefendedTown() const; //returns defended town if current battle is a siege, nullptr instead si8 battleTacticDist() const override; //returns tactic distance in current tactics phase; 0 if not in tactics phase - si8 battleGetTacticsSide() const override; //returns which side is in tactics phase, undefined if none (?) + BattleSide battleGetTacticsSide() const override; //returns which side is in tactics phase, undefined if none (?) bool battleCanFlee(const PlayerColor & player) const; bool battleCanSurrender(const PlayerColor & player) const; - ui8 otherSide(ui8 side) const; + static BattleSide otherSide(BattleSide side); PlayerColor otherPlayer(const PlayerColor & player) const; - BattleSideOpt playerToSide(const PlayerColor & player) const; - PlayerColor sideToPlayer(ui8 side) const; + BattleSide playerToSide(const PlayerColor & player) const; + PlayerColor sideToPlayer(BattleSide side) const; bool playerHasAccessToHeroInfo(const PlayerColor & player, const CGHeroInstance * h) const; - ui8 battleGetSiegeLevel() const; //returns 0 when there is no siege, 1 if fort, 2 is citadel, 3 is castle - bool battleHasHero(ui8 side) const; - uint32_t battleCastSpells(ui8 side) const; //how many spells has given side cast - const CGHeroInstance * battleGetFightingHero(ui8 side) const; //deprecated for players callback, easy to get wrong - const CArmedInstance * battleGetArmyObject(ui8 side) const; - InfoAboutHero battleGetHeroInfo(ui8 side) const; + TownFortifications battleGetFortifications() const; + bool battleHasHero(BattleSide side) const; + uint32_t battleCastSpells(BattleSide side) const; //how many spells has given side cast + const CGHeroInstance * battleGetFightingHero(BattleSide side) const; //deprecated for players callback, easy to get wrong + const CArmedInstance * battleGetArmyObject(BattleSide side) const; + InfoAboutHero battleGetHeroInfo(BattleSide side) const; // for determining state of a part of the wall; format: parameter [0] - keep, [1] - bottom tower, [2] - bottom wall, // [3] - below gate, [4] - over gate, [5] - upper wall, [6] - uppert tower, [7] - gate; returned value: 1 - intact, 2 - damaged, 3 - destroyed; 0 - no battle @@ -103,7 +94,7 @@ public: TStacks battleGetAllStacks(bool includeTurrets = false) const; const CStack * battleGetStackByID(int ID, bool onlyAlive = true) const; //returns stack info by given ID - bool battleIsObstacleVisibleForSide(const CObstacleInstance & coi, BattlePerspective::BattlePerspective side) const; + bool battleIsObstacleVisibleForSide(const CObstacleInstance & coi, BattleSide side) const; ///returns player that controls given stack; mind control included PlayerColor battleGetOwner(const battle::Unit * unit) const; diff --git a/lib/battle/CObstacleInstance.cpp b/lib/battle/CObstacleInstance.cpp index ccf01f07c..6bb5c65f3 100644 --- a/lib/battle/CObstacleInstance.cpp +++ b/lib/battle/CObstacleInstance.cpp @@ -9,8 +9,6 @@ */ #include "StdInc.h" #include "CObstacleInstance.h" -#include "../CHeroHandler.h" -#include "../CTownHandler.h" #include "../ObstacleHandler.h" #include "../VCMI_Lib.h" @@ -53,7 +51,7 @@ std::vector CObstacleInstance::getAffectedTiles() const } } -bool CObstacleInstance::visibleForSide(ui8 side, bool hasNativeStack) const +bool CObstacleInstance::visibleForSide(BattleSide side, bool hasNativeStack) const { //by default obstacle is visible for everyone return true; @@ -107,7 +105,6 @@ SpellID CObstacleInstance::getTrigger() const void CObstacleInstance::serializeJson(JsonSerializeFormat & handler) { - auto obstacleInfo = getInfo(); auto hidden = false; auto needAnimationOffsetFix = obstacleType == CObstacleInstance::USUAL; int animationYOffset = 0; @@ -117,11 +114,7 @@ void CObstacleInstance::serializeJson(JsonSerializeFormat & handler) //We need only a subset of obstacle info for correct render handler.serializeInt("position", pos); - handler.serializeStruct("appearSound", obstacleInfo.appearSound); - handler.serializeStruct("appearAnimation", obstacleInfo.appearAnimation); - handler.serializeStruct("animation", obstacleInfo.animation); handler.serializeInt("animationYOffset", animationYOffset); - handler.serializeBool("hidden", hidden); handler.serializeBool("needAnimationOffsetFix", needAnimationOffsetFix); } @@ -140,7 +133,7 @@ SpellCreatedObstacle::SpellCreatedObstacle() : turnsRemaining(-1), casterSpellPower(0), spellLevel(0), - casterSide(0), + casterSide(BattleSide::NONE), hidden(false), passable(false), trigger(false), @@ -154,7 +147,7 @@ SpellCreatedObstacle::SpellCreatedObstacle() obstacleType = SPELL_CREATED; } -bool SpellCreatedObstacle::visibleForSide(ui8 side, bool hasNativeStack) const +bool SpellCreatedObstacle::visibleForSide(BattleSide side, bool hasNativeStack) const { //we hide mines and not discovered quicksands //quicksands are visible to the caster or if owned unit stepped into that particular patch diff --git a/lib/battle/CObstacleInstance.h b/lib/battle/CObstacleInstance.h index c0607b031..3ce98a202 100644 --- a/lib/battle/CObstacleInstance.h +++ b/lib/battle/CObstacleInstance.h @@ -9,9 +9,11 @@ */ #pragma once #include "BattleHex.h" + +#include "../constants/EntityIdentifiers.h" #include "../filesystem/ResourcePath.h" #include "../networkPacks/BattleChanges.h" -#include "../constants/EntityIdentifiers.h" +#include "../serializer/Serializeable.h" VCMI_LIB_NAMESPACE_BEGIN @@ -20,7 +22,7 @@ class ObstacleChanges; class JsonSerializeFormat; class SpellID; -struct DLL_LINKAGE CObstacleInstance +struct DLL_LINKAGE CObstacleInstance : public Serializeable { enum EObstacleType : ui8 { @@ -48,7 +50,7 @@ struct DLL_LINKAGE CObstacleInstance virtual SpellID getTrigger() const; virtual std::vector getAffectedTiles() const; - virtual bool visibleForSide(ui8 side, bool hasNativeStack) const; //0 attacker + virtual bool visibleForSide(BattleSide side, bool hasNativeStack) const; //0 attacker virtual void battleTurnPassed(){}; @@ -78,7 +80,7 @@ struct DLL_LINKAGE SpellCreatedObstacle : CObstacleInstance int32_t casterSpellPower; int32_t spellLevel; int32_t minimalDamage; //How many damage should it do regardless of power and level of caster - si8 casterSide; //0 - obstacle created by attacker; 1 - by defender + BattleSide casterSide; SpellID trigger; @@ -100,7 +102,7 @@ struct DLL_LINKAGE SpellCreatedObstacle : CObstacleInstance SpellCreatedObstacle(); std::vector getAffectedTiles() const override; - bool visibleForSide(ui8 side, bool hasNativeStack) const override; + bool visibleForSide(BattleSide side, bool hasNativeStack) const override; bool blocksTiles() const override; bool stopsMovement() const override; diff --git a/lib/battle/CPlayerBattleCallback.cpp b/lib/battle/CPlayerBattleCallback.cpp index c0457b89e..2de755613 100644 --- a/lib/battle/CPlayerBattleCallback.cpp +++ b/lib/battle/CPlayerBattleCallback.cpp @@ -76,7 +76,7 @@ const CGHeroInstance * CPlayerBattleCallback::battleGetMyHero() const InfoAboutHero CPlayerBattleCallback::battleGetEnemyHero() const { - return battleGetHeroInfo(!battleGetMySide()); + return battleGetHeroInfo(otherSide(battleGetMySide())); } diff --git a/lib/battle/CUnitState.cpp b/lib/battle/CUnitState.cpp index eb026acc4..737a9813f 100644 --- a/lib/battle/CUnitState.cpp +++ b/lib/battle/CUnitState.cpp @@ -14,7 +14,6 @@ #include #include "../CCreatureHandler.h" -#include "../MetaString.h" #include "../serializer/JsonDeserializer.h" #include "../serializer/JsonSerializer.h" @@ -140,7 +139,7 @@ int32_t CRetaliations::total() const if(noRetaliation.getHasBonus()) return 0; - //after dispell bonus should remain during current round + //after dispel bonus should remain during current round int32_t val = 1 + totalProxy->totalValue(); vstd::amax(totalCache, val); return totalCache; @@ -228,7 +227,7 @@ void CHealth::damage(int64_t & amount) addResurrected(getCount() - oldCount); } -void CHealth::heal(int64_t & amount, EHealLevel level, EHealPower power) +HealInfo CHealth::heal(int64_t & amount, EHealLevel level, EHealPower power) { const int32_t unitHealth = owner->getMaxHealth(); const int32_t oldCount = getCount(); @@ -252,7 +251,7 @@ void CHealth::heal(int64_t & amount, EHealLevel level, EHealPower power) vstd::abetween(amount, int64_t(0), maxHeal); if(amount == 0) - return; + return {}; int64_t availableHealth = available(); @@ -263,6 +262,8 @@ void CHealth::heal(int64_t & amount, EHealLevel level, EHealPower power) addResurrected(getCount() - oldCount); else assert(power == EHealPower::PERMANENT); + + return HealInfo(amount, getCount() - oldCount); } void CHealth::setFromTotal(const int64_t totalHealth) @@ -329,6 +330,7 @@ CUnitState::CUnitState(): drainedMana(false), fear(false), hadMorale(false), + castSpellThisTurn(false), ghost(false), ghostPending(false), movedThisRound(false), @@ -361,6 +363,7 @@ CUnitState & CUnitState::operator=(const CUnitState & other) drainedMana = other.drainedMana; fear = other.fear; hadMorale = other.hadMorale; + castSpellThisTurn = other.castSpellThisTurn; ghost = other.ghost; ghostPending = other.ghostPending; movedThisRound = other.movedThisRound; @@ -413,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 @@ -531,7 +534,7 @@ bool CUnitState::hasClone() const bool CUnitState::canCast() const { - return casts.canUse(1);//do not check specific cast abilities here + return casts.canUse(1) && !castSpellThisTurn;//do not check specific cast abilities here } bool CUnitState::isCaster() const @@ -747,6 +750,7 @@ void CUnitState::serializeJson(JsonSerializeFormat & handler) handler.serializeBool("drainedMana", drainedMana); handler.serializeBool("fear", fear); handler.serializeBool("hadMorale", hadMorale); + handler.serializeBool("castSpellThisTurn", castSpellThisTurn); handler.serializeBool("ghost", ghost); handler.serializeBool("ghostPending", ghostPending); handler.serializeBool("moved", movedThisRound); @@ -781,6 +785,7 @@ void CUnitState::reset() drainedMana = false; fear = false; hadMorale = false; + castSpellThisTurn = false; ghost = false; ghostPending = false; movedThisRound = false; @@ -830,18 +835,21 @@ void CUnitState::damage(int64_t & amount) health.damage(amount); } - if(health.available() <= 0 && (cloned || summoned)) + bool disintegrate = hasBonusOfType(BonusType::DISINTEGRATE); + if(health.available() <= 0 && (cloned || summoned || disintegrate)) ghostPending = true; } -void CUnitState::heal(int64_t & amount, EHealLevel level, EHealPower power) +HealInfo CUnitState::heal(int64_t & amount, EHealLevel level, EHealPower power) { if(level == EHealLevel::HEAL && power == EHealPower::ONE_BATTLE) logGlobal->error("Heal for one battle does not make sense"); else if(cloned) logGlobal->error("Attempt to heal clone"); else - health.heal(amount, level, power); + return health.heal(amount, level, power); + + return {}; } void CUnitState::afterAttack(bool ranged, bool counter) @@ -860,6 +868,7 @@ void CUnitState::afterNewRound() waitedThisTurn = false; movedThisRound = false; hadMorale = false; + castSpellThisTurn = false; fear = false; drainedMana = false; counterAttacks.reset(); @@ -897,9 +906,9 @@ CUnitStateDetached::CUnitStateDetached(const IUnitInfo * unit_, const IBonusBear { } -TConstBonusListPtr CUnitStateDetached::getAllBonuses(const CSelector & selector, const CSelector & limit, const CBonusSystemNode * root, const std::string & cachingStr) const +TConstBonusListPtr CUnitStateDetached::getAllBonuses(const CSelector & selector, const CSelector & limit, const std::string & cachingStr) const { - return bonus->getAllBonuses(selector, limit, root, cachingStr); + return bonus->getAllBonuses(selector, limit, cachingStr); } int64_t CUnitStateDetached::getTreeVersion() const @@ -918,7 +927,7 @@ uint32_t CUnitStateDetached::unitId() const return unit->unitId(); } -ui8 CUnitStateDetached::unitSide() const +BattleSide CUnitStateDetached::unitSide() const { return unit->unitSide(); } diff --git a/lib/battle/CUnitState.h b/lib/battle/CUnitState.h index 9e0fb41c9..9bb9570a1 100644 --- a/lib/battle/CUnitState.h +++ b/lib/battle/CUnitState.h @@ -107,7 +107,7 @@ public: void reset(); void damage(int64_t & amount); - void heal(int64_t & amount, EHealLevel level, EHealPower power); + HealInfo heal(int64_t & amount, EHealLevel level, EHealPower power); int32_t getCount() const; int32_t getFirstHPleft() const; @@ -141,6 +141,7 @@ public: bool drainedMana; bool fear; bool hadMorale; + bool castSpellThisTurn; bool ghost; bool ghostPending; bool movedThisRound; @@ -247,12 +248,12 @@ public: void load(const JsonNode & data) override; void damage(int64_t & amount) override; - void heal(int64_t & amount, EHealLevel level, EHealPower power) override; + HealInfo heal(int64_t & amount, EHealLevel level, EHealPower power) override; void localInit(const IUnitEnvironment * env_); void serializeJson(JsonSerializeFormat & handler); - FactionID getFaction() const override; + FactionID getFactionID() const override; void afterAttack(bool ranged, bool counter); @@ -282,14 +283,14 @@ public: explicit CUnitStateDetached(const IUnitInfo * unit_, const IBonusBearer * bonus_); TConstBonusListPtr getAllBonuses(const CSelector & selector, const CSelector & limit, - const CBonusSystemNode * root = nullptr, const std::string & cachingStr = "") const override; + const std::string & cachingStr = "") const override; int64_t getTreeVersion() const override; CUnitStateDetached & operator= (const CUnitState & other); uint32_t unitId() const override; - ui8 unitSide() const override; + BattleSide unitSide() const override; const CCreature * unitType() const override; PlayerColor unitOwner() const override; diff --git a/lib/battle/DamageCalculator.cpp b/lib/battle/DamageCalculator.cpp index 99bce45e6..6aa4e5483 100644 --- a/lib/battle/DamageCalculator.cpp +++ b/lib/battle/DamageCalculator.cpp @@ -17,7 +17,7 @@ #include "../bonuses/Bonus.h" #include "../mapObjects/CGTownInstance.h" #include "../spells/CSpellHandler.h" -#include "../GameSettings.h" +#include "../IGameSettings.h" #include "../VCMI_Lib.h" @@ -145,7 +145,8 @@ int DamageCalculator::getActorAttackIgnored() const if(multAttackReductionPercent > 0) { - int reduction = (getActorAttackBase() * multAttackReductionPercent + 49) / 100; //using ints so 1.5 for 5 attack is rounded down as in HotA / h3assist etc. (keep in mind h3assist 1.2 shows wrong value for 15 attack points and unupg. nix) + //using ints so 1.5 for 5 attack is rounded down as in HotA / h3assist etc. (keep in mind h3assist 1.2 shows wrong value for 15 attack points and unupg. nix) + int reduction = vstd::divideAndRound( getActorAttackBase() * multAttackReductionPercent, 100); return -std::min(reduction, getActorAttackBase()); } return 0; @@ -211,8 +212,9 @@ double DamageCalculator::getAttackSkillFactor() const if(attackAdvantage > 0) { - const double attackMultiplier = VLC->settings()->getDouble(EGameSettings::COMBAT_ATTACK_POINT_DAMAGE_FACTOR); - const double attackMultiplierCap = VLC->settings()->getDouble(EGameSettings::COMBAT_ATTACK_POINT_DAMAGE_FACTOR_CAP); + // FIXME: use cb to acquire these settings + const double attackMultiplier = VLC->engineSettings()->getDouble(EGameSettings::COMBAT_ATTACK_POINT_DAMAGE_FACTOR); + const double attackMultiplierCap = VLC->engineSettings()->getDouble(EGameSettings::COMBAT_ATTACK_POINT_DAMAGE_FACTOR_CAP); const double attackFactor = std::min(attackMultiplier * attackAdvantage, attackMultiplierCap); return attackFactor; @@ -311,8 +313,9 @@ double DamageCalculator::getDefenseSkillFactor() const //bonus from attack/defense skills if(defenseAdvantage > 0) //decreasing dmg { - const double defenseMultiplier = VLC->settings()->getDouble(EGameSettings::COMBAT_DEFENSE_POINT_DAMAGE_FACTOR); - const double defenseMultiplierCap = VLC->settings()->getDouble(EGameSettings::COMBAT_DEFENSE_POINT_DAMAGE_FACTOR_CAP); + // FIXME: use cb to acquire these settings + const double defenseMultiplier = VLC->engineSettings()->getDouble(EGameSettings::COMBAT_DEFENSE_POINT_DAMAGE_FACTOR); + const double defenseMultiplierCap = VLC->engineSettings()->getDouble(EGameSettings::COMBAT_DEFENSE_POINT_DAMAGE_FACTOR_CAP); const double dec = std::min(defenseMultiplier * defenseAdvantage, defenseMultiplierCap); return dec; diff --git a/lib/battle/IBattleInfoCallback.h b/lib/battle/IBattleInfoCallback.h index b5b9ce583..ca43cb767 100644 --- a/lib/battle/IBattleInfoCallback.h +++ b/lib/battle/IBattleInfoCallback.h @@ -65,10 +65,10 @@ public: virtual BattleField battleGetBattlefieldType() const = 0; ///return none if battle is ongoing; otherwise the victorious side (0/1) or 2 if it is a draw - virtual std::optional battleIsFinished() const = 0; + virtual std::optional battleIsFinished() const = 0; virtual si8 battleTacticDist() const = 0; //returns tactic distance in current tactics phase; 0 if not in tactics phase - virtual si8 battleGetTacticsSide() const = 0; //returns which side is in tactics phase, undefined if none (?) + virtual BattleSide battleGetTacticsSide() const = 0; //returns which side is in tactics phase, undefined if none (?) virtual uint32_t battleNextUnitId() const = 0; diff --git a/lib/battle/IBattleState.h b/lib/battle/IBattleState.h index fd643c56c..a2ae55576 100644 --- a/lib/battle/IBattleState.h +++ b/lib/battle/IBattleState.h @@ -16,6 +16,7 @@ VCMI_LIB_NAMESPACE_BEGIN class ObstacleChanges; class UnitChanges; struct Bonus; +struct BattleLayout; class JsonNode; class JsonSerializeFormat; class BattleField; @@ -55,24 +56,24 @@ public: virtual EWallState getWallState(EWallPart partOfWall) const = 0; virtual EGateState getGateState() const = 0; - virtual PlayerColor getSidePlayer(ui8 side) const = 0; - virtual const CArmedInstance * getSideArmy(ui8 side) const = 0; - virtual const CGHeroInstance * getSideHero(ui8 side) const = 0; + virtual PlayerColor getSidePlayer(BattleSide side) const = 0; + virtual const CArmedInstance * getSideArmy(BattleSide side) const = 0; + virtual const CGHeroInstance * getSideHero(BattleSide side) const = 0; /// Returns list of all spells used by specified side (and that can be learned by opposite hero) - virtual std::vector getUsedSpells(ui8 side) const = 0; + virtual std::vector getUsedSpells(BattleSide side) const = 0; - virtual uint32_t getCastSpells(ui8 side) const = 0; - virtual int32_t getEnchanterCounter(ui8 side) const = 0; + virtual uint32_t getCastSpells(BattleSide side) const = 0; + virtual int32_t getEnchanterCounter(BattleSide side) const = 0; virtual ui8 getTacticDist() const = 0; - virtual ui8 getTacticsSide() const = 0; + virtual BattleSide getTacticsSide() const = 0; virtual uint32_t nextUnitId() const = 0; virtual int64_t getActualDamage(const DamageRange & damage, int32_t attackerCount, vstd::RNG & rng) const = 0; virtual int3 getLocation() const = 0; - virtual bool isCreatureBank() const = 0; + virtual BattleLayout getLayout() const = 0; }; class DLL_LINKAGE IBattleState : public IBattleInfo diff --git a/lib/battle/IUnitInfo.h b/lib/battle/IUnitInfo.h index f91ccce2f..3185b84f1 100644 --- a/lib/battle/IUnitInfo.h +++ b/lib/battle/IUnitInfo.h @@ -11,6 +11,7 @@ #pragma once #include "../GameConstants.h" +#include "BattleSide.h" VCMI_LIB_NAMESPACE_BEGIN @@ -35,7 +36,7 @@ public: virtual int32_t unitBaseAmount() const = 0; virtual uint32_t unitId() const = 0; - virtual ui8 unitSide() const = 0; + virtual BattleSide unitSide() const = 0; virtual PlayerColor unitOwner() const = 0; virtual SlotID unitSlot() const = 0; diff --git a/lib/battle/ReachabilityInfo.cpp b/lib/battle/ReachabilityInfo.cpp index ecb6a32bf..c0165b7af 100644 --- a/lib/battle/ReachabilityInfo.cpp +++ b/lib/battle/ReachabilityInfo.cpp @@ -15,7 +15,7 @@ VCMI_LIB_NAMESPACE_BEGIN ReachabilityInfo::Parameters::Parameters(const battle::Unit * Stack, BattleHex StartPosition): - perspective(static_cast(Stack->unitSide())), + perspective(static_cast(Stack->unitSide())), startPosition(StartPosition), doubleWide(Stack->doubleWide()), side(Stack->unitSide()), @@ -66,9 +66,25 @@ uint32_t ReachabilityInfo::distToNearestNeighbour( if(attacker->doubleWide()) { - vstd::concatenate(attackableHexes, battle::Unit::getHexes(defender->occupiedHex(), true, attacker->unitSide())); + if(defender->doubleWide()) + { + // It can be back to back attack o==o or head to head =oo=. + // In case of back-to-back the distance between heads (unit positions) may be up to 3 tiles + vstd::concatenate(attackableHexes, battle::Unit::getHexes(defender->occupiedHex(), true, defender->unitSide())); + } + else + { + vstd::concatenate(attackableHexes, battle::Unit::getHexes(defender->getPosition(), true, defender->unitSide())); + } } + vstd::removeDuplicates(attackableHexes); + + vstd::erase_if(attackableHexes, [defender](BattleHex h) -> bool + { + return h.getY() != defender->getPosition().getY() || !h.isAvailable(); + }); + return distToNearestNeighbour(attackableHexes, chosenHex); } diff --git a/lib/battle/ReachabilityInfo.h b/lib/battle/ReachabilityInfo.h index 8f6bbb663..f0c5ed948 100644 --- a/lib/battle/ReachabilityInfo.h +++ b/lib/battle/ReachabilityInfo.h @@ -15,7 +15,7 @@ VCMI_LIB_NAMESPACE_BEGIN // Reachability info is result of BFS calculation. It's dependent on stack (it's owner, whether it's flying), -// startPosition and perpective. +// startPosition and perspective. struct DLL_LINKAGE ReachabilityInfo { using TDistances = std::array; @@ -25,14 +25,16 @@ struct DLL_LINKAGE ReachabilityInfo struct DLL_LINKAGE Parameters { - ui8 side = 0; + BattleSide side = BattleSide::NONE; bool doubleWide = false; bool flying = false; bool ignoreKnownAccessible = false; //Ignore obstacles if it is in accessible hexes + bool bypassEnemyStacks = false; // in case of true will count amount of turns needed to kill enemy and thus move forward std::vector knownAccessible; //hexes that will be treated as accessible, even if they're occupied by stack (by default - tiles occupied by stack we do reachability for, so it doesn't block itself) + std::map destructibleEnemyTurns; // hom many turns it is needed to kill enemy on specific hex BattleHex startPosition; //assumed position of stack - BattlePerspective::BattlePerspective perspective = BattlePerspective::ALL_KNOWING; //some obstacles (eg. quicksands) may be invisible for some side + BattleSide perspective = BattleSide::ALL_KNOWING; //some obstacles (eg. quicksands) may be invisible for some side Parameters() = default; Parameters(const battle::Unit * Stack, BattleHex StartPosition); diff --git a/lib/battle/Unit.cpp b/lib/battle/Unit.cpp index c08713f43..51bf21d77 100644 --- a/lib/battle/Unit.cpp +++ b/lib/battle/Unit.cpp @@ -12,8 +12,7 @@ #include "Unit.h" #include "../VCMI_Lib.h" -#include "../CGeneralTextHandler.h" -#include "../MetaString.h" +#include "../texts/CGeneralTextHandler.h" #include "../serializer/JsonDeserializer.h" #include "../serializer/JsonSerializer.h" @@ -42,7 +41,7 @@ bool Unit::isTurret() const std::string Unit::getDescription() const { boost::format fmt("Unit %d of side %d"); - fmt % unitId() % unitSide(); + fmt % unitId() % static_cast(unitSide()); return fmt.str(); } @@ -59,7 +58,7 @@ std::vector Unit::getSurroundingHexes(BattleHex assumedPosition) cons return getSurroundingHexes(hex, doubleWide(), unitSide()); } -std::vector Unit::getSurroundingHexes(BattleHex position, bool twoHex, ui8 side) +std::vector Unit::getSurroundingHexes(BattleHex position, bool twoHex, BattleSide side) { std::vector hexes; if(twoHex) @@ -136,7 +135,7 @@ std::vector Unit::getHexes(BattleHex assumedPos) const return getHexes(assumedPos, doubleWide(), unitSide()); } -std::vector Unit::getHexes(BattleHex assumedPos, bool twoHex, ui8 side) +std::vector Unit::getHexes(BattleHex assumedPos, bool twoHex, BattleSide side) { std::vector hexes; hexes.push_back(assumedPos); @@ -157,7 +156,7 @@ BattleHex Unit::occupiedHex(BattleHex assumedPos) const return occupiedHex(assumedPos, doubleWide(), unitSide()); } -BattleHex Unit::occupiedHex(BattleHex assumedPos, bool twoHex, ui8 side) +BattleHex Unit::occupiedHex(BattleHex assumedPos, bool twoHex, BattleSide side) { if(twoHex) { diff --git a/lib/battle/Unit.h b/lib/battle/Unit.h index 8a2f2f560..cbe585593 100644 --- a/lib/battle/Unit.h +++ b/lib/battle/Unit.h @@ -41,6 +41,25 @@ namespace BattlePhases }; } +// Healed HP (also drained life) and resurrected units info +struct HealInfo +{ + HealInfo() = default; + HealInfo(int64_t healedHP, int32_t resurrected) + : healedHealthPoints(healedHP), resurrectedCount(resurrected) + { } + + int64_t healedHealthPoints = 0; + int32_t resurrectedCount = 0; + + HealInfo & operator+=(const HealInfo & other) + { + healedHealthPoints += other.healedHealthPoints; + resurrectedCount += other.resurrectedCount; + return *this; + } +}; + class CUnitState; class DLL_LINKAGE Unit : public IUnitInfo, public spells::Caster, public virtual IBonusBearer, public ACreature @@ -110,17 +129,17 @@ public: std::vector getSurroundingHexes(BattleHex assumedPosition = BattleHex::INVALID) const; // get six or 8 surrounding hexes depending on creature size std::vector getAttackableHexes(const Unit * attacker) const; - static std::vector getSurroundingHexes(BattleHex position, bool twoHex, ui8 side); + static std::vector getSurroundingHexes(BattleHex position, bool twoHex, BattleSide side); bool coversPos(BattleHex position) const; //checks also if unit is double-wide std::vector getHexes() const; //up to two occupied hexes, starting from front std::vector getHexes(BattleHex assumedPos) const; //up to two occupied hexes, starting from front - static std::vector getHexes(BattleHex assumedPos, bool twoHex, ui8 side); + static std::vector getHexes(BattleHex assumedPos, bool twoHex, BattleSide side); BattleHex occupiedHex() const; //returns number of occupied hex (not the position) if stack is double wide; otherwise -1 BattleHex occupiedHex(BattleHex assumedPos) const; //returns number of occupied hex (not the position) if stack is double wide and would stand on assumedPos; otherwise -1 - static BattleHex occupiedHex(BattleHex assumedPos, bool twoHex, ui8 side); + static BattleHex occupiedHex(BattleHex assumedPos, bool twoHex, BattleSide side); ///MetaStrings void addText(MetaString & text, EMetaText type, int32_t serial, const boost::logic::tribool & plural = boost::logic::indeterminate) const; @@ -138,7 +157,7 @@ public: virtual void load(const JsonNode & data) = 0; virtual void damage(int64_t & amount) = 0; - virtual void heal(int64_t & amount, EHealLevel level, EHealPower power) = 0; + virtual HealInfo heal(int64_t & amount, EHealLevel level, EHealPower power) = 0; }; class DLL_LINKAGE UnitInfo @@ -147,7 +166,7 @@ public: uint32_t id = 0; TQuantity count = 0; CreatureID type; - ui8 side = 0; + BattleSide side = BattleSide::NONE; BattleHex position; bool summoned = false; diff --git a/lib/bonuses/Bonus.cpp b/lib/bonuses/Bonus.cpp index e82b6eb13..95d665d76 100644 --- a/lib/bonuses/Bonus.cpp +++ b/lib/bonuses/Bonus.cpp @@ -14,19 +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 "../CTownHandler.h" -#include "../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 @@ -88,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); @@ -113,13 +115,17 @@ 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)); } } if(descriptionHelper.empty()) { // still no description - try to generate one based on duration - if ((duration & BonusDuration::ONE_BATTLE).any()) + if ((duration & BonusDuration::ONE_BATTLE) != 0) { if (val > 0) descriptionHelper.appendTextID("core.arraytxt.110"); //+%d Temporary until next battle" @@ -248,7 +254,7 @@ DLL_LINKAGE std::ostream & operator<<(std::ostream &out, const Bonus &bonus) #define printField(field) out << "\t" #field ": " << (int)bonus.field << "\n" printField(val); out << "\tSubtype: " << bonus.subtype.toString() << "\n"; - printField(duration.to_ulong()); + printField(duration); printField(source); out << "\tSource ID: " << bonus.sid.toString() << "\n"; if(bonus.additionalInfo != CAddInfo::NONE) diff --git a/lib/bonuses/Bonus.h b/lib/bonuses/Bonus.h index 48dc76f34..caefbc46a 100644 --- a/lib/bonuses/Bonus.h +++ b/lib/bonuses/Bonus.h @@ -13,7 +13,8 @@ #include "BonusCustomTypes.h" #include "../constants/VariantIdentifier.h" #include "../constants/EntityIdentifiers.h" -#include "../MetaString.h" +#include "../serializer/Serializeable.h" +#include "../texts/MetaString.h" VCMI_LIB_NAMESPACE_BEGIN @@ -25,6 +26,7 @@ class IPropagator; class IUpdater; class BonusList; class CSelector; +class IGameInfoCallback; using BonusSubtypeID = VariantIdentifier; using BonusSourceID = VariantIdentifier; @@ -52,26 +54,27 @@ public: JsonNode toJsonNode() const; }; -#define BONUS_TREE_DESERIALIZATION_FIX if(!h.saving && h.smartPointerSerialization) deserializationFix(); +#define BONUS_TREE_DESERIALIZATION_FIX if(!h.saving && h.loadingGamestate) deserializationFix(); /// Struct for handling bonuses of several types. Can be transferred to any hero -struct DLL_LINKAGE Bonus : public std::enable_shared_from_this +struct DLL_LINKAGE Bonus : public std::enable_shared_from_this, public Serializeable { - BonusDuration::Type duration = BonusDuration::PERMANENT; //uses BonusDuration values + BonusDuration::Type duration = BonusDuration::PERMANENT; //uses BonusDuration values - 2 bytes si16 turnsRemain = 0; //used if duration is N_TURNS, N_DAYS or ONE_WEEK - - BonusType type = BonusType::NONE; //uses BonusType values - says to what is this bonus - 1 byte - BonusSubtypeID subtype; - - BonusSource source = BonusSource::OTHER; //source type" uses BonusSource values - what gave that bonus - BonusSource targetSourceType = BonusSource::OTHER;//Bonuses of what origin this amplifies, uses BonusSource values. Needed for PERCENT_TO_TARGET_TYPE. si32 val = 0; + + BonusValueType valType = BonusValueType::ADDITIVE_VALUE; // 1 byte + BonusSource source = BonusSource::OTHER; //source type" uses BonusSource values - what gave that bonus - 1 byte + BonusSource targetSourceType = BonusSource::OTHER;//Bonuses of what origin this amplifies, uses BonusSource values. Needed for PERCENT_TO_TARGET_TYPE. - 1 byte + BonusType type = BonusType::NONE; //uses BonusType values - says to what is this bonus - 1 byte + BonusLimitEffect effectRange = BonusLimitEffect::NO_LIMIT; // 1 byte + // 3 bytes padding + + BonusSubtypeID subtype; BonusSourceID sid; //source id: id of object/artifact/spell - BonusValueType valType = BonusValueType::ADDITIVE_VALUE; std::string stacking; // bonuses with the same stacking value don't stack (e.g. Angel/Archangel morale bonus) CAddInfo additionalInfo; - BonusLimitEffect effectRange = BonusLimitEffect::NO_LIMIT; TLimiterPtr limiter; TPropagatorPtr propagator; @@ -93,15 +96,7 @@ struct DLL_LINKAGE Bonus : public std::enable_shared_from_this h & source; h & val; h & sid; - if (h.version < Handler::Version::BONUS_META_STRING) - { - std::string oldDescription; - h & oldDescription; - description = MetaString::createFromRawString(oldDescription); - } - else - h & description; - + h & description; h & additionalInfo; h & turnsRemain; h & valType; @@ -112,11 +107,6 @@ struct DLL_LINKAGE Bonus : public std::enable_shared_from_this h & updater; h & propagationUpdater; h & targetSourceType; - if (h.version < Handler::Version::MANA_LIMIT && type == BonusType::MANA_PER_KNOWLEDGE_PERCENTAGE) - { - if (valType == BonusValueType::ADDITIVE_VALUE || valType == BonusValueType::BASE_NUMBER) - val *= 100; - } } template @@ -127,57 +117,57 @@ struct DLL_LINKAGE Bonus : public std::enable_shared_from_this static bool NDays(const Bonus *hb) { auto set = hb->duration & BonusDuration::N_DAYS; - return set.any(); + return set != 0; } static bool NTurns(const Bonus *hb) { auto set = hb->duration & BonusDuration::N_TURNS; - return set.any(); + return set != 0; } static bool OneDay(const Bonus *hb) { auto set = hb->duration & BonusDuration::ONE_DAY; - return set.any(); + return set != 0; } static bool OneWeek(const Bonus *hb) { auto set = hb->duration & BonusDuration::ONE_WEEK; - return set.any(); + return set != 0; } static bool OneBattle(const Bonus *hb) { auto set = hb->duration & BonusDuration::ONE_BATTLE; - return set.any(); + return set != 0; } static bool Permanent(const Bonus *hb) { auto set = hb->duration & BonusDuration::PERMANENT; - return set.any(); + return set != 0; } static bool UntilGetsTurn(const Bonus *hb) { auto set = hb->duration & BonusDuration::STACK_GETS_TURN; - return set.any(); + return set != 0; } static bool UntilAttack(const Bonus *hb) { auto set = hb->duration & BonusDuration::UNTIL_ATTACK; - return set.any(); + return set != 0; } static bool UntilBeingAttacked(const Bonus *hb) { auto set = hb->duration & BonusDuration::UNTIL_BEING_ATTACKED; - return set.any(); + return set != 0; } static bool UntilCommanderKilled(const Bonus *hb) { auto set = hb->duration & BonusDuration::COMMANDER_KILLED; - return set.any(); + return set != 0; } static bool UntilOwnAttack(const Bonus *hb) { auto set = hb->duration & BonusDuration::UNTIL_OWN_ATTACK; - return set.any(); + return set != 0; } inline bool operator == (const BonusType & cf) const { @@ -188,7 +178,7 @@ struct DLL_LINKAGE Bonus : public std::enable_shared_from_this 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.cpp b/lib/bonuses/BonusEnum.cpp index e48d15dcf..1408a4aee 100644 --- a/lib/bonuses/BonusEnum.cpp +++ b/lib/bonuses/BonusEnum.cpp @@ -60,10 +60,11 @@ namespace BonusDuration JsonNode toJson(const Type & duration) { std::vector durationNames; - for(auto durBit = 0; durBit < duration.size(); durBit++) + for(size_t durBit = 0; durBit < Size; durBit++) { - if(duration[durBit]) - durationNames.push_back(vstd::findKey(bonusDurationMap, duration & Type().set(durBit))); + Type value = duration & (1 << durBit); + if(value) + durationNames.push_back(vstd::findKey(bonusDurationMap, value)); } if(durationNames.size() == 1) { diff --git a/lib/bonuses/BonusEnum.h b/lib/bonuses/BonusEnum.h index 524dcd9fd..1ef515e1f 100644 --- a/lib/bonuses/BonusEnum.h +++ b/lib/bonuses/BonusEnum.h @@ -33,7 +33,7 @@ class JsonNode; BONUS_NAME(NEGATE_ALL_NATURAL_IMMUNITIES) \ BONUS_NAME(STACK_HEALTH) \ BONUS_NAME(GENERATE_RESOURCE) /*daily value, uses subtype (resource type)*/ \ - BONUS_NAME(CREATURE_GROWTH) /*for legion artifacts: value - week growth bonus, subtype - monster level if aplicable*/ \ + BONUS_NAME(CREATURE_GROWTH) /*for legion artifacts: value - week growth bonus, subtype - monster level if applicable*/ \ BONUS_NAME(WHIRLPOOL_PROTECTION) /*hero won't lose army when teleporting through whirlpool*/ \ BONUS_NAME(SPELL) /*hero knows spell, val - skill level (0 - 3), subtype - spell id*/ \ BONUS_NAME(SPELLS_OF_LEVEL) /*hero knows all spells of given level, val - skill level; subtype - level*/ \ @@ -150,7 +150,7 @@ class JsonNode; BONUS_NAME(GARGOYLE) /* gargoyle is special than NON_LIVING, cannot be rised or healed */ \ BONUS_NAME(SPECIAL_ADD_VALUE_ENCHANT) /*specialty spell like Aenin has, increased effect of spell, additionalInfo = value to add*/\ BONUS_NAME(SPECIAL_FIXED_VALUE_ENCHANT) /*specialty spell like Melody has, constant spell effect (i.e. 3 luck), additionalInfo = value to fix.*/\ - BONUS_NAME(TOWN_MAGIC_WELL) /*one-time pseudo-bonus to implement Magic Well in the town*/\ + BONUS_NAME(THIEVES_GUILD_ACCESS) \ BONUS_NAME(LIMITED_SHOOTING_RANGE) /*limits range of shooting creatures, doesn't adjust any other mechanics (half vs full damage etc). val - range in hexes, additional info - optional new range for broken arrow mechanic */\ BONUS_NAME(LEARN_BATTLE_SPELL_CHANCE) /*skill-agnostic eagle eye chance. subtype = 0 - from enemy, 1 - TODO: from entire battlefield*/\ BONUS_NAME(LEARN_BATTLE_SPELL_LEVEL_LIMIT) /*skill-agnostic eagle eye limit, subtype - school (-1 for all), others TODO*/\ @@ -177,7 +177,11 @@ class JsonNode; BONUS_NAME(ENEMY_ATTACK_REDUCTION) /*in % (value) eg. Nix (HotA)*/ \ BONUS_NAME(REVENGE) /*additional damage based on how many units in stack died - formula: sqrt((number of creatures at battle start + 1) * creature health) / (total health now + 1 creature health) - 1) * 100% */ \ BONUS_NAME(RESOURCES_CONSTANT_BOOST) /*Bonus that does not account for propagation and gives extra resources per day. val - resource amount, subtype - resource type*/ \ - 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 multipled times number of owned towns, subtype - resource type*/ \ + 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 */ @@ -212,7 +216,7 @@ class JsonNode; BONUS_VALUE(INDEPENDENT_MIN) //used for SECONDARY_SKILL_PREMY bonus -enum class BonusType +enum class BonusType : uint8_t { #define BONUS_NAME(x) x, BONUS_LIST @@ -220,21 +224,27 @@ enum class BonusType }; namespace BonusDuration //when bonus is automatically removed { - using Type = std::bitset<11>; + // We use uint16_t directly because std::bitset<11> eats whole 8 byte word. + using Type = uint16_t; + constexpr size_t Size = 11; + + enum BonusDuration : Type { + PERMANENT = 1 << 0, + ONE_BATTLE = 1 << 1, //at the end of battle + ONE_DAY = 1 << 2, //at the end of day + ONE_WEEK = 1 << 3, //at the end of week (bonus lasts till the end of week, thats NOT 7 days + N_TURNS = 1 << 4, //used during battles, after battle bonus is always removed + N_DAYS = 1 << 5, + UNTIL_BEING_ATTACKED = 1 << 6, /*removed after attack and counterattacks are performed*/ + UNTIL_ATTACK = 1 << 7, /*removed after attack and counterattacks are performed*/ + STACK_GETS_TURN = 1 << 8, /*removed when stack gets its turn - used for defensive stance*/ + COMMANDER_KILLED = 1 << 9, + UNTIL_OWN_ATTACK = 1 << 10 /*removed after attack is performed (not counterattack)*/, + }; + extern JsonNode toJson(const Type & duration); - constexpr Type PERMANENT = 1 << 0; - constexpr Type ONE_BATTLE = 1 << 1; //at the end of battle - constexpr Type ONE_DAY = 1 << 2; //at the end of day - constexpr Type ONE_WEEK = 1 << 3; //at the end of week (bonus lasts till the end of week, thats NOT 7 days - constexpr Type N_TURNS = 1 << 4; //used during battles, after battle bonus is always removed - constexpr Type N_DAYS = 1 << 5; - constexpr Type UNTIL_BEING_ATTACKED = 1 << 6; /*removed after attack and counterattacks are performed*/ - constexpr Type UNTIL_ATTACK = 1 << 7; /*removed after attack and counterattacks are performed*/ - constexpr Type STACK_GETS_TURN = 1 << 8; /*removed when stack gets its turn - used for defensive stance*/ - constexpr Type COMMANDER_KILLED = 1 << 9; - constexpr Type UNTIL_OWN_ATTACK = 1 << 10; /*removed after attack is performed (not counterattack)*/; }; -enum class BonusSource +enum class BonusSource : uint8_t { #define BONUS_SOURCE(x) x, BONUS_SOURCE_LIST @@ -242,13 +252,13 @@ enum class BonusSource NUM_BONUS_SOURCE /*This is a dummy value, which will be always last*/ }; -enum class BonusLimitEffect +enum class BonusLimitEffect : uint8_t { NO_LIMIT = 0, ONLY_DISTANCE_FIGHT=1, ONLY_MELEE_FIGHT, //used to mark bonuses for attack/defense primary skills from spells like Precision (distance only) }; -enum class BonusValueType +enum class BonusValueType : uint8_t { #define BONUS_VALUE(x) x, BONUS_VALUE_LIST diff --git a/lib/bonuses/BonusList.cpp b/lib/bonuses/BonusList.cpp index c3e645ac3..c2d389413 100644 --- a/lib/bonuses/BonusList.cpp +++ b/lib/bonuses/BonusList.cpp @@ -99,13 +99,13 @@ int BonusList::totalValue() const int indepMax = std::numeric_limits::min(); }; - auto percent = [](int base, int percent) -> int { + auto applyPercentage = [](int base, int percent) -> int { return (static_cast(base) * (100 + percent)) / 100; }; BonusCollection accumulated; - bool hasIndepMax = false; - bool hasIndepMin = false; + int indexMaxCount = 0; + int indexMinCount = 0; std::array percentToSource = {}; @@ -125,7 +125,7 @@ int BonusList::totalValue() const for(const auto & b : bonuses) { int sourceIndex = vstd::to_underlying(b->source); - int valModified = percent(b->val, percentToSource[sourceIndex]); + int valModified = applyPercentage(b->val, percentToSource[sourceIndex]); switch(b->valType) { @@ -141,33 +141,36 @@ int BonusList::totalValue() const case BonusValueType::ADDITIVE_VALUE: accumulated.additive += valModified; break; - case BonusValueType::INDEPENDENT_MAX: - hasIndepMax = true; + case BonusValueType::INDEPENDENT_MAX: // actual meaning: at least this value + indexMaxCount++; vstd::amax(accumulated.indepMax, valModified); break; - case BonusValueType::INDEPENDENT_MIN: - hasIndepMin = true; + case BonusValueType::INDEPENDENT_MIN: // actual meaning: at most this value + indexMinCount++; vstd::amin(accumulated.indepMin, valModified); break; } } - accumulated.base = percent(accumulated.base, accumulated.percentToBase); + accumulated.base = applyPercentage(accumulated.base, accumulated.percentToBase); accumulated.base += accumulated.additive; - auto valFirst = percent(accumulated.base ,accumulated.percentToAll); + auto valFirst = applyPercentage(accumulated.base ,accumulated.percentToAll); - if(hasIndepMin && hasIndepMax && accumulated.indepMin < accumulated.indepMax) + if(indexMinCount && indexMaxCount && accumulated.indepMin < accumulated.indepMax) accumulated.indepMax = accumulated.indepMin; - const int notIndepBonuses = static_cast(std::count_if(bonuses.cbegin(), bonuses.cend(), [](const std::shared_ptr& b) - { - return b->valType != BonusValueType::INDEPENDENT_MAX && b->valType != BonusValueType::INDEPENDENT_MIN; - })); + const int notIndepBonuses = bonuses.size() - indexMaxCount - indexMinCount; if(notIndepBonuses) return std::clamp(valFirst, accumulated.indepMax, accumulated.indepMin); - return hasIndepMin ? accumulated.indepMin : hasIndepMax ? accumulated.indepMax : 0; + if (indexMinCount) + return accumulated.indepMin; + + if (indexMaxCount) + return accumulated.indepMax; + + return 0; } std::shared_ptr BonusList::getFirst(const CSelector &select) diff --git a/lib/bonuses/CBonusSystemNode.cpp b/lib/bonuses/CBonusSystemNode.cpp index 2f6d4db0b..b5a2d582d 100644 --- a/lib/bonuses/CBonusSystemNode.cpp +++ b/lib/bonuses/CBonusSystemNode.cpp @@ -107,10 +107,9 @@ void CBonusSystemNode::getAllBonusesRec(BonusList &out, const CSelector & select } } -TConstBonusListPtr CBonusSystemNode::getAllBonuses(const CSelector &selector, const CSelector &limit, const CBonusSystemNode *root, const std::string &cachingStr) const +TConstBonusListPtr CBonusSystemNode::getAllBonuses(const CSelector &selector, const CSelector &limit, const std::string &cachingStr) const { - bool limitOnUs = (!root || root == this); //caching won't work when we want to limit bonuses against an external node - if (CBonusSystemNode::cachingEnabled && limitOnUs) + if (CBonusSystemNode::cachingEnabled) { // Exclusive access for one thread boost::lock_guard lock(sync); @@ -157,11 +156,11 @@ TConstBonusListPtr CBonusSystemNode::getAllBonuses(const CSelector &selector, co } else { - return getAllBonusesWithoutCaching(selector, limit, root); + return getAllBonusesWithoutCaching(selector, limit); } } -TConstBonusListPtr CBonusSystemNode::getAllBonusesWithoutCaching(const CSelector &selector, const CSelector &limit, const CBonusSystemNode *root) const +TConstBonusListPtr CBonusSystemNode::getAllBonusesWithoutCaching(const CSelector &selector, const CSelector &limit) const { auto ret = std::make_shared(); @@ -169,29 +168,7 @@ TConstBonusListPtr CBonusSystemNode::getAllBonusesWithoutCaching(const CSelector BonusList beforeLimiting; BonusList afterLimiting; getAllBonusesRec(beforeLimiting, selector); - - if(!root || root == this) - { - limitBonuses(beforeLimiting, afterLimiting); - } - else if(root) - { - //We want to limit our query against an external node. We get all its bonuses, - // add the ones we're considering and see if they're cut out by limiters - BonusList rootBonuses; - BonusList limitedRootBonuses; - getAllBonusesRec(rootBonuses, selector); - - for(const auto & b : beforeLimiting) - rootBonuses.push_back(b); - - root->limitBonuses(rootBonuses, limitedRootBonuses); - - for(const auto & b : beforeLimiting) - if(vstd::contains(limitedRootBonuses, b)) - afterLimiting.push_back(b); - - } + limitBonuses(beforeLimiting, afterLimiting); afterLimiting.getBonuses(*ret, selector, limit); ret->stackBonuses(); return ret; @@ -401,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; @@ -415,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) { @@ -648,13 +625,6 @@ void CBonusSystemNode::limitBonuses(const BonusList &allBonuses, BonusList &out) } } -TBonusListPtr CBonusSystemNode::limitBonuses(const BonusList &allBonuses) const -{ - auto ret = std::make_shared(); - limitBonuses(allBonuses, *ret); - return ret; -} - void CBonusSystemNode::treeHasChanged() { treeChanged++; diff --git a/lib/bonuses/CBonusSystemNode.h b/lib/bonuses/CBonusSystemNode.h index 9938a1978..460957f8b 100644 --- a/lib/bonuses/CBonusSystemNode.h +++ b/lib/bonuses/CBonusSystemNode.h @@ -9,11 +9,11 @@ */ #pragma once -#include "GameConstants.h" - #include "BonusList.h" #include "IBonusBearer.h" +#include "../serializer/Serializeable.h" + VCMI_LIB_NAMESPACE_BEGIN using TNodes = std::set; @@ -21,7 +21,7 @@ using TCNodes = std::set; using TNodesVector = std::vector; using TCNodesVector = std::vector; -class DLL_LINKAGE CBonusSystemNode : public virtual IBonusBearer, public boost::noncopyable +class DLL_LINKAGE CBonusSystemNode : public virtual IBonusBearer, public virtual Serializeable, public boost::noncopyable { public: enum ENodeTypes @@ -47,14 +47,15 @@ private: static std::atomic treeChanged; // Setting a value to cachingStr before getting any bonuses caches the result for later requests. - // This string needs to be unique, that's why it has to be setted in the following manner: + // This string needs to be unique, that's why it has to be set in the following manner: // [property key]_[value] => only for selector mutable std::map cachedRequests; mutable boost::mutex sync; void getAllBonusesRec(BonusList &out, const CSelector & selector) const; - TConstBonusListPtr getAllBonusesWithoutCaching(const CSelector &selector, const CSelector &limit, const CBonusSystemNode *root = nullptr) const; + TConstBonusListPtr getAllBonusesWithoutCaching(const CSelector &selector, const CSelector &limit) const; std::shared_ptr getUpdatedBonus(const std::shared_ptr & b, const TUpdaterPtr & updater) const; + void limitBonuses(const BonusList &allBonuses, BonusList &out) const; //out will bo populed with bonuses that are not limited here void getRedParents(TCNodes &out) const; //retrieves list of red parent nodes (nodes bonuses propagate from) void getRedAncestors(TCNodes &out) const; @@ -84,9 +85,7 @@ public: explicit CBonusSystemNode(ENodeTypes NodeType); virtual ~CBonusSystemNode(); - void limitBonuses(const BonusList &allBonuses, BonusList &out) const; //out will bo populed with bonuses that are not limited here - TBonusListPtr limitBonuses(const BonusList &allBonuses) const; //same as above, returns out by val for convienence - TConstBonusListPtr getAllBonuses(const CSelector &selector, const CSelector &limit, const CBonusSystemNode *root = nullptr, const std::string &cachingStr = "") const override; + TConstBonusListPtr getAllBonuses(const CSelector &selector, const CSelector &limit, const std::string &cachingStr = "") const override; void getParents(TCNodes &out) const; //retrieves list of parent nodes (nodes to inherit bonuses from), /// Returns first bonus matching selector diff --git a/lib/bonuses/IBonusBearer.cpp b/lib/bonuses/IBonusBearer.cpp index a0bfc7087..924c8f110 100644 --- a/lib/bonuses/IBonusBearer.cpp +++ b/lib/bonuses/IBonusBearer.cpp @@ -17,7 +17,7 @@ VCMI_LIB_NAMESPACE_BEGIN int IBonusBearer::valOfBonuses(const CSelector &selector, const std::string &cachingStr) const { - TConstBonusListPtr hlp = getAllBonuses(selector, nullptr, nullptr, cachingStr); + TConstBonusListPtr hlp = getAllBonuses(selector, nullptr, cachingStr); return hlp->totalValue(); } @@ -34,12 +34,12 @@ bool IBonusBearer::hasBonus(const CSelector &selector, const CSelector &limit, c TConstBonusListPtr IBonusBearer::getBonuses(const CSelector &selector, const std::string &cachingStr) const { - return getAllBonuses(selector, nullptr, nullptr, cachingStr); + return getAllBonuses(selector, nullptr, cachingStr); } TConstBonusListPtr IBonusBearer::getBonuses(const CSelector &selector, const CSelector &limit, const std::string &cachingStr) const { - return getAllBonuses(selector, limit, nullptr, cachingStr); + return getAllBonuses(selector, limit, cachingStr); } int IBonusBearer::valOfBonuses(BonusType type) const @@ -84,11 +84,9 @@ bool IBonusBearer::hasBonusOfType(BonusType type, BonusSubtypeID subtype) const bool IBonusBearer::hasBonusFrom(BonusSource source, BonusSourceID sourceID) const { - boost::format fmt("source_%did_%s"); - fmt % static_cast(source) % sourceID.toString(); - - return hasBonus(Selector::source(source,sourceID), fmt.str()); + return hasBonus(Selector::source(source,sourceID)); } + std::shared_ptr IBonusBearer::getBonus(const CSelector &selector) const { auto bonuses = getAllBonuses(selector, Selector::all); diff --git a/lib/bonuses/IBonusBearer.h b/lib/bonuses/IBonusBearer.h index 559752a69..b272edea4 100644 --- a/lib/bonuses/IBonusBearer.h +++ b/lib/bonuses/IBonusBearer.h @@ -17,12 +17,10 @@ class DLL_LINKAGE IBonusBearer { public: //new bonusing node interface - // * selector is predicate that tests if HeroBonus matches our criteria - // * root is node on which call was made (nullptr will be replaced with this) - //interface + // * selector is predicate that tests if Bonus matches our criteria IBonusBearer() = default; virtual ~IBonusBearer() = default; - virtual TConstBonusListPtr getAllBonuses(const CSelector &selector, const CSelector &limit, const CBonusSystemNode *root = nullptr, const std::string &cachingStr = "") const = 0; + virtual TConstBonusListPtr getAllBonuses(const CSelector &selector, const CSelector &limit, const std::string &cachingStr = "") const = 0; int valOfBonuses(const CSelector &selector, const std::string &cachingStr = "") const; bool hasBonus(const CSelector &selector, const std::string &cachingStr = "") const; bool hasBonus(const CSelector &selector, const CSelector &limit, const std::string &cachingStr = "") const; diff --git a/lib/bonuses/Limiters.cpp b/lib/bonuses/Limiters.cpp index e17bf3965..e5aa2fae3 100644 --- a/lib/bonuses/Limiters.cpp +++ b/lib/bonuses/Limiters.cpp @@ -12,12 +12,12 @@ #include "Limiters.h" #include "../VCMI_Lib.h" +#include "../entities/faction/CFaction.h" +#include "../entities/faction/CTownHandler.h" #include "../spells/CSpellHandler.h" #include "../CCreatureHandler.h" #include "../CCreatureSet.h" -#include "../CHeroHandler.h" -#include "../CTownHandler.h" -#include "../CGeneralTextHandler.h" +#include "../texts/CGeneralTextHandler.h" #include "../CSkillHandler.h" #include "../CStack.h" #include "../CArtHandler.h" @@ -75,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; } } @@ -103,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(); } @@ -130,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; @@ -299,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 b4a74c4fc..9ad0e56d2 100644 --- a/lib/bonuses/Limiters.h +++ b/lib/bonuses/Limiters.h @@ -10,8 +10,9 @@ #include "Bonus.h" -#include "../GameConstants.h" #include "../battle/BattleHex.h" +#include "../serializer/Serializeable.h" +#include "../constants/Enumerations.h" VCMI_LIB_NAMESPACE_BEGIN @@ -27,7 +28,7 @@ struct BonusLimitationContext const BonusList & stillUndecided; }; -class DLL_LINKAGE ILimiter +class DLL_LINKAGE ILimiter : public Serializeable { public: enum class EDecision : uint8_t {ACCEPT, DISCARD, NOT_SURE}; @@ -93,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; @@ -107,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/bonuses/Propagators.h b/lib/bonuses/Propagators.h index e9affb097..f0519d87d 100644 --- a/lib/bonuses/Propagators.h +++ b/lib/bonuses/Propagators.h @@ -12,11 +12,13 @@ #include "Bonus.h" #include "CBonusSystemNode.h" +#include "../serializer/Serializeable.h" + VCMI_LIB_NAMESPACE_BEGIN extern DLL_LINKAGE const std::map bonusPropagatorMap; -class DLL_LINKAGE IPropagator +class DLL_LINKAGE IPropagator : public Serializeable { public: virtual ~IPropagator() = default; @@ -42,4 +44,4 @@ public: } }; -VCMI_LIB_NAMESPACE_END \ No newline at end of file +VCMI_LIB_NAMESPACE_END diff --git a/lib/bonuses/Updaters.cpp b/lib/bonuses/Updaters.cpp index 9a40a2976..064818a58 100644 --- a/lib/bonuses/Updaters.cpp +++ b/lib/bonuses/Updaters.cpp @@ -145,7 +145,7 @@ JsonNode ArmyMovementUpdater::toJsonNode() const } std::shared_ptr TimesStackLevelUpdater::createUpdatedBonus(const std::shared_ptr & b, const CBonusSystemNode & context) const { - if(context.getNodeType() == CBonusSystemNode::STACK_INSTANCE) + if(context.getNodeType() == CBonusSystemNode::STACK_INSTANCE || context.getNodeType() == CBonusSystemNode::COMMANDER) { int level = dynamic_cast(context).getLevel(); auto newBonus = std::make_shared(*b); @@ -155,8 +155,7 @@ std::shared_ptr TimesStackLevelUpdater::createUpdatedBonus(const std::sha else if(context.getNodeType() == CBonusSystemNode::STACK_BATTLE) { const auto & stack = dynamic_cast(context); - //only update if stack doesn't have an instance (summons, war machines) - //otherwise we'd end up multiplying twice + //update if stack doesn't have an instance (summons, war machines) if(stack.base == nullptr) { int level = stack.unitType()->getLevel(); @@ -164,6 +163,14 @@ std::shared_ptr TimesStackLevelUpdater::createUpdatedBonus(const std::sha newBonus->val *= level; return newBonus; } + // If these are not handled here, the final outcome may potentially be incorrect. + else + { + int level = dynamic_cast(stack.base)->getLevel(); + auto newBonus = std::make_shared(*b); + newBonus->val *= level; + return newBonus; + } } return b; } diff --git a/lib/bonuses/Updaters.h b/lib/bonuses/Updaters.h index 551304f4c..c559433b2 100644 --- a/lib/bonuses/Updaters.h +++ b/lib/bonuses/Updaters.h @@ -10,12 +10,13 @@ #pragma once #include "Bonus.h" +#include "../serializer/Serializeable.h" VCMI_LIB_NAMESPACE_BEGIN // observers for updating bonuses based on certain events (e.g. hero gaining level) -class DLL_LINKAGE IUpdater +class DLL_LINKAGE IUpdater : public Serializeable { public: virtual ~IUpdater() = default; diff --git a/lib/campaign/CampaignConstants.h b/lib/campaign/CampaignConstants.h index e4a615d47..c380b77f0 100644 --- a/lib/campaign/CampaignConstants.h +++ b/lib/campaign/CampaignConstants.h @@ -18,7 +18,7 @@ enum class CampaignVersion : uint8_t AB = 5, SoD = 6, WoG = 6, - // Chr = 7, // Heroes Chronicles, likely identical to SoD, untested + Chr = 7, VCMI = 1, VCMI_MIN = 1, diff --git a/lib/campaign/CampaignHandler.cpp b/lib/campaign/CampaignHandler.cpp index 5aed7fe91..7d94bb1b8 100644 --- a/lib/campaign/CampaignHandler.cpp +++ b/lib/campaign/CampaignHandler.cpp @@ -16,16 +16,16 @@ #include "../filesystem/CCompressedStream.h" #include "../filesystem/CMemoryStream.h" #include "../filesystem/CBinaryReader.h" -#include "../modding/IdentifierStorage.h" +#include "../filesystem/CZipLoader.h" #include "../VCMI_Lib.h" -#include "../CGeneralTextHandler.h" -#include "../TextOperations.h" -#include "../Languages.h" #include "../constants/StringConstants.h" #include "../mapping/CMapHeader.h" #include "../mapping/CMapService.h" #include "../modding/CModHandler.h" +#include "../modding/IdentifierStorage.h" #include "../modding/ModScope.h" +#include "../texts/CGeneralTextHandler.h" +#include "../texts/TextOperations.h" VCMI_LIB_NAMESPACE_BEGIN @@ -37,16 +37,18 @@ void CampaignHandler::readCampaign(Campaign * ret, const std::vector & inpu CBinaryReader reader(&stream); readHeaderFromMemory(*ret, reader, filename, modName, encoding); + ret->overrideCampaign(); for(int g = 0; g < ret->numberOfScenarios; ++g) { auto scenarioID = static_cast(ret->scenarios.size()); ret->scenarios[scenarioID] = readScenarioFromMemory(reader, *ret); } + ret->overrideCampaignScenarios(); } else // text format (json) { - JsonNode jsonCampaign(reinterpret_cast(input.data()), input.size()); + JsonNode jsonCampaign(reinterpret_cast(input.data()), input.size(), filename); readHeaderFromJson(*ret, jsonCampaign, filename, modName, encoding); for(auto & scenario : jsonCampaign["scenarios"].Vector()) @@ -61,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); @@ -77,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(); @@ -126,14 +126,19 @@ static std::string convertMapName(std::string input) std::string CampaignHandler::readLocalizedString(CampaignHeader & target, CBinaryReader & reader, std::string filename, std::string modName, std::string encoding, std::string identifier) { - TextIdentifier stringID( "campaign", convertMapName(filename), identifier); - std::string input = TextOperations::toUnicode(reader.readBaseString(), encoding); - if (input.empty()) + return readLocalizedString(target, input, filename, modName, identifier); +} + +std::string CampaignHandler::readLocalizedString(CampaignHeader & target, std::string text, std::string filename, std::string modName, std::string identifier) +{ + TextIdentifier stringID( "campaign", convertMapName(filename), identifier); + + if (text.empty()) return ""; - target.getTexts().registerString(modName, stringID, input); + target.getTexts().registerString(modName, stringID, text); return stringID.get(); } @@ -149,13 +154,21 @@ void CampaignHandler::readHeaderFromJson(CampaignHeader & ret, JsonNode & reader ret.version = CampaignVersion::VCMI; ret.campaignRegions = CampaignRegions::fromJson(reader["regions"]); ret.numberOfScenarios = reader["scenarios"].Vector().size(); - ret.name.appendTextID(reader["name"].String()); - ret.description.appendTextID(reader["description"].String()); - ret.difficultyChoosenByPlayer = reader["allowDifficultySelection"].Bool(); + ret.name.appendTextID(readLocalizedString(ret, reader["name"].String(), filename, modName, "name")); + ret.description.appendTextID(readLocalizedString(ret, reader["description"].String(), filename, modName, "description")); + ret.author.appendRawString(reader["author"].String()); + ret.authorContact.appendRawString(reader["authorContact"].String()); + ret.campaignVersion.appendRawString(reader["campaignVersion"].String()); + ret.creationDateTime = reader["creationDateTime"].Integer(); + ret.difficultyChosenByPlayer = reader["allowDifficultySelection"].Bool(); ret.music = AudioPath::fromJson(reader["music"]); ret.filename = filename; ret.modName = modName; ret.encoding = encoding; + ret.loadingBackground = ImagePath::fromJson(reader["loadingBackground"]); + ret.videoRim = ImagePath::fromJson(reader["videoRim"]); + ret.introVideo = VideoPath::fromJson(reader["introVideo"]); + ret.outroVideo = VideoPath::fromJson(reader["outroVideo"]); } CampaignScenario CampaignHandler::readScenarioFromJson(JsonNode & reader) @@ -382,13 +395,18 @@ void CampaignHandler::readHeaderFromMemory( CampaignHeader & ret, CBinaryReader { ret.version = static_cast(reader.readUInt32()); ui8 campId = reader.readUInt8() - 1;//change range of it from [1, 20] to [0, 19] - ret.loadLegacyData(campId); + if(ret.version != CampaignVersion::Chr) // For chronicles: Will be overridden later; Chronicles uses own logic (reusing OH3 ID's) + ret.loadLegacyData(campId); ret.name.appendTextID(readLocalizedString(ret, reader, filename, modName, encoding, "name")); ret.description.appendTextID(readLocalizedString(ret, reader, filename, modName, encoding, "description")); + ret.author.appendRawString(""); + ret.authorContact.appendRawString(""); + ret.campaignVersion.appendRawString(""); + ret.creationDateTime = 0; if (ret.version > CampaignVersion::RoE) - ret.difficultyChoosenByPlayer = reader.readInt8(); + ret.difficultyChosenByPlayer = reader.readInt8(); else - ret.difficultyChoosenByPlayer = false; + ret.difficultyChosenByPlayer = false; ret.music = prologMusicName(reader.readInt8()); ret.filename = filename; @@ -580,32 +598,69 @@ CampaignTravel CampaignHandler::readScenarioTravelFromMemory(CBinaryReader & rea std::vector< std::vector > CampaignHandler::getFile(std::unique_ptr file, const std::string & filename, bool headerOnly) { - CCompressedStream stream(std::move(file), true); + std::array magic; + file->read(magic.data(), magic.size()); + file->seek(0); std::vector< std::vector > ret; - try + static const std::array zipHeaderMagic{0x50, 0x4B}; + if (magic == zipHeaderMagic) // ZIP archive - assume VCMP format { - do - { - std::vector block(stream.getSize()); - stream.read(block.data(), block.size()); - ret.push_back(block); - ret.back().shrink_to_fit(); - } - while (!headerOnly && stream.getNextBlock()); - } - catch (const DecompressionException & e) - { - // Some campaigns in French version from gog.com have trailing garbage bytes - // For example, slayer.h3c consist from 5 parts: header + 4 maps - // However file also contains ~100 "extra" bytes after those 5 parts are decompressed that do not represent gzip stream - // leading to exception "Incorrect header check" - // Since H3 handles these files correctly, simply log this as warning and proceed - logGlobal->warn("Failed to read file %s. Encountered error during decompression: %s", filename, e.what()); - } + CInputStream * buffer(file.get()); + auto ioApi = std::make_shared(buffer); + CZipLoader loader("", "_", ioApi); - return ret; + // load header + JsonPath jsonPath = JsonPath::builtin(VCMP_HEADER_FILE_NAME); + if(!loader.existsResource(jsonPath)) + throw std::runtime_error(jsonPath.getName() + " not found in " + filename); + auto data = loader.load(jsonPath)->readAll(); + ret.emplace_back(data.first.get(), data.first.get() + data.second); + + if(headerOnly) + return ret; + + // load scenarios + JsonNode header(reinterpret_cast(data.first.get()), data.second, VCMP_HEADER_FILE_NAME); + for(auto scenario : header["scenarios"].Vector()) + { + ResourcePath mapPath(scenario["map"].String(), EResType::MAP); + if(!loader.existsResource(mapPath)) + throw std::runtime_error(mapPath.getName() + " not found in " + filename); + auto data = loader.load(mapPath)->readAll(); + ret.emplace_back(data.first.get(), data.first.get() + data.second); + } + + return ret; + } + else // H3C + { + CCompressedStream stream(std::move(file), true); + + try + { + do + { + std::vector block(stream.getSize()); + stream.read(block.data(), block.size()); + ret.push_back(block); + ret.back().shrink_to_fit(); + } + while (!headerOnly && stream.getNextBlock()); + } + catch (const DecompressionException & e) + { + // Some campaigns in French version from gog.com have trailing garbage bytes + // For example, slayer.h3c consist from 5 parts: header + 4 maps + // However file also contains ~100 "extra" bytes after those 5 parts are decompressed that do not represent gzip stream + // leading to exception "Incorrect header check" + // Since H3 handles these files correctly, simply log this as warning and proceed + logGlobal->warn("Failed to read file %s. Encountered error during decompression: %s", filename, e.what()); + } + + return ret; + } } VideoPath CampaignHandler::prologVideoName(ui8 index) diff --git a/lib/campaign/CampaignHandler.h b/lib/campaign/CampaignHandler.h index cd7332627..cda84fb05 100644 --- a/lib/campaign/CampaignHandler.h +++ b/lib/campaign/CampaignHandler.h @@ -17,6 +17,7 @@ VCMI_LIB_NAMESPACE_BEGIN class DLL_LINKAGE CampaignHandler { static std::string readLocalizedString(CampaignHeader & target, CBinaryReader & reader, std::string filename, std::string modName, std::string encoding, std::string identifier); + static std::string readLocalizedString(CampaignHeader & target, std::string text, std::string filename, std::string modName, std::string identifier); static void readCampaign(Campaign * target, const std::vector & stream, std::string filename, std::string modName, std::string encoding); @@ -37,6 +38,7 @@ class DLL_LINKAGE CampaignHandler static AudioPath prologMusicName(ui8 index); static AudioPath prologVoiceName(ui8 index); + static constexpr auto VCMP_HEADER_FILE_NAME = "header.json"; public: static std::unique_ptr getHeader( const std::string & name); //name - name of appropriate file diff --git a/lib/campaign/CampaignScenarioPrologEpilog.h b/lib/campaign/CampaignScenarioPrologEpilog.h index 8bf9a1aa5..17dc1c506 100644 --- a/lib/campaign/CampaignScenarioPrologEpilog.h +++ b/lib/campaign/CampaignScenarioPrologEpilog.h @@ -10,7 +10,7 @@ #pragma once #include "../filesystem/ResourcePath.h" -#include "../MetaString.h" +#include "../texts/MetaString.h" VCMI_LIB_NAMESPACE_BEGIN diff --git a/lib/campaign/CampaignState.cpp b/lib/campaign/CampaignState.cpp index 335722d24..5dc6a2e10 100644 --- a/lib/campaign/CampaignState.cpp +++ b/lib/campaign/CampaignState.cpp @@ -13,13 +13,14 @@ #include "../Point.h" #include "../filesystem/ResourcePath.h" #include "../VCMI_Lib.h" -#include "../CGeneralTextHandler.h" +#include "../texts/CGeneralTextHandler.h" #include "../mapping/CMapService.h" #include "../mapping/CMapInfo.h" #include "../mapping/CMap.h" #include "../mapObjects/CGHeroInstance.h" #include "../serializer/JsonDeserializer.h" #include "../serializer/JsonSerializer.h" +#include "../json/JsonUtils.h" VCMI_LIB_NAMESPACE_BEGIN @@ -36,8 +37,11 @@ CampaignRegions::RegionDescription CampaignRegions::RegionDescription::fromJson( { CampaignRegions::RegionDescription rd; rd.infix = node["infix"].String(); - rd.xpos = static_cast(node["x"].Float()); - rd.ypos = static_cast(node["y"].Float()); + rd.pos = Point(static_cast(node["x"].Float()), static_cast(node["y"].Float())); + if(!node["labelPos"].isNull()) + rd.labelPos = Point(static_cast(node["labelPos"]["x"].Float()), static_cast(node["labelPos"]["y"].Float())); + else + rd.labelPos = std::nullopt; return rd; } @@ -45,7 +49,9 @@ CampaignRegions CampaignRegions::fromJson(const JsonNode & node) { CampaignRegions cr; cr.campPrefix = node["prefix"].String(); - cr.colorSuffixLength = static_cast(node["color_suffix_length"].Float()); + cr.colorSuffixLength = static_cast(node["colorSuffixLength"].Float()); + cr.campSuffix = node["suffix"].isNull() ? std::vector() : std::vector{node["suffix"].Vector()[0].String(), node["suffix"].Vector()[1].String(), node["suffix"].Vector()[2].String()}; + cr.campBackground = node["background"].isNull() ? "" : node["background"].String(); for(const JsonNode & desc : node["desc"].Vector()) cr.regions.push_back(CampaignRegions::RegionDescription::fromJson(desc)); @@ -68,43 +74,61 @@ CampaignRegions CampaignRegions::getLegacy(int campId) ImagePath CampaignRegions::getBackgroundName() const { - return ImagePath::builtin(campPrefix + "_BG.BMP"); + if(campBackground.empty()) + return ImagePath::builtin(campPrefix + "_BG.BMP"); + else + return ImagePath::builtin(campBackground); } Point CampaignRegions::getPosition(CampaignScenarioID which) const { auto const & region = regions[which.getNum()]; - return Point(region.xpos, region.ypos); + return region.pos; +} + +std::optional CampaignRegions::getLabelPosition(CampaignScenarioID which) const +{ + auto const & region = regions[which.getNum()]; + return region.labelPos; } ImagePath CampaignRegions::getNameFor(CampaignScenarioID which, int colorIndex, std::string type) const { auto const & region = regions[which.getNum()]; - static const std::string colors[2][8] = - { - {"R", "B", "N", "G", "O", "V", "T", "P"}, - {"Re", "Bl", "Br", "Gr", "Or", "Vi", "Te", "Pi"} - }; + static const std::array, 3> colors = {{ + { "", "", "", "", "", "", "", "" }, + { "R", "B", "N", "G", "O", "V", "T", "P" }, + { "Re", "Bl", "Br", "Gr", "Or", "Vi", "Te", "Pi" } + }}; - std::string color = colors[colorSuffixLength - 1][colorIndex]; + std::string color = colors[colorSuffixLength][colorIndex]; return ImagePath::builtin(campPrefix + region.infix + "_" + type + color + ".BMP"); } ImagePath CampaignRegions::getAvailableName(CampaignScenarioID which, int color) const { - return getNameFor(which, color, "En"); + if(campSuffix.empty()) + return getNameFor(which, color, "En"); + else + return getNameFor(which, color, campSuffix[0]); } ImagePath CampaignRegions::getSelectedName(CampaignScenarioID which, int color) const { - return getNameFor(which, color, "Se"); + if(campSuffix.empty()) + return getNameFor(which, color, "Se"); + else + return getNameFor(which, color, campSuffix[1]); } ImagePath CampaignRegions::getConqueredName(CampaignScenarioID which, int color) const { - return getNameFor(which, color, "Co"); + if(campSuffix.empty()) + return getNameFor(which, color, "Co"); + else + return getNameFor(which, color, campSuffix[2]); } @@ -124,9 +148,15 @@ void CampaignHeader::loadLegacyData(ui8 campId) numberOfScenarios = VLC->generaltexth->getCampaignLength(campId); } +void CampaignHeader::loadLegacyData(CampaignRegions regions, int numOfScenario) +{ + campaignRegions = regions; + numberOfScenarios = numOfScenario; +} + bool CampaignHeader::playerSelectedDifficulty() const { - return difficultyChoosenByPlayer; + return difficultyChosenByPlayer; } bool CampaignHeader::formatVCMI() const @@ -144,6 +174,26 @@ std::string CampaignHeader::getNameTranslated() const return name.toString(); } +std::string CampaignHeader::getAuthor() const +{ + return authorContact.toString(); +} + +std::string CampaignHeader::getAuthorContact() const +{ + return authorContact.toString(); +} + +std::string CampaignHeader::getCampaignVersion() const +{ + return campaignVersion.toString(); +} + +time_t CampaignHeader::getCreationDateTime() const +{ + return creationDateTime; +} + std::string CampaignHeader::getFilename() const { return filename; @@ -164,6 +214,26 @@ AudioPath CampaignHeader::getMusic() const return music; } +ImagePath CampaignHeader::getLoadingBackground() const +{ + return loadingBackground; +} + +ImagePath CampaignHeader::getVideoRim() const +{ + return videoRim; +} + +VideoPath CampaignHeader::getIntroVideo() const +{ + return introVideo; +} + +VideoPath CampaignHeader::getOutroVideo() const +{ + return outroVideo; +} + const CampaignRegions & CampaignHeader::getRegions() const { return campaignRegions; @@ -269,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()); @@ -281,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); } } @@ -373,7 +443,10 @@ CGHeroInstance * CampaignState::crossoverDeserialize(const JsonNode & node, CMap hero->ID = Obj::HERO; hero->serializeJsonOptions(handler); if (map) - hero->serializeJsonArtifacts(handler, "artifacts", map); + { + hero->serializeJsonArtifacts(handler, "artifacts"); + map->addNewArtifactInstance(*hero); + } return hero; } @@ -421,6 +494,47 @@ std::set Campaign::allScenarios() const return result; } +void Campaign::overrideCampaign() +{ + const JsonNode node = JsonUtils::assembleFromFiles("config/campaignOverrides.json"); + for (auto & entry : node.Struct()) + if(filename == entry.first) + { + if(!entry.second["regions"].isNull() && !entry.second["scenarioCount"].isNull()) + loadLegacyData(CampaignRegions::fromJson(entry.second["regions"]), entry.second["scenarioCount"].Integer()); + if(!entry.second["loadingBackground"].isNull()) + loadingBackground = ImagePath::builtin(entry.second["loadingBackground"].String()); + if(!entry.second["videoRim"].isNull()) + videoRim = ImagePath::builtin(entry.second["videoRim"].String()); + if(!entry.second["introVideo"].isNull()) + introVideo = VideoPath::builtin(entry.second["introVideo"].String()); + if(!entry.second["outroVideo"].isNull()) + outroVideo = VideoPath::builtin(entry.second["outroVideo"].String()); + } +} + +void Campaign::overrideCampaignScenarios() +{ + const JsonNode node = JsonUtils::assembleFromFiles("config/campaignOverrides.json"); + for (auto & entry : node.Struct()) + if(filename == entry.first) + { + if(!entry.second["scenarios"].isNull()) + { + auto sc = entry.second["scenarios"].Vector(); + for(int i = 0; i < sc.size(); i++) + { + auto it = scenarios.begin(); + std::advance(it, i); + if(!sc.at(i)["voiceProlog"].isNull()) + it->second.prolog.prologVoice = AudioPath::builtin(sc.at(i)["voiceProlog"].String()); + if(!sc.at(i)["voiceEpilog"].isNull()) + it->second.epilog.prologVoice = AudioPath::builtin(sc.at(i)["voiceEpilog"].String()); + } + } + } +} + int Campaign::scenariosCount() const { return allScenarios().size(); diff --git a/lib/campaign/CampaignState.h b/lib/campaign/CampaignState.h index ddc1b4451..c4103c128 100644 --- a/lib/campaign/CampaignState.h +++ b/lib/campaign/CampaignState.h @@ -10,11 +10,13 @@ #pragma once #include "../GameConstants.h" -#include "../MetaString.h" #include "../filesystem/ResourcePath.h" -#include "../CGeneralTextHandler.h" +#include "../serializer/Serializeable.h" +#include "../texts/TextLocalizationContainer.h" #include "CampaignConstants.h" #include "CampaignScenarioPrologEpilog.h" +#include "../gameState/HighScore.h" +#include "../Point.h" VCMI_LIB_NAMESPACE_BEGIN @@ -26,25 +28,34 @@ class CMap; class CMapHeader; class CMapInfo; class JsonNode; -class Point; class IGameCallback; class DLL_LINKAGE CampaignRegions { std::string campPrefix; + std::vector campSuffix; + std::string campBackground; int colorSuffixLength; struct DLL_LINKAGE RegionDescription { std::string infix; - int xpos; - int ypos; + Point pos; + std::optional labelPos; template void serialize(Handler &h) { h & infix; - h & xpos; - h & ypos; + if (h.version >= Handler::Version::REGION_LABEL) + { + h & pos; + h & labelPos; + } + else + { + h & pos.x; + h & pos.y; + } } static CampaignRegions::RegionDescription fromJson(const JsonNode & node); @@ -57,6 +68,7 @@ class DLL_LINKAGE CampaignRegions public: ImagePath getBackgroundName() const; Point getPosition(CampaignScenarioID which) const; + std::optional getLabelPosition(CampaignScenarioID which) const; ImagePath getAvailableName(CampaignScenarioID which, int color) const; ImagePath getSelectedName(CampaignScenarioID which, int color) const; ImagePath getConqueredName(CampaignScenarioID which, int color) const; @@ -66,6 +78,11 @@ public: h & campPrefix; h & colorSuffixLength; h & regions; + if (h.version >= Handler::Version::CAMPAIGN_REGIONS) + { + h & campSuffix; + h & campBackground; + } } static CampaignRegions fromJson(const JsonNode & node); @@ -75,20 +92,30 @@ public: class DLL_LINKAGE CampaignHeader : public boost::noncopyable { friend class CampaignHandler; + friend class Campaign; CampaignVersion version = CampaignVersion::NONE; CampaignRegions campaignRegions; MetaString name; MetaString description; + MetaString author; + MetaString authorContact; + MetaString campaignVersion; + std::time_t creationDateTime; AudioPath music; std::string filename; std::string modName; std::string encoding; + ImagePath loadingBackground; + ImagePath videoRim; + VideoPath introVideo; + VideoPath outroVideo; int numberOfScenarios = 0; - bool difficultyChoosenByPlayer = false; + bool difficultyChosenByPlayer = false; void loadLegacyData(ui8 campId); + void loadLegacyData(CampaignRegions regions, int numOfScenario); TextContainerRegistrable textContainer; @@ -98,10 +125,18 @@ public: std::string getDescriptionTranslated() const; std::string getNameTranslated() const; + std::string getAuthor() const; + std::string getAuthorContact() const; + std::string getCampaignVersion() const; + time_t getCreationDateTime() const; std::string getFilename() const; std::string getModName() const; std::string getEncoding() const; AudioPath getMusic() const; + ImagePath getLoadingBackground() const; + ImagePath getVideoRim() const; + VideoPath getIntroVideo() const; + VideoPath getOutroVideo() const; const CampaignRegions & getRegions() const; TextContainerRegistrable & getTexts(); @@ -113,13 +148,27 @@ public: h & numberOfScenarios; h & name; h & description; - h & difficultyChoosenByPlayer; + if (h.version >= Handler::Version::MAP_FORMAT_ADDITIONAL_INFOS) + { + h & author; + h & authorContact; + h & campaignVersion; + h & creationDateTime; + } + h & difficultyChosenByPlayer; h & filename; h & modName; h & music; h & encoding; - if (h.version >= Handler::Version::RELEASE_143) - h & textContainer; + h & textContainer; + if (h.version >= Handler::Version::CHRONICLES_SUPPORT) + { + h & loadingBackground; + h & videoRim; + h & introVideo; + } + if (h.version >= Handler::Version::CAMPAIGN_OUTRO_SUPPORT) + h & outroVideo; } }; @@ -214,7 +263,7 @@ struct DLL_LINKAGE CampaignScenario }; /// Class that represents loaded campaign information -class DLL_LINKAGE Campaign : public CampaignHeader +class DLL_LINKAGE Campaign : public CampaignHeader, public Serializeable { friend class CampaignHandler; @@ -225,6 +274,9 @@ public: std::set allScenarios() const; int scenariosCount() const; + void overrideCampaign(); + void overrideCampaignScenarios(); + template void serialize(Handler &h) { h & static_cast(*this); @@ -307,6 +359,8 @@ public: std::string campaignSet; + std::vector highscoreParameters; + template void serialize(Handler &h) { h & static_cast(*this); @@ -317,8 +371,9 @@ public: h & currentMap; h & chosenCampaignBonuses; h & campaignSet; - if (h.version >= Handler::Version::CAMPAIGN_MAP_TRANSLATIONS) - h & mapTranslations; + h & mapTranslations; + if (h.version >= Handler::Version::HIGHSCORE_PARAMETERS) + h & highscoreParameters; } }; diff --git a/lib/constants/EntityIdentifiers.cpp b/lib/constants/EntityIdentifiers.cpp index 44ae4d0e3..2c3f183c5 100644 --- a/lib/constants/EntityIdentifiers.cpp +++ b/lib/constants/EntityIdentifiers.cpp @@ -29,20 +29,21 @@ #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 "CGeneralTextHandler.h" +#include "texts/CGeneralTextHandler.h" #include "TerrainHandler.h" //TODO: remove #include "RiverHandler.h" #include "RoadHandler.h" #include "BattleFieldHandler.h" #include "ObstacleHandler.h" -#include "CTownHandler.h" #include "mapObjectConstructors/CObjectClassesHandler.h" VCMI_LIB_NAMESPACE_BEGIN @@ -125,6 +126,25 @@ namespace GameConstants #endif } +BuildingTypeUniqueID::BuildingTypeUniqueID(FactionID factionID, BuildingID buildingID ): + BuildingTypeUniqueID(factionID.getNum() * 0x10000 + buildingID.getNum()) +{ + assert(factionID.getNum() >= 0); + assert(factionID.getNum() < 0x10000); + assert(buildingID.getNum() >= 0); + assert(buildingID.getNum() < 0x10000); +} + +BuildingID BuildingTypeUniqueID::getBuilding() const +{ + return BuildingID(getNum() % 0x10000); +} + +FactionID BuildingTypeUniqueID::getFaction() const +{ + return FactionID(getNum() / 0x10000); +} + int32_t IdentifierBase::resolveIdentifier(const std::string & entityType, const std::string identifier) { if (identifier.empty()) diff --git a/lib/constants/EntityIdentifiers.h b/lib/constants/EntityIdentifiers.h index 58356a826..3bc547247 100644 --- a/lib/constants/EntityIdentifiers.h +++ b/lib/constants/EntityIdentifiers.h @@ -280,6 +280,7 @@ public: enum Type { DEFAULT = -50, + HORDE_PLACEHOLDER8 = -37, HORDE_PLACEHOLDER7 = -36, HORDE_PLACEHOLDER6 = -35, HORDE_PLACEHOLDER5 = -34, @@ -297,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_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, @@ -313,6 +314,58 @@ public: }; +private: + static std::vector> getDwellings() + { + std::vector dwellings = { DWELL_LVL_1, DWELL_LVL_2, DWELL_LVL_3, DWELL_LVL_4, DWELL_LVL_5, DWELL_LVL_6, DWELL_LVL_7, DWELL_LVL_8 }; + std::vector dwellingsUp = { DWELL_LVL_1_UP, DWELL_LVL_2_UP, DWELL_LVL_3_UP, DWELL_LVL_4_UP, DWELL_LVL_5_UP, DWELL_LVL_6_UP, DWELL_LVL_7_UP, DWELL_LVL_8_UP }; + return {dwellings, dwellingsUp}; + } + +public: + static Type getDwellingFromLevel(int level, int upgradeIndex) + { + return getDwellings()[upgradeIndex][level]; + } + + static int getLevelFromDwelling(BuildingIDBase dwelling) + { + for(int i = 0; i < 2; i++) + { + auto tmp = getDwellings()[i]; + auto it = std::find(tmp.begin(), tmp.end(), dwelling); + if (it != tmp.end()) + return std::distance(tmp.begin(), it); + } + 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) + { + for(int i = 0; i < 2; i++) + { + auto tmp = getDwellings()[i]; + auto it = std::find(tmp.begin(), tmp.end(), dwelling); + if (it != tmp.end()) + return i; + } + 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 < BuildingIDBase::DWELL_LVL_8 + 5) + dwelling.advance(1); + else + dwelling.advance(GameConstants::CREATURES_PER_TOWN - 1); + } + bool IsSpecialOrGrail() const { return num == SPECIAL_1 || num == SPECIAL_2 || num == SPECIAL_3 || num == SPECIAL_4 || num == GRAIL; diff --git a/lib/constants/Enumerations.h b/lib/constants/Enumerations.h index db3b6320d..f9bcc5d2c 100644 --- a/lib/constants/Enumerations.h +++ b/lib/constants/Enumerations.h @@ -25,34 +25,13 @@ namespace BuildingSubID { DEFAULT = -50, NONE = -1, - STABLES, - BROTHERHOOD_OF_SWORD, CASTLE_GATE, - CREATURE_TRANSFORMER, MYSTIC_POND, - FOUNTAIN_OF_FORTUNE, - ARTIFACT_MERCHANT, - LOOKOUT_TOWER, LIBRARY, - MANA_VORTEX, PORTAL_OF_SUMMONING, ESCAPE_TUNNEL, - FREELANCERS_GUILD, - BALLISTA_YARD, - ATTACK_VISITING_BONUS, - MAGIC_UNIVERSITY, - SPELL_POWER_GARRISON_BONUS, - ATTACK_GARRISON_BONUS, - DEFENSE_GARRISON_BONUS, - DEFENSE_VISITING_BONUS, - SPELL_POWER_VISITING_BONUS, - KNOWLEDGE_VISITING_BONUS, - EXPERIENCE_VISITING_BONUS, - LIGHTHOUSE, TREASURY, - THIEVES_GUILD, - CUSTOM_VISITING_BONUS, - CUSTOM_VISITING_REWARD + BANK }; } @@ -263,4 +242,21 @@ enum class EMovementMode : int8_t TOWN_PORTAL, }; +enum class EMapLevel : int8_t +{ + ANY = -1, + SURFACE = 0, + UNDERGROUND = 1 +}; + +enum class EWeekType : int8_t +{ + FIRST_WEEK, + NORMAL, + DOUBLE_GROWTH, + BONUS_GROWTH, + DEITYOFFIRE, + PLAGUE +}; + VCMI_LIB_NAMESPACE_END diff --git a/lib/constants/NumericConstants.h b/lib/constants/NumericConstants.h index b0cda4270..f8833e640 100644 --- a/lib/constants/NumericConstants.h +++ b/lib/constants/NumericConstants.h @@ -22,7 +22,7 @@ namespace GameConstants constexpr int ALL_PLAYERS = 255; //bitfield - constexpr int CREATURES_PER_TOWN = 7; //without upgrades + constexpr int CREATURES_PER_TOWN = 8; //without upgrades constexpr int SPELL_LEVELS = 5; constexpr int SPELL_SCHOOL_LEVELS = 4; constexpr int DEFAULT_SCHOOLS = 4; @@ -54,6 +54,7 @@ namespace GameConstants constexpr int ALTAR_ARTIFACTS_SLOTS = 22; constexpr int TOURNAMENT_RULES_DD_MAP_TILES_THRESHOLD = 144*144*2; //map tiles count threshold for 2 dimension door casts with tournament rules constexpr int KINGDOM_WINDOW_HEROES_SLOTS = 4; + constexpr int INFO_WINDOW_ARTIFACTS_MAX_ITEMS = 14; } VCMI_LIB_NAMESPACE_END diff --git a/lib/constants/StringConstants.h b/lib/constants/StringConstants.h index c017ef659..78fde625f 100644 --- a/lib/constants/StringConstants.h +++ b/lib/constants/StringConstants.h @@ -56,7 +56,7 @@ namespace NSecondarySkill namespace EBuildingType { - const std::string names [44] = + const std::string names [46] = { "mageGuild1", "mageGuild2", "mageGuild3", "mageGuild4", "mageGuild5", // 5 "tavern", "shipyard", "fort", "citadel", "castle", // 10 @@ -66,7 +66,8 @@ namespace EBuildingType "horde2Upgr", "grail", "extraTownHall", "extraCityHall", "extraCapitol", // 30 "dwellingLvl1", "dwellingLvl2", "dwellingLvl3", "dwellingLvl4", "dwellingLvl5", // 35 "dwellingLvl6", "dwellingLvl7", "dwellingUpLvl1", "dwellingUpLvl2", "dwellingUpLvl3", // 40 - "dwellingUpLvl4", "dwellingUpLvl5", "dwellingUpLvl6", "dwellingUpLvl7" + "dwellingUpLvl4", "dwellingUpLvl5", "dwellingUpLvl6", "dwellingUpLvl7", "dwellingLvl8", + "dwellingUpLvl8" }; } @@ -163,6 +164,7 @@ namespace MappedKeys { "dwellingLvl5", BuildingID::DWELL_LVL_5 }, { "dwellingLvl6", BuildingID::DWELL_LVL_6 }, { "dwellingLvl7", BuildingID::DWELL_LVL_7 }, + { "dwellingLvl8", BuildingID::DWELL_LVL_8 }, { "dwellingUpLvl1", BuildingID::DWELL_LVL_1_UP }, { "dwellingUpLvl2", BuildingID::DWELL_LVL_2_UP }, { "dwellingUpLvl3", BuildingID::DWELL_LVL_3_UP }, @@ -170,36 +172,18 @@ namespace MappedKeys { "dwellingUpLvl5", BuildingID::DWELL_LVL_5_UP }, { "dwellingUpLvl6", BuildingID::DWELL_LVL_6_UP }, { "dwellingUpLvl7", BuildingID::DWELL_LVL_7_UP }, + { "dwellingUpLvl8", BuildingID::DWELL_LVL_8_UP }, }; static const std::map SPECIAL_BUILDINGS = { { "mysticPond", BuildingSubID::MYSTIC_POND }, - { "artifactMerchant", BuildingSubID::ARTIFACT_MERCHANT }, - { "freelancersGuild", BuildingSubID::FREELANCERS_GUILD }, - { "magicUniversity", BuildingSubID::MAGIC_UNIVERSITY }, { "castleGate", BuildingSubID::CASTLE_GATE }, - { "creatureTransformer", BuildingSubID::CREATURE_TRANSFORMER },//only skeleton transformer yet { "portalOfSummoning", BuildingSubID::PORTAL_OF_SUMMONING }, - { "ballistaYard", BuildingSubID::BALLISTA_YARD }, - { "stables", BuildingSubID::STABLES }, - { "manaVortex", BuildingSubID::MANA_VORTEX }, - { "lookoutTower", BuildingSubID::LOOKOUT_TOWER }, { "library", BuildingSubID::LIBRARY }, - { "brotherhoodOfSword", BuildingSubID::BROTHERHOOD_OF_SWORD },//morale garrison bonus - { "fountainOfFortune", BuildingSubID::FOUNTAIN_OF_FORTUNE },//luck garrison bonus - { "spellPowerGarrisonBonus", BuildingSubID::SPELL_POWER_GARRISON_BONUS },//such as 'stormclouds', but this name is not ok for good towns - { "attackGarrisonBonus", BuildingSubID::ATTACK_GARRISON_BONUS }, - { "defenseGarrisonBonus", BuildingSubID::DEFENSE_GARRISON_BONUS }, { "escapeTunnel", BuildingSubID::ESCAPE_TUNNEL }, - { "attackVisitingBonus", BuildingSubID::ATTACK_VISITING_BONUS }, - { "defenceVisitingBonus", BuildingSubID::DEFENSE_VISITING_BONUS }, - { "spellPowerVisitingBonus", BuildingSubID::SPELL_POWER_VISITING_BONUS }, - { "knowledgeVisitingBonus", BuildingSubID::KNOWLEDGE_VISITING_BONUS }, - { "experienceVisitingBonus", BuildingSubID::EXPERIENCE_VISITING_BONUS }, - { "lighthouse", BuildingSubID::LIGHTHOUSE }, { "treasury", BuildingSubID::TREASURY }, - { "thievesGuild", BuildingSubID::THIEVES_GUILD } + { "bank", BuildingSubID::BANK } }; static const std::map MARKET_NAMES_TO_TYPES = diff --git a/lib/entities/building/CBuilding.cpp b/lib/entities/building/CBuilding.cpp new file mode 100644 index 000000000..b9b7d4401 --- /dev/null +++ b/lib/entities/building/CBuilding.cpp @@ -0,0 +1,101 @@ +/* + * CBuilding.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 "CBuilding.h" + +#include "../../VCMI_Lib.h" +#include "../../texts/CGeneralTextHandler.h" +#include "../faction/CFaction.h" +#include "../faction/CTown.h" + +VCMI_LIB_NAMESPACE_BEGIN + +const std::map CBuilding::MODES = + { + { "normal", CBuilding::BUILD_NORMAL }, + { "auto", CBuilding::BUILD_AUTO }, + { "special", CBuilding::BUILD_SPECIAL }, + { "grail", CBuilding::BUILD_GRAIL } +}; + +const std::map CBuilding::TOWER_TYPES = + { + { "low", CBuilding::HEIGHT_LOW }, + { "average", CBuilding::HEIGHT_AVERAGE }, + { "high", CBuilding::HEIGHT_HIGH }, + { "skyship", CBuilding::HEIGHT_SKYSHIP } +}; + +BuildingTypeUniqueID CBuilding::getUniqueTypeID() const +{ + return BuildingTypeUniqueID(town->faction->getId(), bid); +} + +std::string CBuilding::getJsonKey() const +{ + return modScope + ':' + identifier; +} + +std::string CBuilding::getNameTranslated() const +{ + return VLC->generaltexth->translate(getNameTextID()); +} + +std::string CBuilding::getDescriptionTranslated() const +{ + return VLC->generaltexth->translate(getDescriptionTextID()); +} + +std::string CBuilding::getBaseTextID() const +{ + return TextIdentifier("building", modScope, town->faction->identifier, identifier).get(); +} + +std::string CBuilding::getNameTextID() const +{ + return TextIdentifier(getBaseTextID(), "name").get(); +} + +std::string CBuilding::getDescriptionTextID() const +{ + return TextIdentifier(getBaseTextID(), "description").get(); +} + +BuildingID CBuilding::getBase() const +{ + const CBuilding * build = this; + while (build->upgrade != BuildingID::NONE) + { + build = build->town->buildings.at(build->upgrade); + } + + return build->bid; +} + +si32 CBuilding::getDistance(const BuildingID & buildID) const +{ + const CBuilding * build = town->buildings.at(buildID); + int distance = 0; + while (build->upgrade != BuildingID::NONE && build != this) + { + build = build->town->buildings.at(build->upgrade); + distance++; + } + if (build == this) + return distance; + return -1; +} + +void CBuilding::addNewBonus(const std::shared_ptr & b, BonusList & bonusList) const +{ + bonusList.push_back(b); +} + +VCMI_LIB_NAMESPACE_END diff --git a/lib/entities/building/CBuilding.h b/lib/entities/building/CBuilding.h new file mode 100644 index 000000000..9c35fd14c --- /dev/null +++ b/lib/entities/building/CBuilding.h @@ -0,0 +1,102 @@ +/* + * CBuilding.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 "TownFortifications.h" + +#include "../../constants/EntityIdentifiers.h" +#include "../../LogicalExpression.h" +#include "../../ResourceSet.h" +#include "../../bonuses/BonusList.h" +#include "../../rewardable/Info.h" + +VCMI_LIB_NAMESPACE_BEGIN + +class CTown; + +/// a typical building encountered in every castle ;] +/// this is structure available to both client and server +/// contains all mechanics-related data about town structures +class DLL_LINKAGE CBuilding +{ + std::string modScope; + std::string identifier; + +public: + using TRequired = LogicalExpression; + + CTown * town; // town this building belongs to + TResources resources; + TResources produce; + TRequired requirements; + ArtifactID warMachine; + TownFortifications fortifications; + std::set marketModes; + + BuildingID bid; //structure ID + BuildingID upgrade; /// indicates that building "upgrade" can be improved by this, -1 = empty + BuildingSubID::EBuildingSubID subId; /// subtype for special buildings, -1 = the building is not special + bool upgradeReplacesBonuses = false; + bool manualHeroVisit = false; + BonusList buildingBonuses; + + Rewardable::Info rewardableObjectInfo; ///configurable rewards for special buildings + + enum EBuildMode + { + BUILD_NORMAL, // 0 - normal, default + BUILD_AUTO, // 1 - auto - building appears when all requirements are built + BUILD_SPECIAL, // 2 - special - building can not be built normally + BUILD_GRAIL // 3 - grail - building requires grail to be built + } mode; + + enum ETowerHeight // for lookup towers and some grails + { + HEIGHT_NO_TOWER = 5, // building has not 'lookout tower' ability + HEIGHT_LOW = 10, // low lookout tower, but castle without lookout tower gives radius 5 + HEIGHT_AVERAGE = 15, + HEIGHT_HIGH = 20, // such tower is in the Tower town + HEIGHT_SKYSHIP = std::numeric_limits::max() // grail, open entire map + } height; + + static const std::map MODES; + static const std::map TOWER_TYPES; + + CBuilding() : town(nullptr), mode(BUILD_NORMAL) {}; + + BuildingTypeUniqueID getUniqueTypeID() const; + + std::string getJsonKey() const; + + std::string getNameTranslated() const; + std::string getDescriptionTranslated() const; + + std::string getBaseTextID() const; + std::string getNameTextID() const; + std::string getDescriptionTextID() const; + + //return base of upgrade(s) or this + BuildingID getBase() const; + + // returns how many times build has to be upgraded to become build + si32 getDistance(const BuildingID & build) const; + + STRONG_INLINE + bool IsTradeBuilding() const + { + return !marketModes.empty(); + } + + void addNewBonus(const std::shared_ptr & b, BonusList & bonusList) const; + + friend class CTownHandler; +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/CBuildingHandler.cpp b/lib/entities/building/CBuildingHandler.cpp similarity index 86% rename from lib/CBuildingHandler.cpp rename to lib/entities/building/CBuildingHandler.cpp index be986ee8b..a74fe31b9 100644 --- a/lib/CBuildingHandler.cpp +++ b/lib/entities/building/CBuildingHandler.cpp @@ -9,6 +9,9 @@ */ #include "StdInc.h" #include "CBuildingHandler.h" +#include "VCMI_Lib.h" +#include "../faction/CTown.h" +#include "../faction/CTownHandler.h" VCMI_LIB_NAMESPACE_BEGIN @@ -36,7 +39,7 @@ BuildingID CBuildingHandler::campToERMU(int camp, FactionID townType, const std: }; int curPos = static_cast(campToERMU.size()); - for (int i=0; itownh)[townType]->town->creatures.size(); ++i) { if(camp == curPos) //non-upgraded return BuildingID(30 + i); @@ -53,7 +56,7 @@ BuildingID CBuildingHandler::campToERMU(int camp, FactionID townType, const std: { if (hordeLvlsPerTType[townType.getNum()][0] == i) { - BuildingID dwellingID(BuildingID::DWELL_UP_FIRST + hordeLvlsPerTType[townType.getNum()][0]); + BuildingID dwellingID(BuildingID::getDwellingFromLevel(hordeLvlsPerTType[townType.getNum()][0], 1)); if(vstd::contains(builtBuildings, dwellingID)) //if upgraded dwelling is built return BuildingID::HORDE_1_UPGR; @@ -64,7 +67,7 @@ BuildingID CBuildingHandler::campToERMU(int camp, FactionID townType, const std: { if(hordeLvlsPerTType[townType.getNum()].size() > 1) { - BuildingID dwellingID(BuildingID::DWELL_UP_FIRST + hordeLvlsPerTType[townType.getNum()][1]); + BuildingID dwellingID(BuildingID::getDwellingFromLevel(hordeLvlsPerTType[townType.getNum()][1], 1)); if(vstd::contains(builtBuildings, dwellingID)) //if upgraded dwelling is built return BuildingID::HORDE_2_UPGR; @@ -81,5 +84,4 @@ BuildingID CBuildingHandler::campToERMU(int camp, FactionID townType, const std: return BuildingID::NONE; //not found } - VCMI_LIB_NAMESPACE_END diff --git a/lib/CBuildingHandler.h b/lib/entities/building/CBuildingHandler.h similarity index 90% rename from lib/CBuildingHandler.h rename to lib/entities/building/CBuildingHandler.h index 059b9f087..c2b777312 100644 --- a/lib/CBuildingHandler.h +++ b/lib/entities/building/CBuildingHandler.h @@ -9,7 +9,7 @@ */ #pragma once -#include "GameConstants.h" +#include "../../constants/EntityIdentifiers.h" VCMI_LIB_NAMESPACE_BEGIN diff --git a/lib/entities/building/TownFortifications.h b/lib/entities/building/TownFortifications.h new file mode 100644 index 000000000..e87184141 --- /dev/null +++ b/lib/entities/building/TownFortifications.h @@ -0,0 +1,49 @@ +/* + * TownFortifications.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 TownFortifications +{ + CreatureID citadelShooter; + CreatureID upperTowerShooter; + CreatureID lowerTowerShooter; + SpellID moatSpell; + + int8_t wallsHealth = 0; + int8_t citadelHealth = 0; + int8_t upperTowerHealth = 0; + int8_t lowerTowerHealth = 0; + bool hasMoat = false; + + const TownFortifications & operator +=(const TownFortifications & other) + { + if (other.citadelShooter.hasValue()) + citadelShooter = other.citadelShooter; + if (other.upperTowerShooter.hasValue()) + upperTowerShooter = other.upperTowerShooter; + if (other.lowerTowerShooter.hasValue()) + lowerTowerShooter = other.lowerTowerShooter; + if (other.moatSpell.hasValue()) + moatSpell = other.moatSpell; + + wallsHealth = std::max(wallsHealth, other.wallsHealth); + citadelHealth = std::max(citadelHealth, other.citadelHealth); + upperTowerHealth = std::max(upperTowerHealth, other.upperTowerHealth); + lowerTowerHealth = std::max(lowerTowerHealth, other.lowerTowerHealth); + hasMoat = hasMoat || other.hasMoat; + return *this; + } +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/entities/faction/CFaction.cpp b/lib/entities/faction/CFaction.cpp new file mode 100644 index 000000000..3ef88afae --- /dev/null +++ b/lib/entities/faction/CFaction.cpp @@ -0,0 +1,130 @@ +/* + * CFaction.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 "CFaction.h" + +#include "CTown.h" + +#include "../../VCMI_Lib.h" +#include "../../texts/CGeneralTextHandler.h" + +VCMI_LIB_NAMESPACE_BEGIN + +CFaction::~CFaction() +{ + if (town) + { + delete town; + town = nullptr; + } +} + +int32_t CFaction::getIndex() const +{ + return index.getNum(); +} + +int32_t CFaction::getIconIndex() const +{ + return index.getNum(); //??? +} + +std::string CFaction::getJsonKey() const +{ + return modScope + ':' + identifier; +} + +std::string CFaction::getModScope() const +{ + return modScope; +} + +void CFaction::registerIcons(const IconRegistar & cb) const +{ + if(town) + { + auto & info = town->clientInfo; + cb(info.icons[0][0], 0, "ITPT", info.iconLarge[0][0]); + cb(info.icons[0][1], 0, "ITPT", info.iconLarge[0][1]); + cb(info.icons[1][0], 0, "ITPT", info.iconLarge[1][0]); + cb(info.icons[1][1], 0, "ITPT", info.iconLarge[1][1]); + + cb(info.icons[0][0] + 2, 0, "ITPA", info.iconSmall[0][0]); + cb(info.icons[0][1] + 2, 0, "ITPA", info.iconSmall[0][1]); + cb(info.icons[1][0] + 2, 0, "ITPA", info.iconSmall[1][0]); + cb(info.icons[1][1] + 2, 0, "ITPA", info.iconSmall[1][1]); + + cb(index.getNum(), 1, "CPRSMALL", info.towerIconSmall); + cb(index.getNum(), 1, "TWCRPORT", info.towerIconLarge); + + } +} + +std::string CFaction::getNameTranslated() const +{ + return VLC->generaltexth->translate(getNameTextID()); +} + +std::string CFaction::getNameTextID() const +{ + return TextIdentifier("faction", modScope, identifier, "name").get(); +} + +std::string CFaction::getDescriptionTranslated() const +{ + return VLC->generaltexth->translate(getDescriptionTextID()); +} + +std::string CFaction::getDescriptionTextID() const +{ + return TextIdentifier("faction", modScope, identifier, "description").get(); +} + +FactionID CFaction::getId() const +{ + return FactionID(index); +} + +FactionID CFaction::getFactionID() const +{ + return FactionID(index); +} + +bool CFaction::hasTown() const +{ + return town != nullptr; +} + +EAlignment CFaction::getAlignment() const +{ + return alignment; +} + +BoatId CFaction::getBoatType() const +{ + return boatType; +} + +TerrainId CFaction::getNativeTerrain() const +{ + return nativeTerrain; +} + +void CFaction::updateFrom(const JsonNode & data) +{ + +} + +void CFaction::serializeJson(JsonSerializeFormat & handler) +{ + +} + +VCMI_LIB_NAMESPACE_END diff --git a/lib/entities/faction/CFaction.h b/lib/entities/faction/CFaction.h new file mode 100644 index 000000000..27d6a2239 --- /dev/null +++ b/lib/entities/faction/CFaction.h @@ -0,0 +1,85 @@ +/* + * CFaction.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 "../../Point.h" +#include "../../constants/EntityIdentifiers.h" +#include "../../constants/Enumerations.h" +#include "../../filesystem/ResourcePath.h" + +VCMI_LIB_NAMESPACE_BEGIN + +class CTown; + +struct DLL_LINKAGE SPuzzleInfo +{ + Point position; + ui16 number; //type of puzzle + ui16 whenUncovered; //determines the sequence of discovering (the lesser it is the sooner puzzle will be discovered) + ImagePath filename; //file with graphic of this puzzle +}; + +class DLL_LINKAGE CFaction : public Faction +{ + friend class CTownHandler; + friend class CBuilding; + friend class CTown; + + std::string modScope; + std::string identifier; + + FactionID index = FactionID::NEUTRAL; + + FactionID getFactionID() const override; //This function should not be used + +public: + TerrainId nativeTerrain; + EAlignment alignment = EAlignment::NEUTRAL; + bool preferUndergroundPlacement = false; + bool special = false; + + /// Boat that will be used by town shipyard (if any) + /// and for placing heroes directly on boat (in map editor, water prisons & taverns) + BoatId boatType = BoatId::CASTLE; + + CTown * town = nullptr; //NOTE: can be null + + ImagePath creatureBg120; + ImagePath creatureBg130; + + std::vector puzzleMap; + + CFaction() = default; + ~CFaction(); + + int32_t getIndex() const override; + int32_t getIconIndex() const override; + std::string getJsonKey() const override; + std::string getModScope() const override; + void registerIcons(const IconRegistar & cb) const override; + FactionID getId() const override; + + std::string getNameTranslated() const override; + std::string getNameTextID() const override; + std::string getDescriptionTranslated() const; + std::string getDescriptionTextID() const; + + bool hasTown() const override; + TerrainId getNativeTerrain() const override; + EAlignment getAlignment() const override; + BoatId getBoatType() const override; + + void updateFrom(const JsonNode & data); + void serializeJson(JsonSerializeFormat & handler); +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/entities/faction/CTown.cpp b/lib/entities/faction/CTown.cpp new file mode 100644 index 000000000..d464e306e --- /dev/null +++ b/lib/entities/faction/CTown.cpp @@ -0,0 +1,81 @@ +/* + * CTownHandler.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 "CTown.h" + +#include "CFaction.h" +#include "CTownHandler.h" +#include "../building/CBuilding.h" +#include "../../texts/TextIdentifier.h" + +VCMI_LIB_NAMESPACE_BEGIN + +CTown::CTown() + : faction(nullptr), mageLevel(0), primaryRes(0), defaultTavernChance(0) +{ +} + +CTown::~CTown() +{ + for(auto & build : buildings) + build.second.dellNull(); + + for(auto & str : clientInfo.structures) + str.dellNull(); +} + +std::string CTown::getRandomNameTextID(size_t index) const +{ + return TextIdentifier("faction", faction->modScope, faction->identifier, "randomName", index).get(); +} + +size_t CTown::getRandomNamesCount() const +{ + return namesCount; +} + +std::string CTown::getBuildingScope() const +{ + if(faction == nullptr) + //no faction == random faction + return "building"; + else + return "building." + faction->getJsonKey(); +} + +std::set CTown::getAllBuildings() const +{ + std::set res; + + for(const auto & b : buildings) + { + res.insert(b.first.num); + } + + return res; +} + +const CBuilding * CTown::getSpecialBuilding(BuildingSubID::EBuildingSubID subID) const +{ + for(const auto & kvp : buildings) + { + if(kvp.second->subId == subID) + return buildings.at(kvp.first); + } + return nullptr; +} + +BuildingID CTown::getBuildingType(BuildingSubID::EBuildingSubID subID) const +{ + const auto * building = getSpecialBuilding(subID); + return building == nullptr ? BuildingID::NONE : building->bid.num; +} + +VCMI_LIB_NAMESPACE_END diff --git a/lib/entities/faction/CTown.h b/lib/entities/faction/CTown.h new file mode 100644 index 000000000..7d848c59a --- /dev/null +++ b/lib/entities/faction/CTown.h @@ -0,0 +1,112 @@ +/* + * CTown.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 "../building/TownFortifications.h" +#include "../../ConstTransitivePtr.h" +#include "../../Point.h" +#include "../../constants/EntityIdentifiers.h" +#include "../../constants/Enumerations.h" +#include "../../filesystem/ResourcePath.h" +#include "../../int3.h" + +VCMI_LIB_NAMESPACE_BEGIN + +class CBuilding; + +/// This is structure used only by client +/// Consists of all gui-related data about town structures +/// Should be moved from lib to client +struct DLL_LINKAGE CStructure +{ + CBuilding * building; // base building. If null - this structure will be always present on screen + CBuilding * buildable; // building that will be used to determine built building and visible cost. Usually same as "building" + + int3 pos; + AnimationPath defName; + ImagePath borderName; + ImagePath areaName; + std::string identifier; + + bool hiddenUpgrade; // used only if "building" is upgrade, if true - structure on town screen will behave exactly like parent (mouse clicks, hover texts, etc) +}; + +class DLL_LINKAGE CTown +{ + friend class CTownHandler; + size_t namesCount = 0; + +public: + CTown(); + ~CTown(); + + std::string getBuildingScope() const; + std::set getAllBuildings() const; + const CBuilding * getSpecialBuilding(BuildingSubID::EBuildingSubID subID) const; + BuildingID getBuildingType(BuildingSubID::EBuildingSubID subID) const; + + std::string getRandomNameTextID(size_t index) const; + size_t getRandomNamesCount() const; + + CFaction * faction; + + /// level -> list of creatures on this tier + // TODO: replace with pointers to CCreature + std::vector > creatures; + + std::map > buildings; + + std::vector dwellings; //defs for adventure map dwellings for new towns, [0] means tier 1 creatures etc. + std::vector dwellingNames; + + // should be removed at least from configs in favor of auto-detection + std::map hordeLvl; //[0] - first horde building creature level; [1] - second horde building (-1 if not present) + ui32 mageLevel; //max available mage guild level + GameResID primaryRes; + CreatureID warMachineDeprecated; + + /// Base state of fortifications for empty town. + /// Used to define shooter units and moat spell ID + TownFortifications fortifications; + + // 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; + + // Client-only data. Should be moved away from lib + struct ClientInfo + { + //icons [fort is present?][build limit reached?] -> index of icon in def files + int icons[2][2]; + std::string iconSmall[2][2]; /// icon names used during loading + std::string iconLarge[2][2]; + VideoPath tavernVideo; + std::vector musicTheme; + ImagePath townBackground; + ImagePath guildBackground; + ImagePath guildWindow; + AnimationPath buildingsIcons; + ImagePath hallBackground; + /// vector[row][column] = list of buildings in this slot + std::vector< std::vector< std::vector > > hallSlots; + + /// list of town screen structures. + /// NOTE: index in vector is meaningless. Vector used instead of list for a bit faster access + std::vector > structures; + + std::string siegePrefix; + std::vector siegePositions; + std::string towerIconSmall; + std::string towerIconLarge; + + } clientInfo; +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/CTownHandler.cpp b/lib/entities/faction/CTownHandler.cpp similarity index 63% rename from lib/CTownHandler.cpp rename to lib/entities/faction/CTownHandler.cpp index b65af0271..91d05852b 100644 --- a/lib/CTownHandler.cpp +++ b/lib/entities/faction/CTownHandler.cpp @@ -10,309 +10,36 @@ #include "StdInc.h" #include "CTownHandler.h" -#include "VCMI_Lib.h" -#include "CGeneralTextHandler.h" -#include "constants/StringConstants.h" -#include "CCreatureHandler.h" -#include "CHeroHandler.h" -#include "CArtHandler.h" -#include "GameSettings.h" -#include "TerrainHandler.h" -#include "spells/CSpellHandler.h" -#include "filesystem/Filesystem.h" -#include "bonuses/Bonus.h" -#include "bonuses/Propagators.h" -#include "json/JsonBonus.h" -#include "ResourceSet.h" -#include "mapObjectConstructors/AObjectTypeHandler.h" -#include "mapObjectConstructors/CObjectClassesHandler.h" -#include "modding/IdentifierStorage.h" -#include "modding/ModScope.h" +#include "CTown.h" +#include "CFaction.h" +#include "../building/CBuilding.h" +#include "../hero/CHeroClassHandler.h" + +#include "../../CCreatureHandler.h" +#include "../../IGameSettings.h" +#include "../../TerrainHandler.h" +#include "../../VCMI_Lib.h" + +#include "../../bonuses/Propagators.h" +#include "../../constants/StringConstants.h" +#include "../../mapObjectConstructors/AObjectTypeHandler.h" +#include "../../mapObjectConstructors/CObjectClassesHandler.h" +#include "../../modding/IdentifierStorage.h" +#include "../../modding/ModScope.h" +#include "../../spells/CSpellHandler.h" +#include "../../texts/CGeneralTextHandler.h" +#include "../../texts/CLegacyConfigParser.h" +#include "../../json/JsonBonus.h" +#include "../../json/JsonUtils.h" VCMI_LIB_NAMESPACE_BEGIN const int NAMES_PER_TOWN=16; // number of town names per faction in H3 files. Json can define any number -const std::map CBuilding::MODES = -{ - { "normal", CBuilding::BUILD_NORMAL }, - { "auto", CBuilding::BUILD_AUTO }, - { "special", CBuilding::BUILD_SPECIAL }, - { "grail", CBuilding::BUILD_GRAIL } -}; - -const std::map CBuilding::TOWER_TYPES = -{ - { "low", CBuilding::HEIGHT_LOW }, - { "average", CBuilding::HEIGHT_AVERAGE }, - { "high", CBuilding::HEIGHT_HIGH }, - { "skyship", CBuilding::HEIGHT_SKYSHIP } -}; - -BuildingTypeUniqueID::BuildingTypeUniqueID(FactionID factionID, BuildingID buildingID ): - BuildingTypeUniqueID(factionID.getNum() * 0x10000 + buildingID.getNum()) -{ - assert(factionID.getNum() >= 0); - assert(factionID.getNum() < 0x10000); - assert(buildingID.getNum() >= 0); - assert(buildingID.getNum() < 0x10000); -} - -BuildingID BuildingTypeUniqueID::getBuilding() const -{ - return BuildingID(getNum() % 0x10000); -} - -FactionID BuildingTypeUniqueID::getFaction() const -{ - return FactionID(getNum() / 0x10000); -} - -const BuildingTypeUniqueID CBuilding::getUniqueTypeID() const -{ - return BuildingTypeUniqueID(town->faction->getId(), bid); -} - -std::string CBuilding::getJsonKey() const -{ - return modScope + ':' + identifier; -} - -std::string CBuilding::getNameTranslated() const -{ - return VLC->generaltexth->translate(getNameTextID()); -} - -std::string CBuilding::getDescriptionTranslated() const -{ - return VLC->generaltexth->translate(getDescriptionTextID()); -} - -std::string CBuilding::getBaseTextID() const -{ - return TextIdentifier("building", modScope, town->faction->identifier, identifier).get(); -} - -std::string CBuilding::getNameTextID() const -{ - return TextIdentifier(getBaseTextID(), "name").get(); -} - -std::string CBuilding::getDescriptionTextID() const -{ - return TextIdentifier(getBaseTextID(), "description").get(); -} - -BuildingID CBuilding::getBase() const -{ - const CBuilding * build = this; - while (build->upgrade != BuildingID::NONE) - { - build = build->town->buildings.at(build->upgrade); - } - - return build->bid; -} - -si32 CBuilding::getDistance(const BuildingID & buildID) const -{ - const CBuilding * build = town->buildings.at(buildID); - int distance = 0; - while (build->upgrade != BuildingID::NONE && build != this) - { - build = build->town->buildings.at(build->upgrade); - distance++; - } - if (build == this) - return distance; - return -1; -} - -void CBuilding::addNewBonus(const std::shared_ptr & b, BonusList & bonusList) const -{ - bonusList.push_back(b); -} - -CFaction::~CFaction() -{ - if (town) - { - delete town; - town = nullptr; - } -} - -int32_t CFaction::getIndex() const -{ - return index.getNum(); -} - -int32_t CFaction::getIconIndex() const -{ - return index.getNum(); //??? -} - -std::string CFaction::getJsonKey() const -{ - return modScope + ':' + identifier; -} - -void CFaction::registerIcons(const IconRegistar & cb) const -{ - if(town) - { - auto & info = town->clientInfo; - cb(info.icons[0][0], 0, "ITPT", info.iconLarge[0][0]); - cb(info.icons[0][1], 0, "ITPT", info.iconLarge[0][1]); - cb(info.icons[1][0], 0, "ITPT", info.iconLarge[1][0]); - cb(info.icons[1][1], 0, "ITPT", info.iconLarge[1][1]); - - cb(info.icons[0][0] + 2, 0, "ITPA", info.iconSmall[0][0]); - cb(info.icons[0][1] + 2, 0, "ITPA", info.iconSmall[0][1]); - cb(info.icons[1][0] + 2, 0, "ITPA", info.iconSmall[1][0]); - cb(info.icons[1][1] + 2, 0, "ITPA", info.iconSmall[1][1]); - - cb(index.getNum(), 1, "CPRSMALL", info.towerIconSmall); - cb(index.getNum(), 1, "TWCRPORT", info.towerIconLarge); - - } -} - -std::string CFaction::getNameTranslated() const -{ - return VLC->generaltexth->translate(getNameTextID()); -} - -std::string CFaction::getNameTextID() const -{ - return TextIdentifier("faction", modScope, identifier, "name").get(); -} - -std::string CFaction::getDescriptionTranslated() const -{ - return VLC->generaltexth->translate(getDescriptionTextID()); -} - -std::string CFaction::getDescriptionTextID() const -{ - return TextIdentifier("faction", modScope, identifier, "description").get(); -} - -FactionID CFaction::getId() const -{ - return FactionID(index); -} - -FactionID CFaction::getFaction() const -{ - return FactionID(index); -} - -bool CFaction::hasTown() const -{ - return town != nullptr; -} - -EAlignment CFaction::getAlignment() const -{ - return alignment; -} - -BoatId CFaction::getBoatType() const -{ - return boatType; -} - -TerrainId CFaction::getNativeTerrain() const -{ - return nativeTerrain; -} - -void CFaction::updateFrom(const JsonNode & data) -{ - -} - -void CFaction::serializeJson(JsonSerializeFormat & handler) -{ - -} - - -CTown::CTown() - : faction(nullptr), mageLevel(0), primaryRes(0), moatAbility(SpellID::NONE), defaultTavernChance(0) -{ -} - -CTown::~CTown() -{ - for(auto & build : buildings) - build.second.dellNull(); - - for(auto & str : clientInfo.structures) - str.dellNull(); -} - -std::string CTown::getRandomNameTextID(size_t index) const -{ - return TextIdentifier("faction", faction->modScope, faction->identifier, "randomName", index).get(); -} - -size_t CTown::getRandomNamesCount() const -{ - return namesCount; -} - -std::string CTown::getBuildingScope() const -{ - if(faction == nullptr) - //no faction == random faction - return "building"; - else - return "building." + faction->getJsonKey(); -} - -std::set CTown::getAllBuildings() const -{ - std::set res; - - for(const auto & b : buildings) - { - res.insert(b.first.num); - } - - return res; -} - -const CBuilding * CTown::getSpecialBuilding(BuildingSubID::EBuildingSubID subID) const -{ - for(const auto & kvp : buildings) - { - if(kvp.second->subId == subID) - return buildings.at(kvp.first); - } - return nullptr; -} - -BuildingID CTown::getBuildingType(BuildingSubID::EBuildingSubID subID) const -{ - const auto * building = getSpecialBuilding(subID); - return building == nullptr ? BuildingID::NONE : building->bid.num; -} - -std::string CTown::getGreeting(BuildingSubID::EBuildingSubID subID) const -{ - return CTownHandler::getMappedValue(subID, std::string(), specialMessages, false); -} - -void CTown::setGreeting(BuildingSubID::EBuildingSubID subID, const std::string & message) const -{ - specialMessages.insert(std::pair(subID, message)); -} - -CTownHandler::CTownHandler(): - randomTown(new CTown()), - randomFaction(new CFaction()) +CTownHandler::CTownHandler() + : buildingsLibrary(JsonPath::builtin("config/buildingsLibrary")) + , randomTown(new CTown()) + , randomFaction(new CFaction()) { randomFaction->town = randomTown; randomTown->faction = randomFaction; @@ -349,7 +76,7 @@ const TPropagatorPtr & CTownHandler::emptyPropagator() std::vector CTownHandler::loadLegacyData() { - size_t dataSize = VLC->settings()->getInteger(EGameSettings::TEXTS_FACTION); + size_t dataSize = VLC->engineSettings()->getInteger(EGameSettings::TEXTS_FACTION); std::vector dest(dataSize); objects.resize(dataSize); @@ -515,87 +242,7 @@ void CTownHandler::loadBuildingRequirements(CBuilding * building, const JsonNode bidsToLoad.push_back(hlp); } -template -R CTownHandler::getMappedValue(const K key, const R defval, const std::map & map, bool required) -{ - auto it = map.find(key); - - if(it != map.end()) - return it->second; - - if(required) - logMod->warn("Warning: Property: '%s' is unknown. Correct the typo or update VCMI.", key); - return defval; -} - -template -R CTownHandler::getMappedValue(const JsonNode & node, const R defval, const std::map & map, bool required) -{ - if(!node.isNull() && node.getType() == JsonNode::JsonType::DATA_STRING) - return getMappedValue(node.String(), defval, map, required); - return defval; -} - -void CTownHandler::addBonusesForVanilaBuilding(CBuilding * building) const -{ - std::shared_ptr b; - static const TPropagatorPtr playerPropagator = std::make_shared(CBonusSystemNode::ENodeTypes::PLAYER); - - if(building->bid == BuildingID::TAVERN) - { - b = createBonus(building, BonusType::MORALE, +1); - } - - switch(building->subId) - { - case BuildingSubID::BROTHERHOOD_OF_SWORD: - b = createBonus(building, BonusType::MORALE, +2); - building->overrideBids.insert(BuildingID::TAVERN); - break; - case BuildingSubID::FOUNTAIN_OF_FORTUNE: - b = createBonus(building, BonusType::LUCK, +2); - break; - case BuildingSubID::SPELL_POWER_GARRISON_BONUS: - b = createBonus(building, BonusType::PRIMARY_SKILL, +2, BonusSubtypeID(PrimarySkill::SPELL_POWER)); - break; - case BuildingSubID::ATTACK_GARRISON_BONUS: - b = createBonus(building, BonusType::PRIMARY_SKILL, +2, BonusSubtypeID(PrimarySkill::ATTACK)); - break; - case BuildingSubID::DEFENSE_GARRISON_BONUS: - b = createBonus(building, BonusType::PRIMARY_SKILL, +2, BonusSubtypeID(PrimarySkill::DEFENSE)); - break; - case BuildingSubID::LIGHTHOUSE: - b = createBonus(building, BonusType::MOVEMENT, +500, BonusCustomSubtype::heroMovementSea, playerPropagator); - break; - } - - if(b) - building->addNewBonus(b, building->buildingBonuses); -} - -std::shared_ptr CTownHandler::createBonus(CBuilding * build, BonusType type, int val) const -{ - return createBonus(build, type, val, BonusSubtypeID(), emptyPropagator()); -} - -std::shared_ptr CTownHandler::createBonus(CBuilding * build, BonusType type, int val, BonusSubtypeID subtype) const -{ - return createBonus(build, type, val, subtype, emptyPropagator()); -} - -std::shared_ptr CTownHandler::createBonus(CBuilding * build, BonusType type, int val, BonusSubtypeID subtype, const TPropagatorPtr & prop) const -{ - auto b = std::make_shared(BonusDuration::PERMANENT, type, BonusSource::TOWN_STRUCTURE, val, build->getUniqueTypeID(), subtype); - - b->description.appendTextID(build->getNameTextID()); - - if(prop) - b->addPropagator(prop); - - return b; -} - -void CTownHandler::loadSpecialBuildingBonuses(const JsonNode & source, BonusList & bonusList, CBuilding * building) +void CTownHandler::loadBuildingBonuses(const JsonNode & source, BonusList & bonusList, CBuilding * building) const { for(const auto & b : source.Vector()) { @@ -607,6 +254,8 @@ void CTownHandler::loadSpecialBuildingBonuses(const JsonNode & source, BonusList bonus->description.appendTextID(building->getNameTextID()); //JsonUtils::parseBuildingBonus produces UNKNOWN type propagator instead of empty. + assert(bonus->propagator == nullptr || bonus->propagator->getPropagatorType() != CBonusSystemNode::ENodeTypes::UNKNOWN); + if(bonus->propagator != nullptr && bonus->propagator->getPropagatorType() == CBonusSystemNode::ENodeTypes::UNKNOWN) bonus->addPropagator(emptyPropagator()); @@ -620,7 +269,7 @@ void CTownHandler::loadBuilding(CTown * town, const std::string & stringID, cons assert(!source.getModScope().empty()); auto * ret = new CBuilding(); - ret->bid = getMappedValue(stringID, BuildingID::NONE, MappedKeys::BUILDING_NAMES_TO_TYPES, false); + ret->bid = vstd::find_or(MappedKeys::BUILDING_NAMES_TO_TYPES, stringID, BuildingID::NONE); ret->subId = BuildingSubID::NONE; if(ret->bid == BuildingID::NONE && !source["id"].isNull()) @@ -635,80 +284,81 @@ void CTownHandler::loadBuilding(CTown * town, const std::string & stringID, cons ret->mode = ret->bid == BuildingID::GRAIL ? CBuilding::BUILD_GRAIL - : getMappedValue(source["mode"], CBuilding::BUILD_NORMAL, CBuilding::MODES); + : vstd::find_or(CBuilding::MODES, source["mode"].String(), CBuilding::BUILD_NORMAL); - ret->height = getMappedValue(source["height"], CBuilding::HEIGHT_NO_TOWER, CBuilding::TOWER_TYPES); + ret->height = vstd::find_or(CBuilding::TOWER_TYPES, source["height"].String(), CBuilding::HEIGHT_NO_TOWER); ret->identifier = stringID; ret->modScope = source.getModScope(); ret->town = town; - 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"]); ret->produce = TResources(source["produce"]); - if(ret->bid == BuildingID::TAVERN) - addBonusesForVanilaBuilding(ret); - else if(ret->bid.IsSpecialOrGrail()) + ret->manualHeroVisit = source["manualHeroVisit"].Bool(); + ret->upgradeReplacesBonuses = source["upgradeReplacesBonuses"].Bool(); + + const JsonNode & fortifications = source["fortifications"]; + if (!fortifications.isNull()) { - loadSpecialBuildingBonuses(source["bonuses"], ret->buildingBonuses, ret); - - if(ret->buildingBonuses.empty()) + VLC->identifiers()->requestIdentifierOptional("creature", fortifications["citadelShooter"], [=](si32 identifier) { - ret->subId = getMappedValue(source["type"], BuildingSubID::NONE, MappedKeys::SPECIAL_BUILDINGS); - addBonusesForVanilaBuilding(ret); - } + ret->fortifications.citadelShooter = CreatureID(identifier); + }); - loadSpecialBuildingBonuses(source["onVisitBonuses"], ret->onVisitBonuses, ret); - - if(!ret->onVisitBonuses.empty()) + VLC->identifiers()->requestIdentifierOptional("creature", fortifications["upperTowerShooter"], [=](si32 identifier) { - if(ret->subId == BuildingSubID::NONE) - ret->subId = BuildingSubID::CUSTOM_VISITING_BONUS; + ret->fortifications.upperTowerShooter = CreatureID(identifier); + }); - for(auto & bonus : ret->onVisitBonuses) - bonus->sid = BonusSourceID(ret->getUniqueTypeID()); - } - - if(source["type"].String() == "configurable" && ret->subId == BuildingSubID::NONE) + VLC->identifiers()->requestIdentifierOptional("creature", fortifications["lowerTowerShooter"], [=](si32 identifier) { - ret->subId = BuildingSubID::CUSTOM_VISITING_REWARD; - ret->rewardableObjectInfo.init(source, ret->getBaseTextID()); - } + ret->fortifications.lowerTowerShooter = CreatureID(identifier); + }); + + ret->fortifications.wallsHealth = fortifications["wallsHealth"].Integer(); + ret->fortifications.citadelHealth = fortifications["citadelHealth"].Integer(); + ret->fortifications.upperTowerHealth = fortifications["upperTowerHealth"].Integer(); + ret->fortifications.lowerTowerHealth = fortifications["lowerTowerHealth"].Integer(); + ret->fortifications.hasMoat = fortifications["hasMoat"].Bool(); } - //MODS COMPATIBILITY FOR 0.96 - if(!ret->produce.nonZero()) + + loadBuildingBonuses(source["bonuses"], ret->buildingBonuses, ret); + + if(!source["configuration"].isNull()) + ret->rewardableObjectInfo.init(source["configuration"], ret->getBaseTextID()); + + //MODS COMPATIBILITY FOR pre-1.6 + if(ret->produce.empty() && ret->bid == BuildingID::RESOURCE_SILO) { - switch (ret->bid.toEnum()) { - break; case BuildingID::VILLAGE_HALL: ret->produce[EGameResID::GOLD] = 500; - break; case BuildingID::TOWN_HALL : ret->produce[EGameResID::GOLD] = 1000; - break; case BuildingID::CITY_HALL : ret->produce[EGameResID::GOLD] = 2000; - break; case BuildingID::CAPITOL : ret->produce[EGameResID::GOLD] = 4000; - break; case BuildingID::GRAIL : ret->produce[EGameResID::GOLD] = 5000; - break; case BuildingID::RESOURCE_SILO : - { - switch (ret->town->primaryRes.toEnum()) - { - case EGameResID::GOLD: - ret->produce[ret->town->primaryRes] = 500; - break; - case EGameResID::WOOD_AND_ORE: - ret->produce[EGameResID::WOOD] = 1; - ret->produce[EGameResID::ORE] = 1; - break; - default: - ret->produce[ret->town->primaryRes] = 1; - break; - } - } + logGlobal->warn("Resource silo in town '%s' does not produces any resources!", ret->town->faction->getJsonKey()); + switch (ret->town->primaryRes.toEnum()) + { + case EGameResID::GOLD: + ret->produce[ret->town->primaryRes] = 500; + break; + case EGameResID::WOOD_AND_ORE: + ret->produce[EGameResID::WOOD] = 1; + ret->produce[EGameResID::ORE] = 1; + break; + default: + ret->produce[ret->town->primaryRes] = 1; + break; } } loadBuildingRequirements(ret, source["requires"], requirementsToLoad); - if(ret->bid.IsSpecialOrGrail()) - loadBuildingRequirements(ret, source["overrides"], overriddenBidsToLoad); + if (!source["warMachine"].isNull()) + { + VLC->identifiers()->requestIdentifier("artifact", source["warMachine"], [=](si32 identifier) + { + ret->warMachine = ArtifactID(identifier); + }); + } if (!source["upgrades"].isNull()) { @@ -728,6 +378,11 @@ void CTownHandler::loadBuilding(CTown * town, const std::string & stringID, cons ret->upgrade = BuildingID::NONE; ret->town->buildings[ret->bid] = ret; + for(const auto & element : source["marketModes"].Vector()) + { + if(MappedKeys::MARKET_NAMES_TO_TYPES.count(element.String())) + ret->marketModes.insert(MappedKeys::MARKET_NAMES_TO_TYPES.at(element.String())); + } registerObject(source.getModScope(), ret->town->getBuildingScope(), ret->identifier, ret->bid.getNum()); } @@ -845,12 +500,14 @@ void CTownHandler::loadSiegeScreen(CTown &town, const JsonNode & source) const VLC->identifiers()->requestIdentifier("creature", source["shooter"], [&town](si32 creature) { auto crId = CreatureID(creature); - if((*VLC->creh)[crId]->animation.missleFrameAngles.empty()) + if((*VLC->creh)[crId]->animation.missileFrameAngles.empty()) logMod->error("Mod '%s' error: Creature '%s' on the Archer's tower is not a shooter. Mod should be fixed. Siege will not work properly!" , town.faction->getNameTranslated() , (*VLC->creh)[crId]->getNameSingularTranslated()); - town.clientInfo.siegeShooter = crId; + town.fortifications.citadelShooter = crId; + town.fortifications.upperTowerShooter = crId; + town.fortifications.lowerTowerShooter = crId; }); auto & pos = town.clientInfo.siegePositions; @@ -902,8 +559,17 @@ void CTownHandler::loadClientData(CTown &town, const JsonNode & source) const readIcon(source["icons"]["fort"]["normal"], info.iconSmall[1][0], info.iconLarge[1][0]); readIcon(source["icons"]["fort"]["built"], info.iconSmall[1][1], info.iconLarge[1][1]); + if (source["musicTheme"].isVector()) + { + for (auto const & entry : source["musicTheme"].Vector()) + info.musicTheme.push_back(AudioPath::fromJson(entry)); + } + else + { + info.musicTheme.push_back(AudioPath::fromJson(source["musicTheme"])); + } + info.hallBackground = ImagePath::fromJson(source["hallBackground"]); - info.musicTheme = AudioPath::fromJson(source["musicTheme"]); info.townBackground = ImagePath::fromJson(source["townBackground"]); info.guildWindow = ImagePath::fromJson(source["guildWindow"]); info.buildingsIcons = AnimationPath::fromJson(source["buildingsIcons"]); @@ -924,14 +590,20 @@ void CTownHandler::loadTown(CTown * town, const JsonNode & source) else town->primaryRes = GameResID(resIter - std::begin(GameConstants::RESOURCE_NAMES)); - warMachinesToLoad[town] = source["warMachine"]; + if (!source["warMachine"].isNull()) + { + VLC->identifiers()->requestIdentifier( "creature", source["warMachine"], [=](si32 creatureID) + { + town->warMachineDeprecated = creatureID; + }); + } town->mageLevel = static_cast(source["mageGuild"].Float()); 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; } @@ -939,14 +611,14 @@ void CTownHandler::loadTown(CTown * town, const JsonNode & source) { VLC->identifiers()->requestIdentifier( "spell", source["moatAbility"], [=](si32 ability) { - town->moatAbility = SpellID(ability); + town->fortifications.moatSpell = SpellID(ability); }); } else { VLC->identifiers()->requestIdentifier( source.getModScope(), "spell", "castleMoat", [=](si32 ability) { - town->moatAbility = SpellID(ability); + town->fortifications.moatSpell = SpellID(ability); }); } @@ -1020,8 +692,8 @@ void CTownHandler::loadPuzzle(CFaction &faction, const JsonNode &source) const size_t index = faction.puzzleMap.size(); SPuzzleInfo spi; - spi.x = static_cast(piece["x"].Float()); - spi.y = static_cast(piece["y"].Float()); + spi.position.x = static_cast(piece["x"].Float()); + spi.position.y = static_cast(piece["y"].Float()); spi.whenUncovered = static_cast(piece["index"].Float()); spi.number = static_cast(index); @@ -1036,18 +708,18 @@ void CTownHandler::loadPuzzle(CFaction &faction, const JsonNode &source) const assert(faction.puzzleMap.size() == GameConstants::PUZZLE_MAP_PIECES); } -CFaction * CTownHandler::loadFromJson(const std::string & scope, const JsonNode & source, const std::string & identifier, size_t index) +std::shared_ptr CTownHandler::loadFromJson(const std::string & scope, const JsonNode & source, const std::string & identifier, size_t index) { assert(identifier.find(':') == std::string::npos); - auto * faction = new CFaction(); + auto faction = std::make_shared(); faction->index = static_cast(index); faction->modScope = scope; faction->identifier = identifier; - VLC->generaltexth->registerString(scope, faction->getNameTextID(), source["name"].String()); - VLC->generaltexth->registerString(scope, faction->getDescriptionTranslated(), 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"]); @@ -1091,7 +763,7 @@ CFaction * CTownHandler::loadFromJson(const std::string & scope, const JsonNode if (!source["town"].isNull()) { faction->town = new CTown(); - faction->town->faction = faction; + faction->town->faction = faction.get(); loadTown(faction->town, source["town"]); } else @@ -1105,7 +777,7 @@ CFaction * CTownHandler::loadFromJson(const std::string & scope, const JsonNode void CTownHandler::loadObject(std::string scope, std::string name, const JsonNode & data) { - auto * object = loadFromJson(scope, data, name, objects.size()); + auto object = loadFromJson(scope, data, name, objects.size()); objects.emplace_back(object); @@ -1144,7 +816,7 @@ void CTownHandler::loadObject(std::string scope, std::string name, const JsonNod void CTownHandler::loadObject(std::string scope, std::string name, const JsonNode & data, size_t index) { - auto * object = loadFromJson(scope, data, name, index); + auto object = loadFromJson(scope, data, name, index); if (objects.size() > index) assert(objects[index] == nullptr); // ensure that this id was not loaded before @@ -1185,11 +857,41 @@ void CTownHandler::loadCustom() loadRandomFaction(); } +void CTownHandler::beforeValidate(JsonNode & object) +{ + if (object.Struct().count("town") == 0) + return; + + const auto & inheritBuilding = [this](const std::string & name, JsonNode & target) + { + if (buildingsLibrary.Struct().count(name) == 0) + return; + + JsonNode baseCopy(buildingsLibrary[name]); + baseCopy.setModScope(target.getModScope()); + JsonUtils::inherit(target, baseCopy); + }; + + for (auto & building : object["town"]["buildings"].Struct()) + { + inheritBuilding(building.first, building.second); + if (building.second.Struct().count("type")) + inheritBuilding(building.second["type"].String(), building.second); + + // MODS COMPATIBILITY FOR pre-1.6 + // convert old buildigns with onVisitBonuses into configurable building + if (building.second.Struct().count("onVisitBonuses")) + { + building.second["configuration"]["visitMode"] = JsonNode("bonus"); + building.second["configuration"]["rewards"][0]["message"] = building.second["description"]; + building.second["configuration"]["rewards"][0]["bonuses"] = building.second["onVisitBonuses"]; + } + } +} + void CTownHandler::afterLoadFinalization() { initializeRequirements(); - initializeOverridden(); - initializeWarMachines(); } void CTownHandler::initializeRequirements() @@ -1219,48 +921,11 @@ void CTownHandler::initializeRequirements() requirementsToLoad.clear(); } -void CTownHandler::initializeOverridden() -{ - for(auto & bidHelper : overriddenBidsToLoad) - { - auto jsonNode = bidHelper.json; - auto scope = bidHelper.town->getBuildingScope(); - - for(const auto & b : jsonNode.Vector()) - { - auto bid = BuildingID(VLC->identifiers()->getIdentifier(scope, b).value()); - bidHelper.building->overrideBids.insert(bid); - } - } - overriddenBidsToLoad.clear(); -} - -void CTownHandler::initializeWarMachines() -{ - // must be done separately after all objects are loaded - for(auto & p : warMachinesToLoad) - { - CTown * t = p.first; - JsonNode creatureKey = p.second; - - auto ret = VLC->identifiers()->getIdentifier("creature", creatureKey, false); - - if(ret) - { - const CCreature * creature = CreatureID(*ret).toCreature(); - - t->warMachine = creature->warMachine; - } - } - - warMachinesToLoad.clear(); -} - std::set CTownHandler::getDefaultAllowed() const { std::set allowedFactions; - for(auto town : objects) + for(const auto & town : objects) if (town->town != nullptr && !town->special) allowedFactions.insert(town->getId()); @@ -1273,11 +938,10 @@ std::set CTownHandler::getAllowedFactions(bool withTown) const return getDefaultAllowed(); std::set result; - for(auto town : objects) + for(const auto & town : objects) result.insert(town->getId()); return result; - } const std::vector & CTownHandler::getTypeNames() const @@ -1286,5 +950,4 @@ const std::vector & CTownHandler::getTypeNames() const return typeNames; } - VCMI_LIB_NAMESPACE_END diff --git a/lib/entities/faction/CTownHandler.h b/lib/entities/faction/CTownHandler.h new file mode 100644 index 000000000..f4066934f --- /dev/null +++ b/lib/entities/faction/CTownHandler.h @@ -0,0 +1,91 @@ +/* + * CTownHandler.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 "CFaction.h" + +#include "../../IHandlerBase.h" +#include "../../bonuses/Bonus.h" +#include "../../constants/EntityIdentifiers.h" +#include "../../json/JsonNode.h" + +VCMI_LIB_NAMESPACE_BEGIN + +class CBuilding; +class CTown; + +class DLL_LINKAGE CTownHandler : public CHandlerBase +{ + JsonNode buildingsLibrary; + + struct BuildingRequirementsHelper + { + JsonNode json; + CBuilding * building; + CTown * town; + }; + + std::vector requirementsToLoad; + std::vector overriddenBidsToLoad; //list of buildings, which bonuses should be overridden. + + static const TPropagatorPtr & emptyPropagator(); + + void initializeRequirements(); + + /// loads CBuilding's into town + void loadBuildingRequirements(CBuilding * building, const JsonNode & source, std::vector & bidsToLoad) const; + void loadBuilding(CTown * town, const std::string & stringID, const JsonNode & source); + void loadBuildings(CTown * town, const JsonNode & source); + + /// loads CStructure's into town + void loadStructure(CTown & town, const std::string & stringID, const JsonNode & source) const; + void loadStructures(CTown & town, const JsonNode & source) const; + + /// loads town hall vector (hallSlots) + void loadTownHall(CTown & town, const JsonNode & source) const; + void loadSiegeScreen(CTown & town, const JsonNode & source) const; + + void loadClientData(CTown & town, const JsonNode & source) const; + + void loadTown(CTown * town, const JsonNode & source); + + void loadPuzzle(CFaction & faction, const JsonNode & source) const; + + void loadRandomFaction(); + +public: + CTown * randomTown; + CFaction * randomFaction; + + CTownHandler(); + ~CTownHandler(); + + std::vector loadLegacyData() 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 loadCustom() override; + void afterLoadFinalization() override; + void beforeValidate(JsonNode & object) override; + + std::set getDefaultAllowed() const; + std::set getAllowedFactions(bool withTown = true) const; + +protected: + + void loadBuildingBonuses(const JsonNode & source, BonusList & bonusList, CBuilding * building) const; + const std::vector & getTypeNames() const override; + std::shared_ptr loadFromJson(const std::string & scope, const JsonNode & data, const std::string & identifier, size_t index) override; +}; + +VCMI_LIB_NAMESPACE_END 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/serializer/Cast.h b/lib/entities/hero/EHeroGender.h similarity index 50% rename from lib/serializer/Cast.h rename to lib/entities/hero/EHeroGender.h index 7525f6d85..2b9dfb61f 100644 --- a/lib/serializer/Cast.h +++ b/lib/entities/hero/EHeroGender.h @@ -1,5 +1,5 @@ /* - * Cast.h, part of VCMI engine + * EHeroGender.h, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * @@ -11,16 +11,11 @@ VCMI_LIB_NAMESPACE_BEGIN -template -inline const T * dynamic_ptr_cast(const F * ptr) + enum class EHeroGender : int8_t { - return dynamic_cast(ptr); -} - -template -inline T * dynamic_ptr_cast(F * ptr) -{ - return dynamic_cast(ptr); -} + DEFAULT = -1, // from h3m, instance has same gender as hero type + MALE = 0, + FEMALE = 1, +}; VCMI_LIB_NAMESPACE_END diff --git a/lib/events/ApplyDamage.cpp b/lib/events/ApplyDamage.cpp index 82e499cd9..95930f3a0 100644 --- a/lib/events/ApplyDamage.cpp +++ b/lib/events/ApplyDamage.cpp @@ -29,7 +29,7 @@ CApplyDamage::CApplyDamage(const Environment * env_, BattleStackAttacked * pack_ : pack(pack_), target(std::move(target_)) { - initalDamage = pack->damageAmount; + initialDamage = pack->damageAmount; } bool CApplyDamage::isEnabled() const @@ -37,9 +37,9 @@ bool CApplyDamage::isEnabled() const return true; } -int64_t CApplyDamage::getInitalDamage() const +int64_t CApplyDamage::getInitialDamage() const { - return initalDamage; + return initialDamage; } int64_t CApplyDamage::getDamage() const diff --git a/lib/events/ApplyDamage.h b/lib/events/ApplyDamage.h index a7a426c45..3fbcda8a3 100644 --- a/lib/events/ApplyDamage.h +++ b/lib/events/ApplyDamage.h @@ -23,12 +23,12 @@ public: CApplyDamage(const Environment * env_, BattleStackAttacked * pack_, std::shared_ptr target_); bool isEnabled() const override; - int64_t getInitalDamage() const override; + int64_t getInitialDamage() const override; int64_t getDamage() const override; void setDamage(int64_t value) override; const battle::Unit * getTarget() const override; private: - int64_t initalDamage; + int64_t initialDamage; BattleStackAttacked * pack; std::shared_ptr target; diff --git a/lib/filesystem/AdapterLoaders.cpp b/lib/filesystem/AdapterLoaders.cpp index 7ad58b155..90d76ea2c 100644 --- a/lib/filesystem/AdapterLoaders.cpp +++ b/lib/filesystem/AdapterLoaders.cpp @@ -68,10 +68,8 @@ std::unique_ptr CFilesystemList::load(const ResourcePath & resourc { // load resource from last loader that have it (last overridden version) for(const auto & loader : boost::adaptors::reverse(loaders)) - { if (loader->existsResource(resourceName)) return loader->load(resourceName); - } throw std::runtime_error("Resource with name " + resourceName.getName() + " and type " + EResTypeHelper::getEResTypeAsString(resourceName.getType()) + " wasn't found."); diff --git a/lib/filesystem/CArchiveLoader.cpp b/lib/filesystem/CArchiveLoader.cpp index c7b47f59c..f0212d6ea 100644 --- a/lib/filesystem/CArchiveLoader.cpp +++ b/lib/filesystem/CArchiveLoader.cpp @@ -197,6 +197,11 @@ std::string CArchiveLoader::getMountPoint() const return mountPoint; } +const std::unordered_map & CArchiveLoader::getEntries() const +{ + return entries; +} + std::unordered_set CArchiveLoader::getFilteredFiles(std::function filter) const { std::unordered_set foundID; @@ -209,7 +214,7 @@ std::unordered_set CArchiveLoader::getFilteredFiles(std::function< return foundID; } -void CArchiveLoader::extractToFolder(const std::string & outputSubFolder, CInputStream & fileStream, const ArchiveEntry & entry) const +void CArchiveLoader::extractToFolder(const std::string & outputSubFolder, CInputStream & fileStream, const ArchiveEntry & entry, bool absolute) const { si64 currentPosition = fileStream.tell(); // save filestream position @@ -217,7 +222,7 @@ void CArchiveLoader::extractToFolder(const std::string & outputSubFolder, CInput fileStream.seek(entry.offset); fileStream.read(data.data(), entry.fullSize); - boost::filesystem::path extractedFilePath = createExtractedFilePath(outputSubFolder, entry.name); + boost::filesystem::path extractedFilePath = createExtractedFilePath(outputSubFolder, entry.name, absolute); // writeToOutputFile std::ofstream out(extractedFilePath.string(), std::ofstream::binary); @@ -227,17 +232,17 @@ void CArchiveLoader::extractToFolder(const std::string & outputSubFolder, CInput fileStream.seek(currentPosition); // restore filestream position } -void CArchiveLoader::extractToFolder(const std::string & outputSubFolder, const std::string & mountPoint, ArchiveEntry entry) const +void CArchiveLoader::extractToFolder(const std::string & outputSubFolder, const std::string & mountPoint, ArchiveEntry entry, bool absolute) const { std::unique_ptr inputStream = load(ResourcePath(mountPoint + entry.name)); entry.offset = 0; - extractToFolder(outputSubFolder, *inputStream, entry); + extractToFolder(outputSubFolder, *inputStream, entry, absolute); } -boost::filesystem::path createExtractedFilePath(const std::string & outputSubFolder, const std::string & entryName) +boost::filesystem::path createExtractedFilePath(const std::string & outputSubFolder, const std::string & entryName, bool absolute) { - boost::filesystem::path extractionFolderPath = VCMIDirs::get().userExtractedPath() / outputSubFolder; + boost::filesystem::path extractionFolderPath = absolute ? outputSubFolder : VCMIDirs::get().userExtractedPath() / outputSubFolder; boost::filesystem::path extractedFilePath = extractionFolderPath / entryName; boost::filesystem::create_directories(extractionFolderPath); diff --git a/lib/filesystem/CArchiveLoader.h b/lib/filesystem/CArchiveLoader.h index 33b410062..d8797d7a5 100644 --- a/lib/filesystem/CArchiveLoader.h +++ b/lib/filesystem/CArchiveLoader.h @@ -63,12 +63,13 @@ public: std::unique_ptr load(const ResourcePath & resourceName) const override; bool existsResource(const ResourcePath & resourceName) const override; std::string getMountPoint() const override; + const std::unordered_map & getEntries() const; void updateFilteredFiles(std::function filter) const override {} std::unordered_set getFilteredFiles(std::function filter) const override; /** Extracts one archive entry to the specified subfolder. Used for Video and Sound */ - void extractToFolder(const std::string & outputSubFolder, CInputStream & fileStream, const ArchiveEntry & entry) const; + void extractToFolder(const std::string & outputSubFolder, CInputStream & fileStream, const ArchiveEntry & entry, bool absolute = false) const; /** Extracts one archive entry to the specified subfolder. Used for Images, Sprites, etc */ - void extractToFolder(const std::string & outputSubFolder, const std::string & mountPoint, ArchiveEntry entry) const; + void extractToFolder(const std::string & outputSubFolder, const std::string & mountPoint, ArchiveEntry entry, bool absolute = false) const; private: /** @@ -105,6 +106,6 @@ private: }; /** Constructs the file path for the extracted file. Creates the subfolder hierarchy aswell **/ -boost::filesystem::path createExtractedFilePath(const std::string & outputSubFolder, const std::string & entryName); +boost::filesystem::path createExtractedFilePath(const std::string & outputSubFolder, const std::string & entryName, bool absolute); VCMI_LIB_NAMESPACE_END diff --git a/lib/filesystem/CBinaryReader.cpp b/lib/filesystem/CBinaryReader.cpp index fda958e88..b196baa5f 100644 --- a/lib/filesystem/CBinaryReader.cpp +++ b/lib/filesystem/CBinaryReader.cpp @@ -11,7 +11,6 @@ #include "CBinaryReader.h" #include "CInputStream.h" -#include "../TextOperations.h" VCMI_LIB_NAMESPACE_BEGIN 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/CCompressedStream.h b/lib/filesystem/CCompressedStream.h index 2fad04ebb..a16d7b67b 100644 --- a/lib/filesystem/CCompressedStream.h +++ b/lib/filesystem/CCompressedStream.h @@ -103,8 +103,8 @@ public: /** * C-tor. * - * @param stream - stream with compresed data - * @param gzip - this is gzipp'ed file e.g. campaign or maps, false for files in lod + * @param stream - stream with compressed data + * @param gzip - this is gzipp'ed file e.g. campaign or maps, false for files in .lod * @param decompressedSize - optional parameter to hint size of decompressed data */ CCompressedStream(std::unique_ptr stream, bool gzip, size_t decompressedSize=0); @@ -136,7 +136,7 @@ private: enum EState { - ERROR_OCCURED, + ERROR_OCCURRED, INITIALIZED, IN_PROGRESS, STREAM_END, diff --git a/lib/filesystem/CFilesystemLoader.cpp b/lib/filesystem/CFilesystemLoader.cpp index 8086989e3..c877bffd4 100644 --- a/lib/filesystem/CFilesystemLoader.cpp +++ b/lib/filesystem/CFilesystemLoader.cpp @@ -118,10 +118,11 @@ std::unordered_map CFilesystemLoader::lis EResType::ARCHIVE_SND, EResType::ARCHIVE_ZIP }; static const std::set initialTypes(initArray, initArray + std::size(initArray)); - - assert(boost::filesystem::is_directory(baseDirectory)); std::unordered_map fileList; + if(!boost::filesystem::is_directory(baseDirectory)) + return fileList; + std::vector path; //vector holding relative path to our file boost::filesystem::recursive_directory_iterator enddir; diff --git a/lib/filesystem/Filesystem.cpp b/lib/filesystem/Filesystem.cpp index 7b4821c5c..4840b3f87 100644 --- a/lib/filesystem/Filesystem.cpp +++ b/lib/filesystem/Filesystem.cpp @@ -117,7 +117,7 @@ void CFilesystemGenerator::loadJsonMap(const std::string &mountPoint, const Json if (filename) { auto configData = CResourceHandler::get("initial")->load(JsonPath::builtin(URI))->readAll(); - const JsonNode configInitial(reinterpret_cast(configData.first.get()), configData.second); + const JsonNode configInitial(reinterpret_cast(configData.first.get()), configData.second, URI); filesystem->addLoader(new CMappedFileLoader(mountPoint, configInitial), false); } } @@ -183,9 +183,16 @@ void CResourceHandler::initialize() knownLoaders["saves"] = new CFilesystemLoader("SAVES/", VCMIDirs::get().userSavePath()); knownLoaders["config"] = new CFilesystemLoader("CONFIG/", VCMIDirs::get().userConfigPath()); + if(boost::filesystem::is_directory(VCMIDirs::get().userDataPath() / "Generated")) + boost::filesystem::remove_all(VCMIDirs::get().userDataPath() / "Generated"); + knownLoaders["gen_data"] = new CFilesystemLoader("DATA/", VCMIDirs::get().userDataPath() / "Generated" / "Data"); + knownLoaders["gen_sprites"] = new CFilesystemLoader("SPRITES/", VCMIDirs::get().userDataPath() / "Generated" / "Sprites"); + auto * localFS = new CFilesystemList(); localFS->addLoader(knownLoaders["saves"], true); localFS->addLoader(knownLoaders["config"], true); + localFS->addLoader(knownLoaders["gen_data"], true); + localFS->addLoader(knownLoaders["gen_sprites"], true); addFilesystem("root", "initial", createInitial()); addFilesystem("root", "data", new CFilesystemList()); @@ -205,6 +212,7 @@ ISimpleResourceLoader * CResourceHandler::get() ISimpleResourceLoader * CResourceHandler::get(const std::string & identifier) { + assert(knownLoaders.count(identifier)); return knownLoaders.at(identifier); } @@ -212,7 +220,7 @@ void CResourceHandler::load(const std::string &fsConfigURI, bool extractArchives { auto fsConfigData = get("initial")->load(JsonPath::builtin(fsConfigURI))->readAll(); - const JsonNode fsConfig(reinterpret_cast(fsConfigData.first.get()), fsConfigData.second); + const JsonNode fsConfig(reinterpret_cast(fsConfigData.first.get()), fsConfigData.second, fsConfigURI); addFilesystem("data", ModScope::scopeBuiltin(), createFileSystem("", fsConfig["filesystem"], extractArchives)); } diff --git a/lib/filesystem/Filesystem.h b/lib/filesystem/Filesystem.h index 1ead410f1..532a3b2e7 100644 --- a/lib/filesystem/Filesystem.h +++ b/lib/filesystem/Filesystem.h @@ -101,7 +101,7 @@ public: static void addFilesystem(const std::string & parent, const std::string & identifier, ISimpleResourceLoader * loader); /** - * @brief removeFilesystem removes previously added filesystem from global resouce holder + * @brief removeFilesystem removes previously added filesystem from global resource holder * @param parent parent loader containing filesystem * @param identifier name of this loader * @return if filesystem was successfully removed diff --git a/lib/filesystem/ISimpleResourceLoader.h b/lib/filesystem/ISimpleResourceLoader.h index 836a3a505..06b7c9a65 100644 --- a/lib/filesystem/ISimpleResourceLoader.h +++ b/lib/filesystem/ISimpleResourceLoader.h @@ -25,7 +25,7 @@ public: /** * Loads a resource with the given resource name. * - * @param resourceName The unqiue resource name in space of the archive. + * @param resourceName The unique resource name in space of the archive. * @return a input stream object */ virtual std::unique_ptr load(const ResourcePath & resourceName) const = 0; diff --git a/lib/filesystem/ResourcePath.cpp b/lib/filesystem/ResourcePath.cpp index 15efb5cb7..347c3e9fd 100644 --- a/lib/filesystem/ResourcePath.cpp +++ b/lib/filesystem/ResourcePath.cpp @@ -113,11 +113,9 @@ EResType EResTypeHelper::getTypeFromExtension(std::string extension) {".MP3", EResType::SOUND}, {".OGG", EResType::SOUND}, {".FLAC", EResType::SOUND}, - {".SMK", EResType::VIDEO}, + {".SMK", EResType::VIDEO_LOW_QUALITY}, {".BIK", EResType::VIDEO}, - {".MJPG", EResType::VIDEO}, - {".MPG", EResType::VIDEO}, - {".AVI", EResType::VIDEO}, + {".OGV", EResType::VIDEO}, {".WEBM", EResType::VIDEO}, {".ZIP", EResType::ARCHIVE_ZIP}, {".LOD", EResType::ARCHIVE_LOD}, @@ -157,6 +155,7 @@ std::string EResTypeHelper::getEResTypeAsString(EResType type) MAP_ENUM(TTF_FONT) MAP_ENUM(IMAGE) MAP_ENUM(VIDEO) + MAP_ENUM(VIDEO_LOW_QUALITY) MAP_ENUM(SOUND) MAP_ENUM(ARCHIVE_ZIP) MAP_ENUM(ARCHIVE_LOD) diff --git a/lib/filesystem/ResourcePath.h b/lib/filesystem/ResourcePath.h index 250e76bfc..4f4b4e9a1 100644 --- a/lib/filesystem/ResourcePath.h +++ b/lib/filesystem/ResourcePath.h @@ -28,7 +28,7 @@ class JsonSerializeFormat; * Font: .fnt * Image: .bmp, .jpg, .pcx, .png, .tga * Sound: .wav .82m - * Video: .smk, .bik .mjpg .mpg .webm + * Video: .smk, .bik .ogv .webm * Music: .mp3, .ogg * Archive: .lod, .snd, .vid .pac .zip * Palette: .pal @@ -46,6 +46,7 @@ enum class EResType TTF_FONT, IMAGE, VIDEO, + VIDEO_LOW_QUALITY, SOUND, ARCHIVE_VID, ARCHIVE_ZIP, diff --git a/lib/gameState/CGameState.cpp b/lib/gameState/CGameState.cpp index a99f5fe29..4b8f99d58 100644 --- a/lib/gameState/CGameState.cpp +++ b/lib/gameState/CGameState.cpp @@ -17,12 +17,10 @@ #include "SThievesGuildInfo.h" #include "../ArtifactUtils.h" -#include "../CBuildingHandler.h" -#include "../CGeneralTextHandler.h" -#include "../CHeroHandler.h" +#include "../texts/CGeneralTextHandler.h" #include "../CPlayerState.h" #include "../CStopWatch.h" -#include "../GameSettings.h" +#include "../IGameSettings.h" #include "../StartInfo.h" #include "../TerrainHandler.h" #include "../VCMIDirs.h" @@ -30,6 +28,9 @@ #include "../battle/BattleInfo.h" #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" @@ -45,45 +46,23 @@ #include "../mapping/CMapService.h" #include "../modding/IdentifierStorage.h" #include "../modding/ModScope.h" +#include "../networkPacks/NetPacksBase.h" #include "../pathfinder/CPathfinder.h" #include "../pathfinder/PathfinderOptions.h" -#include "../registerTypes/RegisterTypesClientPacks.h" #include "../rmg/CMapGenerator.h" #include "../serializer/CMemorySerializer.h" -#include "../serializer/CTypeList.h" #include "../spells/CSpellHandler.h" +#include + VCMI_LIB_NAMESPACE_BEGIN boost::shared_mutex CGameState::mutex; -template class CApplyOnGS; - -class CBaseForGSApply -{ -public: - virtual void applyOnGS(CGameState *gs, CPack * pack) const =0; - virtual ~CBaseForGSApply() = default; - template static CBaseForGSApply *getApplier(const U * t=nullptr) - { - return new CApplyOnGS(); - } -}; - -template class CApplyOnGS : public CBaseForGSApply -{ -public: - void applyOnGS(CGameState *gs, CPack * pack) const override - { - T *ptr = static_cast(pack); - ptr->applyGs(gs); - } -}; - 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); } @@ -127,26 +106,26 @@ HeroTypeID CGameState::pickUnusedHeroTypeRandomly(const PlayerColor & owner) throw std::runtime_error("Can not allocate hero. All heroes are already used."); } -int CGameState::getDate(Date mode) const +int CGameState::getDate(int d, Date mode) { int temp; switch (mode) { case Date::DAY: - return day; + return d; case Date::DAY_OF_WEEK: //day of week - temp = (day)%7; // 1 - Monday, 7 - Sunday + temp = (d)%7; // 1 - Monday, 7 - Sunday return temp ? temp : 7; case Date::WEEK: //current week - temp = ((day-1)/7)+1; + temp = ((d-1)/7)+1; if (!(temp%4)) return 4; else return (temp%4); case Date::MONTH: //current month - return ((day-1)/28)+1; + return ((d-1)/28)+1; case Date::DAY_OF_MONTH: //day of month - temp = (day)%28; + temp = (d)%28; if (temp) return temp; else return 28; @@ -154,12 +133,15 @@ int CGameState::getDate(Date mode) const return 0; } +int CGameState::getDate(Date mode) const +{ + return getDate(day, mode); +} + CGameState::CGameState() { gs = this; heroesPool = std::make_unique(); - applier = std::make_shared>(); - registerTypesClientPacks(*applier); globalEffects.setNodeType(CBonusSystemNode::GLOBAL_EFFECTS); } @@ -172,6 +154,11 @@ CGameState::~CGameState() initialOpts.dellNull(); } +const IGameSettings & CGameState::getSettings() const +{ + return map->getSettings(); +} + void CGameState::preInit(Services * newServices, IGameCallback * newCallback) { services = newServices; @@ -182,8 +169,6 @@ void CGameState::init(const IMapService * mapService, StartInfo * si, Load::Prog { assert(services); assert(callback); - logGlobal->info("\tUsing random seed: %d", si->seedToBeUsed); - getRandomGenerator().setSeed(si->seedToBeUsed); scenarioOps = CMemorySerializer::deepCopy(*si).release(); initialOpts = CMemorySerializer::deepCopy(*si).release(); si = nullptr; @@ -202,8 +187,6 @@ void CGameState::init(const IMapService * mapService, StartInfo * si, Load::Prog } logGlobal->info("Map loaded!"); - checkMapChecksum(); - day = 0; logGlobal->debug("Initialization:"); @@ -217,6 +200,7 @@ void CGameState::init(const IMapService * mapService, StartInfo * si, Load::Prog initRandomFactionsForPlayers(); randomizeMapObjects(); placeStartingHeroes(); + initOwnedObjects(); initDifficulty(); initHeroes(); initStartingBonus(); @@ -235,18 +219,6 @@ void CGameState::init(const IMapService * mapService, StartInfo * si, Load::Prog logGlobal->debug("\tChecking objectives"); map->checkForObjectives(); //needs to be run when all objects are properly placed - - auto seedAfterInit = getRandomGenerator().nextInt(); - logGlobal->info("Seed after init is %d (before was %d)", seedAfterInit, scenarioOps->seedToBeUsed); - if(scenarioOps->seedPostInit > 0) - { - //RNG must be in the same state on all machines when initialization is done (otherwise we have desync) - assert(scenarioOps->seedPostInit == seedAfterInit); - } - else - { - scenarioOps->seedPostInit = seedAfterInit; //store the post init "seed" - } } void CGameState::updateEntity(Metatype metatype, int32_t index, const JsonNode & data) @@ -283,7 +255,7 @@ void CGameState::updateEntity(Metatype metatype, int32_t index, const JsonNode & } break; default: - services->updateEntity(metatype, index, data); + logGlobal->error("This metatype update is not implemented"); break; } } @@ -296,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) @@ -306,7 +280,7 @@ void CGameState::initNewGame(const IMapService * mapService, bool allowSavingRan CStopWatch sw; // Gen map - CMapGenerator mapGenerator(*scenarioOps->mapGenOptions, callback, scenarioOps->seedToBeUsed); + CMapGenerator mapGenerator(*scenarioOps->mapGenOptions, callback, getRandomGenerator().nextInt()); progressTracking.include(mapGenerator); std::unique_ptr randomMap = mapGenerator.generate(); @@ -343,10 +317,9 @@ void CGameState::initNewGame(const IMapService * mapService, bool allowSavingRan std::shared_ptr options = scenarioOps->mapGenOptions; const std::string templateName = options->getMapTemplate()->getName(); - const ui32 seed = scenarioOps->seedToBeUsed; const std::string dt = vstd::getDateTimeISO8601Basic(std::time(nullptr)); - const std::string fileName = boost::str(boost::format("%s_%s_%d.vmap") % dt % templateName % seed ); + const std::string fileName = boost::str(boost::format("%s_%s.vmap") % dt % templateName ); const auto fullPath = path / fileName; randomMap->name.appendRawString(boost::str(boost::format(" %s") % dt)); @@ -380,27 +353,18 @@ void CGameState::initCampaign() map = campaign->getCurrentMap().release(); } -void CGameState::checkMapChecksum() +void CGameState::generateOwnedObjectsAfterDeserialize() { - logGlobal->info("\tOur checksum for the map: %d", map->checksum); - if(scenarioOps->mapfileChecksum) + for (auto & object : map->objects) { - logGlobal->info("\tServer checksum for %s: %d", scenarioOps->mapname, scenarioOps->mapfileChecksum); - if(map->checksum != scenarioOps->mapfileChecksum) - { - logGlobal->error("Wrong map checksum!!!"); - throw std::runtime_error("Wrong checksum"); - } - } - else - { - scenarioOps->mapfileChecksum = map->checksum; + if (object && object->asOwnable() && object->getOwner().isValidPlayer()) + players.at(object->getOwner()).addOwnedObject(object.get()); } } void CGameState::initGlobalBonuses() { - const JsonNode & baseBonuses = VLC->settings()->getValue(EGameSettings::BONUSES_GLOBAL); + const JsonNode & baseBonuses = getSettings().getValue(EGameSettings::BONUSES_GLOBAL); logGlobal->debug("\tLoading global bonuses"); for(const auto & b : baseBonuses.Struct()) { @@ -421,10 +385,14 @@ void CGameState::initDifficulty() const JsonNode & difficultyAI(config["ai"][GameConstants::DIFFICULTY_NAMES[scenarioOps->difficulty]]); const JsonNode & difficultyHuman(config["human"][GameConstants::DIFFICULTY_NAMES[scenarioOps->difficulty]]); - auto setDifficulty = [](PlayerState & state, const JsonNode & json) + auto setDifficulty = [this](PlayerState & state, const JsonNode & json) { //set starting resources state.resources = TResources(json["resources"]); + + //handicap + const PlayerSettings &ps = scenarioOps->getIthPlayersSettings(state.color); + state.resources += ps.handicap.startBonus; //set global bonuses for(auto & jsonBonus : json["globalBonuses"].Vector()) @@ -469,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); } @@ -482,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()) { @@ -528,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; } } @@ -536,6 +504,15 @@ void CGameState::randomizeMapObjects() } } +void CGameState::initOwnedObjects() +{ + for(CGObjectInstance *object : map->objects) + { + if (object && object->getOwner().isValidPlayer()) + getPlayerState(object->getOwner())->addOwnedObject(object); + } +} + void CGameState::initPlayerStates() { logGlobal->debug("\tCreating player entries in gs"); @@ -554,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; @@ -569,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); } @@ -624,8 +600,7 @@ void CGameState::initHeroes() } hero->initHero(getRandomGenerator()); - getPlayerState(hero->getOwner())->heroes.push_back(hero); - map->allHeroes[hero->getHeroType().getNum()] = hero; + map->allHeroes[hero->getHeroTypeID().getNum()] = hero; } // generate boats for all heroes on water @@ -633,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())); @@ -655,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 @@ -696,8 +671,8 @@ void CGameState::initFogOfWar() for(auto & elem : teams) { auto & fow = elem.second.fogOfWarMap; - fow->resize(boost::extents[layers][map->width][map->height]); - std::fill(fow->data(), fow->data() + fow->num_elements(), 0); + fow.resize(boost::extents[layers][map->width][map->height]); + std::fill(fow.data(), fow.data() + fow.num_elements(), 0); for(CGObjectInstance *obj : map->objects) { @@ -707,7 +682,7 @@ void CGameState::initFogOfWar() getTilesInRange(tiles, obj->getSightCenter(), obj->getSightRadius(), ETileVisibility::HIDDEN, obj->tempOwner); for(const int3 & tile : tiles) { - (*elem.second.fogOfWarMap)[tile.z][tile.x][tile.y] = 1; + elem.second.fogOfWarMap[tile.z][tile.x][tile.y] = 1; } } } @@ -749,14 +724,14 @@ void CGameState::initStartingBonus() } case PlayerStartingBonus::ARTIFACT: { - if(elem.second.heroes.empty()) + if(elem.second.getHeroes().empty()) { logGlobal->error("Cannot give starting artifact - no heroes!"); break; } const Artifact * toGive = pickRandomArtifact(getRandomGenerator(), CArtifact::ART_TREASURE).toEntity(VLC); - CGHeroInstance *hero = elem.second.heroes[0]; + CGHeroInstance *hero = elem.second.getHeroes()[0]; if(!giveHeroArtifact(hero, toGive->getId())) logGlobal->error("Cannot give starting artifact - no free slots!"); } @@ -782,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()) { @@ -824,68 +799,70 @@ void CGameState::initTowns() for (auto & vti : map->towns) { - assert(vti->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 }; - 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 }; - constexpr std::array hordes = { BuildingID::HORDE_PLACEHOLDER1, BuildingID::HORDE_PLACEHOLDER2, BuildingID::HORDE_PLACEHOLDER3, BuildingID::HORDE_PLACEHOLDER4, BuildingID::HORDE_PLACEHOLDER5, BuildingID::HORDE_PLACEHOLDER6, BuildingID::HORDE_PLACEHOLDER7 }; + 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 }; + constexpr std::array hordes = { BuildingID::HORDE_PLACEHOLDER1, BuildingID::HORDE_PLACEHOLDER2, BuildingID::HORDE_PLACEHOLDER3, BuildingID::HORDE_PLACEHOLDER4, BuildingID::HORDE_PLACEHOLDER5, BuildingID::HORDE_PLACEHOLDER6, BuildingID::HORDE_PLACEHOLDER7, BuildingID::HORDE_PLACEHOLDER8 }; //init buildings - if(vstd::contains(vti->builtBuildings, BuildingID::DEFAULT)) //give standard set of buildings + if(vti->hasBuilt(BuildingID::DEFAULT)) //give standard set of buildings { - vti->builtBuildings.erase(BuildingID::DEFAULT); - vti->builtBuildings.insert(BuildingID::VILLAGE_HALL); + vti->removeBuilding(BuildingID::DEFAULT); + vti->addBuilding(BuildingID::VILLAGE_HALL); if(vti->tempOwner != PlayerColor::NEUTRAL) - vti->builtBuildings.insert(BuildingID::TAVERN); + vti->addBuilding(BuildingID::TAVERN); - auto definesBuildingsChances = VLC->settings()->getVector(EGameSettings::TOWNS_STARTING_DWELLING_CHANCES); + auto definesBuildingsChances = getSettings().getVector(EGameSettings::TOWNS_STARTING_DWELLING_CHANCES); for(int i = 0; i < definesBuildingsChances.size(); i++) { if((getRandomGenerator().nextInt(1,100) <= definesBuildingsChances[i])) { - vti->builtBuildings.insert(basicDwellings[i]); + vti->addBuilding(basicDwellings[i]); } } } // village hall must always exist - vti->builtBuildings.insert(BuildingID::VILLAGE_HALL); + vti->addBuilding(BuildingID::VILLAGE_HALL); //init hordes - for (int i = 0; i < GameConstants::CREATURES_PER_TOWN; i++) + for (int i = 0; i < vti->getTown()->creatures.size(); i++) { - if (vstd::contains(vti->builtBuildings, hordes[i])) //if we have horde for this level + if(vti->hasBuilt(hordes[i])) //if we have horde for this level { - vti->builtBuildings.erase(hordes[i]);//remove old ID + vti->removeBuilding(hordes[i]);//remove old ID if (vti->getTown()->hordeLvl.at(0) == i)//if town first horde is this one { - vti->builtBuildings.insert(BuildingID::HORDE_1);//add it + vti->addBuilding(BuildingID::HORDE_1);//add it //if we have upgraded dwelling as well - if (vstd::contains(vti->builtBuildings, upgradedDwellings[i])) - vti->builtBuildings.insert(BuildingID::HORDE_1_UPGR);//add it as well + if(vti->hasBuilt(upgradedDwellings[i])) + vti->addBuilding(BuildingID::HORDE_1_UPGR);//add it as well } if (vti->getTown()->hordeLvl.at(1) == i)//if town second horde is this one { - vti->builtBuildings.insert(BuildingID::HORDE_2); - if (vstd::contains(vti->builtBuildings, upgradedDwellings[i])) - vti->builtBuildings.insert(BuildingID::HORDE_2_UPGR); + vti->addBuilding(BuildingID::HORDE_2); + if(vti->hasBuilt(upgradedDwellings[i])) + vti->addBuilding(BuildingID::HORDE_2_UPGR); } } } //#1444 - remove entries that don't have buildings defined (like some unused extra town hall buildings) //But DO NOT remove horde placeholders before they are replaced - vstd::erase_if(vti->builtBuildings, [vti](const BuildingID & bid) - { - return !vti->getTown()->buildings.count(bid) || !vti->getTown()->buildings.at(bid); - }); + for(const auto & building : vti->getBuildings()) + { + if(!vti->getTown()->buildings.count(building) || !vti->getTown()->buildings.at(building)) + vti->removeBuilding(building); + } - if (vstd::contains(vti->builtBuildings, BuildingID::SHIPYARD) && vti->shipyardStatus()==IBoatGenerator::TILE_BLOCKED) - vti->builtBuildings.erase(BuildingID::SHIPYARD);//if we have harbor without water - erase it (this is H3 behaviour) + if(vti->hasBuilt(BuildingID::SHIPYARD) && vti->shipyardStatus()==IBoatGenerator::TILE_BLOCKED) + vti->removeBuilding(BuildingID::SHIPYARD);//if we have harbor without water - erase it (this is H3 behaviour) //Early check for #1444-like problems - for([[maybe_unused]] const auto & building : vti->builtBuildings) + for([[maybe_unused]] const auto & building : vti->getBuildings()) { assert(vti->getTown()->buildings.at(building) != nullptr); } @@ -893,7 +870,7 @@ void CGameState::initTowns() //town events for(CCastleEvent &ev : vti->events) { - for (int i = 0; igetTown()->creatures.size(); i++) if (vstd::contains(ev.buildings,hordes[i])) //if we have horde for this level { ev.buildings.erase(hordes[i]); @@ -905,7 +882,7 @@ void CGameState::initTowns() } //init spells vti->spells.resize(GameConstants::SPELL_LEVELS); - + vti->possibleSpells -= SpellID::PRESET; for(ui32 z=0; zobligatorySpells.size();z++) { const auto * s = vti->obligatorySpells[z].toSpell(); @@ -918,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; @@ -926,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; @@ -941,8 +918,6 @@ void CGameState::initTowns() vti->possibleSpells -= s->id; } vti->possibleSpells.clear(); - if(vti->getOwner() != PlayerColor::NEUTRAL) - getPlayerState(vti->getOwner())->towns.emplace_back(vti); } } @@ -985,26 +960,22 @@ void CGameState::placeHeroesInTowns() if(player.first == PlayerColor::NEUTRAL) continue; - for(CGHeroInstance * h : player.second.heroes) + for(CGHeroInstance * h : player.second.getHeroes()) { - for(CGTownInstance * t : player.second.towns) + 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())); } } } @@ -1019,14 +990,14 @@ void CGameState::initVisitingAndGarrisonedHeroes() continue; //init visiting and garrisoned heroes - for(CGHeroInstance * h : player.second.heroes) + for(CGHeroInstance * h : player.second.getHeroes()) { - for(CGTownInstance * t : player.second.towns) + for(CGTownInstance * t : player.second.getTowns()) { 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); @@ -1049,7 +1020,7 @@ const BattleInfo * CGameState::getBattle(const PlayerColor & player) const return nullptr; for (const auto & battlePtr : currentBattles) - if (battlePtr->sides[0].color == player || battlePtr->sides[1].color == player) + if (battlePtr->getSide(BattleSide::ATTACKER).color == player || battlePtr->getSide(BattleSide::DEFENDER).color == player) return battlePtr.get(); return nullptr; @@ -1073,7 +1044,7 @@ BattleInfo * CGameState::getBattle(const BattleID & battle) return nullptr; } -BattleField CGameState::battleGetBattlefieldType(int3 tile, CRandomGenerator & rand) +BattleField CGameState::battleGetBattlefieldType(int3 tile, vstd::RNG & rand) { assert(tile.valid()); @@ -1091,7 +1062,7 @@ BattleField CGameState::battleGetBattlefieldType(int3 tile, CRandomGenerator & r 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(); @@ -1103,10 +1074,10 @@ BattleField CGameState::battleGetBattlefieldType(int3 tile, CRandomGenerator & r 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 @@ -1120,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) { @@ -1168,10 +1139,9 @@ PlayerRelations CGameState::getPlayerRelations( PlayerColor color1, PlayerColor return PlayerRelations::ENEMIES; } -void CGameState::apply(CPack *pack) +void CGameState::apply(CPackForClient & pack) { - ui16 typ = CTypeList::getInstance().getTypeID(pack); - applier->getApplier(typ)->applyOnGS(this, pack); + pack.applyGs(this); } void CGameState::calculatePaths(const CGHeroInstance *hero, CPathsInfo &out) @@ -1201,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) { @@ -1220,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) { @@ -1246,78 +1216,6 @@ int3 CGameState::guardingCreaturePosition (int3 pos) const return gs->map->guardingCreaturePositions[pos.z][pos.x][pos.y]; } -void CGameState::updateRumor() -{ - static const std::vector rumorTypes = {RumorState::TYPE_MAP, RumorState::TYPE_SPECIAL, RumorState::TYPE_RAND, RumorState::TYPE_RAND}; - std::vector sRumorTypes = { - RumorState::RUMOR_OBELISKS, RumorState::RUMOR_ARTIFACTS, RumorState::RUMOR_ARMY, RumorState::RUMOR_INCOME}; - if(map->grailPos.valid()) // Grail should always be on map, but I had related crash I didn't manage to reproduce - sRumorTypes.push_back(RumorState::RUMOR_GRAIL); - - int rumorId = -1; - int rumorExtra = -1; - auto & rand = getRandomGenerator(); - rumor.type = *RandomGeneratorUtil::nextItem(rumorTypes, rand); - - do - { - switch(rumor.type) - { - case RumorState::TYPE_SPECIAL: - { - SThievesGuildInfo tgi; - obtainPlayersStats(tgi, 20); - rumorId = *RandomGeneratorUtil::nextItem(sRumorTypes, rand); - if(rumorId == RumorState::RUMOR_GRAIL) - { - rumorExtra = getTile(map->grailPos)->terType->getIndex(); - break; - } - - std::vector players = {}; - switch(rumorId) - { - case RumorState::RUMOR_OBELISKS: - players = tgi.obelisks[0]; - break; - - case RumorState::RUMOR_ARTIFACTS: - players = tgi.artifacts[0]; - break; - - case RumorState::RUMOR_ARMY: - players = tgi.army[0]; - break; - - case RumorState::RUMOR_INCOME: - players = tgi.income[0]; - break; - } - rumorExtra = RandomGeneratorUtil::nextItem(players, rand)->getNum(); - - break; - } - case RumorState::TYPE_MAP: - // Makes sure that map rumors only used if there enough rumors too choose from - if(!map->rumors.empty() && (map->rumors.size() > 1 || !rumor.last.count(RumorState::TYPE_MAP))) - { - rumorId = rand.nextInt((int)map->rumors.size() - 1); - break; - } - else - rumor.type = RumorState::TYPE_RAND; - [[fallthrough]]; - - case RumorState::TYPE_RAND: - auto vector = VLC->generaltexth->findStringsWithPrefix("core.randtvrn"); - rumorId = rand.nextInt((int)vector.size() - 1); - - break; - } - } - while(!rumor.update(rumorId, rumorExtra)); -} - bool CGameState::isVisible(int3 pos, const std::optional & player) const { if (!map->isInTheMap(pos)) @@ -1329,7 +1227,7 @@ bool CGameState::isVisible(int3 pos, const std::optional & player) if(player->isSpectator()) return true; - return (*getPlayerTeam(*player)->fogOfWarMap)[pos.z][pos.x][pos.y]; + return getPlayerTeam(*player)->fogOfWarMap[pos.z][pos.x][pos.y]; } bool CGameState::isVisible(const CGObjectInstance * obj, const std::optional & player) const @@ -1348,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; } @@ -1416,7 +1314,7 @@ bool CGameState::checkForVictory(const PlayerColor & player, const EventConditio } case EventCondition::HAVE_ARTIFACT: //check if any hero has winning artifact { - for(const auto & elem : p->heroes) + for(const auto & elem : p->getHeroes()) if(elem->hasArt(condition.objectType.as())) return true; return false; @@ -1450,7 +1348,7 @@ bool CGameState::checkForVictory(const PlayerColor & player, const EventConditio } else // any town { - for (const CGTownInstance * t : p->towns) + for (const CGTownInstance * t : p->getTowns()) { if (t->hasBuilt(condition.objectType.as())) return true; @@ -1502,8 +1400,10 @@ bool CGameState::checkForVictory(const PlayerColor & player, const EventConditio case EventCondition::TRANSPORT: { const auto * t = getTown(condition.objectID); - return (t->visitingHero && t->visitingHero->getOwner() == player && t->visitingHero->hasArt(condition.objectType.as())) || - (t->garrisonHero && t->garrisonHero->getOwner() == player && t->garrisonHero->hasArt(condition.objectType.as())); + bool garrisonedWon = t->garrisonHero && t->garrisonHero->getOwner() == player && t->garrisonHero->hasArt(condition.objectType.as()); + bool visitingWon = t->visitingHero && t->visitingHero->getOwner() == player && t->visitingHero->hasArt(condition.objectType.as()); + + return garrisonedWon || visitingWon; } case EventCondition::DAYS_PASSED: { @@ -1538,6 +1438,9 @@ PlayerColor CGameState::checkForStandardWin() const TeamID winnerTeam = TeamID::NO_TEAM; for(const auto & elem : players) { + if(elem.second.status == EPlayerStatus::WINNER) + return elem.second.color; + if(elem.second.status == EPlayerStatus::INGAME && elem.first.isValidPlayer()) { if(supposedWinner == PlayerColor::NEUTRAL) @@ -1564,137 +1467,6 @@ bool CGameState::checkForStandardLoss(const PlayerColor & player) const return pState.checkVanquished(); } -struct statsHLP -{ - using TStat = std::pair; - //converts [] to vec[place] -> platers - static std::vector< std::vector< PlayerColor > > getRank( std::vector stats ) - { - std::sort(stats.begin(), stats.end(), statsHLP()); - - //put first element - std::vector< std::vector > ret; - std::vector tmp; - tmp.push_back( stats[0].first ); - ret.push_back( tmp ); - - //the rest of elements - for(int g=1; gpush_back( stats[g].first ); - } - else - { - //create next occupied rank - std::vector tmp; - tmp.push_back(stats[g].first); - ret.push_back(tmp); - } - } - - return ret; - } - - bool operator()(const TStat & a, const TStat & b) const - { - return a.second > b.second; - } - - static const CGHeroInstance * findBestHero(CGameState * gs, const PlayerColor & color) - { - std::vector > &h = gs->players[color].heroes; - if(h.empty()) - return nullptr; - //best hero will be that with highest exp - int best = 0; - for(int b=1; bexp > h[best]->exp) - { - best = b; - } - } - return h[best]; - } - - //calculates total number of artifacts that belong to given player - static int getNumberOfArts(const PlayerState * ps) - { - int ret = 0; - for(auto h : ps->heroes) - { - ret += (int)h->artifactsInBackpack.size() + (int)h->artifactsWorn.size(); - } - return ret; - } - - // get total strength of player army - static si64 getArmyStrength(const PlayerState * ps) - { - si64 str = 0; - - for(auto h : ps->heroes) - { - if(!h->inTownGarrison) //original h3 behavior - str += h->getArmyStrength(); - } - return str; - } - - // get total gold income - static int getIncome(const PlayerState * ps) - { - int totalIncome = 0; - const CGObjectInstance * heroOrTown = nullptr; - - //Heroes can produce gold as well - skill, specialty or arts - for(const auto & h : ps->heroes) - { - totalIncome += h->valOfBonuses(Selector::typeSubtype(BonusType::GENERATE_RESOURCE, BonusSubtypeID(GameResID(GameResID::GOLD)))); - - if(!heroOrTown) - heroOrTown = h; - } - - //Add town income of all towns - for(const auto & t : ps->towns) - { - totalIncome += t->dailyIncome()[EGameResID::GOLD]; - - if(!heroOrTown) - heroOrTown = t; - } - - /// FIXME: Dirty dirty hack - /// Stats helper need some access to gamestate. - std::vector ownedObjects; - for(const CGObjectInstance * obj : heroOrTown->cb->gameState()->map->objects) - { - if(obj && obj->tempOwner == ps->color) - ownedObjects.push_back(obj); - } - /// This is code from CPlayerSpecificInfoCallback::getMyObjects - /// I'm really need to find out about callback interface design... - - for(const auto * object : ownedObjects) - { - //Mines - if ( object->ID == Obj::MINE ) - { - const auto * mine = dynamic_cast(object); - assert(mine); - - if (mine->producedResource == EGameResID::GOLD) - totalIncome += mine->producedQuantity; - } - } - - return totalIncome; - } -}; - void CGameState::obtainPlayersStats(SThievesGuildInfo & tgi, int level) { auto playerInactive = [&](const PlayerColor & color) @@ -1714,7 +1486,7 @@ void CGameState::obtainPlayersStats(SThievesGuildInfo & tgi, int level) stat.second = VAL_GETTER; \ stats.push_back(stat); \ } \ - tgi.FIELD = statsHLP::getRank(stats); \ + tgi.FIELD = Statistic::getRank(stats); \ } for(auto & elem : players) @@ -1726,9 +1498,9 @@ void CGameState::obtainPlayersStats(SThievesGuildInfo & tgi, int level) if(level >= 0) //num of towns & num of heroes { //num of towns - FILL_FIELD(numOfTowns, g->second.towns.size()) + FILL_FIELD(numOfTowns, g->second.getTowns().size()) //num of heroes - FILL_FIELD(numOfHeroes, g->second.heroes.size()) + FILL_FIELD(numOfHeroes, g->second.getHeroes().size()) } if(level >= 1) //best hero's portrait { @@ -1736,7 +1508,7 @@ void CGameState::obtainPlayersStats(SThievesGuildInfo & tgi, int level) { if(playerInactive(player.second.color)) continue; - const CGHeroInstance * best = statsHLP::findBestHero(this, player.second.color); + const CGHeroInstance * best = Statistic::findBestHero(this, player.second.color); InfoAboutHero iah; iah.initFromHero(best, (level >= 2) ? InfoAboutHero::EInfoLevel::DETAILED : InfoAboutHero::EInfoLevel::BASIC); iah.army.clear(); @@ -1757,27 +1529,19 @@ void CGameState::obtainPlayersStats(SThievesGuildInfo & tgi, int level) } if(level >= 3) //obelisks found { - auto getObeliskVisited = [&](const TeamID & t) - { - if(map->obelisksVisited.count(t)) - return map->obelisksVisited[t]; - else - return ui8(0); - }; - - FILL_FIELD(obelisks, getObeliskVisited(gs->getPlayerTeam(g->second.color)->id)) + FILL_FIELD(obelisks, Statistic::getObeliskVisited(gs, gs->getPlayerTeam(g->second.color)->id)) } if(level >= 4) //artifacts { - FILL_FIELD(artifacts, statsHLP::getNumberOfArts(&g->second)) + FILL_FIELD(artifacts, Statistic::getNumberOfArts(&g->second)) } if(level >= 4) //army strength { - FILL_FIELD(army, statsHLP::getArmyStrength(&g->second)) + FILL_FIELD(army, Statistic::getArmyStrength(&g->second)) } if(level >= 5) //income { - FILL_FIELD(income, statsHLP::getIncome(&g->second)) + FILL_FIELD(income, Statistic::getIncome(gs, &g->second)) } if(level >= 2) //best hero's stats { @@ -1808,11 +1572,11 @@ void CGameState::obtainPlayersStats(SThievesGuildInfo & tgi, int level) if(playerInactive(player.second.color)) //do nothing for neutral player continue; CreatureID bestCre; //best creature's ID - for(const auto & elem : player.second.heroes) + for(const auto & elem : player.second.getHeroes()) { 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; @@ -1872,12 +1636,12 @@ void CGameState::attachArmedObjects() bool CGameState::giveHeroArtifact(CGHeroInstance * h, const ArtifactID & aid) { - CArtifactInstance * ai = ArtifactUtils::createNewArtifactInstance(aid); + CArtifactInstance * ai = ArtifactUtils::createArtifact(aid); map->addNewArtifactInstance(ai); 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 @@ -1897,18 +1661,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; @@ -1932,47 +1691,30 @@ 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; } return nullptr; } -bool RumorState::update(int id, int extra) -{ - if(vstd::contains(last, type)) - { - if(last[type].first != id) - { - last[type].first = id; - last[type].second = extra; - } - else - return false; - } - else - last[type] = std::make_pair(id, extra); - return true; -} TeamState::TeamState() { setNodeType(TEAM); - fogOfWarMap = std::make_unique>(); } -CRandomGenerator & CGameState::getRandomGenerator() +vstd::RNG & CGameState::getRandomGenerator() { - return rand; + return callback->getRandomGenerator(); } -ArtifactID CGameState::pickRandomArtifact(CRandomGenerator & rand, int flags, std::function accepts) +ArtifactID CGameState::pickRandomArtifact(vstd::RNG & rand, int flags, std::function accepts) { std::set potentialPicks; - // Select artifacts that satisfy provided criterias + // Select artifacts that satisfy provided criteria for (auto const & artifactID : map->allowedArtifact) { if (!VLC->arth->legalArtifact(artifactID)) @@ -2003,7 +1745,7 @@ ArtifactID CGameState::pickRandomArtifact(CRandomGenerator & rand, int flags, st return pickRandomArtifact(rand, potentialPicks); } -ArtifactID CGameState::pickRandomArtifact(CRandomGenerator & rand, std::set potentialPicks) +ArtifactID CGameState::pickRandomArtifact(vstd::RNG & rand, std::set potentialPicks) { // No allowed artifacts at all - give Grail - this can't be banned (hopefully) // FIXME: investigate how such cases are handled by H3 - some heavily customized user-made maps likely rely on H3 behavior @@ -2032,12 +1774,12 @@ ArtifactID CGameState::pickRandomArtifact(CRandomGenerator & rand, std::set accepts) +ArtifactID CGameState::pickRandomArtifact(vstd::RNG & rand, std::function accepts) { return pickRandomArtifact(rand, 0xff, std::move(accepts)); } -ArtifactID CGameState::pickRandomArtifact(CRandomGenerator & rand, int flags) +ArtifactID CGameState::pickRandomArtifact(vstd::RNG & rand, int flags) { return pickRandomArtifact(rand, flags, [](const ArtifactID &) { return true; }); } diff --git a/lib/gameState/CGameState.h b/lib/gameState/CGameState.h index ce2a4b102..a149575b4 100644 --- a/lib/gameState/CGameState.h +++ b/lib/gameState/CGameState.h @@ -9,11 +9,13 @@ */ #pragma once -#include "bonuses/CBonusSystemNode.h" -#include "IGameCallback.h" -#include "LoadProgress.h" -#include "ConstTransitivePtr.h" -#include "../CRandomGenerator.h" +#include "../bonuses/CBonusSystemNode.h" +#include "../IGameCallback.h" +#include "../LoadProgress.h" +#include "../ConstTransitivePtr.h" + +#include "RumorState.h" +#include "GameStatistics.h" namespace boost { @@ -34,38 +36,8 @@ class CStackInstance; class CGameStateCampaign; class TavernHeroesPool; struct SThievesGuildInfo; - -template class CApplier; -class CBaseForGSApply; - -struct DLL_LINKAGE RumorState -{ - enum ERumorType : ui8 - { - TYPE_NONE = 0, TYPE_RAND, TYPE_SPECIAL, TYPE_MAP - }; - - enum ERumorTypeSpecial : ui8 - { - RUMOR_OBELISKS = 208, - RUMOR_ARTIFACTS = 209, - RUMOR_ARMY = 210, - RUMOR_INCOME = 211, - RUMOR_GRAIL = 212 - }; - - ERumorType type; - std::map> last; - - RumorState(){type = TYPE_NONE;}; - bool update(int id, int extra); - - template void serialize(Handler &h) - { - h & type; - h & last; - } -}; +class CRandomGenerator; +class GameSettings; struct UpgradeInfo { @@ -79,10 +51,9 @@ class BattleInfo; DLL_LINKAGE std::ostream & operator<<(std::ostream & os, const EVictoryLossCheckResult & victoryLossCheckResult); -class DLL_LINKAGE CGameState : public CNonConstInfoCallback +class DLL_LINKAGE CGameState : public CNonConstInfoCallback, public Serializeable { friend class CGameStateCampaign; - public: /// Stores number of times each artifact was placed on map via randomization std::map allocatedArtifacts; @@ -115,7 +86,9 @@ public: std::map players; std::map teams; CBonusSystemNode globalEffects; - RumorState rumor; + RumorState currentRumor; + + StatisticDataSet statistic; static boost::shared_mutex mutex; @@ -125,8 +98,8 @@ 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(CPack *pack); - BattleField battleGetBattlefieldType(int3 tile, CRandomGenerator & rand); + void apply(CPackForClient & pack); + BattleField battleGetBattlefieldType(int3 tile, vstd::RNG & rand); void fillUpgradeInfo(const CArmedInstance *obj, SlotID stackPos, UpgradeInfo &out) const override; PlayerRelations getPlayerRelations(PlayerColor color1, PlayerColor color2) const override; @@ -135,13 +108,12 @@ public: void calculatePaths(const std::shared_ptr & config) override; int3 guardingCreaturePosition (int3 pos) const override; std::vector guardingCreatures (int3 pos) const; - void updateRumor(); /// Gets a artifact ID randomly and removes the selected artifact from this handler. - ArtifactID pickRandomArtifact(CRandomGenerator & rand, int flags); - ArtifactID pickRandomArtifact(CRandomGenerator & rand, std::function accepts); - ArtifactID pickRandomArtifact(CRandomGenerator & rand, int flags, std::function accepts); - ArtifactID pickRandomArtifact(CRandomGenerator & rand, std::set filtered); + ArtifactID pickRandomArtifact(vstd::RNG & rand, int flags); + ArtifactID pickRandomArtifact(vstd::RNG & rand, std::function accepts); + ArtifactID pickRandomArtifact(vstd::RNG & rand, int flags, std::function accepts); + ArtifactID pickRandomArtifact(vstd::RNG & rand, std::set filtered); /// Returns battle in which selected player is engaged, or nullptr if none. /// Can NOT be used with neutral player, use battle by ID instead @@ -158,10 +130,12 @@ public: bool checkForStandardLoss(const PlayerColor & player) const; //checks if given player lost the game void obtainPlayersStats(SThievesGuildInfo & tgi, int level); //fills tgi with info about other players that is available at given level of thieves' guild + const IGameSettings & getSettings() const; bool isVisible(int3 pos, const std::optional & player) const override; bool isVisible(const CGObjectInstance * obj, const std::optional & player) const override; + static int getDate(int day, Date mode); int getDate(Date mode=Date::DAY) const override; //mode=0 - total days in game, mode=1 - day of week, mode=2 - current week, mode=3 - current month // ----- getters, setters ----- @@ -169,11 +143,11 @@ public: /// This RNG should only be used inside GS or CPackForClient-derived applyGs /// If this doesn't work for your code that mean you need a new netpack /// - /// Client-side must use CRandomGenerator::getDefault which is not serialized + /// Client-side must use vstd::RNG::getDefault which is not serialized /// - /// CGameHandler have it's own getter for CRandomGenerator::getDefault - /// Any server-side code outside of GH must use CRandomGenerator::getDefault - CRandomGenerator & getRandomGenerator(); + /// CGameHandler have it's own getter for vstd::RNG::getDefault + /// Any server-side code outside of GH must use vstd::RNG::getDefault + vstd::RNG & getRandomGenerator(); template void serialize(Handler &h) { @@ -183,13 +157,21 @@ public: h & day; h & map; h & players; + if (h.version < Handler::Version::PLAYER_STATE_OWNED_OBJECTS) + generateOwnedObjectsAfterDeserialize(); h & teams; h & heroesPool; h & globalEffects; - h & rand; - h & rumor; + if (h.version < Handler::Version::REMOVE_LIB_RNG) + { + std::string oldStateOfRNG; + h & oldStateOfRNG; + } + h & currentRumor; h & campaign; h & allocatedArtifacts; + if (h.version >= Handler::Version::STATISTICS) + h & statistic; BONUS_TREE_DESERIALIZATION_FIX } @@ -197,10 +179,10 @@ public: private: // ----- initialization ----- void initNewGame(const IMapService * mapService, bool allowSavingRandomMap, Load::ProgressAccumulator & progressTracking); - void checkMapChecksum(); void initGlobalBonuses(); void initGrailPosition(); void initRandomFactionsForPlayers(); + void initOwnedObjects(); void randomizeMapObjects(); void initPlayerStates(); void placeStartingHeroes(); @@ -217,6 +199,8 @@ private: void initVisitingAndGarrisonedHeroes(); void initCampaign(); + void generateOwnedObjectsAfterDeserialize(); + // ----- bonus system handling ----- void buildBonusSystemTree(); @@ -233,11 +217,9 @@ private: UpgradeInfo fillUpgradeInfo(const CStackInstance &stack) const; // ---- data ----- - std::shared_ptr> applier; - CRandomGenerator rand; Services * services; - /// Ponter to campaign state manager. Nullptr for single scenarios + /// Pointer to campaign state manager. Nullptr for single scenarios std::unique_ptr campaign; friend class IGameCallback; diff --git a/lib/gameState/CGameStateCampaign.cpp b/lib/gameState/CGameStateCampaign.cpp index 7f11c92fb..892435cc6 100644 --- a/lib/gameState/CGameStateCampaign.cpp +++ b/lib/gameState/CGameStateCampaign.cpp @@ -14,6 +14,10 @@ #include "QuestInfo.h" #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" @@ -21,13 +25,14 @@ #include "../mapObjectConstructors/AObjectTypeHandler.h" #include "../mapObjectConstructors/CObjectClassesHandler.h" #include "../StartInfo.h" -#include "../CBuildingHandler.h" -#include "../CHeroHandler.h" #include "../mapping/CMap.h" #include "../ArtifactUtils.h" #include "../CPlayerState.h" #include "../serializer/CMemorySerializer.h" +#include +#include + VCMI_LIB_NAMESPACE_BEGIN CampaignHeroReplacement::CampaignHeroReplacement(CGHeroInstance * hero, const ObjectInstanceID & heroPlaceholderId): @@ -83,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()]; } } } @@ -93,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(); } } @@ -132,15 +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->getType()->getJsonKey(), al.slot.getNum(), hero.hero->getHeroTypeName()); hero.transferrableArtifacts.push_back(artifactPosition); + } if (!locked && !takeable) { - 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; @@ -233,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) { @@ -320,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; @@ -361,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; @@ -410,19 +419,21 @@ void CGameStateCampaign::transferMissingArtifacts(const CampaignTravel & travelO if (!donorHero) throw std::runtime_error("Failed to find hero to take artifacts from! Scenario: " + gameState->map->name.toString()); - for (auto const & artLocation : campaignHeroReplacement.transferrableArtifacts) + // process in reverse - 2nd artifact from a backpack must be processed before 1st one to avoid invalidation of artifact positions + for (auto const & artLocation : boost::adaptors::reverse(campaignHeroReplacement.transferrableArtifacts)) { auto * artifact = donorHero->getArt(artLocation); - if (!donorHero) - throw std::runtime_error("Failed to find artifacts to transfer to travelling hero! Scenario: " + gameState->map->name.toString()); - 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->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!"); } @@ -533,7 +544,7 @@ void CGameStateCampaign::initHeroes() } assert(humanPlayer != PlayerColor::NEUTRAL); - std::vector > & heroes = gameState->players[humanPlayer].heroes; + const auto & heroes = gameState->players[humanPlayer].getHeroes(); if (chosenBonus->info1 == 0xFFFD) //most powerful { @@ -552,11 +563,11 @@ void CGameStateCampaign::initHeroes() } else //specific hero { - for (auto & heroe : heroes) + for (auto & hero : heroes) { - if (heroe->getHeroType().getNum() == chosenBonus->info1) + if (hero->getHeroTypeID().getNum() == chosenBonus->info1) { - giveCampaignBonusToHero(heroe); + giveCampaignBonusToHero(hero); break; } } @@ -646,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->builtBuildings); + 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) @@ -661,12 +672,12 @@ void CGameStateCampaign::initTowns() if (newBuilding == BuildingID::NONE) break; - if (town->builtBuildings.count(newBuilding) != 0) + if(town->hasBuilt(newBuilding)) break; - town->builtBuildings.insert(newBuilding); + 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/CGameStateCampaign.h b/lib/gameState/CGameStateCampaign.h index 2614d0621..4b4d6645e 100644 --- a/lib/gameState/CGameStateCampaign.h +++ b/lib/gameState/CGameStateCampaign.h @@ -11,6 +11,7 @@ #include "../GameConstants.h" #include "../campaign/CampaignConstants.h" +#include "../serializer/Serializeable.h" VCMI_LIB_NAMESPACE_BEGIN @@ -28,7 +29,7 @@ struct CampaignHeroReplacement std::vector transferrableArtifacts; }; -class CGameStateCampaign +class CGameStateCampaign : public Serializeable { CGameState * gameState; diff --git a/lib/gameState/EVictoryLossCheckResult.h b/lib/gameState/EVictoryLossCheckResult.h index c6f056222..f7860d78d 100644 --- a/lib/gameState/EVictoryLossCheckResult.h +++ b/lib/gameState/EVictoryLossCheckResult.h @@ -9,8 +9,6 @@ */ #pragma once -#include "MetaString.h" - VCMI_LIB_NAMESPACE_BEGIN class DLL_LINKAGE EVictoryLossCheckResult diff --git a/lib/gameState/GameStatistics.cpp b/lib/gameState/GameStatistics.cpp new file mode 100644 index 000000000..46b797845 --- /dev/null +++ b/lib/gameState/GameStatistics.cpp @@ -0,0 +1,394 @@ +/* + * GameStatistics.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 "GameStatistics.h" +#include "../CPlayerState.h" +#include "../constants/StringConstants.h" +#include "../VCMIDirs.h" +#include "CGameState.h" +#include "TerrainHandler.h" +#include "StartInfo.h" +#include "HighScore.h" +#include "../mapObjects/CGHeroInstance.h" +#include "../mapObjects/CGTownInstance.h" +#include "../mapObjects/CGObjectInstance.h" +#include "../mapObjects/MiscObjects.h" +#include "../mapping/CMap.h" +#include "../entities/building/CBuilding.h" + + +VCMI_LIB_NAMESPACE_BEGIN + +void StatisticDataSet::add(StatisticDataSetEntry entry) +{ + data.push_back(entry); +} + +StatisticDataSetEntry StatisticDataSet::createEntry(const PlayerState * ps, const CGameState * gs) +{ + StatisticDataSetEntry data; + + HighScoreParameter param = HighScore::prepareHighScores(gs, ps->color, false); + HighScoreCalculation scenarioHighScores; + scenarioHighScores.parameters.push_back(param); + scenarioHighScores.isCampaign = false; + + data.map = gs->map->name.toString(); + data.timestamp = std::time(nullptr); + data.day = gs->getDate(Date::DAY); + data.player = ps->color; + data.playerName = gs->getStartInfo()->playerInfos.at(ps->color).name; + data.team = ps->team; + data.isHuman = ps->isHuman(); + data.status = ps->status; + data.resources = ps->resources; + data.numberHeroes = ps->getHeroes().size(); + data.numberTowns = gs->howManyTowns(ps->color); + data.numberArtifacts = Statistic::getNumberOfArts(ps); + data.numberDwellings = Statistic::getNumberOfDwellings(ps); + data.armyStrength = Statistic::getArmyStrength(ps, true); + data.totalExperience = Statistic::getTotalExperience(ps); + data.income = Statistic::getIncome(gs, ps); + data.mapExploredRatio = Statistic::getMapExploredRatio(gs, ps->color); + data.obeliskVisitedRatio = Statistic::getObeliskVisitedRatio(gs, ps->team); + data.townBuiltRatio = Statistic::getTownBuiltRatio(ps); + data.hasGrail = param.hasGrail; + data.numMines = Statistic::getNumMines(gs, ps); + data.score = scenarioHighScores.calculate().total; + data.maxHeroLevel = Statistic::findBestHero(gs, ps->color) ? Statistic::findBestHero(gs, ps->color)->level : 0; + data.numBattlesNeutral = gs->statistic.accumulatedValues.count(ps->color) ? gs->statistic.accumulatedValues.at(ps->color).numBattlesNeutral : 0; + data.numBattlesPlayer = gs->statistic.accumulatedValues.count(ps->color) ? gs->statistic.accumulatedValues.at(ps->color).numBattlesPlayer : 0; + data.numWinBattlesNeutral = gs->statistic.accumulatedValues.count(ps->color) ? gs->statistic.accumulatedValues.at(ps->color).numWinBattlesNeutral : 0; + data.numWinBattlesPlayer = gs->statistic.accumulatedValues.count(ps->color) ? gs->statistic.accumulatedValues.at(ps->color).numWinBattlesPlayer : 0; + data.numHeroSurrendered = gs->statistic.accumulatedValues.count(ps->color) ? gs->statistic.accumulatedValues.at(ps->color).numHeroSurrendered : 0; + data.numHeroEscaped = gs->statistic.accumulatedValues.count(ps->color) ? gs->statistic.accumulatedValues.at(ps->color).numHeroEscaped : 0; + data.spentResourcesForArmy = gs->statistic.accumulatedValues.count(ps->color) ? gs->statistic.accumulatedValues.at(ps->color).spentResourcesForArmy : TResources(); + data.spentResourcesForBuildings = gs->statistic.accumulatedValues.count(ps->color) ? gs->statistic.accumulatedValues.at(ps->color).spentResourcesForBuildings : TResources(); + data.tradeVolume = gs->statistic.accumulatedValues.count(ps->color) ? gs->statistic.accumulatedValues.at(ps->color).tradeVolume : TResources(); + data.eventCapturedTown = gs->statistic.accumulatedValues.count(ps->color) ? gs->statistic.accumulatedValues.at(ps->color).lastCapturedTownDay == gs->getDate(Date::DAY) : false; + data.eventDefeatedStrongestHero = gs->statistic.accumulatedValues.count(ps->color) ? gs->statistic.accumulatedValues.at(ps->color).lastDefeatedStrongestHeroDay == gs->getDate(Date::DAY) : false; + data.movementPointsUsed = gs->statistic.accumulatedValues.count(ps->color) ? gs->statistic.accumulatedValues.at(ps->color).movementPointsUsed : 0; + + return data; +} + +std::string StatisticDataSet::toCsv(std::string sep) +{ + std::stringstream ss; + + auto resources = std::vector{EGameResID::GOLD, EGameResID::WOOD, EGameResID::MERCURY, EGameResID::ORE, EGameResID::SULFUR, EGameResID::CRYSTAL, EGameResID::GEMS}; + + ss << "Map" << sep; + ss << "Timestamp" << sep; + ss << "Day" << sep; + ss << "Player" << sep; + ss << "PlayerName" << sep; + ss << "Team" << sep; + ss << "IsHuman" << sep; + ss << "Status" << sep; + ss << "NumberHeroes" << sep; + ss << "NumberTowns" << sep; + ss << "NumberArtifacts" << sep; + ss << "NumberDwellings" << sep; + ss << "ArmyStrength" << sep; + ss << "TotalExperience" << sep; + ss << "Income" << sep; + ss << "MapExploredRatio" << sep; + ss << "ObeliskVisitedRatio" << sep; + ss << "TownBuiltRatio" << sep; + ss << "HasGrail" << sep; + ss << "Score" << sep; + ss << "MaxHeroLevel" << sep; + ss << "NumBattlesNeutral" << sep; + ss << "NumBattlesPlayer" << sep; + ss << "NumWinBattlesNeutral" << sep; + ss << "NumWinBattlesPlayer" << sep; + ss << "NumHeroSurrendered" << sep; + ss << "NumHeroEscaped" << sep; + ss << "EventCapturedTown" << sep; + ss << "EventDefeatedStrongestHero" << sep; + ss << "MovementPointsUsed"; + for(auto & resource : resources) + ss << sep << GameConstants::RESOURCE_NAMES[resource]; + for(auto & resource : resources) + ss << sep << GameConstants::RESOURCE_NAMES[resource] + "Mines"; + for(auto & resource : resources) + ss << sep << GameConstants::RESOURCE_NAMES[resource] + "SpentResourcesForArmy"; + for(auto & resource : resources) + ss << sep << GameConstants::RESOURCE_NAMES[resource] + "SpentResourcesForBuildings"; + for(auto & resource : resources) + ss << sep << GameConstants::RESOURCE_NAMES[resource] + "TradeVolume"; + ss << "\r\n"; + + for(auto & entry : data) + { + ss << entry.map << sep; + ss << vstd::getFormattedDateTime(entry.timestamp, "%Y-%m-%dT%H:%M:%S") << sep; + ss << entry.day << sep; + ss << GameConstants::PLAYER_COLOR_NAMES[entry.player] << sep; + ss << entry.playerName << sep; + ss << entry.team.getNum() << sep; + ss << entry.isHuman << sep; + ss << static_cast(entry.status) << sep; + ss << entry.numberHeroes << sep; + ss << entry.numberTowns << sep; + ss << entry.numberArtifacts << sep; + ss << entry.numberDwellings << sep; + ss << entry.armyStrength << sep; + ss << entry.totalExperience << sep; + ss << entry.income << sep; + ss << entry.mapExploredRatio << sep; + ss << entry.obeliskVisitedRatio << sep; + ss << entry.townBuiltRatio << sep; + ss << entry.hasGrail << sep; + ss << entry.score << sep; + ss << entry.maxHeroLevel << sep; + ss << entry.numBattlesNeutral << sep; + ss << entry.numBattlesPlayer << sep; + ss << entry.numWinBattlesNeutral << sep; + ss << entry.numWinBattlesPlayer << sep; + ss << entry.numHeroSurrendered << sep; + ss << entry.numHeroEscaped << sep; + ss << entry.eventCapturedTown << sep; + ss << entry.eventDefeatedStrongestHero << sep; + ss << entry.movementPointsUsed; + for(auto & resource : resources) + ss << sep << entry.resources[resource]; + for(auto & resource : resources) + ss << sep << entry.numMines[resource]; + for(auto & resource : resources) + ss << sep << entry.spentResourcesForArmy[resource]; + for(auto & resource : resources) + ss << sep << entry.spentResourcesForBuildings[resource]; + for(auto & resource : resources) + ss << sep << entry.tradeVolume[resource]; + ss << "\r\n"; + } + + return ss.str(); +} + +std::string StatisticDataSet::writeCsv() +{ + const boost::filesystem::path outPath = VCMIDirs::get().userCachePath() / "statistic"; + boost::filesystem::create_directories(outPath); + + const boost::filesystem::path filePath = outPath / (vstd::getDateTimeISO8601Basic(std::time(nullptr)) + ".csv"); + std::ofstream file(filePath.c_str()); + std::string csv = toCsv(";"); + file << csv; + + return filePath.string(); +} + +std::vector Statistic::getMines(const CGameState * gs, const PlayerState * ps) +{ + std::vector tmp; + + std::vector ownedObjects; + for(const CGObjectInstance * obj : gs->map->objects) + { + if(obj && obj->tempOwner == ps->color) + ownedObjects.push_back(obj); + } + /// This is code from CPlayerSpecificInfoCallback::getMyObjects + /// I'm really need to find out about callback interface design... + + for(const auto * object : ownedObjects) + { + //Mines + if ( object->ID == Obj::MINE ) + { + const auto * mine = dynamic_cast(object); + assert(mine); + + tmp.push_back(mine); + } + } + + return tmp; +} + +//calculates total number of artifacts that belong to given player +int Statistic::getNumberOfArts(const PlayerState * ps) +{ + int ret = 0; + for(auto h : ps->getHeroes()) + { + ret += h->artifactsInBackpack.size() + h->artifactsWorn.size(); + } + return ret; +} + +int Statistic::getNumberOfDwellings(const PlayerState * ps) +{ + int ret = 0; + for(const auto * obj : ps->getOwnedObjects()) + if (!obj->asOwnable()->providedCreatures().empty()) + ret += 1; + + return ret; +} + +// get total strength of player army +si64 Statistic::getArmyStrength(const PlayerState * ps, bool withTownGarrison) +{ + si64 str = 0; + + for(auto h : ps->getHeroes()) + { + if(!h->inTownGarrison || withTownGarrison) //original h3 behavior + str += h->getArmyStrength(); + } + return str; +} + +// get total experience of all heroes +si64 Statistic::getTotalExperience(const PlayerState * ps) +{ + si64 tmp = 0; + + for(auto h : ps->getHeroes()) + tmp += h->exp; + + return tmp; +} + +// get total gold income +int Statistic::getIncome(const CGameState * gs, const PlayerState * ps) +{ + int totalIncome = 0; + + //Heroes can produce gold as well - skill, specialty or arts + for(const auto & h : ps->getHeroes()) + totalIncome += h->dailyIncome()[EGameResID::GOLD]; + + //Add town income of all towns + for(const auto & t : ps->getTowns()) + totalIncome += t->dailyIncome()[EGameResID::GOLD]; + + for(const CGMine * mine : getMines(gs, ps)) + totalIncome += mine->dailyIncome()[EGameResID::GOLD]; + + return totalIncome; +} + +float Statistic::getMapExploredRatio(const CGameState * gs, PlayerColor player) +{ + float visible = 0.0; + float numTiles = 0.0; + + for(int layer = 0; layer < (gs->map->twoLevel ? 2 : 1); layer++) + for(int y = 0; y < gs->map->height; ++y) + for(int x = 0; x < gs->map->width; ++x) + { + TerrainTile tile = gs->map->getTile(int3(x, y, layer)); + + if(tile.blocked() && !tile.visitable()) + continue; + + if(gs->isVisible(int3(x, y, layer), player)) + visible++; + numTiles++; + } + + return visible / numTiles; +} + +const CGHeroInstance * Statistic::findBestHero(const CGameState * gs, const PlayerColor & color) +{ + const auto &h = gs->players.at(color).getHeroes(); + if(h.empty()) + return nullptr; + //best hero will be that with highest exp + int best = 0; + for(int b=1; bexp > h[best]->exp) + { + best = b; + } + } + return h[best]; +} + +std::vector> Statistic::getRank(std::vector> stats) +{ + std::sort(stats.begin(), stats.end(), [](const std::pair & a, const std::pair & b) { return a.second > b.second; }); + + //put first element + std::vector< std::vector > ret; + ret.push_back( { stats[0].first } ); + + //the rest of elements + for(int g=1; gpush_back( stats[g].first ); + } + else + { + //create next occupied rank + ret.push_back( { stats[g].first }); + } + } + + return ret; +} + +int Statistic::getObeliskVisited(const CGameState * gs, const TeamID & t) +{ + if(gs->map->obelisksVisited.count(t)) + return gs->map->obelisksVisited.at(t); + else + return 0; +} + +float Statistic::getObeliskVisitedRatio(const CGameState * gs, const TeamID & t) +{ + if(!gs->map->obeliskCount) + return 0; + return static_cast(getObeliskVisited(gs, t)) / gs->map->obeliskCount; +} + +std::map Statistic::getNumMines(const CGameState * gs, const PlayerState * ps) +{ + std::map tmp; + + for(auto & res : EGameResID::ALL_RESOURCES()) + tmp[res] = 0; + + for(const CGMine * mine : getMines(gs, ps)) + tmp[mine->producedResource]++; + + return tmp; +} + +float Statistic::getTownBuiltRatio(const PlayerState * ps) +{ + float built = 0.0; + float total = 0.0; + + for(const auto & t : ps->getTowns()) + { + built += t->getBuildings().size(); + for(const auto & b : t->getTown()->buildings) + if(!t->forbiddenBuildings.count(b.first)) + total += 1; + } + + if(total < 1) + return 0; + + return built / total; +} + +VCMI_LIB_NAMESPACE_END diff --git a/lib/gameState/GameStatistics.h b/lib/gameState/GameStatistics.h new file mode 100644 index 000000000..851f6417c --- /dev/null +++ b/lib/gameState/GameStatistics.h @@ -0,0 +1,174 @@ +/* + * GameSTatistics.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 "../GameConstants.h" +#include "../ResourceSet.h" + +VCMI_LIB_NAMESPACE_BEGIN + +class PlayerState; +class CGameState; +class CGHeroInstance; +class CGMine; + +struct DLL_LINKAGE StatisticDataSetEntry +{ + std::string map; + time_t timestamp; + int day; + PlayerColor player; + std::string playerName; + TeamID team; + bool isHuman; + EPlayerStatus status; + TResources resources; + int numberHeroes; + int numberTowns; + int numberArtifacts; + int numberDwellings; + si64 armyStrength; + si64 totalExperience; + int income; + float mapExploredRatio; + float obeliskVisitedRatio; + float townBuiltRatio; + bool hasGrail; + std::map numMines; + int score; + int maxHeroLevel; + int numBattlesNeutral; + int numBattlesPlayer; + int numWinBattlesNeutral; + int numWinBattlesPlayer; + int numHeroSurrendered; + int numHeroEscaped; + TResources spentResourcesForArmy; + TResources spentResourcesForBuildings; + TResources tradeVolume; + bool eventCapturedTown; + bool eventDefeatedStrongestHero; + si64 movementPointsUsed; + + template void serialize(Handler &h) + { + h & map; + h & timestamp; + h & day; + h & player; + if(h.version >= Handler::Version::STATISTICS_SCREEN) + h & playerName; + h & team; + h & isHuman; + h & status; + h & resources; + h & numberHeroes; + h & numberTowns; + h & numberArtifacts; + h & numberDwellings; + h & armyStrength; + h & totalExperience; + h & income; + h & mapExploredRatio; + h & obeliskVisitedRatio; + h & townBuiltRatio; + h & hasGrail; + h & numMines; + h & score; + h & maxHeroLevel; + h & numBattlesNeutral; + h & numBattlesPlayer; + h & numWinBattlesNeutral; + h & numWinBattlesPlayer; + h & numHeroSurrendered; + h & numHeroEscaped; + h & spentResourcesForArmy; + h & spentResourcesForBuildings; + h & tradeVolume; + if(h.version >= Handler::Version::STATISTICS_SCREEN) + { + h & eventCapturedTown; + h & eventDefeatedStrongestHero; + } + h & movementPointsUsed; + } +}; + +class DLL_LINKAGE StatisticDataSet +{ +public: + void add(StatisticDataSetEntry entry); + static StatisticDataSetEntry createEntry(const PlayerState * ps, const CGameState * gs); + std::string toCsv(std::string sep); + std::string writeCsv(); + + struct PlayerAccumulatedValueStorage // holds some actual values needed for stats + { + int numBattlesNeutral; + int numBattlesPlayer; + int numWinBattlesNeutral; + int numWinBattlesPlayer; + int numHeroSurrendered; + int numHeroEscaped; + TResources spentResourcesForArmy; + TResources spentResourcesForBuildings; + TResources tradeVolume; + si64 movementPointsUsed; + int lastCapturedTownDay; + int lastDefeatedStrongestHeroDay; + + template void serialize(Handler &h) + { + h & numBattlesNeutral; + h & numBattlesPlayer; + h & numWinBattlesNeutral; + h & numWinBattlesPlayer; + h & numHeroSurrendered; + h & numHeroEscaped; + h & spentResourcesForArmy; + h & spentResourcesForBuildings; + h & tradeVolume; + h & movementPointsUsed; + if(h.version >= Handler::Version::STATISTICS_SCREEN) + { + h & lastCapturedTownDay; + h & lastDefeatedStrongestHeroDay; + } + } + }; + std::vector data; + std::map accumulatedValues; + + template void serialize(Handler &h) + { + h & data; + h & accumulatedValues; + } +}; + +class DLL_LINKAGE Statistic +{ + static std::vector getMines(const CGameState * gs, const PlayerState * ps); +public: + static int getNumberOfArts(const PlayerState * ps); + static int getNumberOfDwellings(const PlayerState * ps); + static si64 getArmyStrength(const PlayerState * ps, bool withTownGarrison = false); + static si64 getTotalExperience(const PlayerState * ps); + static int getIncome(const CGameState * gs, const PlayerState * ps); + static float getMapExploredRatio(const CGameState * gs, PlayerColor player); + static const CGHeroInstance * findBestHero(const CGameState * gs, const PlayerColor & color); + static std::vector> getRank(std::vector> stats); + static int getObeliskVisited(const CGameState * gs, const TeamID & t); + static float getObeliskVisitedRatio(const CGameState * gs, const TeamID & t); + static std::map getNumMines(const CGameState * gs, const PlayerState * ps); + static float getTownBuiltRatio(const PlayerState * ps); +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/gameState/HighScore.cpp b/lib/gameState/HighScore.cpp new file mode 100644 index 000000000..9507946b5 --- /dev/null +++ b/lib/gameState/HighScore.cpp @@ -0,0 +1,111 @@ +/* + * HighScore.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 "HighScore.h" +#include "../CPlayerState.h" +#include "../constants/StringConstants.h" +#include "CGameState.h" +#include "StartInfo.h" +#include "../mapping/CMapHeader.h" +#include "../mapObjects/CGHeroInstance.h" +#include "../mapObjects/CGTownInstance.h" + +VCMI_LIB_NAMESPACE_BEGIN + +HighScoreParameter HighScore::prepareHighScores(const CGameState * gs, PlayerColor player, bool victory) +{ + const auto * playerState = gs->getPlayerState(player); + + HighScoreParameter param; + param.difficulty = gs->getStartInfo()->difficulty; + param.day = gs->getDate(); + param.townAmount = gs->howManyTowns(player); + param.usedCheat = gs->getPlayerState(player)->cheated; + param.hasGrail = false; + for(const CGHeroInstance * h : playerState->getHeroes()) + if(h->hasArt(ArtifactID::GRAIL)) + param.hasGrail = true; + for(const CGTownInstance * t : playerState->getTowns()) + if(t->hasBuilt(BuildingID::GRAIL)) + param.hasGrail = true; + param.allEnemiesDefeated = true; + for (PlayerColor otherPlayer(0); otherPlayer < PlayerColor::PLAYER_LIMIT; ++otherPlayer) + { + auto ps = gs->getPlayerState(otherPlayer, false); + if(ps && otherPlayer != player && !ps->checkVanquished()) + param.allEnemiesDefeated = false; + } + param.scenarioName = gs->getMapHeader()->name.toString(); + param.playerName = gs->getStartInfo()->playerInfos.find(player)->second.name; + + return param; +} + +HighScoreCalculation::Result HighScoreCalculation::calculate() +{ + Result firstResult; + Result summary; + const std::array difficultyMultipliers{0.8, 1.0, 1.3, 1.6, 2.0}; + for(const auto & param : parameters) + { + double tmp = 200 - (param.day + 10) / (param.townAmount + 5) + (param.allEnemiesDefeated ? 25 : 0) + (param.hasGrail ? 25 : 0); + firstResult = Result{static_cast(tmp), static_cast(tmp * difficultyMultipliers.at(param.difficulty)), param.day, param.usedCheat}; + summary.basic += firstResult.basic * 5.0 / parameters.size(); + summary.total += firstResult.total * 5.0 / parameters.size(); + summary.sumDays += firstResult.sumDays; + summary.cheater |= firstResult.cheater; + } + + if(parameters.size() == 1) + return firstResult; + + return summary; +} + +struct HighScoreCreature +{ + CreatureID creature; + int min; + int max; +}; + +static std::vector getHighscoreCreaturesList() +{ + JsonNode configCreatures(JsonPath::builtin("CONFIG/highscoreCreatures.json")); + + std::vector ret; + + for(auto & json : configCreatures["creatures"].Vector()) + { + HighScoreCreature entry; + entry.creature = CreatureID::decode(json["creature"].String()); + entry.max = json["max"].isNull() ? std::numeric_limits::max() : json["max"].Integer(); + entry.min = json["min"].isNull() ? std::numeric_limits::min() : json["min"].Integer(); + + ret.push_back(entry); + } + + return ret; +} + +CreatureID HighScoreCalculation::getCreatureForPoints(int points, bool campaign) +{ + static const std::vector creatures = getHighscoreCreaturesList(); + + int divide = campaign ? 5 : 1; + + for(auto & creature : creatures) + if(points / divide <= creature.max && points / divide >= creature.min) + return creature.creature; + + throw std::runtime_error("Unable to find creature for score " + std::to_string(points)); +} + +VCMI_LIB_NAMESPACE_END diff --git a/lib/gameState/HighScore.h b/lib/gameState/HighScore.h new file mode 100644 index 000000000..b031ed1b4 --- /dev/null +++ b/lib/gameState/HighScore.h @@ -0,0 +1,68 @@ +/* + * HighScore.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 "../GameConstants.h" + +VCMI_LIB_NAMESPACE_BEGIN + +class CGameState; + +class DLL_LINKAGE HighScoreParameter +{ +public: + int difficulty; + int day; + int townAmount; + bool usedCheat; + bool hasGrail; + bool allEnemiesDefeated; + std::string campaignName; + std::string scenarioName; + std::string playerName; + + template void serialize(Handler &h) + { + h & difficulty; + h & day; + h & townAmount; + h & usedCheat; + h & hasGrail; + h & allEnemiesDefeated; + h & campaignName; + h & scenarioName; + h & playerName; + } +}; +class DLL_LINKAGE HighScore +{ +public: + static HighScoreParameter prepareHighScores(const CGameState * gs, PlayerColor player, bool victory); +}; + +class DLL_LINKAGE HighScoreCalculation +{ +public: + struct Result + { + int basic = 0; + int total = 0; + int sumDays = 0; + bool cheater = false; + }; + + std::vector parameters; + bool isCampaign = false; + + Result calculate(); + static CreatureID getCreatureForPoints(int points, bool campaign); +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/gameState/InfoAboutArmy.cpp b/lib/gameState/InfoAboutArmy.cpp index f5c807fe5..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(); @@ -166,7 +168,7 @@ void InfoAboutTown::initFromTown(const CGTownInstance *t, bool detailed) { initFromArmy(t, detailed); army = ArmyDescriptor(t->getUpperArmy(), detailed); - built = t->builded; + built = t->built; fortLevel = t->fortLevel(); name = t->getNameTranslated(); tType = t->getTown(); diff --git a/lib/gameState/RumorState.cpp b/lib/gameState/RumorState.cpp new file mode 100644 index 000000000..b4c1caeb2 --- /dev/null +++ b/lib/gameState/RumorState.cpp @@ -0,0 +1,33 @@ +/* + * RumorState.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 "RumorState.h" + +VCMI_LIB_NAMESPACE_BEGIN + +bool RumorState::update(int id, int extra) +{ + if(vstd::contains(last, type)) + { + if(last[type].first != id) + { + last[type].first = id; + last[type].second = extra; + } + else + return false; + } + else + last[type] = std::make_pair(id, extra); + + return true; +} + +VCMI_LIB_NAMESPACE_END diff --git a/lib/gameState/RumorState.h b/lib/gameState/RumorState.h new file mode 100644 index 000000000..571bd49c2 --- /dev/null +++ b/lib/gameState/RumorState.h @@ -0,0 +1,43 @@ +/* + * RumorState.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 DLL_LINKAGE RumorState +{ + enum ERumorType : ui8 + { + TYPE_NONE = 0, TYPE_RAND, TYPE_SPECIAL, TYPE_MAP + }; + + enum ERumorTypeSpecial : ui8 + { + RUMOR_OBELISKS = 208, + RUMOR_ARTIFACTS = 209, + RUMOR_ARMY = 210, + RUMOR_INCOME = 211, + RUMOR_GRAIL = 212 + }; + + ERumorType type; + std::map> last; + + RumorState(){type = TYPE_NONE;}; + bool update(int id, int extra); + + template void serialize(Handler &h) + { + h & type; + h & last; + } +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/gameState/SThievesGuildInfo.h b/lib/gameState/SThievesGuildInfo.h index 6e4c06aeb..661e1e5db 100644 --- a/lib/gameState/SThievesGuildInfo.h +++ b/lib/gameState/SThievesGuildInfo.h @@ -20,7 +20,7 @@ struct DLL_LINKAGE SThievesGuildInfo std::vector< std::vector< PlayerColor > > numOfTowns, numOfHeroes, gold, woodOre, mercSulfCrystGems, obelisks, artifacts, army, income; // [place] -> [colours of players] - std::map colorToBestHero; //maps player's color to his best heros' + std::map colorToBestHero; //maps player's color to his best hero's std::map personality; // color to personality // ai tactic std::map bestCreature; // color to ID // id or -1 if not known 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/gameState/TavernHeroesPool.h b/lib/gameState/TavernHeroesPool.h index ceab10c15..250cfb0a8 100644 --- a/lib/gameState/TavernHeroesPool.h +++ b/lib/gameState/TavernHeroesPool.h @@ -11,17 +11,18 @@ #include "../GameConstants.h" #include "TavernSlot.h" +#include "../serializer/Serializeable.h" + VCMI_LIB_NAMESPACE_BEGIN class CGHeroInstance; class CTown; -class CRandomGenerator; class CHeroClass; class CGameState; class CSimpleArmy; -class DLL_LINKAGE TavernHeroesPool +class DLL_LINKAGE TavernHeroesPool : public Serializeable { struct TavernSlot { @@ -52,7 +53,7 @@ class DLL_LINKAGE TavernHeroesPool public: ~TavernHeroesPool(); - /// Returns heroes currently availabe in tavern of a specific player + /// Returns heroes currently available in tavern of a specific player std::vector getHeroesFor(PlayerColor color) const; /// returns heroes in pool without heroes that are available in taverns diff --git a/lib/json/JsonBonus.cpp b/lib/json/JsonBonus.cpp index 82c5c485d..64d2c9331 100644 --- a/lib/json/JsonBonus.cpp +++ b/lib/json/JsonBonus.cpp @@ -13,7 +13,7 @@ #include "JsonValidator.h" -#include "../CGeneralTextHandler.h" +#include "../texts/CGeneralTextHandler.h" #include "../VCMI_Lib.h" #include "../bonuses/BonusParams.h" #include "../bonuses/Limiters.h" diff --git a/lib/json/JsonNode.cpp b/lib/json/JsonNode.cpp index 17fbe4406..9df1fb38d 100644 --- a/lib/json/JsonNode.cpp +++ b/lib/json/JsonNode.cpp @@ -26,7 +26,7 @@ Node & resolvePointer(Node & in, const std::string & pointer) size_t splitPos = pointer.find('/', 1); std::string entry = pointer.substr(1, splitPos - 1); - std::string remainer = splitPos == std::string::npos ? "" : pointer.substr(splitPos); + std::string remainder = splitPos == std::string::npos ? "" : pointer.substr(splitPos); if(in.getType() == VCMI_LIB_WRAP_NAMESPACE(JsonNode)::JsonType::DATA_VECTOR) { @@ -39,9 +39,9 @@ Node & resolvePointer(Node & in, const std::string & pointer) auto index = boost::lexical_cast(entry); if(in.Vector().size() > index) - return in.Vector()[index].resolvePointer(remainer); + return in.Vector()[index].resolvePointer(remainder); } - return in[entry].resolvePointer(remainer); + return in[entry].resolvePointer(remainder); } VCMI_LIB_NAMESPACE_BEGIN @@ -86,15 +86,20 @@ JsonNode::JsonNode(const std::string & string) { } -JsonNode::JsonNode(const std::byte * data, size_t datasize) - : JsonNode(data, datasize, JsonParsingSettings()) +JsonNode::JsonNode(const JsonMap & map) + : data(map) { } -JsonNode::JsonNode(const std::byte * data, size_t datasize, const JsonParsingSettings & parserSettings) +JsonNode::JsonNode(const std::byte * data, size_t datasize, const std::string & fileName) + : JsonNode(data, datasize, JsonParsingSettings(), fileName) +{ +} + +JsonNode::JsonNode(const std::byte * data, size_t datasize, const JsonParsingSettings & parserSettings, const std::string & fileName) { JsonParser parser(data, datasize, parserSettings); - *this = parser.parse(""); + *this = parser.parse(fileName); } JsonNode::JsonNode(const JsonPath & fileURI) @@ -110,17 +115,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 c4afbcae1..9ccb13cba 100644 --- a/lib/json/JsonNode.h +++ b/lib/json/JsonNode.h @@ -71,15 +71,18 @@ public: explicit JsonNode(const char * string); explicit JsonNode(const std::string & string); + /// Create tree from map + explicit JsonNode(const JsonMap & map); + /// Create tree from Json-formatted input - explicit JsonNode(const std::byte * data, size_t datasize); - explicit JsonNode(const std::byte * data, size_t datasize, const JsonParsingSettings & parserSettings); + explicit JsonNode(const std::byte * data, size_t datasize, const std::string & fileName); + explicit JsonNode(const std::byte * data, size_t datasize, const JsonParsingSettings & parserSettings, const std::string & fileName); /// Create tree from JSON file 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; @@ -152,16 +155,7 @@ public: void serialize(Handler & h) { h & modScope; - - if(h.version >= Handler::Version::JSON_FLAGS) - { - h & overrideFlag; - } - else - { - std::vector oldFlags; - h & oldFlags; - } + h & overrideFlag; h & data; } }; @@ -196,7 +190,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 e4c5f6f8c..32b5de0d2 100644 --- a/lib/json/JsonParser.cpp +++ b/lib/json/JsonParser.cpp @@ -12,7 +12,7 @@ #include "JsonParser.h" #include "../ScopeGuard.h" -#include "../TextOperations.h" +#include "../texts/TextOperations.h" #include "JsonFormatException.h" VCMI_LIB_NAMESPACE_BEGIN @@ -55,7 +55,7 @@ JsonNode JsonParser::parse(const std::string & fileName) if(!errors.empty()) { - logMod->warn("File %s is not a valid JSON file!", fileName); + logMod->warn("%s is not valid JSON!", fileName); logMod->warn(errors); } return root; @@ -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!"); } @@ -587,7 +608,12 @@ bool JsonParser::error(const std::string & message, bool warning) std::ostringstream stream; std::string type(warning ? " warning: " : " error: "); - stream << "At line " << lineCount << ", position " << pos - lineStart << type << message << "\n"; + if(!errors.empty()) + { + // only add the line breaks between error messages so we don't have a trailing line break + stream << "\n"; + } + stream << "At line " << lineCount << ", position " << pos - lineStart << type << message; errors += stream.str(); return warning; diff --git a/lib/json/JsonRandom.cpp b/lib/json/JsonRandom.cpp index df71283b2..7b1f9dabd 100644 --- a/lib/json/JsonRandom.cpp +++ b/lib/json/JsonRandom.cpp @@ -12,10 +12,12 @@ #include "JsonRandom.h" #include +#include +#include +#include #include "JsonBonus.h" -#include "../CRandomGenerator.h" #include "../constants/StringConstants.h" #include "../VCMI_Lib.h" #include "../CArtHandler.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" @@ -50,7 +53,7 @@ VCMI_LIB_NAMESPACE_BEGIN return variables.at(variableID); } - si32 JsonRandom::loadValue(const JsonNode & value, CRandomGenerator & rng, const Variables & variables, si32 defaultValue) + si32 JsonRandom::loadValue(const JsonNode & value, vstd::RNG & rng, const Variables & variables, si32 defaultValue) { if(value.isNull()) return defaultValue; @@ -63,7 +66,7 @@ VCMI_LIB_NAMESPACE_BEGIN { const auto & vector = value.Vector(); - size_t index= rng.getIntRange(0, vector.size()-1)(); + size_t index= rng.nextInt64(0, vector.size()-1); return loadValue(vector[index], rng, variables, 0); } if(value.isStruct()) @@ -72,7 +75,7 @@ VCMI_LIB_NAMESPACE_BEGIN return loadValue(value["amount"], rng, variables, defaultValue); si32 min = loadValue(value["min"], rng, variables, 0); si32 max = loadValue(value["max"], rng, variables, 0); - return rng.getIntRange(min, max)(); + return rng.nextInt64(min, max); } return defaultValue; } @@ -256,7 +259,7 @@ VCMI_LIB_NAMESPACE_BEGIN return valuesSet; } - TResources JsonRandom::loadResources(const JsonNode & value, CRandomGenerator & rng, const Variables & variables) + TResources JsonRandom::loadResources(const JsonNode & value, vstd::RNG & rng, const Variables & variables) { TResources ret; @@ -274,7 +277,7 @@ VCMI_LIB_NAMESPACE_BEGIN return ret; } - TResources JsonRandom::loadResource(const JsonNode & value, CRandomGenerator & rng, const Variables & variables) + TResources JsonRandom::loadResource(const JsonNode & value, vstd::RNG & rng, const Variables & variables) { std::set defaultResources{ GameResID::WOOD, @@ -295,7 +298,7 @@ VCMI_LIB_NAMESPACE_BEGIN return ret; } - PrimarySkill JsonRandom::loadPrimary(const JsonNode & value, CRandomGenerator & rng, const Variables & variables) + PrimarySkill JsonRandom::loadPrimary(const JsonNode & value, vstd::RNG & rng, const Variables & variables) { std::set defaultSkills{ PrimarySkill::ATTACK, @@ -307,7 +310,7 @@ VCMI_LIB_NAMESPACE_BEGIN return *RandomGeneratorUtil::nextItem(potentialPicks, rng); } - std::vector JsonRandom::loadPrimaries(const JsonNode & value, CRandomGenerator & rng, const Variables & variables) + std::vector JsonRandom::loadPrimaries(const JsonNode & value, vstd::RNG & rng, const Variables & variables) { std::vector ret(GameConstants::PRIMARY_SKILLS, 0); std::set defaultSkills{ @@ -339,7 +342,7 @@ VCMI_LIB_NAMESPACE_BEGIN return ret; } - SecondarySkill JsonRandom::loadSecondary(const JsonNode & value, CRandomGenerator & rng, const Variables & variables) + SecondarySkill JsonRandom::loadSecondary(const JsonNode & value, vstd::RNG & rng, const Variables & variables) { std::set defaultSkills; for(const auto & skill : VLC->skillh->objects) @@ -350,7 +353,7 @@ VCMI_LIB_NAMESPACE_BEGIN return *RandomGeneratorUtil::nextItem(potentialPicks, rng); } - std::map JsonRandom::loadSecondaries(const JsonNode & value, CRandomGenerator & rng, const Variables & variables) + std::map JsonRandom::loadSecondaries(const JsonNode & value, vstd::RNG & rng, const Variables & variables) { std::map ret; if(value.isStruct()) @@ -380,7 +383,7 @@ VCMI_LIB_NAMESPACE_BEGIN return ret; } - ArtifactID JsonRandom::loadArtifact(const JsonNode & value, CRandomGenerator & rng, const Variables & variables) + ArtifactID JsonRandom::loadArtifact(const JsonNode & value, vstd::RNG & rng, const Variables & variables) { std::set allowedArts; for(const auto & artifact : VLC->arth->objects) @@ -392,7 +395,7 @@ VCMI_LIB_NAMESPACE_BEGIN return cb->gameState()->pickRandomArtifact(rng, potentialPicks); } - std::vector JsonRandom::loadArtifacts(const JsonNode & value, CRandomGenerator & rng, const Variables & variables) + std::vector JsonRandom::loadArtifacts(const JsonNode & value, vstd::RNG & rng, const Variables & variables) { std::vector ret; for (const JsonNode & entry : value.Vector()) @@ -402,7 +405,7 @@ VCMI_LIB_NAMESPACE_BEGIN return ret; } - SpellID JsonRandom::loadSpell(const JsonNode & value, CRandomGenerator & rng, const Variables & variables) + SpellID JsonRandom::loadSpell(const JsonNode & value, vstd::RNG & rng, const Variables & variables) { std::set defaultSpells; for(const auto & spell : VLC->spellh->objects) @@ -419,7 +422,7 @@ VCMI_LIB_NAMESPACE_BEGIN return *RandomGeneratorUtil::nextItem(potentialPicks, rng); } - std::vector JsonRandom::loadSpells(const JsonNode & value, CRandomGenerator & rng, const Variables & variables) + std::vector JsonRandom::loadSpells(const JsonNode & value, vstd::RNG & rng, const Variables & variables) { std::vector ret; for (const JsonNode & entry : value.Vector()) @@ -429,7 +432,7 @@ VCMI_LIB_NAMESPACE_BEGIN return ret; } - std::vector JsonRandom::loadColors(const JsonNode & value, CRandomGenerator & rng, const Variables & variables) + std::vector JsonRandom::loadColors(const JsonNode & value, vstd::RNG & rng, const Variables & variables) { std::vector ret; std::set defaultPlayers; @@ -445,7 +448,7 @@ VCMI_LIB_NAMESPACE_BEGIN return ret; } - std::vector JsonRandom::loadHeroes(const JsonNode & value, CRandomGenerator & rng) + std::vector JsonRandom::loadHeroes(const JsonNode & value, vstd::RNG & rng) { std::vector ret; for(auto & entry : value.Vector()) @@ -455,7 +458,7 @@ VCMI_LIB_NAMESPACE_BEGIN return ret; } - std::vector JsonRandom::loadHeroClasses(const JsonNode & value, CRandomGenerator & rng) + std::vector JsonRandom::loadHeroClasses(const JsonNode & value, vstd::RNG & rng) { std::vector ret; for(auto & entry : value.Vector()) @@ -465,7 +468,7 @@ VCMI_LIB_NAMESPACE_BEGIN return ret; } - CStackBasicDescriptor JsonRandom::loadCreature(const JsonNode & value, CRandomGenerator & rng, const Variables & variables) + CStackBasicDescriptor JsonRandom::loadCreature(const JsonNode & value, vstd::RNG & rng, const Variables & variables) { CStackBasicDescriptor stack; @@ -482,19 +485,19 @@ 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; } - std::vector JsonRandom::loadCreatures(const JsonNode & value, CRandomGenerator & rng, const Variables & variables) + std::vector JsonRandom::loadCreatures(const JsonNode & value, vstd::RNG & rng, const Variables & variables) { std::vector ret; for (const JsonNode & node : value.Vector()) diff --git a/lib/json/JsonRandom.h b/lib/json/JsonRandom.h index 8129c11bb..5098dc306 100644 --- a/lib/json/JsonRandom.h +++ b/lib/json/JsonRandom.h @@ -15,9 +15,13 @@ VCMI_LIB_NAMESPACE_BEGIN +namespace vstd +{ +class RNG; +} + class JsonNode; using JsonVector = std::vector; -class CRandomGenerator; struct Bonus; struct Component; @@ -53,28 +57,28 @@ public: si32 maxAmount; }; - si32 loadValue(const JsonNode & value, CRandomGenerator & rng, const Variables & variables, si32 defaultValue = 0); + si32 loadValue(const JsonNode & value, vstd::RNG & rng, const Variables & variables, si32 defaultValue = 0); - TResources loadResources(const JsonNode & value, CRandomGenerator & rng, const Variables & variables); - TResources loadResource(const JsonNode & value, CRandomGenerator & rng, const Variables & variables); - PrimarySkill loadPrimary(const JsonNode & value, CRandomGenerator & rng, const Variables & variables); - std::vector loadPrimaries(const JsonNode & value, CRandomGenerator & rng, const Variables & variables); - SecondarySkill loadSecondary(const JsonNode & value, CRandomGenerator & rng, const Variables & variables); - std::map loadSecondaries(const JsonNode & value, CRandomGenerator & rng, const Variables & variables); + TResources loadResources(const JsonNode & value, vstd::RNG & rng, const Variables & variables); + TResources loadResource(const JsonNode & value, vstd::RNG & rng, const Variables & variables); + PrimarySkill loadPrimary(const JsonNode & value, vstd::RNG & rng, const Variables & variables); + std::vector loadPrimaries(const JsonNode & value, vstd::RNG & rng, const Variables & variables); + SecondarySkill loadSecondary(const JsonNode & value, vstd::RNG & rng, const Variables & variables); + std::map loadSecondaries(const JsonNode & value, vstd::RNG & rng, const Variables & variables); - ArtifactID loadArtifact(const JsonNode & value, CRandomGenerator & rng, const Variables & variables); - std::vector loadArtifacts(const JsonNode & value, CRandomGenerator & rng, const Variables & variables); + ArtifactID loadArtifact(const JsonNode & value, vstd::RNG & rng, const Variables & variables); + std::vector loadArtifacts(const JsonNode & value, vstd::RNG & rng, const Variables & variables); - SpellID loadSpell(const JsonNode & value, CRandomGenerator & rng, const Variables & variables); - std::vector loadSpells(const JsonNode & value, CRandomGenerator & rng, const Variables & variables); + SpellID loadSpell(const JsonNode & value, vstd::RNG & rng, const Variables & variables); + std::vector loadSpells(const JsonNode & value, vstd::RNG & rng, const Variables & variables); - CStackBasicDescriptor loadCreature(const JsonNode & value, CRandomGenerator & rng, const Variables & variables); - std::vector loadCreatures(const JsonNode & value, CRandomGenerator & rng, const Variables & variables); + CStackBasicDescriptor loadCreature(const JsonNode & value, vstd::RNG & rng, const Variables & variables); + std::vector loadCreatures(const JsonNode & value, vstd::RNG & rng, const Variables & variables); std::vector evaluateCreatures(const JsonNode & value, const Variables & variables); - std::vector loadColors(const JsonNode & value, CRandomGenerator & rng, const Variables & variables); - std::vector loadHeroes(const JsonNode & value, CRandomGenerator & rng); - std::vector loadHeroClasses(const JsonNode & value, CRandomGenerator & rng); + std::vector loadColors(const JsonNode & value, vstd::RNG & rng, const Variables & variables); + std::vector loadHeroes(const JsonNode & value, vstd::RNG & rng); + std::vector loadHeroClasses(const JsonNode & value, vstd::RNG & rng); static std::vector loadBonuses(const JsonNode & value); }; diff --git a/lib/json/JsonUtils.cpp b/lib/json/JsonUtils.cpp index f2ea7a78e..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; } @@ -269,10 +292,34 @@ JsonNode JsonUtils::assembleFromFiles(const std::string & filename) for(auto & loader : CResourceHandler::get()->getResourcesWithName(resID)) { auto textData = loader->load(resID)->readAll(); - JsonNode section(reinterpret_cast(textData.first.get()), textData.second); + JsonNode section(reinterpret_cast(textData.first.get()), textData.second, resID.getName()); merge(result, section); } 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 7f550b916..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); @@ -65,13 +67,19 @@ namespace JsonUtils * @param node - JsonNode to check * @param schemaName - name of schema to use * @param dataName - some way to identify data (printed in console in case of errors) - * @returns true if data in node fully compilant with schema + * @returns true if data in node fully compliant with schema */ DLL_LINKAGE bool validate(const JsonNode & node, const std::string & schemaName, const std::string & dataName); /// 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 17d016c4f..2dde7b338 100644 --- a/lib/json/JsonValidator.cpp +++ b/lib/json/JsonValidator.cpp @@ -21,6 +21,80 @@ VCMI_LIB_NAMESPACE_BEGIN +// Algorithm for detection of typos in words +// Determines how 'different' two strings are - how many changes must be done to turn one string into another one +// https://en.wikipedia.org/wiki/Levenshtein_distance#Iterative_with_two_matrix_rows +static int getLevenshteinDistance(const std::string & s, const std::string & t) +{ + int n = t.size(); + int m = s.size(); + + // create two work vectors of integer distances + std::vector v0(n+1, 0); + std::vector v1(n+1, 0); + + // initialize v0 (the previous row of distances) + // this row is A[0][i]: edit distance from an empty s to t; + // that distance is the number of characters to append to s to make t. + for (int i = 0; i < n; ++i) + v0[i] = i; + + for (int i = 0; i < m; ++i) + { + // calculate v1 (current row distances) from the previous row v0 + + // first element of v1 is A[i + 1][0] + // edit distance is delete (i + 1) chars from s to match empty t + v1[0] = i + 1; + + // use formula to fill in the rest of the row + for (int j = 0; j < n; ++j) + { + // calculating costs for A[i + 1][j + 1] + int deletionCost = v0[j + 1] + 1; + int insertionCost = v1[j] + 1; + int substitutionCost; + + if (s[i] == t[j]) + substitutionCost = v0[j]; + else + substitutionCost = v0[j] + 1; + + v1[j + 1] = std::min({deletionCost, insertionCost, substitutionCost}); + } + + // copy v1 (current row) to v0 (previous row) for next iteration + // since data in v1 is always invalidated, a swap without copy could be more efficient + std::swap(v0, v1); + } + + // after the last swap, the results of v1 are now in v0 + return v0[n]; +} + +/// 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(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 + static constexpr int maxDistance = 5; + int bestDistance = maxDistance; + std::string bestMatch; + + for (auto const & candidate : candidates) + { + int newDistance = getLevenshteinDistance(candidate.first, target); + + if (newDistance < bestDistance) + { + bestDistance = newDistance; + bestMatch = candidate.first; + } + } + return bestMatch; +} + static std::string emptyCheck(JsonValidator & validator, const JsonNode & baseSchema, const JsonNode & schema, const JsonNode & data) { // check is not needed - e.g. incorporated into another check @@ -348,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; @@ -417,7 +491,13 @@ static std::string additionalPropertiesCheck(JsonValidator & validator, const Js // or, additionalItems field can be bool which indicates if such items are allowed else if(!schema.isNull() && !schema.Bool()) // present and set to false - error - errors += validator.makeErrorMessage("Unknown entry found: " + entry.first); + { + std::string bestCandidate = findClosestMatch(baseSchema["properties"].Struct(), entry.first); + if (!bestCandidate.empty()) + errors += validator.makeErrorMessage("Unknown entry found: '" + entry.first + "'. Perhaps you meant '" + bestCandidate + "'?"); + else + errors += validator.makeErrorMessage("Unknown entry found: " + entry.first); + } } } return errors; @@ -496,6 +576,7 @@ static std::string imageFile(const JsonNode & node) static std::string videoFile(const JsonNode & node) { TEST_FILE(node.getModScope(), "Video/", node.String(), EResType::VIDEO); + TEST_FILE(node.getModScope(), "Video/", node.String(), EResType::VIDEO_LOW_QUALITY); return "Video file \"" + node.String() + "\" was not found"; } #undef TEST_FILE diff --git a/lib/json/JsonValidator.h b/lib/json/JsonValidator.h index 562559dba..c302d99c2 100644 --- a/lib/json/JsonValidator.h +++ b/lib/json/JsonValidator.h @@ -13,7 +13,7 @@ VCMI_LIB_NAMESPACE_BEGIN -/// Class for Json validation. Mostly compilant with json-schema v6 draf +/// Class for Json validation. Mostly compliant with json-schema v6 draf struct JsonValidator { /// path from root node to current one. diff --git a/lib/logging/CLogger.cpp b/lib/logging/CLogger.cpp index 6d7c43ed3..e33c6425e 100644 --- a/lib/logging/CLogger.cpp +++ b/lib/logging/CLogger.cpp @@ -96,6 +96,7 @@ DLL_LINKAGE vstd::CLoggerBase * logNetwork = CLogger::getLogger(CLoggerDomain("n DLL_LINKAGE vstd::CLoggerBase * logAi = CLogger::getLogger(CLoggerDomain("ai")); DLL_LINKAGE vstd::CLoggerBase * logAnim = CLogger::getLogger(CLoggerDomain("animation")); DLL_LINKAGE vstd::CLoggerBase * logMod = CLogger::getLogger(CLoggerDomain("mod")); +DLL_LINKAGE vstd::CLoggerBase * logRng = CLogger::getLogger(CLoggerDomain("rng")); CLogger * CLogger::getLogger(const CLoggerDomain & domain) { diff --git a/lib/logging/VisualLogger.cpp b/lib/logging/VisualLogger.cpp index ae0f2493b..2decb74d2 100644 --- a/lib/logging/VisualLogger.cpp +++ b/lib/logging/VisualLogger.cpp @@ -15,30 +15,101 @@ VCMI_LIB_NAMESPACE_BEGIN DLL_LINKAGE VisualLogger * logVisual = new VisualLogger(); -void VisualLogger::updateWithLock(std::string channel, std::function func) +void VisualLogger::updateWithLock(const std::string & channel, const std::function & func) { - std::lock_guard lock(mutex); + std::lock_guard lock(mutex); mapLines[channel].clear(); + mapTexts[channel].clear(); + battleTexts[channel].clear(); - VisualLogBuilder builder(mapLines[channel]); + VisualLogBuilder builder(mapLines[channel], mapTexts[channel], battleTexts[channel]); func(builder); } -void VisualLogger::visualize(ILogVisualizer & visulizer) +void VisualLogger::visualize(IMapOverlayLogVisualizer & visulizer) { - std::lock_guard lock(mutex); + std::lock_guard lock(mutex); - for(auto line : mapLines[keyToShow]) + for(const auto & line : mapLines[keyToShow]) { visulizer.drawLine(line.start, line.end); } + + std::map>> textMap; + + for(const auto & line : mapTexts[keyToShow]) + { + textMap[line.tile].push_back(line); + } + + for(const auto & pair : textMap) + { + for(int i = 0; i < pair.second.size(); i++) + { + visulizer.drawText(pair.first, i, pair.second[i].text, pair.second[i].background); + } + } } -void VisualLogger::setKey(std::string key) +void VisualLogger::visualize(IBattleOverlayLogVisualizer & visulizer) +{ + std::lock_guard lock(mutex); + std::map> textMap; + + for(auto line : battleTexts[keyToShow]) + { + textMap[line.tile].push_back(line.text); + } + + for(auto & pair : textMap) + { + for(int i = 0; i < pair.second.size(); i++) + { + visulizer.drawText(pair.first, i, pair.second[i]); + } + } +} + +void VisualLogger::setKey(const std::string & key) { keyToShow = key; } +void IVisualLogBuilder::addText(int3 tile, const std::string & text, PlayerColor background) +{ + std::optional rgbColor; + + switch(background) + { + case 0: + rgbColor = ColorRGBA(255, 0, 0); + break; + case 1: + rgbColor = ColorRGBA(0, 0, 255); + break; + case 2: + rgbColor = ColorRGBA(128, 128, 128); + break; + case 3: + rgbColor = ColorRGBA(0, 255, 0); + break; + case 4: + rgbColor = ColorRGBA(255, 128, 0); + break; + case 5: + rgbColor = ColorRGBA(128, 0, 128); + break; + case 6: + rgbColor = ColorRGBA(0, 255, 255); + break; + case 7: + rgbColor = ColorRGBA(255, 128, 255); + break; + } + + addText(tile, text, rgbColor); +} + VCMI_LIB_NAMESPACE_END diff --git a/lib/logging/VisualLogger.h b/lib/logging/VisualLogger.h index 8e8e6de25..6fe0485dd 100644 --- a/lib/logging/VisualLogger.h +++ b/lib/logging/VisualLogger.h @@ -11,63 +11,108 @@ #include "../int3.h" #include "../constants/EntityIdentifiers.h" +#include "../battle/BattleHex.h" +#include "../Color.h" VCMI_LIB_NAMESPACE_BEGIN -class ILogVisualizer +class IMapOverlayLogVisualizer { public: virtual void drawLine(int3 start, int3 end) = 0; + virtual void drawText(int3 tile, int lineNumber, const std::string & text, const std::optional & color) = 0; }; -class IVisualLogBuilder +class IBattleOverlayLogVisualizer +{ +public: + virtual void drawText(BattleHex tile, int lineNumber, const std::string & text) = 0; +}; + +class DLL_LINKAGE IVisualLogBuilder { public: virtual void addLine(int3 start, int3 end) = 0; + virtual void addText(int3 tile, const std::string & text, const std::optional & color = {}) = 0; + virtual void addText(BattleHex tile, const std::string & text) = 0; + + void addText(int3 tile, const std::string & text, PlayerColor background); }; /// The logger is used to show screen overlay class DLL_LINKAGE VisualLogger { private: - struct MapLine + template + struct Line { - int3 start; - int3 end; + T start; + T end; - MapLine(int3 start, int3 end) + Line(T start, T end) :start(start), end(end) { } }; + template + struct Text + { + T tile; + std::string text; + std::optional background; + + Text(T tile, std::string text, std::optional background) + :tile(tile), text(text), background(background) + { + } + }; + class VisualLogBuilder : public IVisualLogBuilder { private: - std::vector & mapLines; + std::vector> & mapLines; + std::vector> & battleTexts; + std::vector> & mapTexts; public: - VisualLogBuilder(std::vector & mapLines) - :mapLines(mapLines) + VisualLogBuilder( + std::vector> & mapLines, + std::vector> & mapTexts, + std::vector> & battleTexts) + :mapLines(mapLines), mapTexts(mapTexts), battleTexts(battleTexts) { } - virtual void addLine(int3 start, int3 end) override + void addLine(int3 start, int3 end) override { - mapLines.push_back(MapLine(start, end)); + mapLines.emplace_back(start, end); + } + + void addText(BattleHex tile, const std::string & text) override + { + battleTexts.emplace_back(tile, text, std::optional()); + } + + void addText(int3 tile, const std::string & text, const std::optional & background) override + { + mapTexts.emplace_back(tile, text, background); } }; private: - std::map> mapLines; + std::map>> mapLines; + std::map>> mapTexts; + std::map>> battleTexts; std::mutex mutex; std::string keyToShow; public: - void updateWithLock(std::string channel, std::function func); - void visualize(ILogVisualizer & visulizer); - void setKey(std::string key); + void updateWithLock(const std::string & channel, const std::function & func); + void visualize(IMapOverlayLogVisualizer & visulizer); + void visualize(IBattleOverlayLogVisualizer & visulizer); + void setKey(const std::string & key); }; extern DLL_LINKAGE VisualLogger * logVisual; diff --git a/lib/mapObjectConstructors/AObjectTypeHandler.cpp b/lib/mapObjectConstructors/AObjectTypeHandler.cpp index 0871def27..d842d9953 100644 --- a/lib/mapObjectConstructors/AObjectTypeHandler.cpp +++ b/lib/mapObjectConstructors/AObjectTypeHandler.cpp @@ -12,7 +12,7 @@ #include "AObjectTypeHandler.h" #include "IObjectInfo.h" -#include "../CGeneralTextHandler.h" +#include "../texts/CGeneralTextHandler.h" #include "../VCMI_Lib.h" #include "../json/JsonUtils.h" #include "../modding/IdentifierStorage.h" @@ -29,6 +29,11 @@ std::string AObjectTypeHandler::getJsonKey() const return modScope + ':' + subTypeName; } +std::string AObjectTypeHandler::getModScope() const +{ + return modScope; +} + si32 AObjectTypeHandler::getIndex() const { return type; @@ -128,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 ba298d13f..52a2c7c1d 100644 --- a/lib/mapObjectConstructors/AObjectTypeHandler.h +++ b/lib/mapObjectConstructors/AObjectTypeHandler.h @@ -15,9 +15,13 @@ VCMI_LIB_NAMESPACE_BEGIN +namespace vstd +{ +class RNG; +} + class ObjectTemplate; class CGObjectInstance; -class CRandomGenerator; class IObjectInfo; class IGameCallback; @@ -70,6 +74,8 @@ public: /// returns full form of identifier of this object in form of modName:objectName std::string getJsonKey() const; + std::string getModScope() const; + /// Returns object-specific name, if set SObjectSounds getSounds() const; @@ -84,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; @@ -114,7 +120,7 @@ public: /// Configures object properties. Should be re-entrable, resetting state of the object if necessarily /// This should set remaining properties, including randomized or depending on map - virtual void configureObject(CGObjectInstance * object, CRandomGenerator & rng) const = 0; + virtual void configureObject(CGObjectInstance * object, vstd::RNG & rng) const = 0; /// Returns object configuration, if available. Otherwise returns NULL virtual std::unique_ptr getObjectInfo(std::shared_ptr tmpl) const; diff --git a/lib/mapObjectConstructors/CBankInstanceConstructor.cpp b/lib/mapObjectConstructors/CBankInstanceConstructor.cpp index ffac95bbc..71abc90b0 100644 --- a/lib/mapObjectConstructors/CBankInstanceConstructor.cpp +++ b/lib/mapObjectConstructors/CBankInstanceConstructor.cpp @@ -11,9 +11,10 @@ #include "CBankInstanceConstructor.h" #include "../json/JsonRandom.h" -#include "../CGeneralTextHandler.h" +#include "../texts/CGeneralTextHandler.h" #include "../IGameCallback.h" -#include "../CRandomGenerator.h" + +#include VCMI_LIB_NAMESPACE_BEGIN @@ -27,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()); @@ -36,7 +37,7 @@ void CBankInstanceConstructor::initTypeData(const JsonNode & input) regularUnitPlacement = input["regularUnitPlacement"].Bool(); } -BankConfig CBankInstanceConstructor::generateConfig(IGameCallback * cb, const JsonNode & level, CRandomGenerator & rng) const +BankConfig CBankInstanceConstructor::generateLevelConfiguration(IGameCallback * cb, const JsonNode & level, vstd::RNG & rng) const { BankConfig bc; JsonRandom randomizer(cb); @@ -53,13 +54,17 @@ BankConfig CBankInstanceConstructor::generateConfig(IGameCallback * cb, const Js return bc; } -void CBankInstanceConstructor::randomizeObject(CBank * bank, CRandomGenerator & rng) const +void CBankInstanceConstructor::randomizeObject(CBank * bank, vstd::RNG & rng) const { bank->resetDuration = bankResetDuration; bank->blockVisit = blockVisit; bank->coastVisitable = coastVisitable; bank->regularUnitPlacement = regularUnitPlacement; + bank->setConfig(generateConfiguration(bank->cb, rng, bank->ID)); +} +BankConfig CBankInstanceConstructor::generateConfiguration(IGameCallback * cb, vstd::RNG & rng, MapObjectID objectID) const +{ si32 totalChance = 0; for(const auto & node : levels) totalChance += static_cast(node["chance"].Float()); @@ -73,11 +78,10 @@ void CBankInstanceConstructor::randomizeObject(CBank * bank, CRandomGenerator & { cumulativeChance += static_cast(node["chance"].Float()); if(selectedChance < cumulativeChance) - { - bank->setConfig(generateConfig(bank->cb, node, rng)); - break; - } + return generateLevelConfiguration(cb, node, rng); } + + throw std::runtime_error("Failed to select bank configuration"); } CBankInfo::CBankInfo(const JsonVector & Config) : @@ -132,7 +136,7 @@ std::vector> CBankInfo::getPossibleCreatur { JsonRandom::Variables emptyVariables; JsonRandom randomizer(cb); - std::vector> aproximateReward; + std::vector> approximateReward; for(const JsonNode & configEntry : config) { @@ -143,11 +147,11 @@ std::vector> CBankInfo::getPossibleCreatur { const auto * creature = stack.allowedCreatures.front(); - aproximateReward.emplace_back(configEntry["chance"].Integer(), CStackBasicDescriptor(creature, (stack.minAmount + stack.maxAmount) / 2)); + approximateReward.emplace_back(configEntry["chance"].Integer(), CStackBasicDescriptor(creature, (stack.minAmount + stack.maxAmount) / 2)); } } - return aproximateReward; + return approximateReward; } bool CBankInfo::givesResources() const diff --git a/lib/mapObjectConstructors/CBankInstanceConstructor.h b/lib/mapObjectConstructors/CBankInstanceConstructor.h index 1967ebfec..8a30d4bcd 100644 --- a/lib/mapObjectConstructors/CBankInstanceConstructor.h +++ b/lib/mapObjectConstructors/CBankInstanceConstructor.h @@ -16,10 +16,11 @@ #include "../ResourceSet.h" #include "../json/JsonNode.h" #include "../mapObjects/CBank.h" +#include "../serializer/Serializeable.h" VCMI_LIB_NAMESPACE_BEGIN -struct BankConfig +struct BankConfig : public Serializeable { ui32 chance = 0; //chance for this level being chosen std::vector guards; //creature ID, amount @@ -68,7 +69,7 @@ public: class CBankInstanceConstructor : public CDefaultObjectTypeHandler { - BankConfig generateConfig(IGameCallback * cb, const JsonNode & conf, CRandomGenerator & rng) const; + BankConfig generateLevelConfiguration(IGameCallback * cb, const JsonNode & conf, vstd::RNG & rng) const; JsonVector levels; @@ -86,11 +87,13 @@ protected: public: - void randomizeObject(CBank * object, CRandomGenerator & rng) const override; + void randomizeObject(CBank * object, vstd::RNG & rng) const override; bool hasNameTextID() const override; std::unique_ptr getObjectInfo(std::shared_ptr tmpl) const override; + + BankConfig generateConfiguration(IGameCallback * cb, vstd::RNG & rand, MapObjectID objectID) const; }; VCMI_LIB_NAMESPACE_END diff --git a/lib/mapObjectConstructors/CDefaultObjectTypeHandler.h b/lib/mapObjectConstructors/CDefaultObjectTypeHandler.h index c56e5eee5..e0d37469d 100644 --- a/lib/mapObjectConstructors/CDefaultObjectTypeHandler.h +++ b/lib/mapObjectConstructors/CDefaultObjectTypeHandler.h @@ -17,7 +17,7 @@ VCMI_LIB_NAMESPACE_BEGIN template class CDefaultObjectTypeHandler : public AObjectTypeHandler { - void configureObject(CGObjectInstance * object, CRandomGenerator & rng) const final + void configureObject(CGObjectInstance * object, vstd::RNG & rng) const final { ObjectType * castedObject = dynamic_cast(object); @@ -43,7 +43,7 @@ class CDefaultObjectTypeHandler : public AObjectTypeHandler protected: virtual void initializeObject(ObjectType * object) const {} - virtual void randomizeObject(ObjectType * object, CRandomGenerator & rng) const {} + virtual void randomizeObject(ObjectType * object, vstd::RNG & rng) const {} virtual ObjectType * createObject(IGameCallback * cb) const { return new ObjectType(cb); diff --git a/lib/mapObjectConstructors/CObjectClassesHandler.cpp b/lib/mapObjectConstructors/CObjectClassesHandler.cpp index 736f41d7f..422c8e2cf 100644 --- a/lib/mapObjectConstructors/CObjectClassesHandler.cpp +++ b/lib/mapObjectConstructors/CObjectClassesHandler.cpp @@ -16,28 +16,35 @@ #include "../VCMI_Lib.h" #include "../GameConstants.h" #include "../constants/StringConstants.h" -#include "../CGeneralTextHandler.h" -#include "../GameSettings.h" +#include "../IGameSettings.h" #include "../CSoundBase.h" #include "../mapObjectConstructors/CBankInstanceConstructor.h" #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 @@ -54,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); @@ -79,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); @@ -102,7 +109,7 @@ CObjectClassesHandler::~CObjectClassesHandler() = default; std::vector CObjectClassesHandler::loadLegacyData() { - size_t dataSize = VLC->settings()->getInteger(EGameSettings::TEXTS_OBJECT); + size_t dataSize = VLC->engineSettings()->getInteger(EGameSettings::TEXTS_OBJECT); CLegacyConfigParser parser(TextPath::builtin("Data/Objects.txt")); auto totalNumber = static_cast(parser.readNumber()); // first line contains number of objects to read and nothing else @@ -119,7 +126,7 @@ std::vector CObjectClassesHandler::loadLegacyData() legacyTemplates.insert(std::make_pair(key, tmpl)); } - objects.resize(256); + mapObjectTypes.resize(256); std::vector ret(dataSize);// create storage for 256 objects assert(dataSize == 256); @@ -161,39 +168,39 @@ std::vector CObjectClassesHandler::loadLegacyData() return ret; } -void CObjectClassesHandler::loadSubObject(const std::string & scope, const std::string & identifier, const JsonNode & entry, ObjectClass * obj) +void CObjectClassesHandler::loadSubObject(const std::string & scope, const std::string & identifier, const JsonNode & entry, ObjectClass * baseObject) { - auto object = loadSubObjectFromJson(scope, identifier, entry, obj, obj->objects.size()); + auto subObject = loadSubObjectFromJson(scope, identifier, entry, baseObject, baseObject->objectTypeHandlers.size()); - assert(object); - obj->objects.push_back(object); + assert(subObject); + baseObject->objectTypeHandlers.push_back(subObject); - registerObject(scope, obj->getJsonKey(), object->getSubTypeName(), object->subtype); + registerObject(scope, baseObject->getJsonKey(), subObject->getSubTypeName(), subObject->subtype); for(const auto & compatID : entry["compatibilityIdentifiers"].Vector()) - registerObject(scope, obj->getJsonKey(), compatID.String(), object->subtype); + registerObject(scope, baseObject->getJsonKey(), compatID.String(), subObject->subtype); } -void CObjectClassesHandler::loadSubObject(const std::string & scope, const std::string & identifier, const JsonNode & entry, ObjectClass * obj, size_t index) +void CObjectClassesHandler::loadSubObject(const std::string & scope, const std::string & identifier, const JsonNode & entry, ObjectClass * baseObject, size_t index) { - auto object = loadSubObjectFromJson(scope, identifier, entry, obj, index); + auto subObject = loadSubObjectFromJson(scope, identifier, entry, baseObject, index); - assert(object); - if (obj->objects.at(index) != nullptr) + assert(subObject); + if (baseObject->objectTypeHandlers.at(index) != nullptr) throw std::runtime_error("Attempt to load already loaded object:" + identifier); - obj->objects.at(index) = object; + baseObject->objectTypeHandlers.at(index) = subObject; - registerObject(scope, obj->getJsonKey(), object->getSubTypeName(), object->subtype); + registerObject(scope, baseObject->getJsonKey(), subObject->getSubTypeName(), subObject->subtype); for(const auto & compatID : entry["compatibilityIdentifiers"].Vector()) - registerObject(scope, obj->getJsonKey(), compatID.String(), object->subtype); + registerObject(scope, baseObject->getJsonKey(), compatID.String(), subObject->subtype); } -TObjectTypeHandler CObjectClassesHandler::loadSubObjectFromJson(const std::string & scope, const std::string & identifier, const JsonNode & entry, ObjectClass * obj, size_t index) +TObjectTypeHandler CObjectClassesHandler::loadSubObjectFromJson(const std::string & scope, const std::string & identifier, const JsonNode & entry, ObjectClass * baseObject, size_t index) { assert(identifier.find(':') == std::string::npos); assert(!scope.empty()); - std::string handler = obj->handlerName; + std::string handler = baseObject->handlerName; if(!handlerConstructors.count(handler)) { logMod->error("Handler with name %s was not found!", handler); @@ -202,13 +209,23 @@ TObjectTypeHandler CObjectClassesHandler::loadSubObjectFromJson(const std::strin assert(handlerConstructors.count(handler) != 0); } + // 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) + { + if (entry.Struct().count("levels") && !entry.Struct().count("rewards")) + handler = "bank"; + else + handler = "configurable"; + } + auto createdObject = handlerConstructors.at(handler)(); createdObject->modScope = scope; - createdObject->typeName = obj->identifier; + createdObject->typeName = baseObject->identifier; createdObject->subTypeName = identifier; - createdObject->type = obj->id; + createdObject->type = baseObject->id; createdObject->subtype = index; createdObject->init(entry); @@ -222,7 +239,7 @@ TObjectTypeHandler CObjectClassesHandler::loadSubObjectFromJson(const std::strin } } - auto range = legacyTemplates.equal_range(std::make_pair(obj->id, index)); + auto range = legacyTemplates.equal_range(std::make_pair(baseObject->id, index)); for (auto & templ : boost::make_iterator_range(range.first, range.second)) { if (staticObject) @@ -237,7 +254,7 @@ TObjectTypeHandler CObjectClassesHandler::loadSubObjectFromJson(const std::strin } legacyTemplates.erase(range.first, range.second); - logGlobal->debug("Loaded object %s(%d)::%s(%d)", obj->getJsonKey(), obj->id, identifier, index); + logGlobal->debug("Loaded object %s(%d)::%s(%d)", baseObject->getJsonKey(), baseObject->id, identifier, index); return createdObject; } @@ -262,17 +279,17 @@ std::string ObjectClass::getNameTranslated() const std::unique_ptr CObjectClassesHandler::loadFromJson(const std::string & scope, const JsonNode & json, const std::string & name, size_t index) { - auto obj = std::make_unique(); + auto newObject = std::make_unique(); - obj->modScope = scope; - obj->identifier = name; - obj->handlerName = json["handler"].String(); - obj->base = json["base"]; - obj->id = index; + newObject->modScope = scope; + newObject->identifier = name; + newObject->handlerName = json["handler"].String(); + newObject->base = json["base"]; + newObject->id = index; - VLC->generaltexth->registerString(scope, obj->getNameTextID(), json["name"].String()); + VLC->generaltexth->registerString(scope, newObject->getNameTextID(), json["name"]); - obj->objects.resize(json["lastReservedIndex"].Float() + 1); + newObject->objectTypeHandlers.resize(json["lastReservedIndex"].Float() + 1); for (auto subData : json["types"].Struct()) { @@ -283,68 +300,71 @@ std::unique_ptr CObjectClassesHandler::loadFromJson(const std::stri if ( subMeta == "core") { size_t subIndex = subData.second["index"].Integer(); - loadSubObject(subData.second.getModScope(), subData.first, subData.second, obj.get(), subIndex); + loadSubObject(subData.second.getModScope(), subData.first, subData.second, newObject.get(), subIndex); } else { logMod->error("Object %s:%s.%s - attempt to load object with preset index! This option is reserved for built-in mod", subMeta, name, subData.first ); - loadSubObject(subData.second.getModScope(), subData.first, subData.second, obj.get()); + loadSubObject(subData.second.getModScope(), subData.first, subData.second, newObject.get()); } } else - loadSubObject(subData.second.getModScope(), subData.first, subData.second, obj.get()); + loadSubObject(subData.second.getModScope(), subData.first, subData.second, newObject.get()); } - if (obj->id == MapObjectID::MONOLITH_TWO_WAY) - generateExtraMonolithsForRMG(obj.get()); + if (newObject->id == MapObjectID::MONOLITH_TWO_WAY) + generateExtraMonolithsForRMG(newObject.get()); - return obj; + return newObject; } void CObjectClassesHandler::loadObject(std::string scope, std::string name, const JsonNode & data) { - objects.push_back(loadFromJson(scope, data, name, objects.size())); + mapObjectTypes.push_back(loadFromJson(scope, data, name, mapObjectTypes.size())); - VLC->identifiersHandler->registerObject(scope, "object", name, objects.back()->id); + VLC->identifiersHandler->registerObject(scope, "object", name, mapObjectTypes.back()->id); } void CObjectClassesHandler::loadObject(std::string scope, std::string name, const JsonNode & data, size_t index) { - assert(objects.at(index) == nullptr); // ensure that this id was not loaded before + assert(mapObjectTypes.at(index) == nullptr); // ensure that this id was not loaded before - objects.at(index) = loadFromJson(scope, data, name, index); - VLC->identifiersHandler->registerObject(scope, "object", name, objects.at(index)->id); + mapObjectTypes.at(index) = loadFromJson(scope, data, name, index); + VLC->identifiersHandler->registerObject(scope, "object", name, mapObjectTypes.at(index)->id); } void CObjectClassesHandler::loadSubObject(const std::string & identifier, JsonNode config, MapObjectID ID, MapObjectSubID subID) { config.setType(JsonNode::JsonType::DATA_STRUCT); // ensure that input is not NULL - assert(objects.at(ID.getNum())); - if ( subID.getNum() >= objects.at(ID.getNum())->objects.size()) - objects.at(ID.getNum())->objects.resize(subID.getNum()+1); + assert(mapObjectTypes.at(ID.getNum())); - JsonUtils::inherit(config, objects.at(ID.getNum())->base); - loadSubObject(config.getModScope(), identifier, config, objects.at(ID.getNum()).get(), subID.getNum()); + if (subID.getNum() >= mapObjectTypes.at(ID.getNum())->objectTypeHandlers.size()) + { + mapObjectTypes.at(ID.getNum())->objectTypeHandlers.resize(subID.getNum() + 1); + } + + JsonUtils::inherit(config, mapObjectTypes.at(ID.getNum())->base); + loadSubObject(config.getModScope(), identifier, config, mapObjectTypes.at(ID.getNum()).get(), subID.getNum()); } void CObjectClassesHandler::removeSubObject(MapObjectID ID, MapObjectSubID subID) { - assert(objects.at(ID.getNum())); - objects.at(ID.getNum())->objects.at(subID.getNum()) = nullptr; + assert(mapObjectTypes.at(ID.getNum())); + mapObjectTypes.at(ID.getNum())->objectTypeHandlers.at(subID.getNum()) = nullptr; } TObjectTypeHandler CObjectClassesHandler::getHandlerFor(MapObjectID type, MapObjectSubID subtype) const { try { - if (objects.at(type.getNum()) == nullptr) - return objects.front()->objects.front(); + if (mapObjectTypes.at(type.getNum()) == nullptr) + return mapObjectTypes.front()->objectTypeHandlers.front(); auto subID = subtype.getNum(); - if (type == Obj::PRISON) + if (type == Obj::PRISON || type == Obj::HERO_PLACEHOLDER || type == Obj::SPELL_SCROLL) subID = 0; - auto result = objects.at(type.getNum())->objects.at(subID); + auto result = mapObjectTypes.at(type.getNum())->objectTypeHandlers.at(subID); if (result != nullptr) return result; @@ -364,11 +384,11 @@ TObjectTypeHandler CObjectClassesHandler::getHandlerFor(const std::string & scop std::optional id = VLC->identifiers()->getIdentifier(scope, "object", type); if(id) { - const auto & object = objects.at(id.value()); + const auto & object = mapObjectTypes.at(id.value()); std::optional subID = VLC->identifiers()->getIdentifier(scope, object->getJsonKey(), subtype); if (subID) - return object->objects.at(subID.value()); + return object->objectTypeHandlers.at(subID.value()); } std::string errorString = "Failed to find object of type " + type + "::" + subtype; @@ -381,11 +401,67 @@ 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; - for(auto & entry : objects) + for(auto & entry : mapObjectTypes) if (entry) ret.insert(entry->id); @@ -396,13 +472,13 @@ std::set CObjectClassesHandler::knownSubObjects(MapObjectID prim { std::set ret; - if (!objects.at(primaryID.getNum())) + if (!mapObjectTypes.at(primaryID.getNum())) { logGlobal->error("Failed to find object %d", primaryID); return ret; } - for(const auto & entry : objects.at(primaryID.getNum())->objects) + for(const auto & entry : mapObjectTypes.at(primaryID.getNum())->objectTypeHandlers) if (entry) ret.insert(entry->subtype); @@ -435,27 +511,39 @@ void CObjectClassesHandler::beforeValidate(JsonNode & object) void CObjectClassesHandler::afterLoadFinalization() { - for(auto & entry : objects) + for(auto & entry : mapObjectTypes) { if (!entry) continue; - for(const auto & obj : entry->objects) + for(const auto & obj : entry->objectTypeHandlers) { if (!obj) continue; 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) { //duplicate existing two-way portals to make reserve for RMG - auto& portalVec = container->objects; + auto& portalVec = container->objectTypeHandlers; //FIXME: Monoliths in this vector can be already not useful for every terrain const size_t portalCount = portalVec.size(); @@ -499,10 +587,10 @@ std::string CObjectClassesHandler::getObjectName(MapObjectID type, MapObjectSubI if (handler && handler->hasNameTextID()) return handler->getNameTranslated(); - if (objects.at(type.getNum())) - return objects.at(type.getNum())->getNameTranslated(); + if (mapObjectTypes.at(type.getNum())) + return mapObjectTypes.at(type.getNum())->getNameTranslated(); - return objects.front()->getNameTranslated(); + return mapObjectTypes.front()->getNameTranslated(); } SObjectSounds CObjectClassesHandler::getObjectSounds(MapObjectID type, MapObjectSubID subtype) const @@ -514,27 +602,27 @@ SObjectSounds CObjectClassesHandler::getObjectSounds(MapObjectID type, MapObject if(type == Obj::PRISON || type == Obj::HERO || type == Obj::SPELL_SCROLL) subtype = 0; - if(objects.at(type.getNum())) + if(mapObjectTypes.at(type.getNum())) return getHandlerFor(type, subtype)->getSounds(); else - return objects.front()->objects.front()->getSounds(); + return mapObjectTypes.front()->objectTypeHandlers.front()->getSounds(); } std::string CObjectClassesHandler::getObjectHandlerName(MapObjectID type) const { - if (objects.at(type.getNum())) - return objects.at(type.getNum())->handlerName; + if (mapObjectTypes.at(type.getNum())) + return mapObjectTypes.at(type.getNum())->handlerName; else - return objects.front()->handlerName; + return mapObjectTypes.front()->handlerName; } std::string CObjectClassesHandler::getJsonKey(MapObjectID type) const { - if (objects.at(type.getNum()) != nullptr) - return objects.at(type.getNum())->getJsonKey(); + if (mapObjectTypes.at(type.getNum()) != nullptr) + return mapObjectTypes.at(type.getNum())->getJsonKey(); logGlobal->warn("Unknown object of type %d!", type); - return objects.front()->getJsonKey(); + return mapObjectTypes.front()->getJsonKey(); } VCMI_LIB_NAMESPACE_END diff --git a/lib/mapObjectConstructors/CObjectClassesHandler.h b/lib/mapObjectConstructors/CObjectClassesHandler.h index b1cb1ca64..264b6f1ee 100644 --- a/lib/mapObjectConstructors/CObjectClassesHandler.h +++ b/lib/mapObjectConstructors/CObjectClassesHandler.h @@ -9,38 +9,16 @@ */ #pragma once -#include "../constants/EntityIdentifiers.h" +#include "../mapObjects/CompoundMapObjectID.h" #include "../IHandlerBase.h" #include "../json/JsonNode.h" VCMI_LIB_NAMESPACE_BEGIN -class CRandomGenerator; 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; @@ -56,7 +34,7 @@ public: std::string handlerName; // ID of handler that controls this object, should be determined using handlerConstructor map JsonNode base; - std::vector objects; + std::vector objectTypeHandlers; ObjectClass(); ~ObjectClass(); @@ -70,11 +48,13 @@ public: class DLL_LINKAGE CObjectClassesHandler : public IHandlerBase, boost::noncopyable { /// list of object handlers, each of them handles only one type - std::vector< std::unique_ptr > objects; + std::vector< std::unique_ptr > mapObjectTypes; - /// map that is filled during contruction with all known handlers. Not serializeable due to usage of std::function + /// 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; @@ -111,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 83f24b01e..ed0c92142 100644 --- a/lib/mapObjectConstructors/CRewardableConstructor.cpp +++ b/lib/mapObjectConstructors/CRewardableConstructor.cpp @@ -10,9 +10,11 @@ #include "StdInc.h" #include "CRewardableConstructor.h" +#include "../json/JsonUtils.h" #include "../mapObjects/CRewardableObject.h" -#include "../CGeneralTextHandler.h" +#include "../texts/CGeneralTextHandler.h" #include "../IGameCallback.h" +#include "../CConfigHandler.h" VCMI_LIB_NAMESPACE_BEGIN @@ -22,7 +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"]); + + if (settings["mods"]["validation"].String() != "off") + JsonUtils::validate(config, "vcmi:rewardable", getJsonKey()); } @@ -40,27 +45,41 @@ CGObjectInstance * CRewardableConstructor::create(IGameCallback * cb, std::share return ret; } -void CRewardableConstructor::configureObject(CGObjectInstance * object, CRandomGenerator & rng) const +Rewardable::Configuration CRewardableConstructor::generateConfiguration(IGameCallback * cb, vstd::RNG & rand, MapObjectID objectID, const std::map & presetVariables) const { - if(auto * rewardableObject = dynamic_cast(object)) + Rewardable::Configuration result; + result.variables.preset = presetVariables; + objectInfo.configureObject(result, rand, cb); + + for(auto & rewardInfo : result.info) { - objectInfo.configureObject(rewardableObject->configuration, rng, object->cb); - for(auto & rewardInfo : rewardableObject->configuration.info) + for (auto & bonus : rewardInfo.reward.bonuses) { - for (auto & bonus : rewardInfo.reward.bonuses) - { - bonus.source = BonusSource::OBJECT_TYPE; - bonus.sid = BonusSourceID(rewardableObject->ID); - } - } - if (rewardableObject->configuration.info.empty()) - { - if (objectInfo.getParameters()["rewards"].isNull()) - logMod->error("Object %s has invalid configuration! No defined rewards found!", getJsonKey()); - else - logMod->error("Object %s has invalid configuration! Make sure that defined appear chances are continious!", getJsonKey()); + bonus.source = BonusSource::OBJECT_TYPE; + bonus.sid = BonusSourceID(objectID); } } + + return result; +} + +void CRewardableConstructor::configureObject(CGObjectInstance * object, vstd::RNG & rng) const +{ + auto * rewardableObject = dynamic_cast(object); + + 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.variables.preset); + rewardableObject->initializeGuards(); + + if (rewardableObject->configuration.info.empty()) + { + if (objectInfo.getParameters()["rewards"].isNull()) + logMod->error("Object %s has invalid configuration! No defined rewards found!", getJsonKey()); + else + logMod->error("Object %s has invalid configuration! Make sure that defined appear chances are continuous!", getJsonKey()); + } } std::unique_ptr CRewardableConstructor::getObjectInfo(std::shared_ptr tmpl) const diff --git a/lib/mapObjectConstructors/CRewardableConstructor.h b/lib/mapObjectConstructors/CRewardableConstructor.h index 81fd286e6..a9f30d500 100644 --- a/lib/mapObjectConstructors/CRewardableConstructor.h +++ b/lib/mapObjectConstructors/CRewardableConstructor.h @@ -27,9 +27,11 @@ public: CGObjectInstance * create(IGameCallback * cb, std::shared_ptr tmpl = nullptr) const override; - void configureObject(CGObjectInstance * object, CRandomGenerator & rng) const override; + void configureObject(CGObjectInstance * object, vstd::RNG & rng) const override; std::unique_ptr getObjectInfo(std::shared_ptr tmpl) const override; + + 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 806b32194..a3986304a 100644 --- a/lib/mapObjectConstructors/CommonConstructors.cpp +++ b/lib/mapObjectConstructors/CommonConstructors.cpp @@ -10,15 +10,17 @@ #include "StdInc.h" #include "CommonConstructors.h" -#include "../CGeneralTextHandler.h" -#include "../CHeroHandler.h" -#include "../CTownHandler.h" +#include "../texts/CGeneralTextHandler.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, CRandomGenerator & rng) const +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,42 +125,74 @@ 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; + + if (candidateFullMatch) + return candidateFullMatch; + + if (candidateGenderMatch) + return candidateGenderMatch; + + return candidateBase; } -void CHeroInstanceConstructor::initializeObject(CGHeroInstance * obj) const -{ - obj->type = nullptr; //FIXME: set to valid value. somehow. -} - -void CHeroInstanceConstructor::randomizeObject(CGHeroInstance * object, CRandomGenerator & rng) const +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 @@ -238,28 +297,15 @@ CGMarket * MarketInstanceConstructor::createObject(IGameCallback * cb) const return new CGUniversity(cb); } } - else if(marketModes.size() == 2) - { - if(vstd::contains(marketModes, EMarketMode::ARTIFACT_EXP)) - return new CGArtifactsAltar(cb); - } return new CGMarket(cb); } -void MarketInstanceConstructor::initializeObject(CGMarket * market) const +const std::set & MarketInstanceConstructor::availableModes() const { - market->marketModes = marketModes; - market->marketEfficiency = marketEfficiency; - - market->title = market->getObjectName(); - if(!title.empty()) - market->title = VLC->generaltexth->translate(title); - - if (!speech.empty()) - market->speech = VLC->generaltexth->translate(speech); + return marketModes; } -void MarketInstanceConstructor::randomizeObject(CGMarket * object, CRandomGenerator & rng) const +void MarketInstanceConstructor::randomizeObject(CGMarket * object, vstd::RNG & rng) const { JsonRandom randomizer(object->cb); JsonRandom::Variables emptyVariables; @@ -271,4 +317,15 @@ void MarketInstanceConstructor::randomizeObject(CGMarket * object, CRandomGenera } } +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 ea5ef3d72..73ba43874 100644 --- a/lib/mapObjectConstructors/CommonConstructors.h +++ b/lib/mapObjectConstructors/CommonConstructors.h @@ -14,6 +14,7 @@ #include "../mapObjects/MiscObjects.h" #include "../mapObjects/CGCreature.h" +#include "../mapObjects/CGHeroInstance.h" #include "../mapObjects/ObstacleSetHandler.h" VCMI_LIB_NAMESPACE_BEGIN @@ -25,7 +26,6 @@ class CGHeroInstance; class CGMarket; class CHeroClass; class CGCreature; -class CBank; class CGBoat; class CFaction; class CStackBasicDescriptor; @@ -63,7 +63,7 @@ public: std::map> filters; void initializeObject(CGTownInstance * object) const override; - void randomizeObject(CGTownInstance * object, CRandomGenerator & rng) const override; + void randomizeObject(CGTownInstance * object, vstd::RNG & rng) const override; void afterLoadFinalization() override; bool hasNameTextID() const override; @@ -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, CRandomGenerator & rng) const override; - void afterLoadFinalization() override; + void randomizeObject(CGHeroInstance * object, vstd::RNG & rng) const override; bool hasNameTextID() const override; std::string getNameTextID() const override; @@ -112,21 +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, CRandomGenerator & rng) 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 b6fe21715..3572dfb8a 100644 --- a/lib/mapObjectConstructors/DwellingInstanceConstructor.cpp +++ b/lib/mapObjectConstructors/DwellingInstanceConstructor.cpp @@ -11,7 +11,7 @@ #include "DwellingInstanceConstructor.h" #include "../CCreatureHandler.h" -#include "../CGeneralTextHandler.h" +#include "../texts/CGeneralTextHandler.h" #include "../json/JsonRandom.h" #include "../VCMI_Lib.h" #include "../mapObjects/CGDwelling.h" @@ -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(); @@ -74,7 +74,7 @@ void DwellingInstanceConstructor::initializeObject(CGDwelling * obj) const } } -void DwellingInstanceConstructor::randomizeObject(CGDwelling * dwelling, CRandomGenerator &rng) const +void DwellingInstanceConstructor::randomizeObject(CGDwelling * dwelling, vstd::RNG &rng) const { JsonRandom randomizer(dwelling->cb); @@ -88,25 +88,28 @@ void DwellingInstanceConstructor::randomizeObject(CGDwelling * dwelling, CRandom 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/DwellingInstanceConstructor.h b/lib/mapObjectConstructors/DwellingInstanceConstructor.h index ac82e2e87..99b61093c 100644 --- a/lib/mapObjectConstructors/DwellingInstanceConstructor.h +++ b/lib/mapObjectConstructors/DwellingInstanceConstructor.h @@ -33,7 +33,7 @@ public: bool hasNameTextID() const override; void initializeObject(CGDwelling * object) const override; - void randomizeObject(CGDwelling * object, CRandomGenerator & rng) const override; + void randomizeObject(CGDwelling * object, vstd::RNG & rng) const override; bool isBannedForRandomDwelling() const; bool producesCreature(const CCreature * crea) const; 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 baa2eb56e..23d026cd9 100644 --- a/lib/mapObjects/CArmedInstance.cpp +++ b/lib/mapObjects/CArmedInstance.cpp @@ -11,12 +11,13 @@ #include "StdInc.h" #include "CArmedInstance.h" -#include "../CTownHandler.h" #include "../CCreatureHandler.h" -#include "../CGeneralTextHandler.h" -#include "../gameState/CGameState.h" #include "../CPlayerState.h" -#include "../MetaString.h" +#include "../entities/faction/CFaction.h" +#include "../entities/faction/CTown.h" +#include "../entities/faction/CTownHandler.h" +#include "../gameState/CGameState.h" +#include "../texts/CGeneralTextHandler.h" VCMI_LIB_NAMESPACE_BEGIN @@ -77,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 75ac1bcaa..716390e6e 100644 --- a/lib/mapObjects/CBank.cpp +++ b/lib/mapObjects/CBank.cpp @@ -14,9 +14,8 @@ #include #include -#include "../CGeneralTextHandler.h" -#include "../CSoundBase.h" -#include "../GameSettings.h" +#include "../texts/CGeneralTextHandler.h" +#include "../IGameSettings.h" #include "../CPlayerState.h" #include "../mapObjectConstructors/CObjectClassesHandler.h" #include "../mapObjectConstructors/CBankInstanceConstructor.h" @@ -24,7 +23,6 @@ #include "../networkPacks/Component.h" #include "../networkPacks/PacksForClient.h" #include "../networkPacks/PacksForClientBattle.h" -#include "../MetaString.h" #include "../IGameCallback.h" #include "../gameState/CGameState.h" @@ -44,7 +42,7 @@ CBank::CBank(IGameCallback *cb) //must be instantiated in .cpp file for access to complete types of all member fields CBank::~CBank() = default; -void CBank::initObj(CRandomGenerator & rand) +void CBank::initObj(vstd::RNG & rand) { daycounter = 0; resetDuration = 0; @@ -69,7 +67,7 @@ std::vector CBank::getPopupComponents(PlayerColor player) const if (!wasVisited(player)) return {}; - if (!VLC->settings()->getBoolean(EGameSettings::BANKS_SHOW_GUARDS_COMPOSITION)) + if (!cb->getSettings().getBoolean(EGameSettings::BANKS_SHOW_GUARDS_COMPOSITION)) return {}; if (bankConfig == nullptr) @@ -96,7 +94,9 @@ 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 } void CBank::setPropertyDer (ObjProperty what, ObjPropertyID identifier) @@ -106,25 +106,24 @@ void CBank::setPropertyDer (ObjProperty what, ObjPropertyID identifier) case ObjProperty::BANK_DAYCOUNTER: //daycounter daycounter+= identifier.getNum(); break; - case ObjProperty::BANK_RESET: - // FIXME: Object reset must be done by separate netpack from server - initObj(cb->gameState()->getRandomGenerator()); - daycounter = 1; //yes, 1 since "today" daycounter won't be incremented - break; case ObjProperty::BANK_CLEAR: bankConfig.reset(); break; } } -void CBank::newTurn(CRandomGenerator & rand) const +void CBank::newTurn(vstd::RNG & rand) const { if (bankConfig == nullptr) { if (resetDuration != 0) { if (daycounter >= resetDuration) - cb->setObjPropertyValue(id, ObjProperty::BANK_RESET); //daycounter 0 + { + auto handler = std::dynamic_pointer_cast(getObjectHandler()); + auto config = handler->generateConfiguration(cb, rand, ID); + cb->setBankObjectConfiguration(id, config); + } else cb->setObjPropertyValue(id, ObjProperty::BANK_DAYCOUNTER, 1); //daycounter++ } @@ -138,140 +137,31 @@ bool CBank::wasVisited (PlayerColor player) const void CBank::onHeroVisit(const CGHeroInstance * h) const { - ChangeObjectVisitors cov(ChangeObjectVisitors::VISITOR_ADD_TEAM, id, h->id); - cb->sendAndApply(&cov); + ChangeObjectVisitors cov(ChangeObjectVisitors::VISITOR_ADD_PLAYER, id, h->id); + cb->sendAndApply(cov); - if(!bankConfig && (ID.toEnum() == Obj::CREATURE_BANK || ID.toEnum() == Obj::DRAGON_UTOPIA)) - { - blockingDialogAnswered(h, 1); - return; - } - - int banktext = 0; - switch (ID.toEnum()) - { - case Obj::DERELICT_SHIP: - banktext = 41; - break; - case Obj::DRAGON_UTOPIA: - banktext = 47; - break; - case Obj::CRYPT: - banktext = 119; - break; - case Obj::SHIPWRECK: - banktext = 122; - break; - case Obj::PYRAMID: - banktext = 105; - break; - case Obj::CREATURE_BANK: - default: - banktext = 32; - break; - } BlockingDialog bd(true, false); bd.player = h->getOwner(); - bd.soundID = soundBase::invalid; // Sound is handled in json files, else two sounds are played - bd.text.appendLocalString(EMetaText::ADVOB_TXT, banktext); + bd.text.appendLocalString(EMetaText::ADVOB_TXT, 32); bd.components = getPopupComponents(h->getOwner()); - if (banktext == 32) - bd.text.replaceRawString(getObjectName()); - - cb->showBlockingDialog(&bd); + bd.text.replaceTextID(getObjectHandler()->getNameTextID()); + cb->showBlockingDialog(this, &bd); } void CBank::doVisit(const CGHeroInstance * hero) const { - int textID = -1; InfoWindow iw; iw.type = EInfoWindowMode::AUTO; iw.player = hero->getOwner(); MetaString loot; - if (bankConfig) + if (!bankConfig) { - switch (ID.toEnum()) - { - case Obj::DERELICT_SHIP: - textID = 43; - break; - case Obj::CRYPT: - textID = 121; - break; - case Obj::SHIPWRECK: - textID = 124; - break; - case Obj::PYRAMID: - textID = 106; - break; - case Obj::CREATURE_BANK: - case Obj::DRAGON_UTOPIA: - default: - textID = 34; - break; - } - } - else - { - switch (ID.toEnum()) - { - case Obj::SHIPWRECK: - case Obj::DERELICT_SHIP: - case Obj::CRYPT: - { - GiveBonus gbonus; - gbonus.id = hero->id; - gbonus.bonus.duration = BonusDuration::ONE_BATTLE; - gbonus.bonus.source = BonusSource::OBJECT_TYPE; - gbonus.bonus.sid = BonusSourceID(ID); - gbonus.bonus.type = BonusType::MORALE; - gbonus.bonus.val = -1; - switch (ID.toEnum()) - { - case Obj::SHIPWRECK: - textID = 123; - gbonus.bonus.description = MetaString::createFromTextID("core.arraytxt.99"); - break; - case Obj::DERELICT_SHIP: - textID = 42; - gbonus.bonus.description = MetaString::createFromTextID("core.arraytxt.101"); - break; - case Obj::CRYPT: - textID = 120; - gbonus.bonus.description = MetaString::createFromTextID("core.arraytxt.98"); - break; - } - cb->giveHeroBonus(&gbonus); - iw.components.emplace_back(ComponentType::MORALE, -1); - iw.soundID = soundBase::invalid; - break; - } - case Obj::PYRAMID: - { - GiveBonus gb; - gb.bonus = Bonus(BonusDuration::ONE_BATTLE, BonusType::LUCK, BonusSource::OBJECT_INSTANCE, -2, BonusSourceID(id)); - gb.bonus.description = MetaString::createFromTextID("core.arraytxt.70"); - gb.id = hero->id; - cb->giveHeroBonus(&gb); - textID = 107; - iw.components.emplace_back(ComponentType::LUCK, -2); - break; - } - case Obj::CREATURE_BANK: - case Obj::DRAGON_UTOPIA: - default: - iw.text.appendRawString(VLC->generaltexth->advobtxt[33]);// This was X, now is completely empty - iw.text.replaceRawString(getObjectName()); - } - if(textID != -1) - { - iw.text.appendLocalString(EMetaText::ADVOB_TXT, textID); - } + iw.text.appendRawString(VLC->generaltexth->advobtxt[33]);// This was X, now is completely empty + iw.text.replaceTextID(getObjectHandler()->getNameTextID()); cb->showInfoDialog(&iw); } - //grant resources if (bankConfig) { @@ -292,22 +182,20 @@ void CBank::doVisit(const CGHeroInstance * hero) const iw.components.emplace_back(ComponentType::ARTIFACT, elem); loot.appendRawString("%s"); loot.replaceName(elem); - cb->giveHeroNewArtifact(hero, elem.toArtifact(), ArtifactPosition::FIRST_AVAILABLE); + cb->giveHeroNewArtifact(hero, elem, ArtifactPosition::FIRST_AVAILABLE); } //display loot if (!iw.components.empty()) { - iw.text.appendLocalString(EMetaText::ADVOB_TXT, textID); - if (textID == 34) + iw.text.appendLocalString(EMetaText::ADVOB_TXT, 34); + const auto * strongest = boost::range::max_element(bankConfig->guards, [](const CStackBasicDescriptor & a, const CStackBasicDescriptor & b) { - 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()); - iw.text.replaceNamePlural(strongest->getId()); - iw.text.replaceRawString(loot.buildList()); - } cb->showInfoDialog(&iw); } @@ -320,10 +208,7 @@ void CBank::doVisit(const CGHeroInstance * hero) const std::set spells; bool noWisdom = false; - if(textID == 106) - { - iw.text.appendLocalString(EMetaText::ADVOB_TXT, textID); //pyramid - } + for(const SpellID & spellId : bankConfig->spells) { const auto * spell = spellId.toEntity(VLC); @@ -359,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()) @@ -387,18 +272,18 @@ void CBank::doVisit(const CGHeroInstance * hero) const void CBank::battleFinished(const CGHeroInstance *hero, const BattleResult &result) const { - if (result.winner == 0) + if (result.winner == BattleSide::ATTACKER) { doVisit(hero); } } -void CBank::blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) const +void CBank::blockingDialogAnswered(const CGHeroInstance *hero, int32_t answer) const { if (answer) { if (bankConfig) // not looted bank - cb->startBattleI(hero, this, !regularUnitPlacement); + cb->startBattle(hero, this); else doVisit(hero); } diff --git a/lib/mapObjects/CBank.h b/lib/mapObjects/CBank.h index 9c9dd5059..b61563421 100644 --- a/lib/mapObjects/CBank.h +++ b/lib/mapObjects/CBank.h @@ -33,14 +33,14 @@ public: void setConfig(const BankConfig & bc); - void initObj(CRandomGenerator & rand) override; + void initObj(vstd::RNG & rand) override; std::string getHoverText(PlayerColor player) const override; - void newTurn(CRandomGenerator & rand) const override; + void newTurn(vstd::RNG & rand) const override; bool wasVisited (PlayerColor player) const override; bool isCoastVisitable() const override; void onHeroVisit(const CGHeroInstance * h) const override; void battleFinished(const CGHeroInstance *hero, const BattleResult &result) const override; - void blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) const override; + void blockingDialogAnswered(const CGHeroInstance *hero, int32_t answer) const override; std::vector getPopupComponents(PlayerColor player) const override; diff --git a/lib/mapObjects/CGCreature.cpp b/lib/mapObjects/CGCreature.cpp index 504e7d812..b1589c8e3 100644 --- a/lib/mapObjects/CGCreature.cpp +++ b/lib/mapObjects/CGCreature.cpp @@ -12,16 +12,19 @@ #include "CGCreature.h" #include "CGHeroInstance.h" -#include "../CGeneralTextHandler.h" +#include "../texts/CGeneralTextHandler.h" #include "../CConfigHandler.h" -#include "../GameSettings.h" +#include "../IGameSettings.h" #include "../IGameCallback.h" +#include "../gameState/CGameState.h" #include "../mapObjectConstructors/CObjectClassesHandler.h" #include "../networkPacks/PacksForClient.h" #include "../networkPacks/PacksForClientBattle.h" #include "../networkPacks/StackLocation.h" #include "../serializer/JsonSerializeFormat.h" -#include "../CRandomGenerator.h" +#include "../entities/faction/CTownHandler.h" + +#include VCMI_LIB_NAMESPACE_BEGIN @@ -30,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"; } @@ -42,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(); } @@ -54,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 @@ -63,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; @@ -99,10 +114,13 @@ std::string CGCreature::getPopupText(const CGHeroInstance * hero) const if (settings["general"]["enableUiEnhancements"].Bool()) { + 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; @@ -123,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()) }; } @@ -161,8 +182,8 @@ 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))); - cb->showBlockingDialog(&ynd); + ynd.text.replaceName(getCreatureID(), getStackCount(SlotID(0))); + cb->showBlockingDialog(this, &ynd); break; } default: //join for gold @@ -176,20 +197,25 @@ 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(&ynd); + cb->showBlockingDialog(this, &ynd); break; } } } -CreatureID CGCreature::getCreature() const +CreatureID CGCreature::getCreatureID() const { return CreatureID(getObjTypeIndex().getNum()); } -void CGCreature::pickRandomObject(CRandomGenerator & rand) +const CCreature * CGCreature::getCreature() const +{ + return getCreatureID().toCreature(); +} + +void CGCreature::pickRandomObject(vstd::RNG & rand) { switch(ID.toEnum()) { @@ -227,14 +253,14 @@ void CGCreature::pickRandomObject(CRandomGenerator & rand) { // Try to generate some debug information if sanity check failed CreatureID creatureID(subID.getNum()); - throw std::out_of_range("Failed to find handler for creature " + std::to_string(creatureID.getNum()) + ", identifer:" + creatureID.toEntity(VLC)->getJsonKey()); + throw std::out_of_range("Failed to find handler for creature " + std::to_string(creatureID.getNum()) + ", identifier:" + creatureID.toEntity(VLC)->getJsonKey()); } ID = MapObjectID::MONSTER; setType(ID, subID); } -void CGCreature::initObj(CRandomGenerator & rand) +void CGCreature::initObj(vstd::RNG & rand) { blockVisit = true; switch(character) @@ -258,7 +284,7 @@ void CGCreature::initObj(CRandomGenerator & 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()); @@ -270,23 +296,23 @@ void CGCreature::initObj(CRandomGenerator & rand) } } - temppower = stacks[SlotID(0)]->count * static_cast(1000); + temppower = stacks[SlotID(0)]->count * static_cast(1000); refusedJoining = false; } -void CGCreature::newTurn(CRandomGenerator & rand) const +void CGCreature::newTurn(vstd::RNG & rand) const {//Works only for stacks of single type of size up to 2 millions if (!notGrowingTeam) { - if (stacks.begin()->second->count < VLC->settings()->getInteger(EGameSettings::CREATURES_WEEKLY_GROWTH_CAP) && cb->getDate(Date::DAY_OF_WEEK) == 1 && cb->getDate(Date::DAY) > 1) + if (stacks.begin()->second->count < cb->getSettings().getInteger(EGameSettings::CREATURES_WEEKLY_GROWTH_CAP) && cb->getDate(Date::DAY_OF_WEEK) == 1 && cb->getDate(Date::DAY) > 1) { - ui32 power = static_cast(temppower * (100 + VLC->settings()->getInteger(EGameSettings::CREATURES_WEEKLY_GROWTH_PERCENT)) / 100); - cb->setObjPropertyValue(id, ObjProperty::MONSTER_COUNT, std::min(power / 1000, VLC->settings()->getInteger(EGameSettings::CREATURES_WEEKLY_GROWTH_CAP))); //set new amount + ui32 power = static_cast(temppower * (100 + cb->getSettings().getInteger(EGameSettings::CREATURES_WEEKLY_GROWTH_PERCENT)) / 100); + cb->setObjPropertyValue(id, ObjProperty::MONSTER_COUNT, std::min(power / 1000, cb->getSettings().getInteger(EGameSettings::CREATURES_WEEKLY_GROWTH_CAP))); //set new amount cb->setObjPropertyValue(id, ObjProperty::MONSTER_POWER, power); //increase temppower } } - if (VLC->settings()->getBoolean(EGameSettings::MODULE_STACK_EXPERIENCE)) - cb->setObjPropertyValue(id, ObjProperty::MONSTER_EXP, VLC->settings()->getInteger(EGameSettings::CREATURES_DAILY_STACK_EXPERIENCE)); //for testing purpose + if (cb->getSettings().getBoolean(EGameSettings::MODULE_STACK_EXPERIENCE)) + cb->setObjPropertyValue(id, ObjProperty::MONSTER_EXP, cb->getSettings().getInteger(EGameSettings::CREATURES_DAILY_STACK_EXPERIENCE)); //for testing purpose } void CGCreature::setPropertyDer(ObjProperty what, ObjPropertyID identifier) { @@ -332,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; @@ -359,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 } @@ -454,16 +480,16 @@ 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, CRandomGenerator::getDefault()); + auto it = RandomGeneratorUtil::nextItem(upgrades, cb->gameState()->getRandomGenerator()); cb->changeStackType(StackLocation(this, slotID), it->toCreature()); } } } - cb->startBattleI(h, this); + cb->startBattle(h, this); } @@ -472,18 +498,18 @@ 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))); - cb->showBlockingDialog(&ynd); + ynd.text.replaceName(getCreatureID(), getStackCount(SlotID(0))); + cb->showBlockingDialog(this, &ynd); } void CGCreature::battleFinished(const CGHeroInstance *hero, const BattleResult &result) const { - if(result.winner == 0) + if(result.winner == BattleSide::ATTACKER) { giveReward(hero); cb->removeObject(this, hero->getOwner()); } - else if(result.winner > 1) // draw + else if(result.winner == BattleSide::NONE) // draw { // guarded reward is lost forever on draw cb->removeObject(this, hero->getOwner()); @@ -492,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 } @@ -510,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 @@ -521,7 +547,7 @@ void CGCreature::battleFinished(const CGHeroInstance *hero, const BattleResult & } } -void CGCreature::blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) const +void CGCreature::blockingDialogAnswered(const CGHeroInstance *hero, int32_t answer) const { auto action = takenAction(hero); if(!refusedJoining && action >= JOIN_FOR_FREE) //higher means price @@ -541,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; } @@ -570,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; @@ -603,7 +629,7 @@ void CGCreature::giveReward(const CGHeroInstance * h) const if(gainedArtifact != ArtifactID::NONE) { - cb->giveHeroNewArtifact(h, gainedArtifact.toArtifact(), ArtifactPosition::FIRST_AVAILABLE); + cb->giveHeroNewArtifact(h, gainedArtifact, ArtifactPosition::FIRST_AVAILABLE); iw.components.emplace_back(ComponentType::ARTIFACT, gainedArtifact); } diff --git a/lib/mapObjects/CGCreature.h b/lib/mapObjects/CGCreature.h index 0eb70530f..daa554a2e 100644 --- a/lib/mapObjects/CGCreature.h +++ b/lib/mapObjects/CGCreature.h @@ -11,7 +11,6 @@ #include "CArmedInstance.h" #include "../ResourceSet.h" -#include "../MetaString.h" VCMI_LIB_NAMESPACE_BEGIN @@ -35,7 +34,7 @@ public: ArtifactID gainedArtifact; //ID of artifact gained to hero, -1 if none bool neverFlees = false; //if true, the troops will never flee bool notGrowingTeam = false; //if true, number of units won't grow - ui64 temppower = 0; //used to handle fractional stack growth for tiny stacks + int64_t temppower = 0; //used to handle fractional stack growth for tiny stacks bool refusedJoining = false; @@ -45,12 +44,13 @@ public: std::string getPopupText(PlayerColor player) const override; std::string getPopupText(const CGHeroInstance * hero) const override; std::vector getPopupComponents(PlayerColor player) const override; - void initObj(CRandomGenerator & rand) override; - void pickRandomObject(CRandomGenerator & rand) override; - void newTurn(CRandomGenerator & rand) const override; + void initObj(vstd::RNG & rand) override; + void pickRandomObject(vstd::RNG & rand) override; + void newTurn(vstd::RNG & rand) const override; void battleFinished(const CGHeroInstance *hero, const BattleResult &result) const override; - void blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) const override; - CreatureID getCreature() const; + void blockingDialogAnswered(const CGHeroInstance *hero, int32_t answer) const override; + CreatureID getCreatureID() const; + const CCreature * getCreature() const; //stack formation depends on position, bool containsUpgradedStack() const; @@ -82,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 023abd77e..ac9ca1905 100644 --- a/lib/mapObjects/CGDwelling.cpp +++ b/lib/mapObjects/CGDwelling.cpp @@ -11,6 +11,7 @@ #include "StdInc.h" #include "CGDwelling.h" #include "../serializer/JsonSerializeFormat.h" +#include "../entities/faction/CTownHandler.h" #include "../mapping/CMap.h" #include "../mapObjectConstructors/AObjectTypeHandler.h" #include "../mapObjectConstructors/CObjectClassesHandler.h" @@ -20,13 +21,14 @@ #include "../networkPacks/StackLocation.h" #include "../networkPacks/PacksForClient.h" #include "../networkPacks/PacksForClientBattle.h" -#include "../CTownHandler.h" #include "../IGameCallback.h" #include "../gameState/CGameState.h" #include "../CPlayerState.h" -#include "../GameSettings.h" +#include "../IGameSettings.h" #include "../CConfigHandler.h" +#include + VCMI_LIB_NAMESPACE_BEGIN void CGDwellingRandomizationInfo::serializeJson(JsonSerializeFormat & handler) @@ -50,7 +52,7 @@ CGDwelling::CGDwelling(IGameCallback *cb): CGDwelling::~CGDwelling() = default; -FactionID CGDwelling::randomizeFaction(CRandomGenerator & rand) +FactionID CGDwelling::randomizeFaction(vstd::RNG & rand) { if (ID == Obj::RANDOM_DWELLING_FACTION) return FactionID(subID.getNum()); @@ -91,7 +93,7 @@ FactionID CGDwelling::randomizeFaction(CRandomGenerator & rand) assert(linkedTown->ID == Obj::TOWN); if(linkedTown->ID==Obj::TOWN) - return linkedTown->getFaction(); + return linkedTown->getFactionID(); } if(!randomizationInfo->allowedFactions.empty()) @@ -108,7 +110,7 @@ FactionID CGDwelling::randomizeFaction(CRandomGenerator & rand) return *RandomGeneratorUtil::nextItem(potentialPicks, rand); } -int CGDwelling::randomizeLevel(CRandomGenerator & rand) +int CGDwelling::randomizeLevel(vstd::RNG & rand) { if (ID == Obj::RANDOM_DWELLING_LVL) return subID.getNum(); @@ -125,7 +127,7 @@ int CGDwelling::randomizeLevel(CRandomGenerator & rand) return rand.nextInt(randomizationInfo->minLevel, randomizationInfo->maxLevel) - 1; } -void CGDwelling::pickRandomObject(CRandomGenerator & rand) +void CGDwelling::pickRandomObject(vstd::RNG & rand) { if (ID == Obj::RANDOM_DWELLING || ID == Obj::RANDOM_DWELLING_LVL || ID == Obj::RANDOM_DWELLING_FACTION) { @@ -172,18 +174,15 @@ void CGDwelling::pickRandomObject(CRandomGenerator & rand) } } -void CGDwelling::initObj(CRandomGenerator & rand) +void CGDwelling::initObj(vstd::RNG & rand) { switch(ID.toEnum()) { case Obj::CREATURE_GENERATOR1: case Obj::CREATURE_GENERATOR4: + case Obj::WAR_MACHINE_FACTORY: { getObjectHandler()->configureObject(this, rand); - - if (getOwner() != PlayerColor::NEUTRAL) - cb->gameState()->players[getOwner()].dwellings.emplace_back(this); - assert(!creatures.empty()); assert(!creatures[0].second.empty()); break; @@ -192,13 +191,6 @@ void CGDwelling::initObj(CRandomGenerator & 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; @@ -209,19 +201,6 @@ void CGDwelling::setPropertyDer(ObjProperty what, ObjPropertyID identifier) { switch (what) { - case ObjProperty::OWNER: //change owner - if (ID == Obj::CREATURE_GENERATOR1 || ID == Obj::CREATURE_GENERATOR2 - || ID == Obj::CREATURE_GENERATOR3 || ID == Obj::CREATURE_GENERATOR4) - { - if (tempOwner != PlayerColor::NEUTRAL) - { - std::vector >* dwellings = &cb->gameState()->players[tempOwner].dwellings; - dwellings->erase (std::find(dwellings->begin(), dwellings->end(), this)); - } - if (identifier.as().isValidPlayer()) - cb->gameState()->players[identifier.as()].dwellings.emplace_back(this); - } - break; case ObjProperty::AVAILABLE_CREATURE: creatures.resize(1); creatures[0].second.resize(1); @@ -239,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; } @@ -259,7 +238,7 @@ void CGDwelling::onHeroVisit( const CGHeroInstance * h ) const else bd.text.replaceLocalString(EMetaText::ARRAY_TXT, 173 + (int)Slots().begin()->second->getQuantityID()*3); bd.text.replaceName(*Slots().begin()->second); - cb->showBlockingDialog(&bd); + cb->showBlockingDialog(this, &bd); return; } @@ -295,10 +274,10 @@ void CGDwelling::onHeroVisit( const CGHeroInstance * h ) const bd.flags |= BlockingDialog::SAFE_TO_AUTOACCEPT; } - cb->showBlockingDialog(&bd); + cb->showBlockingDialog(this, &bd); } -void CGDwelling::newTurn(CRandomGenerator & rand) const +void CGDwelling::newTurn(vstd::RNG & rand) const { if(cb->getDate(Date::DAY_OF_WEEK) != 1) //not first day of week return; @@ -324,9 +303,9 @@ void CGDwelling::newTurn(CRandomGenerator & rand) const bool creaturesAccumulate = false; if (tempOwner.isValidPlayer()) - creaturesAccumulate = VLC->settings()->getBoolean(EGameSettings::DWELLINGS_ACCUMULATE_WHEN_OWNED); + creaturesAccumulate = cb->getSettings().getBoolean(EGameSettings::DWELLINGS_ACCUMULATE_WHEN_OWNED); else - creaturesAccumulate = VLC->settings()->getBoolean(EGameSettings::DWELLINGS_ACCUMULATE_WHEN_NEUTRAL); + creaturesAccumulate = cb->getSettings().getBoolean(EGameSettings::DWELLINGS_ACCUMULATE_WHEN_NEUTRAL); const CCreature * cre =creatures[i].second[0].toCreature(); TQuantity amount = cre->getGrowth() * (1 + cre->valOfBonuses(BonusType::CREATURE_GROWTH_PERCENT)/100) + cre->valOfBonuses(BonusType::CREATURE_GROWTH, BonusCustomSubtype::creatureLevel(cre->getLevel())); @@ -339,7 +318,7 @@ void CGDwelling::newTurn(CRandomGenerator & rand) const } if(change) - cb->sendAndApply(&sac); + cb->sendAndApply(sac); updateGuards(); } @@ -407,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 { @@ -416,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); } } } @@ -433,13 +412,13 @@ void CGDwelling::heroAcceptsCreatures( const CGHeroInstance *h) const if(count) //there are available creatures { - if (VLC->settings()->getBoolean(EGameSettings::DWELLINGS_MERGE_ON_RECRUIT)) + if (cb->getSettings().getBoolean(EGameSettings::DWELLINGS_MERGE_ON_RECRUIT)) { SlotID testSlot = h->getSlotFor(crid); if(!testSlot.validSlot()) //no available slot - try merging army of visiting hero { std::pair toMerge; - if (h->mergableStacks(toMerge)) + if (h->mergeableStacks(toMerge)) { cb->moveStack(StackLocation(h, toMerge.first), StackLocation(h, toMerge.second), -1); //merge toMerge.first into toMerge.second assert(!h->hasStackAtSlot(toMerge.first)); //we have now a new free slot @@ -473,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); } } @@ -484,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 @@ -498,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; @@ -508,19 +487,19 @@ void CGDwelling::heroAcceptsCreatures( const CGHeroInstance *h) const void CGDwelling::battleFinished(const CGHeroInstance *hero, const BattleResult &result) const { - if (result.winner == 0) + if (result.winner == BattleSide::ATTACKER) { onHeroVisit(hero); } } -void CGDwelling::blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) const +void CGDwelling::blockingDialogAnswered(const CGHeroInstance *hero, int32_t answer) const { auto relations = cb->getPlayerRelations(getOwner(), hero->getOwner()); if(stacksCount() > 0 && relations == PlayerRelations::ENEMIES) //guards present { if(answer) - cb->startBattleI(hero, this); + cb->startBattle(hero, this); } else if(answer) { @@ -549,4 +528,33 @@ void CGDwelling::serializeJsonOptions(JsonSerializeFormat & handler) } } +const IOwnableObject * CGDwelling::asOwnable() const +{ + switch (ID.toEnum()) + { + case Obj::WAR_MACHINE_FACTORY: + case Obj::REFUGEE_CAMP: + return nullptr; // can't be owned + default: + return this; + } +} + +ResourceSet CGDwelling::dailyIncome() const +{ + return {}; +} + +std::vector CGDwelling::providedCreatures() const +{ + if (ID == Obj::WAR_MACHINE_FACTORY || ID == Obj::REFUGEE_CAMP) + return {}; + + std::vector result; + for (const auto & level : creatures) + result.insert(result.end(), level.second.begin(), level.second.end()); + + return result; +} + VCMI_LIB_NAMESPACE_END diff --git a/lib/mapObjects/CGDwelling.h b/lib/mapObjects/CGDwelling.h index 08d592227..250693285 100644 --- a/lib/mapObjects/CGDwelling.h +++ b/lib/mapObjects/CGDwelling.h @@ -11,6 +11,7 @@ #pragma once #include "CArmedInstance.h" +#include "IOwnableObject.h" VCMI_LIB_NAMESPACE_BEGIN @@ -30,10 +31,10 @@ public: void serializeJson(JsonSerializeFormat & handler); }; -class DLL_LINKAGE CGDwelling : public CArmedInstance +class DLL_LINKAGE CGDwelling : public CArmedInstance, public IOwnableObject { public: - typedef std::vector > > TCreaturesSet; + using TCreaturesSet = std::vector > >; std::optional randomizationInfo; //random dwelling options; not serialized TCreaturesSet creatures; //creatures[level] -> @@ -41,20 +42,24 @@ public: CGDwelling(IGameCallback *cb); ~CGDwelling() override; + const IOwnableObject * asOwnable() const final; + ResourceSet dailyIncome() const override; + std::vector providedCreatures() const override; + protected: void serializeJsonOptions(JsonSerializeFormat & handler) override; private: - FactionID randomizeFaction(CRandomGenerator & rand); - int randomizeLevel(CRandomGenerator & rand); + FactionID randomizeFaction(vstd::RNG & rand); + int randomizeLevel(vstd::RNG & rand); - void pickRandomObject(CRandomGenerator & rand) override; - void initObj(CRandomGenerator & rand) override; + void pickRandomObject(vstd::RNG & rand) override; + void initObj(vstd::RNG & rand) override; void onHeroVisit(const CGHeroInstance * h) const override; - void newTurn(CRandomGenerator & rand) const override; + void newTurn(vstd::RNG & rand) const override; void setPropertyDer(ObjProperty what, ObjPropertyID identifier) override; void battleFinished(const CGHeroInstance *hero, const BattleResult &result) const override; - void blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) const override; + void blockingDialogAnswered(const CGHeroInstance *hero, int32_t answer) const override; std::vector getPopupComponents(PlayerColor player) const override; void updateGuards() const; diff --git a/lib/mapObjects/CGHeroInstance.cpp b/lib/mapObjects/CGHeroInstance.cpp index 0a9a127c7..acc563274 100644 --- a/lib/mapObjects/CGHeroInstance.cpp +++ b/lib/mapObjects/CGHeroInstance.cpp @@ -15,22 +15,24 @@ #include #include -#include "../CGeneralTextHandler.h" +#include "../texts/CGeneralTextHandler.h" #include "../ArtifactUtils.h" -#include "../CHeroHandler.h" #include "../TerrainHandler.h" #include "../RoadHandler.h" -#include "../GameSettings.h" +#include "../IGameSettings.h" #include "../CSoundBase.h" #include "../spells/CSpellHandler.h" #include "../CSkillHandler.h" #include "../IGameCallback.h" #include "../gameState/CGameState.h" #include "../CCreatureHandler.h" -#include "../CTownHandler.h" #include "../mapping/CMap.h" #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" #include "../pathfinder/TurnInfo.h" @@ -47,6 +49,8 @@ VCMI_LIB_NAMESPACE_BEGIN +const ui32 CGHeroInstance::NO_PATROLLING = std::numeric_limits::max(); + void CGHeroPlaceholder::serializeJsonOptions(JsonSerializeFormat & handler) { serializeJsonOwner(handler); @@ -98,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; @@ -115,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 @@ -228,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; @@ -281,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), @@ -302,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::initHero(CRandomGenerator & rand, const HeroTypeID & SUBID) +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); } -void CGHeroInstance::initHero(CRandomGenerator & 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 @@ -340,9 +390,9 @@ void CGHeroInstance::initHero(CRandomGenerator & 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::createNewArtifactInstance(ArtifactID::SPELLBOOK); + auto artifact = ArtifactUtils::createArtifact(ArtifactID::SPELLBOOK); putArtifact(ArtifactPosition::SPELLBOOK, artifact); } } @@ -351,7 +401,7 @@ void CGHeroInstance::initHero(CRandomGenerator & rand) if(!getArt(ArtifactPosition::MACH4)) { - auto artifact = ArtifactUtils::createNewArtifactInstance(ArtifactID::CATAPULT); + auto artifact = ArtifactUtils::createArtifact(ArtifactID::CATAPULT); putArtifact(ArtifactPosition::MACH4, artifact); //everyone has a catapult } @@ -359,14 +409,11 @@ void CGHeroInstance::initHero(CRandomGenerator & 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 @@ -392,7 +439,7 @@ void CGHeroInstance::initHero(CRandomGenerator & rand) // are not attached to global bonus node but need access to some global bonuses // e.g. MANA_PER_KNOWLEDGE_PERCENTAGE for correct preview and initial state after recruit for(const auto & ob : VLC->modh->heroBaseBonuses) // or MOVEMENT to compute initial movement before recruiting is finished - const JsonNode & baseBonuses = VLC->settings()->getValue(EGameSettings::BONUSES_PER_HERO); + const JsonNode & baseBonuses = cb->getSettings().getValue(EGameSettings::BONUSES_PER_HERO); for(const auto & b : baseBonuses.Struct()) { auto bonus = JsonUtils::parseBonus(b.second); @@ -402,9 +449,9 @@ void CGHeroInstance::initHero(CRandomGenerator & rand) addNewBonus(bonus); } - if (VLC->settings()->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 } @@ -412,7 +459,7 @@ void CGHeroInstance::initHero(CRandomGenerator & 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 @@ -422,24 +469,24 @@ void CGHeroInstance::initHero(CRandomGenerator & rand) mana = manaLimit(); //after all bonuses are taken into account, make sure this line is the last one } -void CGHeroInstance::initArmy(CRandomGenerator & rand, IArmyDescriptor * dst) +void CGHeroInstance::initArmy(vstd::RNG & rand, IArmyDescriptor * dst) { if(!dst) dst = this; int warMachinesGiven = 0; - auto stacksCountChances = VLC->settings()->getVector(EGameSettings::HEROES_STARTING_STACKS_CHANCES); + 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); @@ -467,7 +514,7 @@ void CGHeroInstance::initArmy(CRandomGenerator & rand, IArmyDescriptor * dst) if(!getArt(slot)) { - auto artifact = ArtifactUtils::createNewArtifactInstance(aid); + auto artifact = ArtifactUtils::createArtifact(aid); putArtifact(slot, artifact); } else @@ -511,12 +558,12 @@ void CGHeroInstance::onHeroVisit(const CGHeroInstance * h) const if(visitedTown) //we're in town visitedTown->onHeroVisit(h); //town will handle attacking else - cb->startBattleI(h, this); + cb->startBattle(h, this); } } else if(ID == Obj::PRISON) { - if (cb->getHeroCount(h->tempOwner, false) < VLC->settings()->getInteger(EGameSettings::HEROES_PER_PLAYER_ON_MAP_CAP))//free hero slot + if (cb->getHeroCount(h->tempOwner, false) < cb->getSettings().getInteger(EGameSettings::HEROES_PER_PLAYER_ON_MAP_CAP))//free hero slot { //update hero parameters SetMovePoints smp; @@ -532,7 +579,7 @@ void CGHeroInstance::onHeroVisit(const CGHeroInstance * h) const if (!boat) { //Create a new boat for hero - cb->createObject(boatPos, h->getOwner(), Obj::BOAT, getBoatType().getNum()); + cb->createBoat(boatPos, getBoatType(), h->getOwner()); boatId = cb->getTopObj(boatPos)->id; } } @@ -566,13 +613,32 @@ std::string CGHeroInstance::getObjectName() const return VLC->objtypeh->getObjectName(ID, 0); } +std::string CGHeroInstance::getHoverText(PlayerColor player) const +{ + std::string hoverText = CArmedInstance::getHoverText(player) + getMovementPointsTextIfOwner(player); + return hoverText; +} + +std::string CGHeroInstance::getMovementPointsTextIfOwner(PlayerColor player) const +{ + std::string output = ""; + if(player == getOwner()) + { + output += " " + VLC->generaltexth->translate("vcmi.adventureMap.movementPointsHeroInfo"); + boost::replace_first(output, "%POINTS", std::to_string(movementPointsLimit(!boat))); + boost::replace_first(output, "%REMAINING", std::to_string(movementPointsRemaining())); + } + + return output; +} + 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(): @@ -589,19 +655,18 @@ void CGHeroInstance::SecondarySkillsInfo::resetWisdomCounter() wisdomCounter = 0; } -void CGHeroInstance::pickRandomObject(CRandomGenerator & rand) +void CGHeroInstance::pickRandomObject(vstd::RNG & rand) { assert(ID == Obj::HERO || ID == Obj::PRISON || ID == Obj::RANDOM_HERO); 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; @@ -609,16 +674,11 @@ void CGHeroInstance::pickRandomObject(CRandomGenerator & 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; } -void CGHeroInstance::initObj(CRandomGenerator & rand) -{ - -} - void CGHeroInstance::recreateSecondarySkillsBonuses() { auto secondarySkillsBonuses = getBonuses(Selector::sourceType()(BonusSource::SECONDARY_SKILL)); @@ -651,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 @@ -659,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); } @@ -789,7 +872,7 @@ void CGHeroInstance::spendMana(ServerCallback * server, const int spellCost) con sm.hid = id; sm.val = -spellCost; - server->apply(&sm); + server->apply(sm); } } @@ -885,7 +968,7 @@ CStackBasicDescriptor CGHeroInstance::calculateNecromancy (const BattleResult &b double necromancySkill = valOfBonuses(BonusType::UNDEAD_RAISE_PERCENTAGE) / 100.0; const ui8 necromancyLevel = valOfBonuses(BonusType::IMPROVED_NECROMANCY); vstd::amin(necromancySkill, 1.0); //it's impossible to raise more creatures than all... - const std::map &casualties = battleResult.casualties[!battleResult.winner]; + const std::map &casualties = battleResult.casualties[CBattleInfoEssentials::otherSide(battleResult.winner)]; // figure out what to raise - pick strongest creature meeting requirements CreatureID creatureTypeRaised = CreatureID::NONE; //now we always have IMPROVED_NECROMANCY, no need for hardcode int requiredCasualtyLevel = 1; @@ -959,7 +1042,7 @@ CStackBasicDescriptor CGHeroInstance::calculateNecromancy (const BattleResult &b * @param raisedStack Pair where the first element represents ID of the raised creature * and the second element the amount. */ -void CGHeroInstance::showNecromancyDialog(const CStackBasicDescriptor &raisedStack, CRandomGenerator & rand) const +void CGHeroInstance::showNecromancyDialog(const CStackBasicDescriptor &raisedStack, vstd::RNG & rand) const { InfoWindow iw; iw.type = EInfoWindowMode::AUTO; @@ -1026,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 @@ -1065,10 +1148,10 @@ void CGHeroInstance::pushPrimSkill( PrimarySkill which, int val ) EAlignment CGHeroInstance::getAlignment() const { - return type->heroClass->getAlignment(); + return getHeroClass()->getAlignment(); } -void CGHeroInstance::initExp(CRandomGenerator & rand) +void CGHeroInstance::initExp(vstd::RNG & rand) { exp = rand.nextInt(40, 89); } @@ -1089,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 @@ -1111,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); @@ -1135,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)); @@ -1150,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); @@ -1186,7 +1269,7 @@ void CGHeroInstance::removeSpellbook() if(hasSpellbook()) { - getArt(ArtifactPosition::SPELLBOOK)->removeFrom(*this, ArtifactPosition::SPELLBOOK); + cb->gameState()->map->removeArtifactInstance(*this, ArtifactPosition::SPELLBOOK); } } @@ -1286,7 +1369,7 @@ ArtBearer::ArtBearer CGHeroInstance::bearerType() const return ArtBearer::HERO; } -std::vector CGHeroInstance::getLevelUpProposedSecondarySkills(CRandomGenerator & rand) const +std::vector CGHeroInstance::getLevelUpProposedSecondarySkills(vstd::RNG & rand) const { auto getObligatorySkills = [](CSkill::Obligatory obl){ std::set obligatory; @@ -1334,11 +1417,11 @@ std::vector CGHeroInstance::getLevelUpProposedSecondarySkills(CR 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); @@ -1365,11 +1448,11 @@ std::vector CGHeroInstance::getLevelUpProposedSecondarySkills(CR return skills; } -PrimarySkill CGHeroInstance::nextPrimarySkill(CRandomGenerator & rand) const +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()) { @@ -1381,7 +1464,7 @@ PrimarySkill CGHeroInstance::nextPrimarySkill(CRandomGenerator & rand) const return static_cast(RandomGeneratorUtil::nextItemWeighted(skillChances, rand)); } -std::optional CGHeroInstance::nextSecondarySkill(CRandomGenerator & rand) const +std::optional CGHeroInstance::nextSecondarySkill(vstd::RNG & rand) const { assert(gainsLevel()); @@ -1469,7 +1552,7 @@ void CGHeroInstance::levelUp(const std::vector & skills) treeHasChanged(); } -void CGHeroInstance::levelUpAutomatically(CRandomGenerator & rand) +void CGHeroInstance::levelUpAutomatically(vstd::RNG & rand) { while(gainsLevel()) { @@ -1499,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) { @@ -1690,7 +1763,7 @@ void CGHeroInstance::serializeCommonOptions(JsonSerializeFormat & handler) handler.serializeIdArray("spellBook", spells); if(handler.saving) - CArtifactSet::serializeJsonArtifacts(handler, "artifacts", nullptr); + CArtifactSet::serializeJsonArtifacts(handler, "artifacts"); } void CGHeroInstance::serializeJsonOptions(JsonSerializeFormat & handler) @@ -1714,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; } } } @@ -1778,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()); } } } @@ -1802,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; @@ -1820,10 +1891,40 @@ 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; } +ResourceSet CGHeroInstance::dailyIncome() const +{ + ResourceSet income; + + for (GameResID k : GameResID::ALL_RESOURCES()) + income[k] += valOfBonuses(BonusType::GENERATE_RESOURCE, BonusSubtypeID(k)); + + const auto & playerSettings = cb->getPlayerSettings(getOwner()); + income.applyHandicap(playerSettings->handicap.percentIncome); + return income; +} + +std::vector CGHeroInstance::providedCreatures() const +{ + return {}; +} + +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)); + auto minSkillValue = VLC->engineSettings()->getVector(EGameSettings::HEROES_MINIMAL_PRIMARY_SKILLS)[which.getNum()]; + return std::max(valOfBonuses(selector, cachingStr), minSkillValue); +} + VCMI_LIB_NAMESPACE_END diff --git a/lib/mapObjects/CGHeroInstance.h b/lib/mapObjects/CGHeroInstance.h index b929ef7e0..e453760b8 100644 --- a/lib/mapObjects/CGHeroInstance.h +++ b/lib/mapObjects/CGHeroInstance.h @@ -12,7 +12,9 @@ #include #include "CArmedInstance.h" +#include "IOwnableObject.h" +#include "../entities/hero/EHeroGender.h" #include "../CArtHandler.h" // For CArtifactSet VCMI_LIB_NAMESPACE_BEGIN @@ -23,7 +25,6 @@ class CGTownInstance; class CMap; struct TerrainTile; struct TurnInfo; -enum class EHeroGender : int8_t; class DLL_LINKAGE CGHeroPlaceholder : public CGObjectInstance { @@ -48,7 +49,7 @@ protected: }; -class DLL_LINKAGE CGHeroInstance : public CArmedInstance, public IBoatGenerator, public CArtifactSet, public spells::Caster, public AFactionMember, public ICreatureUpgrader +class DLL_LINKAGE CGHeroInstance : public CArmedInstance, public IBoatGenerator, public CArtifactSet, public spells::Caster, public AFactionMember, public ICreatureUpgrader, public IOwnableObject { // We serialize heroes into JSON for crossover friend class CampaignState; @@ -71,7 +72,6 @@ public: ////////////////////////////////////////////////////////////////////////// - const CHero * type; TExpType exp; //experience points ui32 level; //current level of hero @@ -92,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 @@ -99,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; @@ -165,8 +165,12 @@ public: EAlignment getAlignment() const; bool needsLastStack()const override; + ResourceSet dailyIncome() const override; + std::vector providedCreatures() const override; + 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 @@ -187,13 +191,13 @@ public: bool gainsLevel() const; /// Returns the next primary skill on level up. Can only be called if hero can gain a level up. - PrimarySkill nextPrimarySkill(CRandomGenerator & rand) const; + PrimarySkill nextPrimarySkill(vstd::RNG & rand) const; /// Returns the next secondary skill randomly on level up. Can only be called if hero can gain a level up. - std::optional nextSecondarySkill(CRandomGenerator & rand) const; + std::optional nextSecondarySkill(vstd::RNG & rand) const; /// Gets 0, 1 or 2 secondary skills which are proposed on hero level up. - std::vector getLevelUpProposedSecondarySkills(CRandomGenerator & rand) const; + std::vector getLevelUpProposedSecondarySkills(vstd::RNG & rand) const; ui8 getSecSkillLevel(const SecondarySkill & skill) const; //0 - no skill @@ -219,27 +223,35 @@ 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, CRandomGenerator & rand) const; + void showNecromancyDialog(const CStackBasicDescriptor &raisedStack, vstd::RNG & rand) const; EDiggingStatus diggingStatus() const; ////////////////////////////////////////////////////////////////////////// - HeroTypeID getHeroType() const; + const CHeroClass * getHeroClass() const; + HeroClassID getHeroClassID() const; + + const CHero * getHeroType() const; + HeroTypeID getHeroTypeID() const; void setHeroType(HeroTypeID type); - void initHero(CRandomGenerator & rand); - void initHero(CRandomGenerator & rand, const HeroTypeID & SUBID); + 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; - void initExp(CRandomGenerator & rand); - void initArmy(CRandomGenerator & rand, IArmyDescriptor *dst = nullptr); + 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); ui8 maxlevelsToMagicSchool() const; ui8 maxlevelsToWisdom() const; @@ -292,11 +304,15 @@ public: void attachToBoat(CGBoat* newBoat); void boatDeserializationFix(); void deserializationFix(); + void updateAppearance(); - void initObj(CRandomGenerator & rand) override; - void pickRandomObject(CRandomGenerator & rand) override; + void pickRandomObject(vstd::RNG & rand) override; void onHeroVisit(const CGHeroInstance * h) const override; std::string getObjectName() const override; + 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; @@ -318,7 +334,7 @@ protected: void serializeJsonOptions(JsonSerializeFormat & handler) override; private: - void levelUpAutomatically(CRandomGenerator & rand); + void levelUpAutomatically(vstd::RNG & rand); public: std::string getHeroTypeName() const; @@ -346,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 dfc072366..a7eef18ad 100644 --- a/lib/mapObjects/CGMarket.cpp +++ b/lib/mapObjects/CGMarket.cpp @@ -11,19 +11,25 @@ #include "StdInc.h" #include "CGMarket.h" -#include "../CGeneralTextHandler.h" +#include "../texts/CGeneralTextHandler.h" #include "../IGameCallback.h" #include "../CCreatureHandler.h" #include "CGTownInstance.h" -#include "../GameSettings.h" +#include "../IGameSettings.h" #include "../CSkillHandler.h" #include "../mapObjectConstructors/AObjectTypeHandler.h" #include "../mapObjectConstructors/CObjectClassesHandler.h" +#include "../mapObjectConstructors/CommonConstructors.h" #include "../networkPacks/PacksForClient.h" VCMI_LIB_NAMESPACE_BEGIN -void CGMarket::initObj(CRandomGenerator & rand) +ObjectInstanceID CGMarket::getObjInstanceID() const +{ + return id; +} + +void CGMarket::initObj(vstd::RNG & rand) { getObjectHandler()->configureObject(this, rand); } @@ -33,14 +39,25 @@ void CGMarket::onHeroVisit(const CGHeroInstance * h) const cb->showObjectWindow(this, EOpenWindowMode::MARKET_WINDOW, h, true); } -int CGMarket::getMarketEfficiency() const +std::string CGMarket::getPopupText(PlayerColor player) const { - return marketEfficiency; + 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(); } -bool CGMarket::allowsTrade(EMarketMode mode) const +std::string CGMarket::getPopupText(const CGHeroInstance * hero) const { - return marketModes.count(mode); + return getPopupText(hero->getOwner()); +} + +int CGMarket::getMarketEfficiency() const +{ + return getMarketHandler()->getMarketEfficiency(); } int CGMarket::availableUnits(EMarketMode mode, int marketItemSerial) const @@ -48,11 +65,16 @@ int CGMarket::availableUnits(EMarketMode mode, int marketItemSerial) const return -1; } -std::vector CGMarket::availableItemsIds(EMarketMode mode) const +std::shared_ptr CGMarket::getMarketHandler() const { - if(allowsTrade(mode)) - return IMarket::availableItemsIds(mode); - return std::vector(); + const auto & baseHandler = getObjectHandler(); + const auto & ourHandler = std::dynamic_pointer_cast(baseHandler); + return ourHandler; +} + +std::set CGMarket::availableModes() const +{ + return getMarketHandler()->availableModes(); } CGMarket::CGMarket(IGameCallback *cb): @@ -63,16 +85,11 @@ std::vector CGBlackMarket::availableItemsIds(EMarketMode mode) con { switch(mode) { - case EMarketMode::ARTIFACT_RESOURCE: - return IMarket::availableItemsIds(mode); 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: @@ -80,12 +97,12 @@ std::vector CGBlackMarket::availableItemsIds(EMarketMode mode) con } } -void CGBlackMarket::newTurn(CRandomGenerator & rand) const +void CGBlackMarket::newTurn(vstd::RNG & rand) const { - int resetPeriod = VLC->settings()->getInteger(EGameSettings::MARKETS_BLACK_MARKET_RESTOCK_PERIOD); + int resetPeriod = cb->getSettings().getInteger(EGameSettings::MARKETS_BLACK_MARKET_RESTOCK_PERIOD); bool isFirstDay = cb->getDate(Date::DAY) == 1; - bool regularResetTriggered = resetPeriod != 0 && ((cb->getDate(Date::DAY)-1) % resetPeriod) != 0; + bool regularResetTriggered = resetPeriod != 0 && ((cb->getDate(Date::DAY)-1) % resetPeriod) == 0; if (!isFirstDay && !regularResetTriggered) return; @@ -93,7 +110,7 @@ void CGBlackMarket::newTurn(CRandomGenerator & 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,14 +125,14 @@ 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); } -ArtBearer::ArtBearer CGArtifactsAltar::bearerType() const -{ - return ArtBearer::ALTAR; -} - VCMI_LIB_NAMESPACE_END diff --git a/lib/mapObjects/CGMarket.h b/lib/mapObjects/CGMarket.h index c0a589f3b..6f4f92a42 100644 --- a/lib/mapObjects/CGMarket.h +++ b/lib/mapObjects/CGMarket.h @@ -15,35 +15,57 @@ VCMI_LIB_NAMESPACE_BEGIN +class MarketInstanceConstructor; + class DLL_LINKAGE CGMarket : public CGObjectInstance, public IMarket { +protected: + std::shared_ptr getMarketHandler() const; + public: - - std::set marketModes; - int marketEfficiency; - - //window variables - std::string title; - std::string speech; //currently shown only in university - CGMarket(IGameCallback *cb); ///IObjectInterface void onHeroVisit(const CGHeroInstance * h) const override; //open trading window - void initObj(CRandomGenerator & rand) override;//set skills for trade + 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; - bool allowsTrade(EMarketMode mode) const override; int availableUnits(EMarketMode mode, int marketItemSerial) const override; //-1 if unlimited - std::vector availableItemsIds(EMarketMode mode) const override; + std::set availableModes() const override; - template void serialize(Handler &h) + template + void serialize(Handler &h) { h & static_cast(*this); - h & marketModes; - h & marketEfficiency; - h & title; - h & speech; + if (h.version < Handler::Version::NEW_MARKETS) + { + std::set marketModes; + h & marketModes; + } + + if (h.version < Handler::Version::MARKET_TRANSLATION_FIX) + { + int unused = 0; + h & unused; + } + + if (h.version < Handler::Version::NEW_MARKETS) + { + std::string speech; + std::string title; + h & speech; + h & title; + } + } + + template void serializeArtifactsAltar(Handler &h) + { + serialize(h); + IMarket::serializeArtifactsAltar(h); } }; @@ -52,15 +74,32 @@ class DLL_LINKAGE CGBlackMarket : public CGMarket public: using CGMarket::CGMarket; - std::vector artifacts; //available artifacts + std::vector artifacts; //available artifacts - void newTurn(CRandomGenerator & rand) const override; //reset artifacts for black market every month + void newTurn(vstd::RNG & rand) const override; //reset artifacts for black market every month std::vector availableItemsIds(EMarketMode mode) const override; 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; + } } }; @@ -69,6 +108,8 @@ class DLL_LINKAGE CGUniversity : public CGMarket public: using CGMarket::CGMarket; + std::string getSpeechTranslated() const; + std::vector skills; //available skills std::vector availableItemsIds(EMarketMode mode) const override; @@ -78,20 +119,12 @@ public: { h & static_cast(*this); h & skills; - } -}; - -class DLL_LINKAGE CGArtifactsAltar : public CGMarket, public CArtifactSet -{ -public: - using CGMarket::CGMarket; - - ArtBearer::ArtBearer bearerType() const override; - - template void serialize(Handler & h) - { - h & static_cast(*this); - h & static_cast(*this); + if (h.version >= Handler::Version::NEW_MARKETS && h.version < Handler::Version::MARKET_TRANSLATION_FIX) + { + std::string temp; + h & temp; + h & temp; + } } }; diff --git a/lib/mapObjects/CGObjectInstance.cpp b/lib/mapObjects/CGObjectInstance.cpp index f23f3a44a..8511a9875 100644 --- a/lib/mapObjects/CGObjectInstance.cpp +++ b/lib/mapObjects/CGObjectInstance.cpp @@ -15,7 +15,7 @@ #include "ObjectTemplate.h" #include "../gameState/CGameState.h" -#include "../CGeneralTextHandler.h" +#include "../texts/CGeneralTextHandler.h" #include "../IGameCallback.h" #include "../constants/StringConstants.h" #include "../TerrainHandler.h" @@ -25,6 +25,8 @@ #include "../networkPacks/PacksForClient.h" #include "../serializer/JsonSerializeFormat.h" +#include + VCMI_LIB_NAMESPACE_BEGIN //TODO: remove constructor @@ -52,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) @@ -67,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(); @@ -77,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 @@ -113,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; @@ -134,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 } @@ -164,12 +158,12 @@ void CGObjectInstance::setType(MapObjectID newID, MapObjectSubID newSubID) cb->gameState()->map->addBlockVisTiles(this); } -void CGObjectInstance::pickRandomObject(CRandomGenerator & rand) +void CGObjectInstance::pickRandomObject(vstd::RNG & rand) { // no-op } -void CGObjectInstance::initObj(CRandomGenerator & rand) +void CGObjectInstance::initObj(vstd::RNG & rand) { // no-op } @@ -198,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 ) {} @@ -213,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(); } @@ -232,7 +238,7 @@ std::string CGObjectInstance::getObjectName() const return VLC->objtypeh->getObjectName(ID, subID); } -std::optional CGObjectInstance::getAmbientSound() const +std::optional CGObjectInstance::getAmbientSound(vstd::RNG & rng) const { const auto & sounds = VLC->objtypeh->getObjectSounds(ID, subID).ambient; if(!sounds.empty()) @@ -241,20 +247,20 @@ std::optional CGObjectInstance::getAmbientSound() const return std::nullopt; } -std::optional CGObjectInstance::getVisitSound() const +std::optional CGObjectInstance::getVisitSound(vstd::RNG & rng) const { const auto & sounds = VLC->objtypeh->getObjectSounds(ID, subID).visit; if(!sounds.empty()) - return *RandomGeneratorUtil::nextItem(sounds, CRandomGenerator::getDefault()); + return *RandomGeneratorUtil::nextItem(sounds, rng); return std::nullopt; } -std::optional CGObjectInstance::getRemovalSound() const +std::optional CGObjectInstance::getRemovalSound(vstd::RNG & rng) const { const auto & sounds = VLC->objtypeh->getObjectSounds(ID, subID).removal; if(!sounds.empty()) - return *RandomGeneratorUtil::nextItem(sounds, CRandomGenerator::getDefault()); + return *RandomGeneratorUtil::nextItem(sounds, rng); return std::nullopt; } @@ -311,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(); } @@ -351,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); @@ -393,4 +405,9 @@ BattleField CGObjectInstance::getBattlefield() const return VLC->objtypeh->getHandlerFor(ID, subID)->getBattlefield(); } +const IOwnableObject * CGObjectInstance::asOwnable() const +{ + return nullptr; +} + VCMI_LIB_NAMESPACE_END diff --git a/lib/mapObjects/CGObjectInstance.h b/lib/mapObjects/CGObjectInstance.h index b1e4f144e..c201dd4db 100644 --- a/lib/mapObjects/CGObjectInstance.h +++ b/lib/mapObjects/CGObjectInstance.h @@ -10,10 +10,11 @@ #pragma once #include "IObjectInterface.h" + +#include "../bonuses/BonusEnum.h" #include "../constants/EntityIdentifiers.h" #include "../filesystem/ResourcePath.h" #include "../int3.h" -#include "../bonuses/BonusEnum.h" VCMI_LIB_NAMESPACE_BEGIN @@ -27,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 @@ -40,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; @@ -50,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 @@ -61,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 @@ -96,11 +97,11 @@ public: virtual bool isTile2Terrain() const { return false; } - std::optional getAmbientSound() const; - std::optional getVisitSound() const; - std::optional getRemovalSound() const; + std::optional getAmbientSound(vstd::RNG & rng) const; + std::optional getVisitSound(vstd::RNG & rng) const; + std::optional getRemovalSound(vstd::RNG & rng) const; - TObjectTypeHandler getObjectHandler() const; + virtual TObjectTypeHandler getObjectHandler() const; /** VIRTUAL METHODS **/ @@ -125,10 +126,12 @@ public: virtual std::vector getPopupComponents(PlayerColor player) const; virtual std::vector getPopupComponents(const CGHeroInstance * hero) const; + const IOwnableObject * asOwnable() const override; + /** OVERRIDES OF IObjectInterface **/ - void initObj(CRandomGenerator & rand) override; - void pickRandomObject(CRandomGenerator & rand) override; + void initObj(vstd::RNG & rand) override; + void pickRandomObject(vstd::RNG & rand) override; void onHeroVisit(const CGHeroInstance * h) const override; /// method for synchronous update. Note: For new properties classes should override setPropertyDer instead void setProperty(ObjProperty what, ObjPropertyID identifier) final; @@ -140,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/CGPandoraBox.cpp b/lib/mapObjects/CGPandoraBox.cpp index b79077948..c75f04dab 100644 --- a/lib/mapObjects/CGPandoraBox.cpp +++ b/lib/mapObjects/CGPandoraBox.cpp @@ -41,7 +41,7 @@ void CGPandoraBox::init() } } -void CGPandoraBox::initObj(CRandomGenerator & rand) +void CGPandoraBox::initObj(vstd::RNG & rand) { init(); @@ -175,25 +175,25 @@ void CGPandoraBox::onHeroVisit(const CGHeroInstance * h) const BlockingDialog bd (true, false); bd.player = h->getOwner(); bd.text.appendLocalString(EMetaText::ADVOB_TXT, 14); - cb->showBlockingDialog(&bd); + cb->showBlockingDialog(this, &bd); } void CGPandoraBox::battleFinished(const CGHeroInstance *hero, const BattleResult &result) const { - if(result.winner == 0) + if(result.winner == BattleSide::ATTACKER) { CRewardableObject::onHeroVisit(hero); } } -void CGPandoraBox::blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) const +void CGPandoraBox::blockingDialogAnswered(const CGHeroInstance *hero, int32_t answer) const { if(answer) { if(stacksCount() > 0) //if pandora's box is protected by army { hero->showInfoDialog(16, 0, EInfoWindowMode::MODAL); - cb->startBattleI(hero, this); //grants things after battle + cb->startBattle(hero, this); //grants things after battle } else if(getAvailableRewards(hero, Rewardable::EEventType::EVENT_FIRST_VISIT).empty()) { @@ -332,7 +332,7 @@ void CGEvent::activated( const CGHeroInstance * h ) const else iw.text.appendLocalString(EMetaText::ADVOB_TXT, 16); cb->showInfoDialog(&iw); - cb->startBattleI(h, this); + cb->startBattle(h, this); } else { diff --git a/lib/mapObjects/CGPandoraBox.h b/lib/mapObjects/CGPandoraBox.h index f739f1fb9..7277a792d 100644 --- a/lib/mapObjects/CGPandoraBox.h +++ b/lib/mapObjects/CGPandoraBox.h @@ -23,10 +23,10 @@ public: MetaString message; - void initObj(CRandomGenerator & rand) override; + void initObj(vstd::RNG & rand) override; void onHeroVisit(const CGHeroInstance * h) const override; void battleFinished(const CGHeroInstance *hero, const BattleResult &result) const override; - void blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) const override; + void blockingDialogAnswered(const CGHeroInstance *hero, int32_t answer) const override; template void serialize(Handler &h) { diff --git a/lib/mapObjects/CGTownBuilding.cpp b/lib/mapObjects/CGTownBuilding.cpp deleted file mode 100644 index 6efd84e31..000000000 --- a/lib/mapObjects/CGTownBuilding.cpp +++ /dev/null @@ -1,530 +0,0 @@ -/* - * CGTownBuilding.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 "CGTownBuilding.h" -#include "CGTownInstance.h" -#include "../CGeneralTextHandler.h" -#include "../IGameCallback.h" -#include "../gameState/CGameState.h" -#include "../mapObjects/CGHeroInstance.h" -#include "../networkPacks/PacksForClient.h" - -VCMI_LIB_NAMESPACE_BEGIN - -CGTownBuilding::CGTownBuilding(IGameCallback * cb) - : IObjectInterface(cb) - , town(nullptr) -{} - -CGTownBuilding::CGTownBuilding(CGTownInstance * town) - : IObjectInterface(town->cb) - , town(town) -{} - -PlayerColor CGTownBuilding::getOwner() const -{ - return town->getOwner(); -} - -MapObjectID CGTownBuilding::getObjGroupIndex() const -{ - return -1; -} - -MapObjectSubID CGTownBuilding::getObjTypeIndex() const -{ - return 0; -} - -int3 CGTownBuilding::visitablePos() const -{ - return town->visitablePos(); -} - -int3 CGTownBuilding::getPosition() const -{ - return town->getPosition(); -} - -std::string CGTownBuilding::getVisitingBonusGreeting() const -{ - auto bonusGreeting = town->getTown()->getGreeting(bType); - - if(!bonusGreeting.empty()) - return bonusGreeting; - - switch(bType) - { - case BuildingSubID::MANA_VORTEX: - bonusGreeting = std::string(VLC->generaltexth->translate("vcmi.townHall.greetingManaVortex")); - break; - case BuildingSubID::KNOWLEDGE_VISITING_BONUS: - bonusGreeting = std::string(VLC->generaltexth->translate("vcmi.townHall.greetingKnowledge")); - break; - case BuildingSubID::SPELL_POWER_VISITING_BONUS: - bonusGreeting = std::string(VLC->generaltexth->translate("vcmi.townHall.greetingSpellPower")); - break; - case BuildingSubID::ATTACK_VISITING_BONUS: - bonusGreeting = std::string(VLC->generaltexth->translate("vcmi.townHall.greetingAttack")); - break; - case BuildingSubID::EXPERIENCE_VISITING_BONUS: - bonusGreeting = std::string(VLC->generaltexth->translate("vcmi.townHall.greetingExperience")); - break; - case BuildingSubID::DEFENSE_VISITING_BONUS: - bonusGreeting = std::string(VLC->generaltexth->translate("vcmi.townHall.greetingDefence")); - break; - } - auto buildingName = town->getTown()->getSpecialBuilding(bType)->getNameTranslated(); - - if(bonusGreeting.empty()) - { - bonusGreeting = "Error: Bonus greeting for '%s' is not localized."; - logGlobal->error("'%s' building of '%s' faction has not localized bonus greeting.", buildingName, town->getTown()->faction->getNameTranslated()); - } - boost::algorithm::replace_first(bonusGreeting, "%s", buildingName); - town->getTown()->setGreeting(bType, bonusGreeting); - return bonusGreeting; -} - -std::string CGTownBuilding::getCustomBonusGreeting(const Bonus & bonus) const -{ - if(bonus.type == BonusType::TOWN_MAGIC_WELL) - { - MetaString wellGreeting = MetaString::createFromTextID("vcmi.townHall.greetingInTownMagicWell"); - - wellGreeting.replaceTextID(town->getTown()->getSpecialBuilding(bType)->getNameTextID()); - return wellGreeting.toString(); - } - - MetaString greeting = MetaString::createFromTextID("vcmi.townHall.greetingCustomBonus"); - - std::string paramTextID; - std::string until; - - if(bonus.type == BonusType::MORALE) - paramTextID = "core.genrltxt.384"; // Morale - - if(bonus.type == BonusType::LUCK) - paramTextID = "core.genrltxt.385"; // Luck - - greeting.replaceTextID(town->getTown()->getSpecialBuilding(bType)->getNameTextID()); - greeting.replaceNumber(bonus.val); - greeting.replaceTextID(paramTextID); - - if (bonus.duration == BonusDuration::ONE_BATTLE) - greeting.replaceTextID("vcmi.townHall.greetingCustomUntil"); - else - greeting.replaceRawString("."); - - return greeting.toString(); -} - -COPWBonus::COPWBonus(IGameCallback *cb) - : CGTownBuilding(cb) -{} - -COPWBonus::COPWBonus(const BuildingID & bid, BuildingSubID::EBuildingSubID subId, CGTownInstance * cgTown) - : CGTownBuilding(cgTown) -{ - bID = bid; - bType = subId; - indexOnTV = static_cast(town->bonusingBuildings.size()); -} - -void COPWBonus::setProperty(ObjProperty what, ObjPropertyID identifier) -{ - switch (what) - { - case ObjProperty::VISITORS: - visitors.insert(identifier.as()); - break; - case ObjProperty::STRUCTURE_CLEAR_VISITORS: - visitors.clear(); - break; - } -} - -void COPWBonus::onHeroVisit (const CGHeroInstance * h) const -{ - ObjectInstanceID heroID = h->id; - if(town->hasBuilt(bID)) - { - InfoWindow iw; - iw.player = h->tempOwner; - - switch (this->bType) - { - case BuildingSubID::STABLES: - if(!h->hasBonusFrom(BonusSource::OBJECT_TYPE, BonusSourceID(Obj(Obj::STABLES)))) //does not stack with advMap Stables - { - GiveBonus gb; - gb.bonus = Bonus(BonusDuration::ONE_WEEK, BonusType::MOVEMENT, BonusSource::OBJECT_TYPE, 600, BonusSourceID(Obj(Obj::STABLES)), BonusCustomSubtype::heroMovementLand); - gb.id = heroID; - cb->giveHeroBonus(&gb); - - cb->setMovePoints(heroID, 600, false); - - iw.text.appendRawString(VLC->generaltexth->allTexts[580]); - cb->showInfoDialog(&iw); - } - break; - - case BuildingSubID::MANA_VORTEX: - if(visitors.empty()) - { - if(h->mana < h->manaLimit() * 2) - { - cb->setManaPoints (heroID, 2 * h->manaLimit()); - //TODO: investigate line below - //cb->setObjProperty (town->id, ObjProperty::VISITED, true); - iw.text.appendRawString(getVisitingBonusGreeting()); - cb->showInfoDialog(&iw); - town->addHeroToStructureVisitors(h, indexOnTV); - } - } - break; - } - } -} - -CTownBonus::CTownBonus(IGameCallback *cb) - : CGTownBuilding(cb) -{} - -CTownBonus::CTownBonus(const BuildingID & index, BuildingSubID::EBuildingSubID subId, CGTownInstance * cgTown) - : CGTownBuilding(cgTown) -{ - bID = index; - bType = subId; - indexOnTV = static_cast(town->bonusingBuildings.size()); -} - -void CTownBonus::setProperty(ObjProperty what, ObjPropertyID identifier) -{ - if(what == ObjProperty::VISITORS) - visitors.insert(identifier.as()); -} - -void CTownBonus::onHeroVisit (const CGHeroInstance * h) const -{ - ObjectInstanceID heroID = h->id; - if(town->hasBuilt(bID) && visitors.find(heroID) == visitors.end()) - { - si64 val = 0; - InfoWindow iw; - PrimarySkill what = PrimarySkill::NONE; - - switch(bType) - { - case BuildingSubID::KNOWLEDGE_VISITING_BONUS: //wall of knowledge - what = PrimarySkill::KNOWLEDGE; - val = 1; - iw.components.emplace_back(ComponentType::PRIM_SKILL, PrimarySkill::KNOWLEDGE, 1); - break; - - case BuildingSubID::SPELL_POWER_VISITING_BONUS: //order of fire - what = PrimarySkill::SPELL_POWER; - val = 1; - iw.components.emplace_back(ComponentType::PRIM_SKILL, PrimarySkill::SPELL_POWER, 1); - break; - - case BuildingSubID::ATTACK_VISITING_BONUS: //hall of Valhalla - what = PrimarySkill::ATTACK; - val = 1; - iw.components.emplace_back(ComponentType::PRIM_SKILL, PrimarySkill::ATTACK, 1); - break; - - case BuildingSubID::EXPERIENCE_VISITING_BONUS: //academy of battle scholars - what = PrimarySkill::EXPERIENCE; - val = static_cast(h->calculateXp(1000)); - iw.components.emplace_back(ComponentType::EXPERIENCE, val); - break; - - case BuildingSubID::DEFENSE_VISITING_BONUS: //cage of warlords - what = PrimarySkill::DEFENSE; - val = 1; - iw.components.emplace_back(ComponentType::PRIM_SKILL, PrimarySkill::DEFENSE, 1); - break; - - case BuildingSubID::CUSTOM_VISITING_BONUS: - const auto building = town->getTown()->buildings.at(bID); - if(!h->hasBonusFrom(BonusSource::TOWN_STRUCTURE, BonusSourceID(building->getUniqueTypeID()))) - { - const auto & bonuses = building->onVisitBonuses; - applyBonuses(const_cast(h), bonuses); - } - break; - } - - if(what != PrimarySkill::NONE) - { - iw.player = cb->getOwner(heroID); - iw.text.appendRawString(getVisitingBonusGreeting()); - cb->showInfoDialog(&iw); - if (what == PrimarySkill::EXPERIENCE) - cb->giveExperience(cb->getHero(heroID), val); - else - cb->changePrimSkill(cb->getHero(heroID), what, val); - - town->addHeroToStructureVisitors(h, indexOnTV); - } - } -} - -void CTownBonus::applyBonuses(CGHeroInstance * h, const BonusList & bonuses) const -{ - auto addToVisitors = false; - - for(const auto & bonus : bonuses) - { - GiveBonus gb; - InfoWindow iw; - - if(bonus->type == BonusType::TOWN_MAGIC_WELL) - { - if(h->mana >= h->manaLimit()) - return; - cb->setManaPoints(h->id, h->manaLimit()); - bonus->duration = BonusDuration::ONE_DAY; - } - gb.bonus = * bonus; - gb.id = h->id; - cb->giveHeroBonus(&gb); - - if(bonus->duration == BonusDuration::PERMANENT) - addToVisitors = true; - - iw.player = cb->getOwner(h->id); - iw.text.appendRawString(getCustomBonusGreeting(gb.bonus)); - cb->showInfoDialog(&iw); - } - if(addToVisitors) - town->addHeroToStructureVisitors(h, indexOnTV); -} - -CTownRewardableBuilding::CTownRewardableBuilding(IGameCallback *cb) - : CGTownBuilding(cb) -{} - -CTownRewardableBuilding::CTownRewardableBuilding(const BuildingID & index, BuildingSubID::EBuildingSubID subId, CGTownInstance * cgTown, CRandomGenerator & rand) - : CGTownBuilding(cgTown) -{ - bID = index; - bType = subId; - indexOnTV = static_cast(town->bonusingBuildings.size()); - initObj(rand); -} - -void CTownRewardableBuilding::initObj(CRandomGenerator & rand) -{ - assert(town && town->town); - - auto building = town->town->buildings.at(bID); - - building->rewardableObjectInfo.configureObject(configuration, rand, cb); - for(auto & rewardInfo : configuration.info) - { - for (auto & bonus : rewardInfo.reward.bonuses) - { - bonus.source = BonusSource::TOWN_STRUCTURE; - bonus.sid = BonusSourceID(building->getUniqueTypeID()); - } - } -} - -void CTownRewardableBuilding::newTurn(CRandomGenerator & rand) const -{ - if (configuration.resetParameters.period != 0 && cb->getDate(Date::DAY) > 1 && ((cb->getDate(Date::DAY)-1) % configuration.resetParameters.period) == 0) - { - if(configuration.resetParameters.rewards) - { - cb->setObjPropertyValue(town->id, ObjProperty::REWARD_RANDOMIZE, indexOnTV); - } - if(configuration.resetParameters.visitors) - { - cb->setObjPropertyValue(town->id, ObjProperty::STRUCTURE_CLEAR_VISITORS, indexOnTV); - } - } -} - -void CTownRewardableBuilding::setProperty(ObjProperty what, ObjPropertyID identifier) -{ - switch (what) - { - case ObjProperty::VISITORS: - visitors.insert(identifier.as()); - break; - case ObjProperty::STRUCTURE_CLEAR_VISITORS: - visitors.clear(); - break; - case ObjProperty::REWARD_RANDOMIZE: - initObj(cb->gameState()->getRandomGenerator()); - break; - case ObjProperty::REWARD_SELECT: - selectedReward = identifier.getNum(); - break; - } -} - -void CTownRewardableBuilding::heroLevelUpDone(const CGHeroInstance *hero) const -{ - grantRewardAfterLevelup(cb, configuration.info.at(selectedReward), town, hero); -} - -void CTownRewardableBuilding::blockingDialogAnswered(const CGHeroInstance *hero, ui32 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"); - } -} - -void CTownRewardableBuilding::grantReward(ui32 rewardID, const CGHeroInstance * hero) const -{ - town->addHeroToStructureVisitors(hero, indexOnTV); - - grantRewardBeforeLevelup(cb, configuration.info.at(rewardID), hero); - - // hero is not blocked by levelup dialog - grant remainer immediately - if(!cb->isVisitCoveredByAnotherQuery(town, hero)) - { - grantRewardAfterLevelup(cb, configuration.info.at(rewardID), town, hero); - } -} - -bool CTownRewardableBuilding::wasVisitedBefore(const CGHeroInstance * contextHero) const -{ - switch (configuration.visitMode) - { - case Rewardable::VISIT_UNLIMITED: - return false; - case Rewardable::VISIT_ONCE: - return !visitors.empty(); - case Rewardable::VISIT_PLAYER: - return false; //not supported - case Rewardable::VISIT_BONUS: - { - const auto building = town->getTown()->buildings.at(bID); - return contextHero->hasBonusFrom(BonusSource::TOWN_STRUCTURE, BonusSourceID(building->getUniqueTypeID())); - } - case Rewardable::VISIT_HERO: - return visitors.find(contextHero->id) != visitors.end(); - case Rewardable::VISIT_LIMITER: - return configuration.visitLimiter.heroAllowed(contextHero); - default: - return false; - } -} - -void CTownRewardableBuilding::onHeroVisit(const CGHeroInstance *h) const -{ - auto grantRewardWithMessage = [&](int index) -> void - { - auto vi = configuration.info.at(index); - logGlobal->debug("Granting reward %d. Message says: %s", index, vi.message.toString()); - - town->addHeroToStructureVisitors(h, indexOnTV); //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(&sd); - }; - - if(!town->hasBuilt(bID) || cb->isVisitCoveredByAnotherQuery(town, h)) - 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; - } - } - } - 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!"); - } -} - - -VCMI_LIB_NAMESPACE_END diff --git a/lib/mapObjects/CGTownBuilding.h b/lib/mapObjects/CGTownBuilding.h deleted file mode 100644 index 9b620a6ac..000000000 --- a/lib/mapObjects/CGTownBuilding.h +++ /dev/null @@ -1,147 +0,0 @@ -/* - * CGTownBuilding.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 "IObjectInterface.h" -#include "../rewardable/Interface.h" - -VCMI_LIB_NAMESPACE_BEGIN - -class CGTownInstance; -class CBuilding; - -class DLL_LINKAGE CGTownBuilding : public IObjectInterface -{ -///basic class for town structures handled as map objects -public: - CGTownBuilding(CGTownInstance * town); - CGTownBuilding(IGameCallback *cb); - - si32 indexOnTV = 0; //identifies its index on towns vector - - CGTownInstance * town; - - STRONG_INLINE - BuildingSubID::EBuildingSubID getBuildingSubtype() const - { - return bType; - } - - STRONG_INLINE - const BuildingID & getBuildingType() const - { - return bID; - } - - STRONG_INLINE - void setBuildingSubtype(BuildingSubID::EBuildingSubID subId) - { - bType = subId; - } - - PlayerColor getOwner() const override; - MapObjectID getObjGroupIndex() const override; - MapObjectSubID getObjTypeIndex() const override; - - int3 visitablePos() const override; - int3 getPosition() const override; - - template void serialize(Handler &h) - { - h & bID; - h & indexOnTV; - h & bType; - } - -protected: - BuildingID bID; //from buildig list - BuildingSubID::EBuildingSubID bType = BuildingSubID::NONE; - - std::string getVisitingBonusGreeting() const; - std::string getCustomBonusGreeting(const Bonus & bonus) const; -}; - -class DLL_LINKAGE COPWBonus : public CGTownBuilding -{///used for OPW bonusing structures -public: - std::set visitors; - void setProperty(ObjProperty what, ObjPropertyID identifier) override; - void onHeroVisit (const CGHeroInstance * h) const override; - - COPWBonus(const BuildingID & index, BuildingSubID::EBuildingSubID subId, CGTownInstance * TOWN); - COPWBonus(IGameCallback *cb); - - template void serialize(Handler &h) - { - h & static_cast(*this); - h & visitors; - } -}; - -class DLL_LINKAGE CTownBonus : public CGTownBuilding -{ -///used for one-time bonusing structures -///feel free to merge inheritance tree -public: - std::set visitors; - void setProperty(ObjProperty what, ObjPropertyID identifier) override; - void onHeroVisit (const CGHeroInstance * h) const override; - - CTownBonus(const BuildingID & index, BuildingSubID::EBuildingSubID subId, CGTownInstance * TOWN); - CTownBonus(IGameCallback *cb); - - template void serialize(Handler &h) - { - h & static_cast(*this); - h & visitors; - } - -private: - void applyBonuses(CGHeroInstance * h, const BonusList & bonuses) const; -}; - -class DLL_LINKAGE CTownRewardableBuilding : public CGTownBuilding, public Rewardable::Interface -{ - /// reward selected by player, no serialize - ui16 selectedReward = 0; - - std::set visitors; - - bool wasVisitedBefore(const CGHeroInstance * contextHero) const; - - void grantReward(ui32 rewardID, const CGHeroInstance * hero) const; - -public: - void setProperty(ObjProperty what, ObjPropertyID identifier) override; - void onHeroVisit(const CGHeroInstance * h) const override; - - void newTurn(CRandomGenerator & rand) const override; - - /// gives second part of reward after hero level-ups for proper granting of spells/mana - void heroLevelUpDone(const CGHeroInstance *hero) const override; - - void initObj(CRandomGenerator & rand) override; - - /// applies player selection of reward - void blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) const override; - - CTownRewardableBuilding(const BuildingID & index, BuildingSubID::EBuildingSubID subId, CGTownInstance * town, CRandomGenerator & rand); - CTownRewardableBuilding(IGameCallback *cb); - - template void serialize(Handler &h) - { - h & static_cast(*this); - h & static_cast(*this); - h & visitors; - } -}; - -VCMI_LIB_NAMESPACE_END diff --git a/lib/mapObjects/CGTownInstance.cpp b/lib/mapObjects/CGTownInstance.cpp index 71784c48e..037e98d22 100644 --- a/lib/mapObjects/CGTownInstance.cpp +++ b/lib/mapObjects/CGTownInstance.cpp @@ -10,18 +10,22 @@ #include "StdInc.h" #include "CGTownInstance.h" -#include "CGTownBuilding.h" + +#include "TownBuildingInstance.h" #include "../spells/CSpellHandler.h" #include "../bonuses/Bonus.h" #include "../battle/IBattleInfoCallback.h" +#include "../battle/BattleLayout.h" #include "../CConfigHandler.h" -#include "../CGeneralTextHandler.h" +#include "../texts/CGeneralTextHandler.h" #include "../IGameCallback.h" #include "../gameState/CGameState.h" #include "../mapping/CMap.h" #include "../CPlayerState.h" #include "../StartInfo.h" #include "../TerrainHandler.h" +#include "../entities/building/CBuilding.h" +#include "../entities/faction/CTownHandler.h" #include "../mapObjectConstructors/AObjectTypeHandler.h" #include "../mapObjectConstructors/CObjectClassesHandler.h" #include "../mapObjects/CGHeroInstance.h" @@ -31,6 +35,8 @@ #include "../networkPacks/PacksForClientBattle.h" #include "../serializer/JsonSerializeFormat.h" +#include + VCMI_LIB_NAMESPACE_BEGIN int CGTownInstance::getSightRadius() const //returns sight distance @@ -39,13 +45,10 @@ int CGTownInstance::getSightRadius() const //returns sight distance for(const auto & bid : builtBuildings) { - if(bid.IsSpecialOrGrail()) - { - auto height = town->buildings.at(bid)->height; - if(ret < height) - ret = height; + auto height = getTown()->buildings.at(bid)->height; + if(ret < height) + ret = height; } -} return ret; } @@ -55,13 +58,13 @@ void CGTownInstance::setPropertyDer(ObjProperty what, ObjPropertyID identifier) switch (what) { case ObjProperty::STRUCTURE_ADD_VISITING_HERO: - bonusingBuildings[identifier.getNum()]->setProperty(ObjProperty::VISITORS, visitingHero->id); + rewardableBuildings.at(identifier.getNum())->setProperty(ObjProperty::VISITORS, visitingHero->id); break; case ObjProperty::STRUCTURE_CLEAR_VISITORS: - bonusingBuildings[identifier.getNum()]->setProperty(ObjProperty::STRUCTURE_CLEAR_VISITORS, NumericID(0)); + rewardableBuildings.at(identifier.getNum())->setProperty(ObjProperty::STRUCTURE_CLEAR_VISITORS, NumericID(0)); break; case ObjProperty::STRUCTURE_ADD_GARRISONED_HERO: //add garrisoned hero to visitors - bonusingBuildings[identifier.getNum()]->setProperty(ObjProperty::VISITORS, garrisonHero->id); + rewardableBuildings.at(identifier.getNum())->setProperty(ObjProperty::VISITORS, garrisonHero->id); break; case ObjProperty::BONUS_VALUE_FIRST: bonusValue.first = identifier.getNum(); @@ -69,9 +72,6 @@ void CGTownInstance::setPropertyDer(ObjProperty what, ObjPropertyID identifier) case ObjProperty::BONUS_VALUE_SECOND: bonusValue.second = identifier.getNum(); break; - case ObjProperty::REWARD_RANDOMIZE: - bonusingBuildings[identifier.getNum()]->setProperty(ObjProperty::REWARD_RANDOMIZE, NumericID(0)); - break; } } CGTownInstance::EFortLevel CGTownInstance::fortLevel() const //0 - none, 1 - fort, 2 - citadel, 3 - castle @@ -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 >=GameConstants::CREATURES_PER_TOWN) + if (level<0 || level >=getTown()->creatures.size()) return ret; if (creatures[level].second.empty()) return ret; //no dwelling @@ -136,6 +136,14 @@ GrowthInfo CGTownInstance::getGrowthInfo(int level) const const int base = creature->getGrowth(); int castleBonus = 0; + if(tempOwner.isValidPlayer()) + { + auto * playerSettings = cb->getPlayerSettings(tempOwner); + ret.handicapPercentage = playerSettings->handicap.percentGrowth; + } + else + ret.handicapPercentage = 100; + ret.entries.emplace_back(VLC->generaltexth->allTexts[590], base); // \n\nBasic growth %d" if (hasBuilt(BuildingID::CASTLE)) @@ -143,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()); @@ -158,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)); } } @@ -166,12 +174,12 @@ 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)) { - dwellingBonus = getDwellingBonus(creatures[level].second, p->dwellings); + dwellingBonus = getDwellingBonus(creatures[level].second, p->getOwnedObjects()); } if(dwellingBonus) ret.entries.emplace_back(VLC->generaltexth->allTexts[591], dwellingBonus); // \nExternal dwellings %+d @@ -182,15 +190,18 @@ GrowthInfo CGTownInstance::getGrowthInfo(int level) const return ret; } -int CGTownInstance::getDwellingBonus(const std::vector& creatureIds, const std::vector >& dwellings) const +int CGTownInstance::getDwellingBonus(const std::vector& creatureIds, const std::vector& dwellings) const { int totalBonus = 0; for (const auto& dwelling : dwellings) { - for (const auto& creature : dwelling->creatures) - { - totalBonus += vstd::contains(creatureIds, creature.second[0]) ? 1 : 0; - } + const auto & dwellingCreatures = dwelling->asOwnable()->providedCreatures(); + bool hasMatch = false; + for (const auto& creature : dwellingCreatures) + hasMatch = vstd::contains(creatureIds, creature); + + if (hasMatch) + totalBonus += 1; } return totalBonus; } @@ -198,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) { @@ -214,9 +225,20 @@ TResources CGTownInstance::dailyIncome() const ret += p.second->produce; } } + + if (!getOwner().isValidPlayer()) + return ret; + + const auto & playerSettings = cb->getPlayerSettings(getOwner()); + ret.applyHandicap(playerSettings->handicap.percentIncome); return ret; } +std::vector CGTownInstance::providedCreatures() const +{ + return {}; +} + bool CGTownInstance::hasFort() const { return hasBuilt(BuildingID::FORT); @@ -227,21 +249,36 @@ bool CGTownInstance::hasCapitol() const return hasBuilt(BuildingID::CAPITOL); } +TownFortifications CGTownInstance::fortificationsLevel() const +{ + auto result = getTown()->fortifications; + + for (auto const & buildingID : builtBuildings) + result += getTown()->buildings.at(buildingID)->fortifications; + + if (result.wallsHealth == 0) + return TownFortifications(); + + return result; +} + CGTownInstance::CGTownInstance(IGameCallback *cb): CGDwelling(cb), - town(nullptr), - builded(0), + built(0), destroyed(0), identifier(0), - alignmentToPlayer(PlayerColor::NEUTRAL) + alignmentToPlayer(PlayerColor::NEUTRAL), + spellResearchCounterDay(0), + spellResearchAcceptedCounter(0), + spellResearchAllowed(true) { this->setNodeType(CBonusSystemNode::TOWN); } CGTownInstance::~CGTownInstance() { - for (auto & elem : bonusingBuildings) - delete elem; + for (auto & elem : rewardableBuildings) + delete elem.second; } int CGTownInstance::spellsAtLevel(int level, bool checkGuild) const @@ -267,12 +304,6 @@ void CGTownInstance::setOwner(const PlayerColor & player) const cb->setOwner(this, player); } -void CGTownInstance::blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) const -{ - for (auto building : bonusingBuildings) - building->blockingDialogAnswered(hero, answer); -} - void CGTownInstance::onHeroVisit(const CGHeroInstance * h) const { if(cb->gameState()->getPlayerRelations( getOwner(), h->getOwner() ) == PlayerRelations::ENEMIES) @@ -293,7 +324,7 @@ void CGTownInstance::onHeroVisit(const CGHeroInstance * h) const const_cast(defendingHero)->inTownGarrison = false; //hack to return visitor from garrison after battle } - cb->startBattlePrimary(h, defendingArmy, getSightCenter(), h, defendingHero, false, (isBattleOutside ? nullptr : this)); + cb->startBattle(h, defendingArmy, getSightCenter(), h, defendingHero, BattleLayout::createDefaultLayout(cb, h, defendingArmy), (isBattleOutside ? nullptr : this)); } else { @@ -308,8 +339,9 @@ void CGTownInstance::onHeroVisit(const CGHeroInstance * h) const cb->heroVisitCastle(this, h); } } - else if(h->visitablePos() == visitablePos()) + else { + assert(h->visitablePos() == this->visitablePos()); bool commander_recover = h->commander && !h->commander->alive; if (commander_recover) // rise commander from dead { @@ -317,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 @@ -330,10 +362,6 @@ void CGTownInstance::onHeroVisit(const CGHeroInstance * h) const cb->showInfoDialog(&iw); } } - else - { - logGlobal->error("%s visits allied town of %s from different pos?", h->getNameTranslated(), getNameTranslated()); - } } void CGTownInstance::onHeroLeave(const CGHeroInstance * h) const @@ -350,57 +378,28 @@ void CGTownInstance::onHeroLeave(const CGHeroInstance * h) const std::string CGTownInstance::getObjectName() const { - return getNameTranslated() + ", " + town->faction->getNameTranslated(); + if(ID == Obj::RANDOM_TOWN ) + return CGObjectInstance::getObjectName(); + + return getNameTranslated() + ", " + getTown()->faction->getNameTranslated(); } bool CGTownInstance::townEnvisagesBuilding(BuildingSubID::EBuildingSubID subId) const { - return town->getBuildingType(subId) != BuildingID::NONE; + return getTown()->getBuildingType(subId) != BuildingID::NONE; } -void CGTownInstance::initOverriddenBids() +void CGTownInstance::initializeConfigurableBuildings(vstd::RNG & rand) { - for(const auto & bid : builtBuildings) + for(const auto & kvp : getTown()->buildings) { - const auto & overrideThem = town->buildings.at(bid)->overrideBids; - - for(const auto & overrideIt : overrideThem) - overriddenBuildings.insert(overrideIt); - } -} - -bool CGTownInstance::isBonusingBuildingAdded(BuildingID bid) const -{ - auto present = std::find_if(bonusingBuildings.begin(), bonusingBuildings.end(), [&](CGTownBuilding* building) - { - return building->getBuildingType() == bid; - }); - - return present != bonusingBuildings.end(); -} - -void CGTownInstance::addTownBonuses(CRandomGenerator & rand) -{ - for(const auto & kvp : town->buildings) - { - if(vstd::contains(overriddenBuildings, kvp.first)) - continue; - - if(kvp.second->IsVisitingBonus()) - bonusingBuildings.push_back(new CTownBonus(kvp.second->bid, kvp.second->subId, this)); - - if(kvp.second->IsWeekBonus()) - bonusingBuildings.push_back(new COPWBonus(kvp.second->bid, kvp.second->subId, this)); - - if(kvp.second->subId == BuildingSubID::CUSTOM_VISITING_REWARD) - bonusingBuildings.push_back(new CTownRewardableBuilding(kvp.second->bid, kvp.second->subId, this, rand)); + if(!kvp.second->rewardableObjectInfo.getParameters().isNull()) + rewardableBuildings[kvp.first] = new TownRewardableBuildingInstance(this, kvp.second->bid, rand); } } DamageRange CGTownInstance::getTowerDamageRange() const { - assert(hasBuilt(BuildingID::CASTLE)); - // http://heroes.thelazy.net/wiki/Arrow_tower // base damage, irregardless of town level static constexpr int baseDamage = 6; @@ -417,8 +416,6 @@ DamageRange CGTownInstance::getTowerDamageRange() const DamageRange CGTownInstance::getKeepDamageRange() const { - assert(hasBuilt(BuildingID::CITADEL)); - // http://heroes.thelazy.net/wiki/Arrow_tower // base damage, irregardless of town level static constexpr int baseDamage = 10; @@ -433,35 +430,7 @@ DamageRange CGTownInstance::getKeepDamageRange() const }; } -void CGTownInstance::deleteTownBonus(BuildingID bid) -{ - size_t i = 0; - CGTownBuilding * freeIt = nullptr; - - for(i = 0; i != bonusingBuildings.size(); i++) - { - if(bonusingBuildings[i]->getBuildingType() == bid) - { - freeIt = bonusingBuildings[i]; - break; - } - } - if(freeIt == nullptr) - return; - - auto building = town->buildings.at(bid); - auto isVisitingBonus = building->IsVisitingBonus(); - auto isWeekBonus = building->IsWeekBonus(); - - if(!isVisitingBonus && !isWeekBonus) - return; - - bonusingBuildings.erase(bonusingBuildings.begin() + i); - - delete freeIt; -} - -FactionID CGTownInstance::randomizeFaction(CRandomGenerator & rand) +FactionID CGTownInstance::randomizeFaction(vstd::RNG & rand) { if(getOwner().isValidPlayer()) return cb->gameState()->scenarioOps->getIthPlayersSettings(getOwner()).castle; @@ -479,7 +448,7 @@ FactionID CGTownInstance::randomizeFaction(CRandomGenerator & rand) return *RandomGeneratorUtil::nextItem(potentialPicks, rand); } -void CGTownInstance::pickRandomObject(CRandomGenerator & rand) +void CGTownInstance::pickRandomObject(vstd::RNG & rand) { assert(ID == MapObjectID::TOWN || ID == MapObjectID::RANDOM_TOWN); if (ID == MapObjectID::RANDOM_TOWN) @@ -490,128 +459,87 @@ void CGTownInstance::pickRandomObject(CRandomGenerator & rand) assert(ID == Obj::TOWN); // just in case setType(ID, subID); - town = (*VLC->townh)[getFaction()]->town; - randomizeArmy(getFaction()); + randomizeArmy(getFactionID()); updateAppearance(); } -void CGTownInstance::initObj(CRandomGenerator & rand) ///initialize town structures +void CGTownInstance::initObj(vstd::RNG & rand) ///initialize town structures { blockVisit = true; if(townEnvisagesBuilding(BuildingSubID::PORTAL_OF_SUMMONING)) //Dungeon for example - creatures.resize(GameConstants::CREATURES_PER_TOWN + 1); + creatures.resize(getTown()->creatures.size() + 1); else - creatures.resize(GameConstants::CREATURES_PER_TOWN); + creatures.resize(getTown()->creatures.size()); - for (int level = 0; level < GameConstants::CREATURES_PER_TOWN; level++) + for (int level = 0; level < getTown()->creatures.size(); level++) { - BuildingID buildID = BuildingID(BuildingID::DWELL_FIRST + level); + BuildingID buildID = BuildingID(BuildingID::getDwellingFromLevel(level, 0)); int upgradeNum = 0; - for (; town->buildings.count(buildID); upgradeNum++, buildID.advance(GameConstants::CREATURES_PER_TOWN)) + 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]); } } - initOverriddenBids(); - addTownBonuses(rand); //add special bonuses from buildings to the bonusingBuildings vector. + initializeConfigurableBuildings(rand); + initializeNeutralTownGarrison(rand); recreateBuildingsBonuses(); updateAppearance(); } -void CGTownInstance::newTurn(CRandomGenerator & rand) const +void CGTownInstance::initializeNeutralTownGarrison(vstd::RNG & rand) { - if (cb->getDate(Date::DAY_OF_WEEK) == 1) //reset on new week + struct RandomGuardsInfo{ + int tier; + int chance; + int min; + int max; + }; + + constexpr std::array randomGuards = { + RandomGuardsInfo{ 0, 33, 8, 15 }, + RandomGuardsInfo{ 1, 33, 5, 7 }, + RandomGuardsInfo{ 2, 20, 3, 5 }, + RandomGuardsInfo{ 3, 14, 1, 3 }, + }; + + // Only neutral towns may get initial garrison + if (getOwner().isValidPlayer()) + return; + + // Only towns with garrison not set in map editor may get initial garrison + // FIXME: H3 editor allow explicitly empty garrison, but vcmi loses this flag on load + if (stacksCount() > 0) + return; + + for (auto const & guard : randomGuards) { - //give resources if there's a Mystic Pond - if (hasBuilt(BuildingSubID::MYSTIC_POND) - && cb->getDate(Date::DAY) != 1 - && (tempOwner.isValidPlayer()) - ) - { - int resID = rand.nextInt(2, 5); //bonus to random rare resource - resID = (resID==2)?1:resID; - int resVal = rand.nextInt(1, 4);//with size 1..4 - cb->giveResource(tempOwner, static_cast(resID), resVal); - cb->setObjPropertyValue(id, ObjProperty::BONUS_VALUE_FIRST, resID); - cb->setObjPropertyValue(id, ObjProperty::BONUS_VALUE_SECOND, resVal); - } - - for(const auto * manaVortex : getBonusingBuildings(BuildingSubID::MANA_VORTEX)) - cb->setObjPropertyValue(id, ObjProperty::STRUCTURE_CLEAR_VISITORS, manaVortex->indexOnTV); //reset visitors for Mana Vortex + if (rand.nextInt(99) >= guard.chance) + continue; - //get Mana Vortex or Stables bonuses - //same code is in the CGameHandler::buildStructure method - if (garrisonHero != nullptr) //garrison hero first - consistent with original H3 Mana Vortex and Battle Scholar Academy levelup windows order - cb->visitCastleObjects(this, garrisonHero); + CreatureID guardID = getTown()->creatures[guard.tier].at(0); + int guardSize = rand.nextInt(guard.min, guard.max); - if (visitingHero != nullptr) - cb->visitCastleObjects(this, visitingHero); - - if (tempOwner == PlayerColor::NEUTRAL) //garrison growth for neutral towns - { - std::vector nativeCrits; //slots - for(const auto & elem : Slots()) - { - if (elem.second->type->getFaction() == getFaction()) //native - { - nativeCrits.push_back(elem.first); //collect matching slots - } - } - if(!nativeCrits.empty()) - { - SlotID pos = *RandomGeneratorUtil::nextItem(nativeCrits, rand); - StackLocation sl(this, pos); - - const CCreature *c = getCreature(pos); - if (rand.nextInt(99) < 90 || c->upgrades.empty()) //increase number if no upgrade available - { - cb->changeStackCount(sl, c->getGrowth()); - } - else //upgrade - { - cb->changeStackType(sl, c->upgrades.begin()->toCreature()); - } - } - if ((stacksCount() < GameConstants::ARMY_SIZE && rand.nextInt(99) < 25) || Slots().empty()) //add new stack - { - int i = rand.nextInt(std::min(GameConstants::CREATURES_PER_TOWN, cb->getDate(Date::MONTH) << 1) - 1); - if (!town->creatures[i].empty()) - { - CreatureID c = town->creatures[i][0]; - SlotID n; - - TQuantity count = creatureGrowth(i); - if (!count) // no dwelling - count = VLC->creatures()->getById(c)->getGrowth(); - - {//no lower tiers or above current month - - if ((n = getSlotFor(c)).validSlot()) - { - StackLocation sl(this, n); - if (slotEmpty(n)) - cb->insertNewStack(sl, c.toCreature(), count); - else //add to existing - cb->changeStackCount(sl, count); - } - } - } - } - } + putStack(getFreeSlot(), new CStackInstance(guardID, guardSize)); } - - for(const auto * rewardableBuilding : getBonusingBuildings(BuildingSubID::CUSTOM_VISITING_REWARD)) - rewardableBuilding->newTurn(rand); } -/* -int3 CGTownInstance::getSightCenter() const + +void CGTownInstance::newTurn(vstd::RNG & rand) const { - return pos - int3(2,0,0); + for(const auto & building : rewardableBuildings) + building.second->newTurn(rand); + + if(hasBuilt(BuildingSubID::BANK) && bonusValue.second > 0) + { + TResources res; + res[EGameResID::GOLD] = -500; + cb->giveResources(getOwner(), res); + cb->setObjPropertyValue(id, ObjProperty::BONUS_VALUE_SECOND, bonusValue.second - 500); + } } -*/ + bool CGTownInstance::passableFor(PlayerColor color) const { if (!armedGarrison())//empty castle - anyone can visit @@ -696,15 +624,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 (auto i = state->towns.cbegin(); i < state->towns.cend(); ++i) + for (const auto & otherTown : state->getTowns()) { - if (*i != this && (*i)->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; } } @@ -721,7 +649,7 @@ void CGTownInstance::clearArmy() const BoatId CGTownInstance::getBoatType() const { - return town->faction->boatType; + return getTown()->faction->boatType; } int CGTownInstance::getMarketEfficiency() const @@ -733,52 +661,21 @@ int CGTownInstance::getMarketEfficiency() const assert(p); int marketCount = 0; - for(const CGTownInstance *t : p->towns) + for(const CGTownInstance *t : p->getTowns()) if(t->hasBuiltSomeTradeBuilding()) marketCount++; return marketCount; } -bool CGTownInstance::allowsTrade(EMarketMode mode) const -{ - switch(mode) - { - case EMarketMode::RESOURCE_RESOURCE: - case EMarketMode::RESOURCE_PLAYER: - return hasBuilt(BuildingID::MARKETPLACE); - - case EMarketMode::ARTIFACT_RESOURCE: - case EMarketMode::RESOURCE_ARTIFACT: - return hasBuilt(BuildingSubID::ARTIFACT_MERCHANT); - - case EMarketMode::CREATURE_RESOURCE: - return hasBuilt(BuildingSubID::FREELANCERS_GUILD); - - case EMarketMode::CREATURE_UNDEAD: - return hasBuilt(BuildingSubID::CREATURE_TRANSFORMER); - - case EMarketMode::RESOURCE_SKILL: - return hasBuilt(BuildingSubID::MAGIC_UNIVERSITY); - case EMarketMode::CREATURE_EXP: - case EMarketMode::ARTIFACT_EXP: - return false; - default: - assert(0); - return false; - } -} - std::vector CGTownInstance::availableItemsIds(EMarketMode mode) const { 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 ) @@ -789,9 +686,14 @@ std::vector CGTownInstance::availableItemsIds(EMarketMode mode) co return IMarket::availableItemsIds(mode); } +ObjectInstanceID CGTownInstance::getObjInstanceID() const +{ + return id; +} + 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) @@ -800,7 +702,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() @@ -841,12 +743,24 @@ void CGTownInstance::recreateBuildingsBonuses() for(const auto & b : bl) removeBonus(b); + + for(const auto & bid : builtBuildings) { - if(vstd::contains(overriddenBuildings, bid)) //tricky! -> checks tavern only if no bratherhood of sword + bool bonusesReplacedByUpgrade = false; + + for(const auto & upgradeID : builtBuildings) + { + const auto & upgrade = getTown()->buildings.at(upgradeID); + if (upgrade->getBase() == bid && upgrade->upgradeReplacesBonuses) + bonusesReplacedByUpgrade = true; + } + + // bonuses from this building are disabled and replaced by bonuses from an upgrade + if (bonusesReplacedByUpgrade) continue; - auto building = town->buildings.at(bid); + auto building = getTown()->buildings.at(bid); if(building->buildingBonuses.empty()) continue; @@ -913,21 +827,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 @@ -935,7 +834,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; @@ -968,33 +867,16 @@ const CArmedInstance * CGTownInstance::getUpperArmy() const return this; } -std::vector CGTownInstance::getBonusingBuildings(BuildingSubID::EBuildingSubID subId) const -{ - std::vector ret; - for(auto * const building : bonusingBuildings) - { - if(building->getBuildingSubtype() == subId) - ret.push_back(building); - } - return ret; -} - - bool CGTownInstance::hasBuiltSomeTradeBuilding() const { - for(const auto & bid : builtBuildings) - { - if(town->buildings.at(bid)->IsTradeBuilding()) - return true; - } - return false; + return availableModes().empty() ? false : true; } 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; @@ -1007,18 +889,56 @@ 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; } +void CGTownInstance::addBuilding(const BuildingID & buildingID) +{ + if(buildingID == BuildingID::NONE) + return; + + builtBuildings.insert(buildingID); +} + +std::set CGTownInstance::availableModes() const +{ + std::set result; + for (const auto & buildingID : builtBuildings) + { + const auto * buildingPtr = getTown()->buildings.at(buildingID).get(); + result.insert(buildingPtr->marketModes.begin(), buildingPtr->marketModes.end()); + } + + return result; +} + +void CGTownInstance::removeBuilding(const BuildingID & buildingID) +{ + if(!vstd::contains(builtBuildings, buildingID)) + return; + + builtBuildings.erase(buildingID); +} + +void CGTownInstance::removeAllBuildings() +{ + builtBuildings.clear(); +} + +std::set CGTownInstance::getBuildings() const +{ + return builtBuildings; +} + 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(); } @@ -1026,7 +946,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; @@ -1034,13 +954,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)) @@ -1065,7 +985,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(); @@ -1182,23 +1102,23 @@ void CGTownInstance::serializeJsonOptions(JsonSerializeFormat & handler) { handler.serializeLIC("buildings", buildingsLIC); - builtBuildings.insert(BuildingID::VILLAGE_HALL); + addBuilding(BuildingID::VILLAGE_HALL); if(buildingsLIC.none.empty() && buildingsLIC.all.empty()) { - builtBuildings.insert(BuildingID::DEFAULT); + addBuilding(BuildingID::DEFAULT); bool hasFort = false; handler.serializeBool("hasFort",hasFort); if(hasFort) - builtBuildings.insert(BuildingID::FORT); + addBuilding(BuildingID::FORT); } else { for(const si32 item : buildingsLIC.none) forbiddenBuildings.insert(BuildingID(item)); for(const si32 item : buildingsLIC.all) - builtBuildings.insert(BuildingID(item)); + addBuilding(BuildingID(item)); } } } @@ -1207,16 +1127,60 @@ void CGTownInstance::serializeJsonOptions(JsonSerializeFormat & handler) handler.serializeIdArray( "possibleSpells", possibleSpells); handler.serializeIdArray( "obligatorySpells", obligatorySpells); } + + { + auto eventsHandler = handler.enterArray("events"); + eventsHandler.syncSize(events, JsonNode::JsonType::DATA_VECTOR); + eventsHandler.serializeStruct(events); + } } -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 +{ + if (builtBuildings.count(building) == 0) + return ArtifactID::NONE; + + if (building == BuildingID::BLACKSMITH && getTown()->warMachineDeprecated.hasValue()) + return getTown()->warMachineDeprecated.toCreature()->warMachine; + + return getTown()->buildings.at(building)->warMachine; +} + +bool CGTownInstance::isWarMachineAvailable(ArtifactID warMachine) const +{ + for (auto const & buildingID : builtBuildings) + if (getTown()->buildings.at(buildingID)->warMachine == warMachine) + return true; + + if (builtBuildings.count(BuildingID::BLACKSMITH) && + getTown()->warMachineDeprecated.hasValue() && + getTown()->warMachineDeprecated.toCreature()->warMachine == warMachine) + return true; + + return false; } GrowthInfo::Entry::Entry(const std::string &format, int _count) @@ -1233,7 +1197,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(); @@ -1256,25 +1220,46 @@ int GrowthInfo::totalGrowth() const for(const Entry &entry : entries) ret += entry.count; - return ret; + // always round up income - we don't want buildings to always produce zero if handicap in use + return vstd::divideAndCeil(ret * handicapPercentage, 100); } void CGTownInstance::fillUpgradeInfo(UpgradeInfo & info, const CStackInstance &stack) const { 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()); } } } } } +void CGTownInstance::postDeserialize() +{ + setNodeType(CBonusSystemNode::TOWN); + for(auto & building : rewardableBuildings) + building.second->town = this; +} + +std::map CGTownInstance::convertOldBuildings(std::vector oldVector) +{ + std::map result; + + for(auto & building : oldVector) + { + result[building->getBuildingType()] = new TownRewardableBuildingInstance(*building); + delete building; + } + + return result; +} + VCMI_LIB_NAMESPACE_END diff --git a/lib/mapObjects/CGTownInstance.h b/lib/mapObjects/CGTownInstance.h index f8fd3f8e3..64eeaf9e2 100644 --- a/lib/mapObjects/CGTownInstance.h +++ b/lib/mapObjects/CGTownInstance.h @@ -11,15 +11,20 @@ #include "IMarket.h" #include "CGDwelling.h" -#include "CGTownBuilding.h" - -#include "../CTownHandler.h" // For CTown +#include "../entities/faction/CFaction.h" // TODO: remove +#include "../entities/faction/CTown.h" // TODO: remove VCMI_LIB_NAMESPACE_BEGIN class CCastleEvent; +class CTown; +class TownBuildingInstance; +struct TownFortifications; +class TownRewardableBuildingInstance; struct DamageRange; +template +class LogicalExpression; class DLL_LINKAGE CTownAndVisitingHero : public CBonusSystemNode { @@ -40,38 +45,42 @@ struct DLL_LINKAGE GrowthInfo std::vector entries; int totalGrowth() const; + int handicapPercentage; }; class DLL_LINKAGE CGTownInstance : public CGDwelling, public IShipyard, public IMarket, public INativeTerrainProvider, public ICreatureUpgrader { + friend class CTownInstanceConstructor; std::string nameTextId; // name of town -public: - using CGDwelling::getPosition; + std::map convertOldBuildings(std::vector oldVector); + std::set builtBuildings; + +public: enum EFortLevel {NONE = 0, FORT = 1, CITADEL = 2, CASTLE = 3}; CTownAndVisitingHero townAndVis; - const CTown * town; - si32 builded; //how many buildings has been built this turn + si32 built; //how many buildings has been built this turn si32 destroyed; //how many buildings has been destroyed this turn ConstTransitivePtr garrisonHero, visitingHero; ui32 identifier; //special identifier from h3m (only > RoE maps) PlayerColor alignmentToPlayer; // if set to non-neutral, random town will have same faction as specified player std::set forbiddenBuildings; - std::set builtBuildings; - std::set overriddenBuildings; ///buildings which bonuses are overridden and should not be applied - std::vector bonusingBuildings; + std::map rewardableBuildings; std::vector possibleSpells, obligatorySpells; std::vector > spells; //spells[level] -> vector of spells, first will be available in guild - std::list events; - std::pair bonusValue;//var to store town bonuses (rampart = resources from mystic pond); + 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) { h & static_cast(*this); h & nameTextId; - h & builded; + h & built; h & destroyed; h & identifier; h & garrisonHero; @@ -84,43 +93,45 @@ public: h & obligatorySpells; h & spells; h & events; - h & bonusingBuildings; - - for(auto * bonusingBuilding : bonusingBuildings) - bonusingBuilding->town = this; - - if (h.saving) + + if (h.version >= Handler::Version::SPELL_RESEARCH) { - CFaction * faction = town ? town->faction : nullptr; - h & faction; + h & spellResearchCounterDay; + h & spellResearchAcceptedCounter; + h & spellResearchAllowed; + } + + if (h.version >= Handler::Version::NEW_TOWN_BUILDINGS) + { + h & rewardableBuildings; } else { - CFaction * faction = nullptr; - h & faction; - town = faction ? faction->town : nullptr; + std::vector oldVector; + h & oldVector; + rewardableBuildings = convertOldBuildings(oldVector); + } + + if (h.version < Handler::Version::REMOVE_TOWN_PTR) + { + FactionID faction; + bool isNull = false; + h & isNull; + if (!isNull) + h & faction; } h & townAndVis; BONUS_TREE_DESERIALIZATION_FIX - if(town) + if (h.version < Handler::Version::NEW_TOWN_BUILDINGS) { - vstd::erase_if(builtBuildings, [this](BuildingID building) -> bool - { - if(!town->buildings.count(building) || !town->buildings.at(building)) - { - logGlobal->error("#1444-like issue in CGTownInstance::serialize. From town %s at %s removing the bogus builtBuildings item %s", nameTextId, pos.toString(), building); - return true; - } - return false; - }); + std::set overriddenBuildings; + h & overriddenBuildings; } - h & overriddenBuildings; - if(!h.saving) - this->setNodeType(CBonusSystemNode::TOWN); + postDeserialize(); } ////////////////////////////////////////////////////////////////////////// @@ -128,6 +139,7 @@ public: std::string nodeName() const override; void updateMoraleBonusFromArmy() override; void deserializationFix(); + void postDeserialize(); void recreateBuildingsBonuses(); void setVisitingHero(CGHeroInstance *h); void setGarrisonedHero(CGHeroInstance *h); @@ -147,15 +159,16 @@ public: EGeneratorState shipyardStatus() const override; const IObjectInterface * getObject() const override; int getMarketEfficiency() const override; //=market count - bool allowsTrade(EMarketMode mode) const override; + std::set availableModes() const override; std::vector availableItemsIds(EMarketMode mode) const override; - + ObjectInstanceID getObjInstanceID() const override; void updateAppearance(); ////////////////////////////////////////////////////////////////////////// bool needsLastStack() const override; CGTownInstance::EFortLevel fortLevel() const; + TownFortifications fortificationsLevel() const; int hallLevel() const; // -1 - none, 0 - village, 1 - town, 2 - city, 3 - capitol int mageGuildLevel() const; // -1 - none, 0 - village, 1 - town, 2 - city, 3 - capitol int getHordeLevel(const int & HID) const; //HID - 0 or 1; returns creature level or -1 if that horde structure is not present @@ -163,21 +176,26 @@ public: GrowthInfo getGrowthInfo(int level) const; bool hasFort() const; bool hasCapitol() const; - std::vector getBonusingBuildings(BuildingSubID::EBuildingSubID subId) const; bool hasBuiltSomeTradeBuilding() const; //checks if special building with type buildingID is constructed bool hasBuilt(BuildingSubID::EBuildingSubID buildingID) const; //checks if building is constructed and town has same subID bool hasBuilt(const BuildingID & buildingID) const; bool hasBuilt(const BuildingID & buildingID, FactionID townID) const; + void addBuilding(const BuildingID & buildingID); + void removeBuilding(const BuildingID & buildingID); + void removeAllBuildings(); + std::set getBuildings() const; TResources getBuildingCost(const BuildingID & buildingID) const; - TResources dailyIncome() const; //calculates daily income of this town + ResourceSet dailyIncome() const override; + std::vector providedCreatures() const override; + int spellsAtLevel(int level, bool checkGuild) const; //levels are counted from 1 (1 - 5) bool armedGarrison() const; //true if town has creatures in garrison or garrisoned hero int getTownLevel() const; - CBuilding::TRequired genBuildingRequirements(const BuildingID & build, bool deep = false) const; + LogicalExpression genBuildingRequirements(const BuildingID & build, bool deep = false) const; void mergeGarrisonOnSiege() const; // merge garrison into army of visiting hero void removeCapitols(const PlayerColor & owner) const; @@ -192,20 +210,26 @@ 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 + ArtifactID getWarMachineInBuilding(BuildingID) const; + /// Returns true if provided war machine is available in any of built buildings of this town + bool isWarMachineAvailable(ArtifactID) const; + CGTownInstance(IGameCallback *cb); virtual ~CGTownInstance(); ///IObjectInterface overrides - void newTurn(CRandomGenerator & rand) const override; + void newTurn(vstd::RNG & rand) const override; void onHeroVisit(const CGHeroInstance * h) const override; void onHeroLeave(const CGHeroInstance * h) const override; - void initObj(CRandomGenerator & rand) override; - void pickRandomObject(CRandomGenerator & rand) override; + void initObj(vstd::RNG & rand) override; + void pickRandomObject(vstd::RNG & rand) override; void battleFinished(const CGHeroInstance * hero, const BattleResult & result) const override; std::string getObjectName() const override; @@ -221,17 +245,15 @@ public: protected: void setPropertyDer(ObjProperty what, ObjPropertyID identifier) override; void serializeJsonOptions(JsonSerializeFormat & handler) override; - void blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) const override; private: - FactionID randomizeFaction(CRandomGenerator & rand); + FactionID randomizeFaction(vstd::RNG & rand); void setOwner(const PlayerColor & owner) const; void onTownCaptured(const PlayerColor & winner) const; - int getDwellingBonus(const std::vector& creatureIds, const std::vector >& dwellings) const; + int getDwellingBonus(const std::vector& creatureIds, const std::vector& dwellings) const; bool townEnvisagesBuilding(BuildingSubID::EBuildingSubID bid) const; - bool isBonusingBuildingAdded(BuildingID bid) const; - void initOverriddenBids(); - void addTownBonuses(CRandomGenerator & rand); + void initializeConfigurableBuildings(vstd::RNG & rand); + void initializeNeutralTownGarrison(vstd::RNG & rand); }; VCMI_LIB_NAMESPACE_END diff --git a/lib/mapObjects/CQuest.cpp b/lib/mapObjects/CQuest.cpp index dfb97696b..9e2e83c47 100644 --- a/lib/mapObjects/CQuest.cpp +++ b/lib/mapObjects/CQuest.cpp @@ -15,10 +15,10 @@ #include "../ArtifactUtils.h" #include "../CSoundBase.h" -#include "../CGeneralTextHandler.h" -#include "../CHeroHandler.h" +#include "../texts/CGeneralTextHandler.h" #include "CGCreature.h" #include "../IGameCallback.h" +#include "../entities/hero/CHeroHandler.h" #include "../mapObjectConstructors/CObjectClassesHandler.h" #include "../serializer/JsonSerializeFormat.h" #include "../GameConstants.h" @@ -31,7 +31,8 @@ #include "../modding/ModUtility.h" #include "../networkPacks/PacksForClient.h" #include "../spells/CSpellHandler.h" -#include "../CRandomGenerator.h" + +#include VCMI_LIB_NAMESPACE_BEGIN @@ -109,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++; @@ -151,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(); @@ -162,7 +163,7 @@ void CQuest::completeQuest(IGameCallback * cb, const CGHeroInstance *h) const for(const auto & ci : parts) { if(ci.art->getTypeId() != elem) - cb->giveHeroNewArtifact(h, ci.art->artType, ArtifactPosition::BACKPACK_START); + cb->giveHeroNewArtifact(h, ci.art->getTypeId(), ArtifactPosition::BACKPACK_START); } } } @@ -430,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(); } @@ -441,7 +442,7 @@ void CGSeerHut::setObjToKill() } } -void CGSeerHut::init(CRandomGenerator & rand) +void CGSeerHut::init(vstd::RNG & rand) { auto names = VLC->generaltexth->findStringsWithPrefix("core.seerhut.names"); @@ -455,7 +456,7 @@ void CGSeerHut::init(CRandomGenerator & rand) configuration.selectMode = Rewardable::ESelectMode::SELECT_PLAYER; } -void CGSeerHut::initObj(CRandomGenerator & rand) +void CGSeerHut::initObj(vstd::RNG & rand) { init(rand); @@ -562,7 +563,7 @@ void CGSeerHut::setPropertyDer(ObjProperty what, ObjPropertyID identifier) } } -void CGSeerHut::newTurn(CRandomGenerator & rand) const +void CGSeerHut::newTurn(vstd::RNG & rand) const { CRewardableObject::newTurn(rand); if(quest->lastDay >= 0 && quest->lastDay <= cb->getDate() - 1) //time is up @@ -587,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) @@ -613,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 @@ -659,7 +660,7 @@ const CGCreature * CGSeerHut::getCreatureToKill(bool allowNull) const return dynamic_cast(o); } -void CGSeerHut::blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) const +void CGSeerHut::blockingDialogAnswered(const CGHeroInstance *hero, int32_t answer) const { CRewardableObject::blockingDialogAnswered(hero, answer); if(answer) @@ -750,7 +751,7 @@ void CGSeerHut::serializeJsonOptions(JsonSerializeFormat & handler) } } -void CGQuestGuard::init(CRandomGenerator & rand) +void CGQuestGuard::init(vstd::RNG & rand) { blockVisit = true; quest->textOption = rand.nextInt(3, 5); @@ -810,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 @@ -818,7 +819,7 @@ void CGKeymasterTent::onHeroVisit( const CGHeroInstance * h ) const h->showInfoDialog(txt_id); } -void CGBorderGuard::initObj(CRandomGenerator & rand) +void CGBorderGuard::initObj(vstd::RNG & rand) { blockVisit = true; } @@ -850,7 +851,7 @@ void CGBorderGuard::onHeroVisit(const CGHeroInstance * h) const BlockingDialog bd (true, false); bd.player = h->getOwner(); bd.text.appendLocalString (EMetaText::ADVOB_TXT, 17); - cb->showBlockingDialog (&bd); + cb->showBlockingDialog (this, &bd); } else { @@ -859,12 +860,12 @@ 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 } } -void CGBorderGuard::blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) const +void CGBorderGuard::blockingDialogAnswered(const CGHeroInstance *hero, int32_t answer) const { if (answer) cb->removeObject(this, hero->getOwner()); @@ -884,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/CQuest.h b/lib/mapObjects/CQuest.h index 69676584f..407c46134 100644 --- a/lib/mapObjects/CQuest.h +++ b/lib/mapObjects/CQuest.h @@ -11,7 +11,8 @@ #include "CRewardableObject.h" #include "../ResourceSet.h" -#include "../MetaString.h" +#include "../serializer/Serializeable.h" +#include "../texts/MetaString.h" VCMI_LIB_NAMESPACE_BEGIN @@ -36,7 +37,7 @@ enum class EQuestMission { HOTA_REACH_DATE = 13, }; -class DLL_LINKAGE CQuest final +class DLL_LINKAGE CQuest final : public Serializeable { public: @@ -74,13 +75,13 @@ public: CQuest(); //TODO: Remove constructor static bool checkMissionArmy(const CQuest * q, const CCreatureSet * army); - virtual bool checkQuest(const CGHeroInstance * h) const; //determines whether the quest is complete or not - virtual void getVisitText(IGameCallback * cb, MetaString &text, std::vector & components, bool FirstVisit, const CGHeroInstance * h = nullptr) const; - virtual void getCompletionText(IGameCallback * cb, MetaString &text) const; - virtual void getRolloverText (IGameCallback * cb, MetaString &text, bool onHover) const; //hover or quest log entry - virtual void completeQuest(IGameCallback *, const CGHeroInstance * h) const; - virtual void addTextReplacements(IGameCallback * cb, MetaString &out, std::vector & components) const; - virtual void addKillTargetReplacements(MetaString &out) const; + bool checkQuest(const CGHeroInstance * h) const; //determines whether the quest is complete or not + void getVisitText(IGameCallback * cb, MetaString &text, std::vector & components, bool FirstVisit, const CGHeroInstance * h = nullptr) const; + void getCompletionText(IGameCallback * cb, MetaString &text) const; + void getRolloverText (IGameCallback * cb, MetaString &text, bool onHover) const; //hover or quest log entry + void completeQuest(IGameCallback *, const CGHeroInstance * h) const; + void addTextReplacements(IGameCallback * cb, MetaString &out, std::vector & components) const; + void addKillTargetReplacements(MetaString &out) const; void defineQuestName(); bool operator== (const CQuest & quest) const @@ -114,7 +115,7 @@ public: void serializeJson(JsonSerializeFormat & handler, const std::string & fieldName); }; -class DLL_LINKAGE IQuestObject +class DLL_LINKAGE IQuestObject : public virtual Serializeable { public: CQuest * quest = new CQuest(); @@ -140,19 +141,19 @@ public: std::string seerName; - void initObj(CRandomGenerator & rand) override; + void initObj(vstd::RNG & rand) override; std::string getHoverText(PlayerColor player) const override; std::string getHoverText(const CGHeroInstance * hero) const override; std::string getPopupText(PlayerColor player) const override; std::string getPopupText(const CGHeroInstance * hero) const override; std::vector getPopupComponents(PlayerColor player) const override; std::vector getPopupComponents(const CGHeroInstance * hero) const override; - void newTurn(CRandomGenerator & rand) const override; + void newTurn(vstd::RNG & rand) const override; void onHeroVisit(const CGHeroInstance * h) const override; - void blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) const override; + void blockingDialogAnswered(const CGHeroInstance *hero, int32_t answer) const override; void getVisitText (MetaString &text, std::vector &components, bool FirstVisit, const CGHeroInstance * h = nullptr) const override; - virtual void init(CRandomGenerator & rand); + virtual void init(vstd::RNG & rand); int checkDirection() const; //calculates the region of map where monster is placed void setObjToKill(); //remember creatures / heroes to kill after they are initialized const CGHeroInstance *getHeroToKill(bool allowNull) const; @@ -178,7 +179,7 @@ class DLL_LINKAGE CGQuestGuard : public CGSeerHut public: using CGSeerHut::CGSeerHut; - void init(CRandomGenerator & rand) override; + void init(vstd::RNG & rand) override; void onHeroVisit(const CGHeroInstance * h) const override; bool passableFor(PlayerColor color) const override; @@ -226,9 +227,9 @@ class DLL_LINKAGE CGBorderGuard : public CGKeys, public IQuestObject public: using CGKeys::CGKeys; - void initObj(CRandomGenerator & rand) override; + void initObj(vstd::RNG & rand) override; void onHeroVisit(const CGHeroInstance * h) const override; - void blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) const override; + void blockingDialogAnswered(const CGHeroInstance *hero, int32_t answer) const override; void getVisitText (MetaString &text, std::vector &components, bool FirstVisit, const CGHeroInstance * h = nullptr) const override; void getRolloverText (MetaString &text, bool onHover) const; diff --git a/lib/mapObjects/CRewardableObject.cpp b/lib/mapObjects/CRewardableObject.cpp index c1f9d546c..9c0d67378 100644 --- a/lib/mapObjects/CRewardableObject.cpp +++ b/lib/mapObjects/CRewardableObject.cpp @@ -10,207 +10,92 @@ #include "StdInc.h" #include "CRewardableObject.h" -#include "../gameState/CGameState.h" -#include "../CGeneralTextHandler.h" + #include "../CPlayerState.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 + 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()) + return this; +} + +void CRewardableObject::markAsScouted(const CGHeroInstance * hero) const +{ + ChangeObjectVisitors cov(ChangeObjectVisitors::VISITOR_ADD_PLAYER, id, hero->id); + cb->sendAndApply(cov); +} + +bool CRewardableObject::isGuarded() const +{ + return stacksCount() > 0; +} + +void CRewardableObject::onHeroVisit(const CGHeroInstance *hero) const +{ + if(!wasScouted(hero->getOwner())) { - 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); + ChangeObjectVisitors cov(ChangeObjectVisitors::VISITOR_SCOUTED, id, hero->id); + cb->sendAndApply(cov); } - // grant reward afterwards. Note that it may remove object - if(markAsVisit) - markAsVisited(contextHero); - grantReward(index, contextHero); -} -void CRewardableObject::selectRewardWthMessage(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); - cb->showBlockingDialog(&sd); - -} - -void CRewardableObject::grantAllRewardsWthMessage(const CGHeroInstance * contextHero, const std::vector & rewardIndices, bool markAsVisit) const -{ - if (rewardIndices.empty()) - return; - - for (auto index : rewardIndices) + if (isGuarded()) { - // 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); -} + auto guardedIndexes = getAvailableRewards(hero, Rewardable::EEventType::EVENT_GUARDED); + auto guardedReward = configuration.info.at(guardedIndexes.at(0)); -std::vector CRewardableObject::loadComponents(const CGHeroInstance * contextHero, const std::vector & rewardIndices) const -{ - std::vector result; + // ask player to confirm attack + BlockingDialog bd(true, false); + bd.player = hero->getOwner(); + bd.text = guardedReward.message; + bd.components = getPopupComponents(hero->getOwner()); - 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)); + cb->showBlockingDialog(this, &bd); } else { - configuration.info.at(rewardIndices.front()).reward.loadComponents(result, contextHero); - } - - return result; -} - -void CRewardableObject::onHeroVisit(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) - selectRewardWthMessage(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 - selectRewardWthMessage(h, rewards, configuration.onSelect); - break; - case Rewardable::SELECT_FIRST: // give first available - if (configuration.canRefuse) - selectRewardWthMessage(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) - selectRewardWthMessage(h, { rewardIndex }, configuration.info.at(rewardIndex).message); - else - grantRewardWithMessage(h, rewardIndex, true); - break; - } - case Rewardable::SELECT_ALL: // grant all possible - grantAllRewardsWthMessage(h, rewards, true); - break; - } - break; - } - } - - if(!objectRemovalPossible && getAvailableRewards(h, Rewardable::EEventType::EVENT_FIRST_VISIT).empty()) - { - ChangeObjectVisitors cov(ChangeObjectVisitors::VISITOR_ADD_TEAM, id, h->id); - cb->sendAndApply(&cov); - } - } - else - { - logGlobal->debug("Revisiting already visited object"); - - if (!wasVisited(h->getOwner())) - { - ChangeObjectVisitors cov(ChangeObjectVisitors::VISITOR_ADD_TEAM, id, h->id); - cb->sendAndApply(&cov); - } - - auto visitedRewards = getAvailableRewards(h, Rewardable::EEventType::EVENT_ALREADY_VISITED); - if (!visitedRewards.empty()) - grantRewardWithMessage(h, visitedRewards[0], false); - else - logMod->warn("No applicable message for visiting already visited object!"); + doHeroVisit(hero); } } void CRewardableObject::heroLevelUpDone(const CGHeroInstance *hero) const { - grantRewardAfterLevelup(cb, configuration.info.at(selectedReward), this, hero); + grantRewardAfterLevelup(configuration.info.at(selectedReward), this, hero); } -void CRewardableObject::blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) const +void CRewardableObject::battleFinished(const CGHeroInstance *hero, const BattleResult &result) const { - if(answer == 0) + if (result.winner == BattleSide::ATTACKER) { - switch (configuration.visitMode) - { - case Rewardable::VISIT_UNLIMITED: - case Rewardable::VISIT_BONUS: - case Rewardable::VISIT_HERO: - case Rewardable::VISIT_LIMITER: - { - // workaround for object with refusable reward not getting marked as visited - // TODO: better solution that would also work for player-visitable objects - if (!wasScouted(hero->getOwner())) - { - ChangeObjectVisitors cov(ChangeObjectVisitors::VISITOR_ADD_TEAM, id, hero->id); - cb->sendAndApply(&cov); - } - } - } - - return; // player refused + doHeroVisit(hero); } +} - if(answer > 0 && answer-1 < configuration.info.size()) +void CRewardableObject::blockingDialogAnswered(const CGHeroInstance * hero, int32_t answer) const +{ + if(isGuarded()) { - auto list = getAvailableRewards(hero, Rewardable::EEventType::EVENT_FIRST_VISIT); - markAsVisited(hero); - grantReward(list[answer - 1], hero); + if (answer) + { + auto layout = BattleLayout::createLayout(cb, configuration.guardsLayout, hero, this); + cb->startBattle(hero, this, visitablePos(), hero, nullptr, layout, nullptr); + } } else { - throw std::runtime_error("Unhandled choice"); + onBlockingDialogAnswered(hero, answer); } } @@ -218,19 +103,19 @@ void CRewardableObject::markAsVisited(const CGHeroInstance * hero) const { cb->setObjPropertyValue(id, ObjProperty::REWARD_CLEARED, true); - ChangeObjectVisitors cov(ChangeObjectVisitors::VISITOR_ADD, id, hero->id); - cb->sendAndApply(&cov); + ChangeObjectVisitors cov(ChangeObjectVisitors::VISITOR_ADD_HERO, id, hero->id); + 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 remainer immediately + // 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); } } @@ -274,7 +159,7 @@ bool CRewardableObject::wasVisited(PlayerColor player) const bool CRewardableObject::wasScouted(PlayerColor player) const { - return vstd::contains(cb->getPlayerState(player)->visitedObjects, ObjectInstanceID(id)); + return vstd::contains(cb->getPlayerTeam(player)->scoutedObjects, ObjectInstanceID(id)); } bool CRewardableObject::wasVisited(const CGHeroInstance * h) const @@ -362,22 +247,44 @@ std::vector CRewardableObject::getPopupComponentsImpl(PlayerColor pla if (!wasScouted(player)) return {}; - if (!configuration.showScoutedPreview) - return {}; - - auto rewardIndices = getAvailableRewards(hero, Rewardable::EEventType::EVENT_FIRST_VISIT); - if (rewardIndices.empty() && !configuration.info.empty()) + if (isGuarded()) { - // Object has valid config, but current hero has no rewards that he can receive. - // Usually this happens if hero has already visited this object -> show reward using context without any hero - // since reward may be context-sensitive - e.g. Witch Hut that gives 1 skill, but always at basic level - return loadComponents(nullptr, {0}); + if (!cb->getSettings().getBoolean(EGameSettings::BANKS_SHOW_GUARDS_COMPOSITION)) + return {}; + + std::map guardsAmounts; + std::vector result; + + for (auto const & slot : Slots()) + if (slot.second) + guardsAmounts[slot.second->getCreatureID()] += slot.second->getCount(); + + for (auto const & guard : guardsAmounts) + { + Component comp(ComponentType::CREATURE, guard.first, guard.second); + result.push_back(comp); + } + return result; } + else + { + if (!configuration.showScoutedPreview) + return {}; - if (rewardIndices.empty()) - return {}; + auto rewardIndices = getAvailableRewards(hero, Rewardable::EEventType::EVENT_FIRST_VISIT); + if (rewardIndices.empty() && !configuration.info.empty()) + { + // Object has valid config, but current hero has no rewards that he can receive. + // Usually this happens if hero has already visited this object -> show reward using context without any hero + // since reward may be context-sensitive - e.g. Witch Hut that gives 1 skill, but always at basic level + return loadComponents(nullptr, {0}); + } - return loadComponents(hero, rewardIndices); + if (rewardIndices.empty()) + return {}; + + return loadComponents(hero, rewardIndices); + } } std::vector CRewardableObject::getPopupComponents(PlayerColor player) const @@ -394,9 +301,6 @@ void CRewardableObject::setPropertyDer(ObjProperty what, ObjPropertyID identifie { switch (what) { - case ObjProperty::REWARD_RANDOMIZE: - initObj(cb->gameState()->getRandomGenerator()); - break; case ObjProperty::REWARD_SELECT: selectedReward = identifier.getNum(); break; @@ -406,24 +310,26 @@ void CRewardableObject::setPropertyDer(ObjProperty what, ObjPropertyID identifie } } -void CRewardableObject::newTurn(CRandomGenerator & rand) const +void CRewardableObject::newTurn(vstd::RNG & rand) const { if (configuration.resetParameters.period != 0 && cb->getDate(Date::DAY) > 1 && ((cb->getDate(Date::DAY)-1) % configuration.resetParameters.period) == 0) { if (configuration.resetParameters.rewards) { - cb->setObjPropertyValue(id, ObjProperty::REWARD_RANDOMIZE, 0); + auto handler = std::dynamic_pointer_cast(getObjectHandler()); + 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); } } } -void CRewardableObject::initObj(CRandomGenerator & rand) +void CRewardableObject::initObj(vstd::RNG & rand) { getObjectHandler()->configureObject(this, rand); } @@ -438,4 +344,31 @@ void CRewardableObject::serializeJsonOptions(JsonSerializeFormat & handler) handler.serializeStruct("rewardable", static_cast(*this)); } +void CRewardableObject::initializeGuards() +{ + clearSlots(); + + // Workaround for default creature banks strings that has placeholder for object name + // TODO: find better location for this code + for (auto & visitInfo : configuration.info) + visitInfo.message.replaceRawString(getObjectName()); + + for (auto const & visitInfo : configuration.info) + { + for (auto const & guard : visitInfo.reward.guards) + { + auto slotID = getFreeSlot(); + if (!slotID.validSlot()) + return; + + putStack(slotID, new CStackInstance(guard.getId(), guard.getCount())); + } + } +} + +bool CRewardableObject::isCoastVisitable() const +{ + return configuration.coastVisitable; +} + VCMI_LIB_NAMESPACE_END diff --git a/lib/mapObjects/CRewardableObject.h b/lib/mapObjects/CRewardableObject.h index 1a7cea319..14bd52af2 100644 --- a/lib/mapObjects/CRewardableObject.h +++ b/lib/mapObjects/CRewardableObject.h @@ -15,7 +15,7 @@ VCMI_LIB_NAMESPACE_BEGIN /// Base class that can handle granting rewards to visiting heroes. -/// Inherits from CArmedInstance for proper trasfer of armies +/// Inherits from CArmedInstance for proper transfer of armies class DLL_LINKAGE CRewardableObject : public CArmedInstance, public Rewardable::Interface { protected: @@ -25,27 +25,26 @@ 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 selectRewardWthMessage(const CGHeroInstance * contextHero, const std::vector & rewardIndices, const MetaString & dialog) const; - - virtual void grantAllRewardsWthMessage(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; + /// Returns true if this object is currently guarded + bool isGuarded() const; public: + /// Visitability checks. Note that hero check includes check for hero owner (returns true if object was visited by player) bool wasVisited(PlayerColor player) const override; bool wasVisited(const CGHeroInstance * h) const override; @@ -56,16 +55,22 @@ public: /// gives reward to player or ask for choice in case of multiple rewards void onHeroVisit(const CGHeroInstance *h) const override; + void battleFinished(const CGHeroInstance *hero, const BattleResult &result) const override; + ///possibly resets object state - void newTurn(CRandomGenerator & rand) const override; + void newTurn(vstd::RNG & rand) const override; /// gives second part of reward after hero level-ups for proper granting of spells/mana void heroLevelUpDone(const CGHeroInstance *hero) const override; /// applies player selection of reward - void blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) const override; + void blockingDialogAnswered(const CGHeroInstance *hero, int32_t answer) const override; - void initObj(CRandomGenerator & rand) override; + void initObj(vstd::RNG & rand) override; + + bool isCoastVisitable() const override; + + void initializeGuards(); void setPropertyDer(ObjProperty what, ObjPropertyID identifier) override; @@ -89,14 +94,6 @@ public: }; //TODO: - -// MAX -// class DLL_LINKAGE CBank : public CArmedInstance -// class DLL_LINKAGE CGPyramid : public CBank - -// EXTRA -// class DLL_LINKAGE COPWBonus : public CGTownBuilding -// class DLL_LINKAGE CTownBonus : public CGTownBuilding // class DLL_LINKAGE CGKeys : public CGObjectInstance //Base class for Keymaster and guards // class DLL_LINKAGE CGKeymasterTent : public CGKeys // class DLL_LINKAGE CGBorderGuard : public CGKeys, public IQuestObject 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..56b64d1e1 --- /dev/null +++ b/lib/mapObjects/FlaggableMapObject.cpp @@ -0,0 +1,111 @@ +/* + * 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::markAsDeleted() const +{ + if(getOwner().isValidPlayer()) + takeBonusFrom(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..6a332320e --- /dev/null +++ b/lib/mapObjects/FlaggableMapObject.h @@ -0,0 +1,42 @@ +/* + * 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 markAsDeleted() const; + 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/IMarket.cpp b/lib/mapObjects/IMarket.cpp index 7f7840408..fc80b7e62 100644 --- a/lib/mapObjects/IMarket.cpp +++ b/lib/mapObjects/IMarket.cpp @@ -20,6 +20,11 @@ VCMI_LIB_NAMESPACE_BEGIN +bool IMarket::allowsTrade(const EMarketMode mode) const +{ + return vstd::contains(availableModes(), mode); +} + bool IMarket::getOffer(int id1, int id2, int &val1, int &val2, EMarketMode mode) const { switch(mode) @@ -122,12 +127,7 @@ bool IMarket::getOffer(int id1, int id2, int &val1, int &val2, EMarketMode mode) return true; } -bool IMarket::allowsTrade(EMarketMode mode) const -{ - return false; -} - -int IMarket::availableUnits(EMarketMode mode, int marketItemSerial) const +int IMarket::availableUnits(const EMarketMode mode, const int marketItemSerial) const { switch(mode) { @@ -140,7 +140,22 @@ int IMarket::availableUnits(EMarketMode mode, int marketItemSerial) const } } -std::vector IMarket::availableItemsIds(EMarketMode mode) const +IMarket::IMarket() + :altarArtifactsStorage(std::make_unique()) +{ +} + +IMarket::~IMarket() = default; + +CArtifactSet * IMarket::getArtifactsStorage() const +{ + if (availableModes().count(EMarketMode::ARTIFACT_EXP)) + return altarArtifactsStorage.get(); + else + return nullptr; +} + +std::vector IMarket::availableItemsIds(const EMarketMode mode) const { std::vector ret; switch(mode) @@ -148,24 +163,10 @@ std::vector IMarket::availableItemsIds(EMarketMode mode) const case EMarketMode::RESOURCE_RESOURCE: case EMarketMode::ARTIFACT_RESOURCE: case EMarketMode::CREATURE_RESOURCE: - for (auto res : GameResID::ALL_RESOURCES()) + for(const auto & res : GameResID::ALL_RESOURCES()) ret.push_back(res); } return ret; } -IMarket::IMarket() -{ -} - -std::vector IMarket::availableModes() const -{ - std::vector ret; - for (EMarketMode i = static_cast(0); i < EMarketMode::MARKET_AFTER_LAST_PLACEHOLDER; i = vstd::next(i, 1)) - if(allowsTrade(i)) - ret.push_back(i); - - return ret; -} - VCMI_LIB_NAMESPACE_END diff --git a/lib/mapObjects/IMarket.h b/lib/mapObjects/IMarket.h index caf3e4cb4..b4822e732 100644 --- a/lib/mapObjects/IMarket.h +++ b/lib/mapObjects/IMarket.h @@ -11,24 +11,38 @@ #include "../networkPacks/TradeItem.h" #include "../constants/Enumerations.h" +#include "../CArtHandler.h" VCMI_LIB_NAMESPACE_BEGIN -class CGObjectInstance; - -class DLL_LINKAGE IMarket +class DLL_LINKAGE IMarket : public virtual Serializeable, boost::noncopyable { public: IMarket(); - virtual ~IMarket() {} + ~IMarket(); + class CArtifactSetAltar : public CArtifactSet + { + public: + ArtBearer::ArtBearer bearerType() const override {return ArtBearer::ALTAR;}; + }; + + virtual ObjectInstanceID getObjInstanceID() const = 0; // The market is always an object on the map virtual int getMarketEfficiency() const = 0; - virtual bool allowsTrade(EMarketMode mode) const; - virtual int availableUnits(EMarketMode mode, int marketItemSerial) const; //-1 if unlimited - virtual std::vector availableItemsIds(EMarketMode mode) const; - + virtual bool allowsTrade(const EMarketMode mode) const; + virtual int availableUnits(const EMarketMode mode, const int marketItemSerial) const; //-1 if unlimited + virtual std::vector availableItemsIds(const EMarketMode mode) const; + virtual std::set availableModes() const = 0; + CArtifactSet * getArtifactsStorage() const; bool getOffer(int id1, int id2, int &val1, int &val2, EMarketMode mode) const; //val1 - how many units of id1 player has to give to receive val2 units - std::vector availableModes() const; + + template void serializeArtifactsAltar(Handler &h) + { + h & *altarArtifactsStorage; + } + +private: + std::unique_ptr altarArtifactsStorage; }; VCMI_LIB_NAMESPACE_END diff --git a/lib/mapObjects/IObjectInterface.cpp b/lib/mapObjects/IObjectInterface.cpp index e7dac1ca0..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 @@ -38,13 +38,13 @@ void IObjectInterface::onHeroVisit(const CGHeroInstance * h) const void IObjectInterface::onHeroLeave(const CGHeroInstance * h) const {} -void IObjectInterface::newTurn(CRandomGenerator & rand) const +void IObjectInterface::newTurn(vstd::RNG & rand) const {} -void IObjectInterface::initObj(CRandomGenerator & rand) +void IObjectInterface::initObj(vstd::RNG & rand) {} -void IObjectInterface::pickRandomObject(CRandomGenerator & rand) +void IObjectInterface::pickRandomObject(vstd::RNG & rand) {} void IObjectInterface::setProperty(ObjProperty what, ObjPropertyID identifier) @@ -68,7 +68,7 @@ void IObjectInterface::preInit() void IObjectInterface::battleFinished(const CGHeroInstance *hero, const BattleResult &result) const {} -void IObjectInterface::blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) const +void IObjectInterface::blockingDialogAnswered(const CGHeroInstance *hero, int32_t answer) const {} void IObjectInterface::garrisonDialogClosed(const CGHeroInstance *hero) const @@ -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 12d2baeb1..4398c5a11 100644 --- a/lib/mapObjects/IObjectInterface.h +++ b/lib/mapObjects/IObjectInterface.h @@ -9,18 +9,23 @@ */ #pragma once +#include "../GameCallbackHolder.h" +#include "../constants/EntityIdentifiers.h" #include "../networkPacks/EInfoWindowMode.h" #include "../networkPacks/ObjProperty.h" -#include "../constants/EntityIdentifiers.h" -#include "../GameCallbackHolder.h" +#include "../serializer/Serializeable.h" VCMI_LIB_NAMESPACE_BEGIN +namespace vstd +{ +class RNG; +} + struct BattleResult; struct UpgradeInfo; class BoatId; class CGObjectInstance; -class CRandomGenerator; class CStackInstance; class CGHeroInstance; class IGameCallback; @@ -28,8 +33,9 @@ class ResourceSet; class int3; class MetaString; class PlayerColor; +class IOwnableObject; -class DLL_LINKAGE IObjectInterface : public GameCallbackHolder +class DLL_LINKAGE IObjectInterface : public GameCallbackHolder, public virtual Serializeable { public: using GameCallbackHolder::GameCallbackHolder; @@ -41,25 +47,30 @@ 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; - virtual void newTurn(CRandomGenerator & rand) const; - virtual void initObj(CRandomGenerator & rand); //synchr - virtual void pickRandomObject(CRandomGenerator & rand); + + /// Called on new turn by server. This method can not modify object state on its own + /// Instead all changes must be propagated via netpacks + virtual void newTurn(vstd::RNG & rand) const; + virtual void initObj(vstd::RNG & rand); //synchr + virtual void pickRandomObject(vstd::RNG & rand); virtual void setProperty(ObjProperty what, ObjPropertyID identifier);//synchr //Called when queries created DURING HERO VISIT are resolved //First parameter is always hero that visited object and triggered the query virtual void battleFinished(const CGHeroInstance *hero, const BattleResult &result) const; - virtual void blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) const; + virtual void blockingDialogAnswered(const CGHeroInstance *hero, int32_t answer) const; virtual void garrisonDialogClosed(const CGHeroInstance *hero) const; virtual void heroLevelUpDone(const CGHeroInstance *hero) const; //unified helper to show info dialog for object owner virtual void showInfoDialog(const ui32 txtID, const ui16 soundID = 0, EInfoWindowMode mode = EInfoWindowMode::AUTO) const; + virtual const IOwnableObject * asOwnable() const = 0; + //unified interface, AI helpers virtual bool wasVisited (PlayerColor player) const; virtual bool wasVisited (const CGHeroInstance * h) const; diff --git a/lib/mapObjects/IOwnableObject.h b/lib/mapObjects/IOwnableObject.h new file mode 100644 index 000000000..1e8b850f1 --- /dev/null +++ b/lib/mapObjects/IOwnableObject.h @@ -0,0 +1,31 @@ +/* +* IOwnableObject.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 ResourceSet; +class CreatureID; + +class DLL_LINKAGE IOwnableObject +{ +public: + /// Fixed daily income of this object + /// May not include random or periodical (e.g. weekly) income sources + virtual ResourceSet dailyIncome() const = 0; + + /// List of creatures that are provided by this building + /// For use in town dwellings growth bonus and for portal of summoning + virtual std::vector providedCreatures() const = 0; + + virtual ~IOwnableObject() = default; +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/mapObjects/MapObjects.h b/lib/mapObjects/MapObjects.h index 01e38b4d8..7a54592ae 100644 --- a/lib/mapObjects/MapObjects.h +++ b/lib/mapObjects/MapObjects.h @@ -14,7 +14,6 @@ #include "CObjectHandler.h" #include "CArmedInstance.h" -#include "CBank.h" #include "CGDwelling.h" #include "CGHeroInstance.h" #include "CGMarket.h" diff --git a/lib/mapObjects/MiscObjects.cpp b/lib/mapObjects/MiscObjects.cpp index 21b0cd0d9..910baa639 100644 --- a/lib/mapObjects/MiscObjects.cpp +++ b/lib/mapObjects/MiscObjects.cpp @@ -15,7 +15,7 @@ #include "../bonuses/Propagators.h" #include "../constants/StringConstants.h" #include "../CConfigHandler.h" -#include "../CGeneralTextHandler.h" +#include "../texts/CGeneralTextHandler.h" #include "../CSoundBase.h" #include "../CSkillHandler.h" #include "../spells/CSpellHandler.h" @@ -23,6 +23,7 @@ #include "../gameState/CGameState.h" #include "../mapping/CMap.h" #include "../CPlayerState.h" +#include "../StartInfo.h" #include "../serializer/JsonSerializeFormat.h" #include "../mapObjectConstructors/AObjectTypeHandler.h" #include "../mapObjectConstructors/CObjectClassesHandler.h" @@ -32,6 +33,8 @@ #include "../networkPacks/PacksForClientBattle.h" #include "../networkPacks/StackLocation.h" +#include + VCMI_LIB_NAMESPACE_BEGIN ///helpers @@ -85,26 +88,14 @@ void CGMine::onHeroVisit( const CGHeroInstance * h ) const BlockingDialog ynd(true,false); ynd.player = h->tempOwner; ynd.text.appendLocalString(EMetaText::ADVOB_TXT, isAbandoned() ? 84 : 187); - cb->showBlockingDialog(&ynd); + cb->showBlockingDialog(this, &ynd); return; } flagMine(h->tempOwner); - } -void CGMine::newTurn(CRandomGenerator & rand) const -{ - if(cb->getDate() == 1) - return; - - if (tempOwner == PlayerColor::NEUTRAL) - return; - - cb->giveResource(tempOwner, producedResource, producedQuantity); -} - -void CGMine::initObj(CRandomGenerator & rand) +void CGMine::initObj(vstd::RNG & rand) { if(isAbandoned()) { @@ -120,7 +111,7 @@ void CGMine::initObj(CRandomGenerator & 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; } } @@ -136,11 +127,24 @@ bool CGMine::isAbandoned() const return subID.getNum() >= 7; } +const IOwnableObject * CGMine::asOwnable() const +{ + return this; +} + +std::vector CGMine::providedCreatures() const +{ + return {}; +} + ResourceSet CGMine::dailyIncome() const { ResourceSet result; result[producedResource] += defaultResProduction(); + const auto & playerSettings = cb->getPlayerSettings(getOwner()); + result.applyHandicap(playerSettings->handicap.percentIncome); + return result; } @@ -175,7 +179,7 @@ void CGMine::flagMine(const PlayerColor & player) const iw.type = EInfoWindowMode::AUTO; iw.text.appendTextID(TextIdentifier("core.mineevnt", producedResource.getNum()).get()); //not use subID, abandoned mines uses default mine texts iw.player = player; - iw.components.emplace_back(ComponentType::RESOURCE_PER_DAY, producedResource, producedQuantity); + iw.components.emplace_back(ComponentType::RESOURCE_PER_DAY, producedResource, getProducedQuantity()); cb->showInfoDialog(&iw); } @@ -193,9 +197,16 @@ ui32 CGMine::defaultResProduction() const } } +ui32 CGMine::getProducedQuantity() const +{ + auto * playerSettings = cb->getPlayerSettings(getOwner()); + // always round up income - we don't want mines to always produce zero if handicap in use + return vstd::divideAndCeil(producedQuantity * playerSettings->handicap.percentIncome, 100); +} + void CGMine::battleFinished(const CGHeroInstance *hero, const BattleResult &result) const { - if(result.winner == 0) //attacker won + if(result.winner == BattleSide::ATTACKER) //attacker won { if(isAbandoned()) { @@ -205,10 +216,10 @@ void CGMine::battleFinished(const CGHeroInstance *hero, const BattleResult &resu } } -void CGMine::blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) const +void CGMine::blockingDialogAnswered(const CGHeroInstance *hero, int32_t answer) const { if(answer) - cb->startBattleI(hero, this); + cb->startBattle(hero, this); } void CGMine::serializeJsonOptions(JsonSerializeFormat & handler) @@ -254,7 +265,7 @@ std::string CGResource::getHoverText(PlayerColor player) const return VLC->generaltexth->restypes[resourceID().getNum()]; } -void CGResource::pickRandomObject(CRandomGenerator & rand) +void CGResource::pickRandomObject(vstd::RNG & rand) { assert(ID == Obj::RESOURCE || ID == Obj::RANDOM_RESOURCE); @@ -269,7 +280,7 @@ void CGResource::pickRandomObject(CRandomGenerator & rand) } } -void CGResource::initObj(CRandomGenerator & rand) +void CGResource::initObj(vstd::RNG & rand) { blockVisit = true; @@ -299,7 +310,7 @@ void CGResource::onHeroVisit( const CGHeroInstance * h ) const BlockingDialog ynd(true,false); ynd.player = h->getOwner(); ynd.text = message; - cb->showBlockingDialog(&ynd); + cb->showBlockingDialog(this, &ynd); } else { @@ -327,21 +338,21 @@ void CGResource::collectRes(const PlayerColor & player) const sii.text.replaceName(resourceID()); } sii.components.emplace_back(ComponentType::RESOURCE, resourceID(), amount); - sii.soundID = soundBase::pickup01 + CRandomGenerator::getDefault().nextInt(6); + sii.soundID = soundBase::pickup01 + cb->gameState()->getRandomGenerator().nextInt(6); cb->showInfoDialog(&sii); cb->removeObject(this, player); } void CGResource::battleFinished(const CGHeroInstance *hero, const BattleResult &result) const { - if(result.winner == 0) //attacker won + if(result.winner == BattleSide::ATTACKER) //attacker won collectRes(hero->getOwner()); } -void CGResource::blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) const +void CGResource::blockingDialogAnswered(const CGHeroInstance *hero, int32_t answer) const { if(answer) - cb->startBattleI(hero, this); + cb->startBattle(hero, this); } void CGResource::serializeJsonOptions(JsonSerializeFormat & handler) @@ -375,7 +386,7 @@ bool CGTeleport::isChannelExit(const ObjectInstanceID & id) const std::vector CGTeleport::getAllEntrances(bool excludeCurrent) const { - auto ret = cb->getTeleportChannelEntraces(channel); + auto ret = cb->getTeleportChannelEntrances(channel); if(excludeCurrent) vstd::erase_if_present(ret, id); @@ -395,7 +406,7 @@ ObjectInstanceID CGTeleport::getRandomExit(const CGHeroInstance * h) const { auto passableExits = getPassableExits(cb->gameState(), h, getAllExits(true)); if(!passableExits.empty()) - return *RandomGeneratorUtil::nextItem(passableExits, CRandomGenerator::getDefault()); + return *RandomGeneratorUtil::nextItem(passableExits, cb->gameState()->getRandomGenerator()); return ObjectInstanceID(); } @@ -499,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); @@ -530,7 +541,7 @@ void CGMonolith::teleportDialogAnswered(const CGHeroInstance *hero, ui32 answer, cb->moveHero(hero->id, hero->convertFromVisitablePos(dPos), EMovementMode::MONOLITH); } -void CGMonolith::initObj(CRandomGenerator & rand) +void CGMonolith::initObj(vstd::RNG & rand) { std::vector IDs; IDs.push_back(ID); @@ -563,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 @@ -575,7 +586,7 @@ void CGSubterraneanGate::onHeroVisit( const CGHeroInstance * h ) const cb->showTeleportDialog(&td); } -void CGSubterraneanGate::initObj(CRandomGenerator & rand) +void CGSubterraneanGate::initObj(vstd::RNG & rand) { type = BOTH; } @@ -591,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) @@ -620,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; @@ -646,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)) { @@ -703,7 +714,7 @@ void CGWhirlpool::teleportDialogAnswered(const CGHeroInstance *hero, ui32 answer const auto * obj = cb->getObj(exit); std::set tiles = obj->getBlockedPos(); - dPos = *RandomGeneratorUtil::nextItem(tiles, CRandomGenerator::getDefault()); + dPos = *RandomGeneratorUtil::nextItem(tiles, cb->gameState()->getRandomGenerator()); } cb->moveHero(hero->id, hero->convertFromVisitablePos(dPos), EMovementMode::MONOLITH); @@ -724,7 +735,7 @@ ArtifactID CGArtifact::getArtifact() const return getObjTypeIndex().getNum(); } -void CGArtifact::pickRandomObject(CRandomGenerator & rand) +void CGArtifact::pickRandomObject(vstd::RNG & rand) { switch(ID.toEnum()) { @@ -754,24 +765,23 @@ void CGArtifact::pickRandomObject(CRandomGenerator & rand) ID = MapObjectID::ARTIFACT; } -void CGArtifact::initObj(CRandomGenerator & rand) +void CGArtifact::initObj(vstd::RNG & rand) { blockVisit = true; if(ID == Obj::ARTIFACT) { 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 @@ -815,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()) { @@ -868,7 +878,7 @@ void CGArtifact::onHeroVisit(const CGHeroInstance * h) const ynd.text.replaceRawString(getArmyDescription()); ynd.text.replaceLocalString(EMetaText::GENERAL_TXT, 43); // creatures } - cb->showBlockingDialog(&ynd); + cb->showBlockingDialog(this, &ynd); } break; case Obj::SPELL_SCROLL: @@ -878,7 +888,7 @@ void CGArtifact::onHeroVisit(const CGHeroInstance * h) const BlockingDialog ynd(true,false); ynd.player = h->getOwner(); ynd.text = message; - cb->showBlockingDialog(&ynd); + cb->showBlockingDialog(this, &ynd); } else blockingDialogAnswered(h, true); @@ -890,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()); } @@ -901,14 +911,14 @@ BattleField CGArtifact::getBattlefield() const void CGArtifact::battleFinished(const CGHeroInstance *hero, const BattleResult &result) const { - if(result.winner == 0) //attacker won + if(result.winner == BattleSide::ATTACKER) //attacker won pick(hero); } -void CGArtifact::blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) const +void CGArtifact::blockingDialogAnswered(const CGHeroInstance *hero, int32_t answer) const { if(answer) - cb->startBattleI(hero, this); + cb->startBattle(hero, this); } void CGArtifact::afterAddToMap(CMap * map) @@ -936,7 +946,7 @@ void CGArtifact::serializeJsonOptions(JsonSerializeFormat& handler) } } -void CGSignBottle::initObj(CRandomGenerator & rand) +void CGSignBottle::initObj(vstd::RNG & rand) { //if no text is set than we pick random from the predefined ones if(message.empty()) @@ -968,12 +978,27 @@ void CGSignBottle::serializeJsonOptions(JsonSerializeFormat& handler) handler.serializeStruct("text", message); } +const IOwnableObject * CGGarrison::asOwnable() const +{ + return this; +} + +ResourceSet CGGarrison::dailyIncome() const +{ + return {}; +} + +std::vector CGGarrison::providedCreatures() const +{ + return {}; +} + void CGGarrison::onHeroVisit (const CGHeroInstance *h) const { auto relations = cb->gameState()->getPlayerRelations(h->tempOwner, tempOwner); if (relations == PlayerRelations::ENEMIES && stacksCount() > 0) { //TODO: Find a way to apply magic garrison effects in battle. - cb->startBattleI(h, this); + cb->startBattle(h, this); return; } @@ -1000,7 +1025,7 @@ bool CGGarrison::passableFor(PlayerColor player) const void CGGarrison::battleFinished(const CGHeroInstance *hero, const BattleResult &result) const { - if (result.winner == 0) + if (result.winner == BattleSide::ATTACKER) onHeroVisit(hero); } @@ -1011,7 +1036,7 @@ void CGGarrison::serializeJsonOptions(JsonSerializeFormat& handler) CArmedInstance::serializeJsonOptions(handler); } -void CGGarrison::initObj(CRandomGenerator &rand) +void CGGarrison::initObj(vstd::RNG &rand) { if(this->subID == MapObjectSubID::decode(this->ID, "antiMagic")) addAntimagicGarrisonBonus(); @@ -1028,7 +1053,7 @@ void CGGarrison::addAntimagicGarrisonBonus() this->addNewBonus(bonus); } -void CGMagi::initObj(CRandomGenerator & rand) +void CGMagi::initObj(vstd::RNG & rand) { if (ID == Obj::EYE_OF_MAGI) blockVisit = true; @@ -1061,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) @@ -1091,7 +1116,7 @@ bool CGBoat::isCoastVisitable() const return true; } -void CGSirens::initObj(CRandomGenerator & rand) +void CGSirens::initObj(vstd::RNG & rand) { blockVisit = true; } @@ -1127,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(); } } @@ -1201,6 +1226,21 @@ BoatId CGShipyard::getBoatType() const return createdBoat; } +const IOwnableObject * CGShipyard::asOwnable() const +{ + return this; +} + +ResourceSet CGShipyard::dailyIncome() const +{ + return {}; +} + +std::vector CGShipyard::providedCreatures() const +{ + return {}; +} + void CGDenOfthieves::onHeroVisit (const CGHeroInstance * h) const { cb->showObjectWindow(this, EOpenWindowMode::THIEVES_GUILD, h, false); @@ -1218,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); @@ -1233,12 +1273,12 @@ void CGObelisk::onHeroVisit( const CGHeroInstance * h ) const else { iw.text.appendLocalString(EMetaText::ADVOB_TXT, 97); - cb->sendAndApply(&iw); + cb->sendAndApply(iw); } } -void CGObelisk::initObj(CRandomGenerator & rand) +void CGObelisk::initObj(vstd::RNG & rand) { cb->gameState()->map->obeliskCount++; } @@ -1271,60 +1311,6 @@ void CGObelisk::setPropertyDer(ObjProperty what, ObjPropertyID identifier) } } -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(CRandomGenerator & 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); @@ -1332,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]; @@ -1340,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 6ed869895..aa5d2ac46 100644 --- a/lib/mapObjects/MiscObjects.h +++ b/lib/mapObjects/MiscObjects.h @@ -10,7 +10,8 @@ #pragma once #include "CArmedInstance.h" -#include "../MetaString.h" +#include "IOwnableObject.h" +#include "../texts/MetaString.h" VCMI_LIB_NAMESPACE_BEGIN @@ -48,7 +49,7 @@ public: MetaString message; void onHeroVisit(const CGHeroInstance * h) const override; - void initObj(CRandomGenerator & rand) override; + void initObj(vstd::RNG & rand) override; template void serialize(Handler &h) { @@ -59,18 +60,22 @@ protected: void serializeJsonOptions(JsonSerializeFormat & handler) override; }; -class DLL_LINKAGE CGGarrison : public CArmedInstance +class DLL_LINKAGE CGGarrison : public CArmedInstance, public IOwnableObject { public: using CArmedInstance::CArmedInstance; bool removableUnits; - void initObj(CRandomGenerator &rand) override; + void initObj(vstd::RNG &rand) override; bool passableFor(PlayerColor color) const override; void onHeroVisit(const CGHeroInstance * h) const override; void battleFinished(const CGHeroInstance *hero, const BattleResult &result) const override; + const IOwnableObject * asOwnable() const final; + ResourceSet dailyIncome() const override; + std::vector providedCreatures() const override; + template void serialize(Handler &h) { h & static_cast(*this); @@ -79,6 +84,7 @@ public: protected: void serializeJsonOptions(JsonSerializeFormat & handler) override; void addAntimagicGarrisonBonus(); + }; class DLL_LINKAGE CGArtifact : public CArmedInstance @@ -91,7 +97,7 @@ public: void onHeroVisit(const CGHeroInstance * h) const override; void battleFinished(const CGHeroInstance *hero, const BattleResult &result) const override; - void blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) const override; + void blockingDialogAnswered(const CGHeroInstance *hero, int32_t answer) const override; std::string getObjectName() const override; std::string getPopupText(PlayerColor player) const override; @@ -99,8 +105,8 @@ public: std::vector getPopupComponents(PlayerColor player) const override; void pick( const CGHeroInstance * h ) const; - void initObj(CRandomGenerator & rand) override; - void pickRandomObject(CRandomGenerator & rand) override; + void initObj(vstd::RNG & rand) override; + void pickRandomObject(vstd::RNG & rand) override; void afterAddToMap(CMap * map) override; BattleField getBattlefield() const override; @@ -129,10 +135,10 @@ public: MetaString message; void onHeroVisit(const CGHeroInstance * h) const override; - void initObj(CRandomGenerator & rand) override; - void pickRandomObject(CRandomGenerator & rand) override; + void initObj(vstd::RNG & rand) override; + void pickRandomObject(vstd::RNG & rand) override; void battleFinished(const CGHeroInstance *hero, const BattleResult &result) const override; - void blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) const override; + void blockingDialogAnswered(const CGHeroInstance *hero, int32_t answer) const override; std::string getHoverText(PlayerColor player) const override; void collectRes(const PlayerColor & player) const; @@ -148,26 +154,22 @@ protected: void serializeJsonOptions(JsonSerializeFormat & handler) override; }; -class DLL_LINKAGE CGMine : public CArmedInstance +class DLL_LINKAGE CGMine : public CArmedInstance, public IOwnableObject { public: GameResID producedResource; ui32 producedQuantity; std::set abandonedMineResources; - bool isAbandoned() const; - ResourceSet dailyIncome() const; - private: using CArmedInstance::CArmedInstance; void onHeroVisit(const CGHeroInstance * h) const override; void battleFinished(const CGHeroInstance *hero, const BattleResult &result) const override; - void blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) const override; + void blockingDialogAnswered(const CGHeroInstance *hero, int32_t answer) const override; void flagMine(const PlayerColor & player) const; - void newTurn(CRandomGenerator & rand) const override; - void initObj(CRandomGenerator & rand) override; + void initObj(vstd::RNG & rand) override; std::string getObjectName() const override; std::string getHoverText(PlayerColor player) const override; @@ -181,12 +183,17 @@ public: h & abandonedMineResources; } ui32 defaultResProduction() const; + ui32 getProducedQuantity() const; + + const IOwnableObject * asOwnable() const final; + ResourceSet dailyIncome() const override; + std::vector providedCreatures() const override; protected: void serializeJsonOptions(JsonSerializeFormat & handler) override; }; -struct DLL_LINKAGE TeleportChannel +struct DLL_LINKAGE TeleportChannel : public Serializeable { enum EPassability {UNKNOWN, IMPASSABLE, PASSABLE}; @@ -248,7 +255,7 @@ class DLL_LINKAGE CGMonolith : public CGTeleport protected: void onHeroVisit(const CGHeroInstance * h) const override; void teleportDialogAnswered(const CGHeroInstance *hero, ui32 answer, TTeleportExitsList exits) const override; - void initObj(CRandomGenerator & rand) override; + void initObj(vstd::RNG & rand) override; public: using CGTeleport::CGTeleport; @@ -262,7 +269,7 @@ public: class DLL_LINKAGE CGSubterraneanGate : public CGMonolith { void onHeroVisit(const CGHeroInstance * h) const override; - void initObj(CRandomGenerator & rand) override; + void initObj(vstd::RNG & rand) override; public: using CGMonolith::CGMonolith; @@ -297,7 +304,7 @@ public: void onHeroVisit(const CGHeroInstance * h) const override; std::string getHoverText(const CGHeroInstance * hero) const override; - void initObj(CRandomGenerator & rand) override; + void initObj(vstd::RNG & rand) override; template void serialize(Handler &h) { @@ -339,7 +346,7 @@ public: } }; -class DLL_LINKAGE CGShipyard : public CGObjectInstance, public IShipyard +class DLL_LINKAGE CGShipyard : public CGObjectInstance, public IShipyard, public IOwnableObject { friend class ShipyardInstanceConstructor; @@ -351,6 +358,10 @@ protected: const IObjectInterface * getObject() const override; BoatId getBoatType() const override; + const IOwnableObject * asOwnable() const final; + ResourceSet dailyIncome() const override; + std::vector providedCreatures() const override; + public: using CGObjectInstance::CGObjectInstance; @@ -369,7 +380,7 @@ class DLL_LINKAGE CGMagi : public CGObjectInstance public: using CGObjectInstance::CGObjectInstance; - void initObj(CRandomGenerator & rand) override; + void initObj(vstd::RNG & rand) override; void onHeroVisit(const CGHeroInstance * h) const override; template void serialize(Handler &h) @@ -391,7 +402,7 @@ public: using CTeamVisited::CTeamVisited; void onHeroVisit(const CGHeroInstance * h) const override; - void initObj(CRandomGenerator & rand) override; + void initObj(vstd::RNG & rand) override; std::string getHoverText(PlayerColor player) const override; template void serialize(Handler &h) @@ -402,24 +413,6 @@ protected: void setPropertyDer(ObjProperty what, ObjPropertyID identifier) override; }; -class DLL_LINKAGE CGLighthouse : public CGObjectInstance -{ -public: - using CGObjectInstance::CGObjectInstance; - - void onHeroVisit(const CGHeroInstance * h) const override; - void initObj(CRandomGenerator & rand) 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: @@ -444,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 3b0e447f1..68c6a6178 100644 --- a/lib/mapObjects/ObjectTemplate.cpp +++ b/lib/mapObjects/ObjectTemplate.cpp @@ -15,7 +15,7 @@ #include "../VCMI_Lib.h" #include "../GameConstants.h" #include "../constants/StringConstants.h" -#include "../CGeneralTextHandler.h" +#include "../texts/CLegacyConfigParser.h" #include "../TerrainHandler.h" #include "../mapObjectConstructors/CRewardableConstructor.h" @@ -59,54 +59,6 @@ ObjectTemplate::ObjectTemplate(): { } -ObjectTemplate::ObjectTemplate(const ObjectTemplate& other): - visitDir(other.visitDir), - allowedTerrains(other.allowedTerrains), - id(other.id), - subid(other.subid), - printPriority(other.printPriority), - animationFile(other.animationFile), - editorAnimationFile(other.editorAnimationFile), - stringID(other.stringID), - width(other.width), - height(other.height), - visitable(other.visitable), - blockedOffsets(other.blockedOffsets), - blockMapOffset(other.blockMapOffset), - visitableOffset(other.visitableOffset) -{ - //default copy constructor is failing with usedTiles this for unknown reason - - usedTiles.resize(other.usedTiles.size()); - for(size_t i = 0; i < usedTiles.size(); i++) - std::copy(other.usedTiles[i].begin(), other.usedTiles[i].end(), std::back_inserter(usedTiles[i])); -} - -ObjectTemplate & ObjectTemplate::operator=(const ObjectTemplate & rhs) -{ - visitDir = rhs.visitDir; - allowedTerrains = rhs.allowedTerrains; - id = rhs.id; - subid = rhs.subid; - printPriority = rhs.printPriority; - animationFile = rhs.animationFile; - editorAnimationFile = rhs.editorAnimationFile; - stringID = rhs.stringID; - width = rhs.width; - height = rhs.height; - visitable = rhs.visitable; - blockedOffsets = rhs.blockedOffsets; - blockMapOffset = rhs.blockMapOffset; - visitableOffset = rhs.visitableOffset; - - usedTiles.clear(); - usedTiles.resize(rhs.usedTiles.size()); - for(size_t i = 0; i < usedTiles.size(); i++) - std::copy(rhs.usedTiles[i].begin(), rhs.usedTiles[i].end(), std::back_inserter(usedTiles[i])); - - return *this; -} - void ObjectTemplate::afterLoadFixup() { if(id == Obj::EVENT) @@ -556,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 0ceb409a2..584750f48 100644 --- a/lib/mapObjects/ObjectTemplate.h +++ b/lib/mapObjects/ObjectTemplate.h @@ -12,6 +12,8 @@ #include "../GameConstants.h" #include "../int3.h" #include "../filesystem/ResourcePath.h" +#include "../serializer/Serializeable.h" +#include "../mapObjects/CompoundMapObjectID.h" VCMI_LIB_NAMESPACE_BEGIN @@ -20,7 +22,7 @@ class CLegacyConfigParser; class JsonNode; class int3; -class DLL_LINKAGE ObjectTemplate +class DLL_LINKAGE ObjectTemplate : public Serializeable { enum EBlockMapBits { @@ -45,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 @@ -121,11 +124,9 @@ public: // Checks if object can be placed on specific terrain bool canBePlacedAt(TerrainId terrain) const; - ObjectTemplate(); - //custom copy constructor is required - ObjectTemplate(const ObjectTemplate & other); + CompoundMapObjectID getCompoundID() const; - ObjectTemplate& operator=(const ObjectTemplate & rhs); + ObjectTemplate(); void readTxt(CLegacyConfigParser & parser); void readMsk(); diff --git a/lib/mapObjects/ObstacleSetHandler.cpp b/lib/mapObjects/ObstacleSetHandler.cpp index 427ff59df..4da7cac88 100644 --- a/lib/mapObjects/ObstacleSetHandler.cpp +++ b/lib/mapObjects/ObstacleSetHandler.cpp @@ -14,20 +14,21 @@ #include "../modding/IdentifierStorage.h" #include "../constants/StringConstants.h" #include "../TerrainHandler.h" +#include "../VCMI_Lib.h" VCMI_LIB_NAMESPACE_BEGIN ObstacleSet::ObstacleSet(): type(INVALID), - allowedTerrains({TerrainId::NONE}), - level(EMapLevel::ANY) + level(EMapLevel::ANY), + allowedTerrains({TerrainId::NONE}) { } ObstacleSet::ObstacleSet(EObstacleType type, TerrainId terrain): type(type), - allowedTerrains({terrain}), - level(EMapLevel::ANY) + level(EMapLevel::ANY), + allowedTerrains({terrain}) { } @@ -42,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; @@ -51,27 +52,27 @@ void ObstacleSet::removeEmptyTemplates() ObstacleSetFilter::ObstacleSetFilter(std::vector allowedTypes, TerrainId terrain = TerrainId::ANY_TERRAIN, - ObstacleSet::EMapLevel level = ObstacleSet::EMapLevel::ANY, + EMapLevel level = EMapLevel::ANY, FactionID faction = FactionID::ANY, EAlignment alignment = EAlignment::ANY): allowedTypes(allowedTypes), - terrain(terrain), - level(level), faction(faction), - alignment(alignment) + alignment(alignment), + terrain(terrain), + level(level) { } ObstacleSetFilter::ObstacleSetFilter(ObstacleSet::EObstacleType allowedType, TerrainId terrain = TerrainId::ANY_TERRAIN, - ObstacleSet::EMapLevel level = ObstacleSet::EMapLevel::ANY, + EMapLevel level = EMapLevel::ANY, FactionID faction = FactionID::ANY, EAlignment alignment = EAlignment::ANY): allowedTypes({allowedType}), - terrain(terrain), - level(level), faction(faction), - alignment(alignment) + alignment(alignment), + terrain(terrain), + level(level) { } @@ -82,7 +83,7 @@ bool ObstacleSetFilter::filter(const ObstacleSet &set) const return false; } - if (level != ObstacleSet::EMapLevel::ANY && set.getLevel() != ObstacleSet::EMapLevel::ANY) + if (level != EMapLevel::ANY && set.getLevel() != EMapLevel::ANY) { if (level != set.getLevel()) { @@ -137,12 +138,12 @@ void ObstacleSet::addTerrain(TerrainId terrain) this->allowedTerrains.insert(terrain); } -ObstacleSet::EMapLevel ObstacleSet::getLevel() const +EMapLevel ObstacleSet::getLevel() const { return level; } -void ObstacleSet::setLevel(ObstacleSet::EMapLevel newLevel) +void ObstacleSet::setLevel(EMapLevel newLevel) { level = newLevel; } @@ -172,9 +173,9 @@ ObstacleSet::EObstacleType ObstacleSet::getType() const return type; } -void ObstacleSet::setType(EObstacleType type) +void ObstacleSet::setType(EObstacleType newType) { - this->type = type; + type = newType; } std::vector> ObstacleSet::getObstacles() const @@ -278,12 +279,12 @@ std::string ObstacleSet::toString() const return OBSTACLE_TYPE_STRINGS.at(type); } -ObstacleSet::EMapLevel ObstacleSet::levelFromString(const std::string &str) +EMapLevel ObstacleSet::levelFromString(const std::string &str) { static const std::map LEVEL_NAMES = { - {"surface", SURFACE}, - {"underground", UNDERGROUND} + {"surface", EMapLevel::SURFACE}, + {"underground", EMapLevel::UNDERGROUND} }; if (LEVEL_NAMES.find(str) != LEVEL_NAMES.end()) @@ -304,7 +305,7 @@ void ObstacleSetFilter::setType(ObstacleSet::EObstacleType type) allowedTypes = {type}; } -void ObstacleSetFilter::setTypes(std::vector types) +void ObstacleSetFilter::setTypes(const std::vector & types) { this->allowedTypes = types; } @@ -456,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 @@ -476,10 +477,9 @@ void ObstacleSetHandler::addObstacleSet(std::shared_ptr os) void ObstacleSetHandler::afterLoadFinalization() { - for (auto &os :biomes) - { + for(const auto & os : biomes) os->removeEmptyTemplates(); - } + vstd::erase_if(biomes, [](const std::shared_ptr &os) { if (os->getObstacles().empty()) @@ -491,10 +491,8 @@ void ObstacleSetHandler::afterLoadFinalization() }); // Populate map - for (auto &os : biomes) - { + for(const auto & os : biomes) obstacleSets[os->getType()].push_back(os); - } } TObstacleTypes ObstacleSetHandler::getObstacles( const ObstacleSetFilter &filter) const diff --git a/lib/mapObjects/ObstacleSetHandler.h b/lib/mapObjects/ObstacleSetHandler.h index d127e49b0..349175175 100644 --- a/lib/mapObjects/ObstacleSetHandler.h +++ b/lib/mapObjects/ObstacleSetHandler.h @@ -29,7 +29,7 @@ public: INVALID = -1, MOUNTAINS = 0, TREES, - LAKES, // Inluding dry or lava lakes + LAKES, // Including dry or lava lakes CRATERS, // Chasms, Canyons, etc. ROCKS, PLANTS, // Flowers, cacti, mushrooms, logs, shrubs, etc. @@ -38,13 +38,6 @@ public: OTHER // Crystals, shipwrecks, barrels, etc. }; - enum EMapLevel // TODO: Move somewhere to map definitions - { - ANY = -1, - SURFACE = 0, - UNDERGROUND = 1 - }; - ObstacleSet(); explicit ObstacleSet(EObstacleType type, TerrainId terrain); @@ -82,18 +75,18 @@ private: std::vector> obstacles; }; -typedef std::vector> TObstacleTypes; +using TObstacleTypes = std::vector>; class DLL_LINKAGE ObstacleSetFilter { public: - ObstacleSetFilter(ObstacleSet::EObstacleType allowedType, TerrainId terrain, ObstacleSet::EMapLevel level, FactionID faction, EAlignment alignment); - ObstacleSetFilter(std::vector allowedTypes, TerrainId terrain, ObstacleSet::EMapLevel level, FactionID faction, EAlignment alignment); + ObstacleSetFilter(ObstacleSet::EObstacleType allowedType, TerrainId terrain, EMapLevel level, FactionID faction, EAlignment alignment); + ObstacleSetFilter(std::vector allowedTypes, TerrainId terrain, EMapLevel level, FactionID faction, EAlignment alignment); bool filter(const ObstacleSet &set) const; void setType(ObstacleSet::EObstacleType type); - void setTypes(std::vector types); + void setTypes(const std::vector & types); std::vector getAllowedTypes() const; TerrainId getTerrain() const; @@ -105,7 +98,7 @@ private: EAlignment alignment; // TODO: Filter by faction, surface/underground, etc. const TerrainId terrain; - ObstacleSet::EMapLevel level; + EMapLevel level; }; // TODO: Instantiate ObstacleSetHandler @@ -117,8 +110,8 @@ public: ~ObstacleSetHandler() = default; std::vector loadLegacyData() override; - virtual void loadObject(std::string scope, std::string name, const JsonNode & data) override; - virtual void loadObject(std::string scope, std::string name, const JsonNode & data, size_t index) 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; std::shared_ptr loadFromJson(const std::string & scope, const JsonNode & json, const std::string & name, size_t index); ObstacleSet::EObstacleType convertObstacleClass(MapObjectID id); @@ -143,4 +136,4 @@ private: std::map>> obstacleSets; }; -VCMI_LIB_NAMESPACE_END \ No newline at end of file +VCMI_LIB_NAMESPACE_END diff --git a/lib/mapObjects/TownBuildingInstance.cpp b/lib/mapObjects/TownBuildingInstance.cpp new file mode 100644 index 000000000..8b4a40e5c --- /dev/null +++ b/lib/mapObjects/TownBuildingInstance.cpp @@ -0,0 +1,218 @@ +/* + * TownBuildingInstance.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 "TownBuildingInstance.h" + +#include "CGTownInstance.h" +#include "../IGameCallback.h" +#include "../mapObjects/CGHeroInstance.h" +#include "../entities/building/CBuilding.h" + +#include + +VCMI_LIB_NAMESPACE_BEGIN + +TownBuildingInstance::TownBuildingInstance(IGameCallback * cb) + : IObjectInterface(cb) + , town(nullptr) +{} + +TownBuildingInstance::TownBuildingInstance(CGTownInstance * town, const BuildingID & index) + : IObjectInterface(town->cb) + , town(town) + , bID(index) +{} + +PlayerColor TownBuildingInstance::getOwner() const +{ + return town->getOwner(); +} + +MapObjectID TownBuildingInstance::getObjGroupIndex() const +{ + return -1; +} + +MapObjectSubID TownBuildingInstance::getObjTypeIndex() const +{ + return 0; +} + +const IOwnableObject * TownBuildingInstance::asOwnable() const +{ + return nullptr; +} + +int3 TownBuildingInstance::visitablePos() const +{ + return town->visitablePos(); +} + +int3 TownBuildingInstance::anchorPos() const +{ + return town->anchorPos(); +} + +TownRewardableBuildingInstance::TownRewardableBuildingInstance(IGameCallback *cb) + : TownBuildingInstance(cb) +{} + +TownRewardableBuildingInstance::TownRewardableBuildingInstance(CGTownInstance * town, const BuildingID & index, vstd::RNG & rand) + : TownBuildingInstance(town, index) +{ + initObj(rand); +} + +void TownRewardableBuildingInstance::initObj(vstd::RNG & rand) +{ + assert(town && town->getTown()); + configuration = generateConfiguration(rand); +} + +Rewardable::Configuration TownRewardableBuildingInstance::generateConfiguration(vstd::RNG & rand) const +{ + Rewardable::Configuration result; + auto building = town->getTown()->buildings.at(getBuildingType()); + + building->rewardableObjectInfo.configureObject(result, rand, cb); + for(auto & rewardInfo : result.info) + { + for (auto & bonus : rewardInfo.reward.bonuses) + { + bonus.source = BonusSource::TOWN_STRUCTURE; + bonus.sid = BonusSourceID(building->getUniqueTypeID()); + } + } + return result; +} + +void TownRewardableBuildingInstance::newTurn(vstd::RNG & rand) const +{ + if (configuration.resetParameters.period != 0 && cb->getDate(Date::DAY) > 1 && ((cb->getDate(Date::DAY)-1) % configuration.resetParameters.period) == 0) + { + auto newConfiguration = generateConfiguration(rand); + cb->setRewardableObjectConfiguration(town->id, getBuildingType(), newConfiguration); + + if(configuration.resetParameters.visitors) + { + cb->setObjPropertyValue(town->id, ObjProperty::STRUCTURE_CLEAR_VISITORS, getBuildingType()); + } + } +} + +void TownRewardableBuildingInstance::setProperty(ObjProperty what, ObjPropertyID identifier) +{ + switch (what) + { + case ObjProperty::VISITORS: + visitors.insert(identifier.as()); + break; + case ObjProperty::STRUCTURE_CLEAR_VISITORS: + visitors.clear(); + break; + case ObjProperty::REWARD_SELECT: + selectedReward = identifier.getNum(); + break; + } +} + +void TownRewardableBuildingInstance::heroLevelUpDone(const CGHeroInstance *hero) const +{ + grantRewardAfterLevelup(configuration.info.at(selectedReward), town, hero); +} + +void TownRewardableBuildingInstance::blockingDialogAnswered(const CGHeroInstance *hero, int32_t answer) const +{ + onBlockingDialogAnswered(hero, answer); +} + +void TownRewardableBuildingInstance::grantReward(ui32 rewardID, const CGHeroInstance * hero) const +{ + grantRewardBeforeLevelup(configuration.info.at(rewardID), hero); + + // hero is not blocked by levelup dialog - grant remainder immediately + if(!cb->isVisitCoveredByAnotherQuery(town, hero)) + { + grantRewardAfterLevelup(configuration.info.at(rewardID), town, hero); + } +} + +bool TownRewardableBuildingInstance::wasVisited(const CGHeroInstance * contextHero) const +{ + return wasVisitedBefore(contextHero); +} + +bool TownRewardableBuildingInstance::wasVisitedBefore(const CGHeroInstance * contextHero) const +{ + switch (configuration.visitMode) + { + case Rewardable::VISIT_UNLIMITED: + return false; + case Rewardable::VISIT_ONCE: + return !visitors.empty(); + case Rewardable::VISIT_PLAYER: + return false; //not supported + case Rewardable::VISIT_BONUS: + { + const auto building = town->getTown()->buildings.at(getBuildingType()); + return contextHero->hasBonusFrom(BonusSource::TOWN_STRUCTURE, BonusSourceID(building->getUniqueTypeID())); + } + case Rewardable::VISIT_HERO: + return visitors.find(contextHero->id) != visitors.end(); + case Rewardable::VISIT_LIMITER: + return configuration.visitLimiter.heroAllowed(contextHero); + default: + return false; + } +} + +void TownRewardableBuildingInstance::onHeroVisit(const CGHeroInstance *h) const +{ + 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) + { + 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; + } +} + +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 +} + + +VCMI_LIB_NAMESPACE_END diff --git a/lib/mapObjects/TownBuildingInstance.h b/lib/mapObjects/TownBuildingInstance.h new file mode 100644 index 000000000..015421448 --- /dev/null +++ b/lib/mapObjects/TownBuildingInstance.h @@ -0,0 +1,101 @@ +/* + * TownBuildingInstance.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 "IObjectInterface.h" +#include "../rewardable/Interface.h" + +VCMI_LIB_NAMESPACE_BEGIN + +class CGTownInstance; +class CBuilding; + +class DLL_LINKAGE TownBuildingInstance : public IObjectInterface +{ +///basic class for town structures handled as map objects +public: + TownBuildingInstance(CGTownInstance * town, const BuildingID & index); + TownBuildingInstance(IGameCallback *cb); + + CGTownInstance * town; + + const BuildingID & getBuildingType() const + { + return bID; + } + + PlayerColor getOwner() const override; + MapObjectID getObjGroupIndex() const override; + MapObjectSubID getObjTypeIndex() const override; + const IOwnableObject * asOwnable() const override; + + int3 visitablePos() const override; + int3 anchorPos() const override; + + template void serialize(Handler &h) + { + h & bID; + if (h.version < Handler::Version::NEW_TOWN_BUILDINGS) + { + // compatibility code + si32 indexOnTV = 0; //identifies its index on towns vector + BuildingSubID::EBuildingSubID bType = BuildingSubID::NONE; + h & indexOnTV; + h & bType; + } + } + +private: + BuildingID bID; //from building list +}; + +class DLL_LINKAGE TownRewardableBuildingInstance : public TownBuildingInstance, public Rewardable::Interface +{ + /// reward selected by player, no serialize + ui16 selectedReward = 0; + std::set visitors; + + 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; + bool wasVisited(const CGHeroInstance * contextHero) const override; + + void newTurn(vstd::RNG & rand) const override; + + /// gives second part of reward after hero level-ups for proper granting of spells/mana + void heroLevelUpDone(const CGHeroInstance *hero) const override; + + void initObj(vstd::RNG & rand) override; + + /// applies player selection of reward + void blockingDialogAnswered(const CGHeroInstance *hero, int32_t answer) const override; + + TownRewardableBuildingInstance(CGTownInstance * town, const BuildingID & index, vstd::RNG & rand); + TownRewardableBuildingInstance(IGameCallback *cb); + + template void serialize(Handler &h) + { + h & static_cast(*this); + if (h.version >= Handler::Version::NEW_TOWN_BUILDINGS) + h & static_cast(*this); + h & visitors; + } +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/mapping/CDrawRoadsOperation.cpp b/lib/mapping/CDrawRoadsOperation.cpp index 9ea49db26..b3df0efa1 100644 --- a/lib/mapping/CDrawRoadsOperation.cpp +++ b/lib/mapping/CDrawRoadsOperation.cpp @@ -12,9 +12,11 @@ #include "CDrawRoadsOperation.h" #include "CMap.h" -#include "../CRandomGenerator.h" #include "../RoadHandler.h" #include "../RiverHandler.h" +#include "../VCMI_Lib.h" + +#include VCMI_LIB_NAMESPACE_BEGIN @@ -154,7 +156,7 @@ static bool ruleIsAny(const std::string & rule) #endif ///CDrawLinesOperation -CDrawLinesOperation::CDrawLinesOperation(CMap * map, CTerrainSelection terrainSel, CRandomGenerator * gen): +CDrawLinesOperation::CDrawLinesOperation(CMap * map, CTerrainSelection terrainSel, vstd::RNG * gen): CMapOperation(map), terrainSel(std::move(terrainSel)), gen(gen) @@ -162,14 +164,14 @@ CDrawLinesOperation::CDrawLinesOperation(CMap * map, CTerrainSelection terrainSe } ///CDrawRoadsOperation -CDrawRoadsOperation::CDrawRoadsOperation(CMap * map, const CTerrainSelection & terrainSel, RoadId roadType, CRandomGenerator * gen): +CDrawRoadsOperation::CDrawRoadsOperation(CMap * map, const CTerrainSelection & terrainSel, RoadId roadType, vstd::RNG * gen): CDrawLinesOperation(map, terrainSel,gen), roadType(roadType) { } ///CDrawRiversOperation -CDrawRiversOperation::CDrawRiversOperation(CMap * map, const CTerrainSelection & terrainSel, RiverId riverType, CRandomGenerator * gen): +CDrawRiversOperation::CDrawRiversOperation(CMap * map, const CTerrainSelection & terrainSel, RiverId riverType, vstd::RNG * gen): CDrawLinesOperation(map, terrainSel, gen), riverType(riverType) { @@ -342,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 @@ -362,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/CDrawRoadsOperation.h b/lib/mapping/CDrawRoadsOperation.h index 16e2ad070..7e9ea754d 100644 --- a/lib/mapping/CDrawRoadsOperation.h +++ b/lib/mapping/CDrawRoadsOperation.h @@ -41,7 +41,7 @@ protected: int flip; }; - CDrawLinesOperation(CMap * map, CTerrainSelection terrainSel, CRandomGenerator * gen); + CDrawLinesOperation(CMap * map, CTerrainSelection terrainSel, vstd::RNG * gen); virtual void executeTile(TerrainTile & tile) = 0; virtual bool canApplyPattern(const CDrawLinesOperation::LinePattern & pattern) const = 0; @@ -58,13 +58,13 @@ protected: ValidationResult validateTile(const LinePattern & pattern, const int3 & pos); CTerrainSelection terrainSel; - CRandomGenerator * gen; + vstd::RNG * gen; }; class CDrawRoadsOperation : public CDrawLinesOperation { public: - CDrawRoadsOperation(CMap * map, const CTerrainSelection & terrainSel, RoadId roadType, CRandomGenerator * gen); + CDrawRoadsOperation(CMap * map, const CTerrainSelection & terrainSel, RoadId roadType, vstd::RNG * gen); std::string getLabel() const override; protected: @@ -81,7 +81,7 @@ private: class CDrawRiversOperation : public CDrawLinesOperation { public: - CDrawRiversOperation(CMap * map, const CTerrainSelection & terrainSel, RiverId roadType, CRandomGenerator * gen); + CDrawRiversOperation(CMap * map, const CTerrainSelection & terrainSel, RiverId roadType, vstd::RNG * gen); std::string getLabel() const override; protected: diff --git a/lib/mapping/CMap.cpp b/lib/mapping/CMap.cpp index 5c0fe16bc..3760c3bc1 100644 --- a/lib/mapping/CMap.cpp +++ b/lib/mapping/CMap.cpp @@ -13,22 +13,24 @@ #include "../CArtHandler.h" #include "../VCMI_Lib.h" #include "../CCreatureHandler.h" -#include "../CTownHandler.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" #include "../mapObjects/ObjectTemplate.h" -#include "../CGeneralTextHandler.h" +#include "../texts/CGeneralTextHandler.h" #include "../spells/CSpellHandler.h" #include "../CSkillHandler.h" #include "CMapEditManager.h" #include "CMapOperation.h" #include "../serializer/JsonSerializeFormat.h" +#include + VCMI_LIB_NAMESPACE_BEGIN void Rumor::serializeJson(JsonSerializeFormat & handler) @@ -43,35 +45,67 @@ DisposedHero::DisposedHero() : heroId(0), portrait(255) } CMapEvent::CMapEvent() - : players(0) - , humanAffected(false) + : humanAffected(false) , computerAffected(false) - , firstOccurence(0) - , nextOccurence(0) + , firstOccurrence(0) + , nextOccurrence(0) { } -bool CMapEvent::earlierThan(const CMapEvent & other) const +bool CMapEvent::occursToday(int currentDay) const { - return firstOccurence < other.firstOccurence; + if (currentDay == firstOccurrence + 1) + return true; + + if (nextOccurrence == 0) + return false; + + if (currentDay < firstOccurrence) + return false; + + return (currentDay - firstOccurrence - 1) % nextOccurrence == 0; } -bool CMapEvent::earlierThanOrEqual(const CMapEvent & other) const +bool CMapEvent::affectsPlayer(PlayerColor color, bool isHuman) const { - return firstOccurence <= other.firstOccurence; + if (players.count(color) == 0) + return false; + + if (!isHuman && !computerAffected) + return false; + + if (isHuman && !humanAffected) + return false; + + return true; } void CMapEvent::serializeJson(JsonSerializeFormat & handler) { handler.serializeString("name", name); handler.serializeStruct("message", message); - handler.serializeInt("players", players); + if (!handler.saving && handler.getCurrent()["players"].isNumber()) + { + // compatibility for old maps + int playersMask = 0; + handler.serializeInt("players", playersMask); + for (int i = 0; i < 8; ++i) + if ((playersMask & (1 << i)) != 0) + players.insert(PlayerColor(i)); + } + else + { + handler.serializeIdArray("players", players); + } handler.serializeInt("humanAffected", humanAffected); handler.serializeInt("computerAffected", computerAffected); - handler.serializeInt("firstOccurence", firstOccurence); - handler.serializeInt("nextOccurence", nextOccurence); + handler.serializeInt("firstOccurrence", firstOccurrence); + handler.serializeInt("nextOccurrence", nextOccurrence); resources.serializeJson(handler, "resources"); + + auto deletedObjects = handler.enterArray("deletedObjectsInstances"); + deletedObjects.serializeArray(deletedObjectsInstances); } void CCastleEvent::serializeJson(JsonSerializeFormat & handler) @@ -100,32 +134,29 @@ void CCastleEvent::serializeJson(JsonSerializeFormat & handler) } TerrainTile::TerrainTile(): - terType(nullptr), + riverType(River::NO_RIVER), + roadType(Road::NO_ROAD), terView(0), - riverType(VLC->riverTypeHandler->getById(River::NO_RIVER)), riverDir(0), - roadType(VLC->roadTypeHandler->getById(Road::NO_ROAD)), 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 @@ -146,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; @@ -163,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) @@ -178,6 +265,9 @@ CMap::CMap(IGameCallback * cb) allowedAbilities = VLC->skillh->getDefaultAllowed(); allowedArtifact = VLC->arth->getDefaultAllowed(); allowedSpells = VLC->spellh->getDefaultAllowed(); + + gameSettings = std::make_unique(); + gameSettings->loadBase(VLC->settingsHandler->getFullConfig()); } CMap::~CMap() @@ -198,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(); - } } } } @@ -225,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; - } } } } @@ -268,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; } @@ -302,11 +382,6 @@ bool CMap::isCoastalTile(const int3 & pos) const return false; } -bool CMap::isInTheMap(const int3 & pos) const -{ - return pos.x >= 0 && pos.y >= 0 && pos.z >= 0 && pos.x < width && pos.y < height && pos.z <= (twoLevel ? 1 : 0); -} - TerrainTile & CMap::getTile(const int3 & tile) { assert(isInTheMap(tile)); @@ -352,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) { @@ -372,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) { @@ -415,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; } @@ -494,10 +569,26 @@ void CMap::checkForObjectives() } } +void CMap::addNewArtifactInstance(CArtifactSet & artSet) +{ + for(const auto & [slot, slotInfo] : artSet.artifactsWorn) + { + if(!slotInfo.locked && slotInfo.getArt()) + addNewArtifactInstance(slotInfo.artifact); + } + for(const auto & slotInfo : artSet.artifactsInBackpack) + addNewArtifactInstance(slotInfo.artifact); +} + void CMap::addNewArtifactInstance(ConstTransitivePtr art) { + assert(art); + assert(art->getId() == -1); art->setId(static_cast(artInstances.size())); artInstances.emplace_back(art); + + for(const auto & partInfo : art->getPartsInfo()) + addNewArtifactInstance(partInfo.art); } void CMap::eraseArtifactInstance(CArtifactInstance * art) @@ -507,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()); @@ -535,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(); } @@ -562,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); } @@ -571,7 +690,7 @@ void CMap::removeObject(CGObjectInstance * obj) removeBlockVisTiles(obj); instanceNames.erase(obj->instanceName); - //update indeces + //update indices auto iter = std::next(objects.begin(), obj->id.getNum()); iter = objects.erase(iter); @@ -582,7 +701,7 @@ void CMap::removeObject(CGObjectInstance * obj) obj->afterRemoveFromMap(this); - //TOOD: Clean artifact instances (mostly worn by hero?) and quests related to this object + //TODO: Clean artifact instances (mostly worn by hero?) and quests related to this object //This causes crash with undo/redo in editor } @@ -730,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 @@ -740,4 +859,19 @@ void CMap::reindexObjects() } } +const IGameSettings & CMap::getSettings() const +{ + return *gameSettings; +} + +void CMap::overrideGameSetting(EGameSettings option, const JsonNode & input) +{ + return gameSettings->addOverride(option, input); +} + +void CMap::overrideGameSettings(const JsonNode & input) +{ + return gameSettings->loadOverrides(input); +} + VCMI_LIB_NAMESPACE_END diff --git a/lib/mapping/CMap.h b/lib/mapping/CMap.h index 5a56e984d..5192f7c1a 100644 --- a/lib/mapping/CMap.h +++ b/lib/mapping/CMap.h @@ -15,12 +15,12 @@ #include "../ConstTransitivePtr.h" #include "../GameCallbackHolder.h" -#include "../MetaString.h" #include "../networkPacks/TradeItem.h" VCMI_LIB_NAMESPACE_BEGIN class CArtifactInstance; +class CArtifactSet; class CGObjectInstance; class CGHeroInstance; class CCommanderInstance; @@ -32,7 +32,10 @@ class IQuestObject; class CInputStream; class CMapEditManager; class JsonSerializeFormat; +class IGameSettings; +class GameSettings; struct TeleportChannel; +enum class EGameSettings; /// The rumor struct consists of a rumor name and text. struct DLL_LINKAGE Rumor @@ -76,6 +79,7 @@ struct DLL_LINKAGE DisposedHero /// The map contains the map header, the tiles of the terrain, objects, heroes, towns, rumors... class DLL_LINKAGE CMap : public CMapHeader, public GameCallbackHolder { + std::unique_ptr gameSettings; public: explicit CMap(IGameCallback *cb); ~CMap(); @@ -85,8 +89,15 @@ public: TerrainTile & getTile(const int3 & tile); const TerrainTile & getTile(const int3 & tile) const; bool isCoastalTile(const int3 & pos) const; - bool isInTheMap(const int3 & pos) const; bool isWaterTile(const int3 & pos) const; + inline bool isInTheMap(const int3 & pos) const + { + // Check whether coord < 0 is done implicitly. Negative signed int overflows to unsigned number larger than all signed ints. + return + static_cast(pos.x) < static_cast(width) && + static_cast(pos.y) < static_cast(height) && + static_cast(pos.z) <= (twoLevel ? 1 : 0); + } bool canMoveBetween(const int3 &src, const int3 &dst) const; bool checkForVisitableDir(const int3 & src, const TerrainTile * pom, const int3 & dst) const; @@ -96,8 +107,12 @@ public: void removeBlockVisTiles(CGObjectInstance * obj, bool total = false); void calculateGuardingGreaturePositions(); + 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); @@ -137,7 +152,7 @@ public: std::set allowedSpells; std::set allowedArtifact; std::set allowedAbilities; - std::list events; + std::vector events; int3 grailPos; int grailRadius; @@ -165,9 +180,13 @@ 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); + void overrideGameSetting(EGameSettings option, const JsonNode & input); + const IGameSettings & getSettings() const; + private: /// a 3-dimensional array of terrain tiles, access is as follows: x, y, level. where level=1 is underground boost::multi_array terrain; @@ -190,14 +209,6 @@ public: h & quests; h & allHeroes; - if (h.version < Handler::Version::DESTROYED_OBJECTS) - { - // old save compatibility - //FIXME: remove this field after save-breaking change - h & questIdentifierToId; - resolveQuestIdentifiers(); - } - //TODO: viccondetails h & terrain; h & guardingCreaturePositions; @@ -211,10 +222,31 @@ 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; + + if (h.version >= Handler::Version::PER_MAP_GAME_SETTINGS) + h & *gameSettings; } }; diff --git a/lib/mapping/CMapDefines.h b/lib/mapping/CMapDefines.h index e1032aff7..b8da3b405 100644 --- a/lib/mapping/CMapDefines.h +++ b/lib/mapping/CMapDefines.h @@ -11,7 +11,8 @@ #pragma once #include "../ResourceSet.h" -#include "../MetaString.h" +#include "../texts/MetaString.h" +#include "../int3.h" VCMI_LIB_NAMESPACE_BEGIN @@ -30,17 +31,19 @@ public: CMapEvent(); virtual ~CMapEvent() = default; - bool earlierThan(const CMapEvent & other) const; - bool earlierThanOrEqual(const CMapEvent & other) const; + bool occursToday(int currentDay) const; + bool affectsPlayer(PlayerColor player, bool isHuman) const; std::string name; MetaString message; TResources resources; - ui8 players; // affected players, bit field? + std::set players; bool humanAffected; bool computerAffected; - ui32 firstOccurence; - ui32 nextOccurence; /// specifies after how many days the event will occur the next time; 0 if event occurs only one time + 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 deletedObjectsInstances; template void serialize(Handler & h) @@ -48,11 +51,26 @@ public: h & name; h & message; h & resources; - h & players; + if (h.version >= Handler::Version::EVENTS_PLAYER_SET) + { + h & players; + } + else + { + ui8 playersMask = 0; + h & playersMask; + for (int i = 0; i < 8; ++i) + if ((playersMask & (1 << i)) != 0) + players.insert(PlayerColor(i)); + } h & humanAffected; h & computerAffected; - h & firstOccurence; - h & nextOccurence; + h & firstOccurrence; + h & nextOccurrence; + if(h.version >= Handler::Version::EVENT_OBJECTS_DELETION) + { + h & deletedObjectsInstances; + } } virtual void serializeJson(JsonSerializeFormat & handler); @@ -93,20 +111,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; + 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; - const RiverType * riverType; ui8 riverDir; - const RoadType * roadType; 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; @@ -114,15 +145,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/CMapEditManager.cpp b/lib/mapping/CMapEditManager.cpp index f97b11125..59d897a74 100644 --- a/lib/mapping/CMapEditManager.cpp +++ b/lib/mapping/CMapEditManager.cpp @@ -15,6 +15,8 @@ #include "CDrawRoadsOperation.h" #include "CMapOperation.h" +#include + VCMI_LIB_NAMESPACE_BEGIN CMapUndoManager::CMapUndoManager() : @@ -113,34 +115,35 @@ void CMapUndoManager::setUndoCallback(std::function functor) CMapEditManager::CMapEditManager(CMap * map) : map(map), terrainSel(map), objectSel(map) { - } +CMapEditManager::~CMapEditManager() = default; + CMap * CMapEditManager::getMap() { return map; } -void CMapEditManager::clearTerrain(CRandomGenerator * gen) +void CMapEditManager::clearTerrain(vstd::RNG * customGen) { - execute(std::make_unique(map, gen ? gen : &(this->gen))); + execute(std::make_unique(map, customGen ? customGen : gen.get())); } -void CMapEditManager::drawTerrain(TerrainId terType, int decorationsPercentage, CRandomGenerator * gen) +void CMapEditManager::drawTerrain(TerrainId terType, int decorationsPercentage, vstd::RNG * customGen) { - execute(std::make_unique(map, terrainSel, terType, decorationsPercentage, gen ? gen : &(this->gen))); + execute(std::make_unique(map, terrainSel, terType, decorationsPercentage, customGen ? customGen : gen.get())); terrainSel.clearSelection(); } -void CMapEditManager::drawRoad(RoadId roadType, CRandomGenerator* gen) +void CMapEditManager::drawRoad(RoadId roadType, vstd::RNG* customGen) { - execute(std::make_unique(map, terrainSel, roadType, gen ? gen : &(this->gen))); + execute(std::make_unique(map, terrainSel, roadType, customGen ? customGen : gen.get())); terrainSel.clearSelection(); } -void CMapEditManager::drawRiver(RiverId riverType, CRandomGenerator* gen) +void CMapEditManager::drawRiver(RiverId riverType, vstd::RNG* customGen) { - execute(std::make_unique(map, terrainSel, riverType, gen ? gen : &(this->gen))); + execute(std::make_unique(map, terrainSel, riverType, customGen ? customGen : gen.get())); terrainSel.clearSelection(); } diff --git a/lib/mapping/CMapEditManager.h b/lib/mapping/CMapEditManager.h index e380db826..10f68a8ae 100644 --- a/lib/mapping/CMapEditManager.h +++ b/lib/mapping/CMapEditManager.h @@ -11,13 +11,17 @@ #pragma once #include "../GameConstants.h" -#include "../CRandomGenerator.h" #include "MapEditUtils.h" VCMI_LIB_NAMESPACE_BEGIN class CMapOperation; +namespace vstd +{ +class RNG; +} + /// The CMapUndoManager provides the functionality to save operations and undo/redo them. class DLL_LINKAGE CMapUndoManager : boost::noncopyable { @@ -64,19 +68,20 @@ class DLL_LINKAGE CMapEditManager : boost::noncopyable { public: CMapEditManager(CMap * map); + ~CMapEditManager(); CMap * getMap(); /// Clears the terrain. The free level is filled with water and the underground level with rock. - void clearTerrain(CRandomGenerator * gen = nullptr); + void clearTerrain(vstd::RNG * gen); /// Draws terrain at the current terrain selection. The selection will be cleared automatically. - void drawTerrain(TerrainId terType, int decorationsPercentage, CRandomGenerator * gen = nullptr); + void drawTerrain(TerrainId terType, int decorationsPercentage, vstd::RNG * gen); /// Draws roads at the current terrain selection. The selection will be cleared automatically. - void drawRoad(RoadId roadType, CRandomGenerator * gen = nullptr); + void drawRoad(RoadId roadType, vstd::RNG * gen); /// Draws rivers at the current terrain selection. The selection will be cleared automatically. - void drawRiver(RiverId riverType, CRandomGenerator * gen = nullptr); + void drawRiver(RiverId riverType, vstd::RNG * gen); void insertObject(CGObjectInstance * obj); void insertObjects(std::set & objects); @@ -94,7 +99,7 @@ private: CMap * map; CMapUndoManager undoManager; - CRandomGenerator gen; + std::unique_ptr gen; CTerrainSelection terrainSel; CObjectSelection objectSel; }; diff --git a/lib/mapping/CMapHeader.cpp b/lib/mapping/CMapHeader.cpp index 74ec81358..66ed48dbd 100644 --- a/lib/mapping/CMapHeader.cpp +++ b/lib/mapping/CMapHeader.cpp @@ -13,12 +13,12 @@ #include "MapFormat.h" #include "../VCMI_Lib.h" -#include "../CTownHandler.h" -#include "../CGeneralTextHandler.h" +#include "../entities/faction/CTownHandler.h" +#include "../entities/hero/CHeroHandler.h" #include "../json/JsonUtils.h" #include "../modding/CModHandler.h" -#include "../CHeroHandler.h" -#include "../Languages.h" +#include "../texts/CGeneralTextHandler.h" +#include "../texts/Languages.h" VCMI_LIB_NAMESPACE_BEGIN @@ -163,7 +163,7 @@ void CMapHeader::registerMapStrings() std::string baseLanguage; std::string language; - //english is preferrable as base language + //english is preferable as base language if(mapBaseLanguages.count(Languages::getLanguageOptions(Languages::ELanguages::ENGLISH).identifier)) baseLanguage = Languages::getLanguageOptions(Languages::ELanguages::ENGLISH).identifier; else @@ -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/CMapHeader.h b/lib/mapping/CMapHeader.h index 84920b9ab..f58826134 100644 --- a/lib/mapping/CMapHeader.h +++ b/lib/mapping/CMapHeader.h @@ -13,11 +13,12 @@ #include "../constants/EntityIdentifiers.h" #include "../constants/Enumerations.h" #include "../constants/VariantIdentifier.h" -#include "../modding/CModInfo.h" +#include "../modding/ModVerificationInfo.h" +#include "../serializer/Serializeable.h" #include "../LogicalExpression.h" #include "../int3.h" -#include "../MetaString.h" -#include "../CGeneralTextHandler.h" +#include "../texts/MetaString.h" +#include "../texts/TextLocalizationContainer.h" VCMI_LIB_NAMESPACE_BEGIN @@ -202,7 +203,7 @@ enum class EMapDifficulty : uint8_t }; /// The map header holds information about loss/victory condition,map format, version, players, height, width,... -class DLL_LINKAGE CMapHeader +class DLL_LINKAGE CMapHeader: public Serializeable { void setupEvents(); public: @@ -229,6 +230,10 @@ public: MetaString name; MetaString description; EMapDifficulty difficulty; + MetaString author; + MetaString authorContact; + MetaString mapVersion; + std::time_t creationDateTime; /// Specifies the maximum level to reach for a hero. A value of 0 states that there is no /// maximum level for heroes. This is the default value. ui8 levelLimit; @@ -241,7 +246,7 @@ public: std::vector players; /// The default size of the vector is PlayerColor::PLAYER_LIMIT. ui8 howManyTeams; std::set allowedHeroes; - std::set reservedCampaignHeroes; /// Heroes that have placeholders in this map and are reserverd for campaign + std::set reservedCampaignHeroes; /// Heroes that have placeholders in this map and are reserved for campaign bool areAnyPlayers; /// Unused. True if there are any playable players on the map. @@ -262,15 +267,26 @@ public: h & mods; h & name; h & description; + if (h.version >= Handler::Version::MAP_FORMAT_ADDITIONAL_INFOS) + { + h & author; + h & authorContact; + h & mapVersion; + h & creationDateTime; + } h & width; h & height; h & twoLevel; - // FIXME: we should serialize enum's according to their underlying type - // should be fixed when we are making breaking change to save compatiblity - static_assert(Handler::Version::MINIMAL < Handler::Version::RELEASE_143); - uint8_t difficultyInteger = static_cast(difficulty); - h & difficultyInteger; - difficulty = static_cast(difficultyInteger); + + if (h.version >= Handler::Version::SAVE_COMPATIBILITY_FIXES) + h & difficulty; + else + { + uint8_t difficultyInteger = static_cast(difficulty); + h & difficultyInteger; + difficulty = static_cast(difficultyInteger); + } + h & levelLimit; h & areAnyPlayers; h & players; diff --git a/lib/mapping/CMapInfo.cpp b/lib/mapping/CMapInfo.cpp index 459cc4120..a13084ac9 100644 --- a/lib/mapping/CMapInfo.cpp +++ b/lib/mapping/CMapInfo.cpp @@ -19,14 +19,12 @@ #include "../campaign/CampaignHandler.h" #include "../filesystem/Filesystem.h" -#include "../serializer/CLoadFile.h" -#include "../CGeneralTextHandler.h" -#include "../TextOperations.h" #include "../rmg/CMapGenOptions.h" +#include "../serializer/CLoadFile.h" +#include "../texts/CGeneralTextHandler.h" +#include "../texts/TextOperations.h" #include "../CCreatureHandler.h" -#include "../GameSettings.h" -#include "../CHeroHandler.h" -#include "../Languages.h" +#include "../IGameSettings.h" #include "../CConfigHandler.h" VCMI_LIB_NAMESPACE_BEGIN @@ -168,17 +166,19 @@ int CMapInfo::getMapSizeFormatIconId() const switch(mapHeader->version) { case EMapFormat::ROE: - return VLC->settings()->getValue(EGameSettings::MAP_FORMAT_RESTORATION_OF_ERATHIA)["iconIndex"].Integer(); + return VLC->engineSettings()->getValue(EGameSettings::MAP_FORMAT_RESTORATION_OF_ERATHIA)["iconIndex"].Integer(); case EMapFormat::AB: - return VLC->settings()->getValue(EGameSettings::MAP_FORMAT_ARMAGEDDONS_BLADE)["iconIndex"].Integer(); + return VLC->engineSettings()->getValue(EGameSettings::MAP_FORMAT_ARMAGEDDONS_BLADE)["iconIndex"].Integer(); case EMapFormat::SOD: - return VLC->settings()->getValue(EGameSettings::MAP_FORMAT_SHADOW_OF_DEATH)["iconIndex"].Integer(); + return VLC->engineSettings()->getValue(EGameSettings::MAP_FORMAT_SHADOW_OF_DEATH)["iconIndex"].Integer(); + case EMapFormat::CHR: + return VLC->engineSettings()->getValue(EGameSettings::MAP_FORMAT_CHRONICLES)["iconIndex"].Integer(); case EMapFormat::WOG: - return VLC->settings()->getValue(EGameSettings::MAP_FORMAT_IN_THE_WAKE_OF_GODS)["iconIndex"].Integer(); + return VLC->engineSettings()->getValue(EGameSettings::MAP_FORMAT_IN_THE_WAKE_OF_GODS)["iconIndex"].Integer(); case EMapFormat::HOTA: - return VLC->settings()->getValue(EGameSettings::MAP_FORMAT_HORN_OF_THE_ABYSS)["iconIndex"].Integer(); + return VLC->engineSettings()->getValue(EGameSettings::MAP_FORMAT_HORN_OF_THE_ABYSS)["iconIndex"].Integer(); case EMapFormat::VCMI: - return VLC->settings()->getValue(EGameSettings::MAP_FORMAT_JSON_VCMI)["iconIndex"].Integer(); + return VLC->engineSettings()->getValue(EGameSettings::MAP_FORMAT_JSON_VCMI)["iconIndex"].Integer(); } return 0; } diff --git a/lib/mapping/CMapInfo.h b/lib/mapping/CMapInfo.h index 40e05baee..517670c05 100644 --- a/lib/mapping/CMapInfo.h +++ b/lib/mapping/CMapInfo.h @@ -9,6 +9,8 @@ */ #pragma once +#include "../serializer/Serializeable.h" + VCMI_LIB_NAMESPACE_BEGIN struct StartInfo; @@ -21,7 +23,7 @@ class ResourcePath; * A class which stores the count of human players and all players, the filename, * scenario options, the map header information,... */ -class DLL_LINKAGE CMapInfo +class DLL_LINKAGE CMapInfo : public Serializeable { public: std::unique_ptr mapHeader; //may be nullptr if campaign diff --git a/lib/mapping/CMapOperation.cpp b/lib/mapping/CMapOperation.cpp index 7c8e76c7c..88a3100e5 100644 --- a/lib/mapping/CMapOperation.cpp +++ b/lib/mapping/CMapOperation.cpp @@ -12,12 +12,13 @@ #include "CMapOperation.h" #include "../VCMI_Lib.h" -#include "../CRandomGenerator.h" #include "../TerrainHandler.h" #include "../mapObjects/CGObjectInstance.h" #include "CMap.h" #include "MapEditUtils.h" +#include + VCMI_LIB_NAMESPACE_BEGIN CMapOperation::CMapOperation(CMap* map) : map(map) @@ -87,7 +88,7 @@ void CComposedOperation::addOperation(std::unique_ptr&& operation operations.push_back(std::move(operation)); } -CDrawTerrainOperation::CDrawTerrainOperation(CMap * map, CTerrainSelection terrainSel, TerrainId terType, int decorationsPercentage, CRandomGenerator * gen): +CDrawTerrainOperation::CDrawTerrainOperation(CMap * map, CTerrainSelection terrainSel, TerrainId terType, int decorationsPercentage, vstd::RNG * gen): CMapOperation(map), terrainSel(std::move(terrainSel)), terType(terType), @@ -102,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); } @@ -136,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); @@ -160,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()); @@ -220,7 +221,7 @@ void CDrawTerrainOperation::updateTerrainTypes() suitableTiles.insert(posToTest); } - terrainTile.terType = formerTerType; + terrainTile.terrainType = formerTerType; } }); @@ -263,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; @@ -339,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; @@ -371,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; @@ -508,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 @@ -560,7 +561,7 @@ CDrawTerrainOperation::ValidationResult::ValidationResult(bool result, std::stri } -CClearTerrainOperation::CClearTerrainOperation(CMap* map, CRandomGenerator* gen) : CComposedOperation(map) +CClearTerrainOperation::CClearTerrainOperation(CMap* map, vstd::RNG* gen) : CComposedOperation(map) { CTerrainSelection terrainSel(map); terrainSel.selectRange(MapRect(int3(0, 0, 0), map->width, map->height)); @@ -614,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/CMapOperation.h b/lib/mapping/CMapOperation.h index a9c36fe27..b081c8b58 100644 --- a/lib/mapping/CMapOperation.h +++ b/lib/mapping/CMapOperation.h @@ -17,7 +17,11 @@ VCMI_LIB_NAMESPACE_BEGIN class CGObjectInstance; class CMap; -class CRandomGenerator; + +namespace vstd +{ +class RNG; +} /// The abstract base class CMapOperation defines an operation that can be executed, undone and redone. class DLL_LINKAGE CMapOperation : public boost::noncopyable @@ -63,7 +67,7 @@ private: class CDrawTerrainOperation : public CMapOperation { public: - CDrawTerrainOperation(CMap * map, CTerrainSelection terrainSel, TerrainId terType, int decorationsPercentage, CRandomGenerator * gen); + CDrawTerrainOperation(CMap * map, CTerrainSelection terrainSel, TerrainId terType, int decorationsPercentage, vstd::RNG * gen); void execute() override; void undo() override; @@ -103,7 +107,7 @@ private: CTerrainSelection terrainSel; TerrainId terType; int decorationsPercentage; - CRandomGenerator* gen; + vstd::RNG* gen; std::set invalidatedTerViews; }; @@ -111,7 +115,7 @@ private: class CClearTerrainOperation : public CComposedOperation { public: - CClearTerrainOperation(CMap * map, CRandomGenerator * gen); + CClearTerrainOperation(CMap * map, vstd::RNG * gen); std::string getLabel() const override; }; diff --git a/lib/mapping/CMapService.cpp b/lib/mapping/CMapService.cpp index cf70f7b08..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 "../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; @@ -157,6 +154,7 @@ std::unique_ptr CMapService::getMapLoader(std::unique_ptr(EMapFormat::AB) : case static_cast(EMapFormat::ROE) : case static_cast(EMapFormat::SOD) : + case static_cast(EMapFormat::CHR) : case static_cast(EMapFormat::HOTA) : return std::unique_ptr(new CMapLoaderH3M(mapName, modName, encoding, stream.get())); default : diff --git a/lib/mapping/MapEditUtils.cpp b/lib/mapping/MapEditUtils.cpp index d0691e082..9a4588727 100644 --- a/lib/mapping/MapEditUtils.cpp +++ b/lib/mapping/MapEditUtils.cpp @@ -13,6 +13,7 @@ #include "../filesystem/Filesystem.h" #include "../TerrainHandler.h" +#include "../VCMI_Lib.h" #include "CMap.h" #include "CMapOperation.h" @@ -355,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/MapFeaturesH3M.cpp b/lib/mapping/MapFeaturesH3M.cpp index 99bda2b09..df4eb6c4c 100644 --- a/lib/mapping/MapFeaturesH3M.cpp +++ b/lib/mapping/MapFeaturesH3M.cpp @@ -25,6 +25,8 @@ MapFormatFeaturesH3M MapFormatFeaturesH3M::find(EMapFormat format, uint32_t hota return getFeaturesAB(); case EMapFormat::SOD: return getFeaturesSOD(); + case EMapFormat::CHR: + return getFeaturesCHR(); case EMapFormat::WOG: return getFeaturesWOG(); case EMapFormat::HOTA: @@ -107,6 +109,16 @@ MapFormatFeaturesH3M MapFormatFeaturesH3M::getFeaturesSOD() return result; } +MapFormatFeaturesH3M MapFormatFeaturesH3M::getFeaturesCHR() +{ + MapFormatFeaturesH3M result = getFeaturesSOD(); + result.levelCHR = true; + + result.heroesPortraitsCount = 169; // +6x tarnum + + return result; +} + MapFormatFeaturesH3M MapFormatFeaturesH3M::getFeaturesWOG() { MapFormatFeaturesH3M result = getFeaturesSOD(); @@ -118,7 +130,7 @@ MapFormatFeaturesH3M MapFormatFeaturesH3M::getFeaturesWOG() MapFormatFeaturesH3M MapFormatFeaturesH3M::getFeaturesHOTA(uint32_t hotaVersion) { // even if changes are minimal, we might not be able to parse map header in map selection screen - // throw exception - to be catched by map selection screen & excluded as invalid + // throw exception - to be caught by map selection screen & excluded as invalid if(hotaVersion > 3) throw std::runtime_error("Invalid map format!"); diff --git a/lib/mapping/MapFeaturesH3M.h b/lib/mapping/MapFeaturesH3M.h index d9fe3fc68..4768f0746 100644 --- a/lib/mapping/MapFeaturesH3M.h +++ b/lib/mapping/MapFeaturesH3M.h @@ -21,6 +21,7 @@ public: static MapFormatFeaturesH3M getFeaturesROE(); static MapFormatFeaturesH3M getFeaturesAB(); static MapFormatFeaturesH3M getFeaturesSOD(); + static MapFormatFeaturesH3M getFeaturesCHR(); static MapFormatFeaturesH3M getFeaturesWOG(); static MapFormatFeaturesH3M getFeaturesHOTA(uint32_t hotaVersion); @@ -64,6 +65,7 @@ public: bool levelROE = false; bool levelAB = false; bool levelSOD = false; + bool levelCHR = false; bool levelWOG = false; bool levelHOTA0 = false; bool levelHOTA1 = false; diff --git a/lib/mapping/MapFormat.h b/lib/mapping/MapFormat.h index 8ae5b82f7..c96bc9894 100644 --- a/lib/mapping/MapFormat.h +++ b/lib/mapping/MapFormat.h @@ -19,7 +19,7 @@ enum class EMapFormat : uint8_t ROE = 0x0e, // 14 AB = 0x15, // 21 SOD = 0x1c, // 28 -// CHR = 0x1d, // 29 Heroes Chronicles, presumably - identical to SoD, untested + CHR = 0x1d, // 29 HOTA = 0x20, // 32 WOG = 0x33, // 51 VCMI = 0x64 diff --git a/lib/mapping/MapFormatH3M.cpp b/lib/mapping/MapFormatH3M.cpp index dcbf09371..969e168b2 100644 --- a/lib/mapping/MapFormatH3M.cpp +++ b/lib/mapping/MapFormatH3M.cpp @@ -17,17 +17,16 @@ #include "../ArtifactUtils.h" #include "../CCreatureHandler.h" -#include "../CGeneralTextHandler.h" -#include "../CHeroHandler.h" +#include "../texts/CGeneralTextHandler.h" #include "../CSkillHandler.h" #include "../CStopWatch.h" -#include "../GameSettings.h" +#include "../IGameSettings.h" #include "../RiverHandler.h" #include "../RoadHandler.h" #include "../TerrainHandler.h" -#include "../TextOperations.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" @@ -39,6 +38,7 @@ #include "../networkPacks/Component.h" #include "../networkPacks/ArtifactLocation.h" #include "../spells/CSpellHandler.h" +#include "../texts/TextOperations.h" #include @@ -130,15 +130,17 @@ static MapIdentifiersH3M generateMapping(EMapFormat format) MapIdentifiersH3M identifierMapper; if(features.levelROE) - identifierMapper.loadMapping(VLC->settings()->getValue(EGameSettings::MAP_FORMAT_RESTORATION_OF_ERATHIA)); + identifierMapper.loadMapping(VLC->engineSettings()->getValue(EGameSettings::MAP_FORMAT_RESTORATION_OF_ERATHIA)); if(features.levelAB) - identifierMapper.loadMapping(VLC->settings()->getValue(EGameSettings::MAP_FORMAT_ARMAGEDDONS_BLADE)); + identifierMapper.loadMapping(VLC->engineSettings()->getValue(EGameSettings::MAP_FORMAT_ARMAGEDDONS_BLADE)); if(features.levelSOD) - identifierMapper.loadMapping(VLC->settings()->getValue(EGameSettings::MAP_FORMAT_SHADOW_OF_DEATH)); + identifierMapper.loadMapping(VLC->engineSettings()->getValue(EGameSettings::MAP_FORMAT_SHADOW_OF_DEATH)); + if(features.levelCHR) + identifierMapper.loadMapping(VLC->engineSettings()->getValue(EGameSettings::MAP_FORMAT_CHRONICLES)); if(features.levelWOG) - identifierMapper.loadMapping(VLC->settings()->getValue(EGameSettings::MAP_FORMAT_IN_THE_WAKE_OF_GODS)); + identifierMapper.loadMapping(VLC->engineSettings()->getValue(EGameSettings::MAP_FORMAT_IN_THE_WAKE_OF_GODS)); if(features.levelHOTA0) - identifierMapper.loadMapping(VLC->settings()->getValue(EGameSettings::MAP_FORMAT_HORN_OF_THE_ABYSS)); + identifierMapper.loadMapping(VLC->engineSettings()->getValue(EGameSettings::MAP_FORMAT_HORN_OF_THE_ABYSS)); return identifierMapper; } @@ -161,6 +163,7 @@ static std::map generateMappings() addMapping(EMapFormat::ROE); addMapping(EMapFormat::AB); addMapping(EMapFormat::SOD); + addMapping(EMapFormat::CHR); addMapping(EMapFormat::HOTA); addMapping(EMapFormat::WOG); @@ -205,20 +208,23 @@ 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(); mapHeader->twoLevel = reader->readBool(); mapHeader->name.appendTextID(readLocalizedString("header.name")); mapHeader->description.appendTextID(readLocalizedString("header.description")); + mapHeader->author.appendRawString(""); + mapHeader->authorContact.appendRawString(""); + mapHeader->mapVersion.appendRawString(""); + mapHeader->creationDateTime = 0; mapHeader->difficulty = static_cast(reader->readInt8Checked(0, 4)); if(features.levelAB) @@ -730,8 +736,7 @@ void CMapLoaderH3M::readMapOptions() { //TODO: HotA bool allowSpecialMonths = reader->readBool(); - if(!allowSpecialMonths) - logGlobal->warn("Map '%s': Option 'allow special months' is not implemented!", mapName); + map->overrideGameSetting(EGameSettings::CREATURES_ALLOW_RANDOM_SPECIAL_WEEKS, JsonNode(allowSpecialMonths)); reader->skipZero(3); } @@ -890,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()); } } @@ -907,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++) @@ -950,14 +955,15 @@ bool CMapLoaderH3M::loadArtifactToSlot(CGHeroInstance * hero, int slot) // H3 bug workaround - Enemy hero on 3rd scenario of Good1.h3c campaign ("Long Live The Queen") // He has Shackles of War (normally - MISC slot artifact) in LEFT_HAND slot set in editor // Artifact seems to be missing in game, so skip artifacts that don't fit target slot - auto * artifact = ArtifactUtils::createArtifact(map, artifactID); - if(artifact->canBePutAt(hero, ArtifactPosition(slot))) + if(ArtifactID(artifactID).toArtifact()->canBePutAt(hero, ArtifactPosition(slot))) { - artifact->putAt(*hero, ArtifactPosition(slot)); + auto * artifact = ArtifactUtils::createArtifact(artifactID); + map->putArtifactInstance(*hero, artifact, slot); + map->addNewArtifactInstance(artifact); } else { - logGlobal->warn("Map '%s': Artifact '%s' can't be put at the slot %d", mapName, artifact->artType->getNameTranslated(), slot); + logGlobal->warn("Map '%s': Artifact '%s' can't be put at the slot %d", mapName, ArtifactID(artifactID).toArtifact()->getNameTranslated(), slot); return false; } @@ -978,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); } } } @@ -1052,7 +1054,7 @@ void CMapLoaderH3M::readBoxContent(CGPandoraBox * object, const int3 & mapPositi if(auto val = reader->readInt8Checked(-3, 3)) reward.bonuses.emplace_back(BonusDuration::ONE_BATTLE, BonusType::LUCK, BonusSource::OBJECT_INSTANCE, val, BonusSourceID(idToBeGiven)); - reader->readResourses(reward.resources); + reader->readResources(reward.resources); for(int x = 0; x < GameConstants::PRIMARY_SKILLS; ++x) reward.primary.at(x) = reader->readUInt8(); @@ -1109,7 +1111,7 @@ CGObjectInstance * CMapLoaderH3M::readMonster(const int3 & mapPosition, const Ob if(hasMessage) { object->message.appendTextID(readLocalizedString(TextIdentifier("monster", mapPosition.x, mapPosition.y, mapPosition.z, "message"))); - reader->readResourses(object->resources); + reader->readResources(object->resources); object->gainedArtifact = reader->readArtifact(); } object->neverFlees = reader->readBool(); @@ -1119,18 +1121,18 @@ CGObjectInstance * CMapLoaderH3M::readMonster(const int3 & mapPosition, const Ob if(features.levelHOTA3) { //TODO: HotA - int32_t agressionExact = reader->readInt32(); // -1 = default, 1-10 = possible values range + int32_t aggressionExact = reader->readInt32(); // -1 = default, 1-10 = possible values range bool joinOnlyForMoney = reader->readBool(); // if true, monsters will only join for money - int32_t joinPercent = reader->readInt32(); // 100 = default, percent of monsters that will join on succesfull agression check + int32_t joinPercent = reader->readInt32(); // 100 = default, percent of monsters that will join on successful aggression check int32_t upgradedStack = reader->readInt32(); // Presence of upgraded stack, -1 = random, 0 = never, 1 = always int32_t stacksCount = reader->readInt32(); // TODO: check possible values. How many creature stacks will be present on battlefield, -1 = default - if(agressionExact != -1 || joinOnlyForMoney || joinPercent != 100 || upgradedStack != -1 || stacksCount != -1) + if(aggressionExact != -1 || joinOnlyForMoney || joinPercent != 100 || upgradedStack != -1 || stacksCount != -1) logGlobal->warn( "Map '%s': Wandering monsters %s settings %d %d %d %d %d are not implemented!", mapName, mapPosition.toString(), - agressionExact, + aggressionExact, int(joinOnlyForMoney), joinPercent, upgradedStack, @@ -1301,7 +1303,8 @@ CGObjectInstance * CMapLoaderH3M::readArtifact(const int3 & mapPosition, std::sh artID = ArtifactID(objectTemplate->subid); } - object->storedArtifact = ArtifactUtils::createArtifact(map, artID, spellID.getNum()); + object->storedArtifact = ArtifactUtils::createArtifact(artID, spellID.getNum()); + map->addNewArtifactInstance(object->storedArtifact); return object; } @@ -1451,7 +1454,7 @@ CGObjectInstance * CMapLoaderH3M::readGeneric(const int3 & mapPosition, std::sha CGObjectInstance * CMapLoaderH3M::readPyramid(const int3 & mapPosition, std::shared_ptr objectTemplate) { if(objectTemplate->subid == 0) - return new CBank(map->cb); + return readGeneric(mapPosition, objectTemplate); return new CGObjectInstance(map->cb); } @@ -1470,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; } @@ -1610,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) @@ -1770,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; @@ -1780,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) { @@ -1883,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); } @@ -1896,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()); @@ -2109,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; @@ -2194,7 +2197,10 @@ CGObjectInstance * CMapLoaderH3M::readTown(const int3 & position, std::shared_pt bool hasCustomBuildings = reader->readBool(); if(hasCustomBuildings) { - reader->readBitmaskBuildings(object->builtBuildings, faction); + std::set builtBuildings; + reader->readBitmaskBuildings(builtBuildings, faction); + for(const auto & building : builtBuildings) + object->addBuilding(building); reader->readBitmaskBuildings(object->forbiddenBuildings, faction); } // Standard buildings @@ -2202,10 +2208,10 @@ CGObjectInstance * CMapLoaderH3M::readTown(const int3 & position, std::shared_pt { bool hasFort = reader->readBool(); if(hasFort) - object->builtBuildings.insert(BuildingID::FORT); + object->addBuilding(BuildingID::FORT); //means that set of standard building should be included - object->builtBuildings.insert(BuildingID::DEFAULT); + object->addBuilding(BuildingID::DEFAULT); } if(features.levelAB) @@ -2224,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(); @@ -2238,17 +2241,17 @@ CGObjectInstance * CMapLoaderH3M::readTown(const int3 & position, std::shared_pt event.name = readBasicString(); event.message.appendTextID(readLocalizedString(TextIdentifier("town", position.x, position.y, position.z, "event", eventID, "description"))); - reader->readResourses(event.resources); + reader->readResources(event.resources); - event.players = reader->readUInt8(); + reader->readBitmaskPlayers(event.players, false); if(features.levelSOD) event.humanAffected = reader->readBool(); else event.humanAffected = true; event.computerAffected = reader->readBool(); - event.firstOccurence = reader->readUInt16(); - event.nextOccurence = reader->readUInt8(); + event.firstOccurrence = reader->readUInt16(); + event.nextOccurrence = reader->readUInt8(); reader->skipZero(17); @@ -2276,7 +2279,7 @@ CGObjectInstance * CMapLoaderH3M::readTown(const int3 & position, std::shared_pt if (mapHeader->players[alignment].canAnyonePlay()) object->alignmentToPlayer = PlayerColor(alignment); else - logGlobal->warn("%s - Aligment of town at %s is invalid! Player %d is not present on map!", mapName, position.toString(), int(alignment)); + logGlobal->warn("%s - Alignment of town at %s is invalid! Player %d is not present on map!", mapName, position.toString(), int(alignment)); } else { @@ -2285,11 +2288,11 @@ CGObjectInstance * CMapLoaderH3M::readTown(const int3 & position, std::shared_pt if(invertedAlignment < PlayerColor::PLAYER_LIMIT.getNum()) { - logGlobal->warn("%s - Aligment of town at %s 'not as player %d' is not implemented!", mapName, position.toString(), alignment - PlayerColor::PLAYER_LIMIT.getNum()); + logGlobal->warn("%s - Alignment of town at %s 'not as player %d' is not implemented!", mapName, position.toString(), alignment - PlayerColor::PLAYER_LIMIT.getNum()); } else { - logGlobal->warn("%s - Aligment of town at %s is corrupted!!", mapName, position.toString()); + logGlobal->warn("%s - Alignment of town at %s is corrupted!!", mapName, position.toString()); } } } @@ -2308,8 +2311,8 @@ void CMapLoaderH3M::readEvents() event.name = readBasicString(); event.message.appendTextID(readLocalizedString(TextIdentifier("event", eventID, "description"))); - reader->readResourses(event.resources); - event.players = reader->readUInt8(); + reader->readResources(event.resources); + reader->readBitmaskPlayers(event.players, false); if(features.levelSOD) { event.humanAffected = reader->readBool(); @@ -2319,8 +2322,8 @@ void CMapLoaderH3M::readEvents() event.humanAffected = true; } event.computerAffected = reader->readBool(); - event.firstOccurence = reader->readUInt16(); - event.nextOccurence = reader->readUInt8(); + event.firstOccurrence = reader->readUInt16(); + event.nextOccurrence = reader->readUInt8(); reader->skipZero(17); 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 26e7c5bb2..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 "../CTownHandler.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" @@ -36,7 +36,7 @@ #include "../constants/StringConstants.h" #include "../serializer/JsonDeserializer.h" #include "../serializer/JsonSerializer.h" -#include "../Languages.h" +#include "../texts/Languages.h" VCMI_LIB_NAMESPACE_BEGIN @@ -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 @@ -296,9 +296,9 @@ void CMapFormatJson::serializeAllowedFactions(JsonSerializeFormat & handler, std if(handler.saving) { - for(auto faction : VLC->townh->objects) - if(faction->town && vstd::contains(value, faction->getId())) - temp.insert(faction->getId()); + for(auto const factionID : VLC->townh->getDefaultAllowed()) + if(vstd::contains(value, factionID)) + temp.insert(factionID); } handler.serializeLIC("allowedFactions", &FactionID::decode, &FactionID::encode, VLC->townh->getDefaultAllowed(), temp); @@ -311,6 +311,10 @@ void CMapFormatJson::serializeHeader(JsonSerializeFormat & handler) { handler.serializeStruct("name", mapHeader->name); handler.serializeStruct("description", mapHeader->description); + handler.serializeStruct("author", mapHeader->author); + handler.serializeStruct("authorContact", mapHeader->authorContact); + handler.serializeStruct("mapVersion", mapHeader->mapVersion); + handler.serializeInt("creationDateTime", mapHeader->creationDateTime, 0); handler.serializeInt("heroLevelLimit", mapHeader->levelLimit, 0); //todo: support arbitrary percentage @@ -325,6 +329,8 @@ void CMapFormatJson::serializeHeader(JsonSerializeFormat & handler) handler.serializeStruct("defeatMessage", mapHeader->defeatMessage); handler.serializeInt("defeatIconIndex", mapHeader->defeatIconIndex); + + handler.serializeIdArray("reservedCampaignHeroes", mapHeader->reservedCampaignHeroes); } void CMapFormatJson::serializePlayerInfo(JsonSerializeFormat & handler) @@ -430,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); } @@ -809,7 +813,7 @@ JsonNode CMapLoaderJson::getFromArchive(const std::string & archiveFilename) auto data = loader.load(resource)->readAll(); - JsonNode res(reinterpret_cast(data.first.get()), data.second); + JsonNode res(reinterpret_cast(data.first.get()), data.second, archiveFilename); return res; } @@ -855,7 +859,6 @@ void CMapLoaderJson::readHeader(const bool complete) //todo: multilevel map load support { auto levels = handler.enterStruct("mapLevels"); - { auto surface = handler.enterStruct("surface"); handler.serializeInt("height", mapHeader->height); @@ -887,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 @@ -917,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) @@ -1011,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): @@ -1067,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); } @@ -1104,13 +1105,15 @@ void CMapLoaderJson::MapObjectLoader::configure() artID = art->getArtifact(); } - art->storedArtifact = ArtifactUtils::createArtifact(owner->map, artID, spellID.getNum()); + art->storedArtifact = ArtifactUtils::createArtifact(artID, spellID.getNum()); + owner->map->addNewArtifactInstance(art->storedArtifact); } if(auto * hero = dynamic_cast(instance)) { auto o = handler.enterStruct("options"); - hero->serializeJsonArtifacts(handler, "artifacts", owner->map); + hero->serializeJsonArtifacts(handler, "artifacts"); + owner->map->addNewArtifactInstance(*hero); } } @@ -1147,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()); } } @@ -1251,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 f51334a2a..e75e58235 100644 --- a/lib/mapping/MapIdentifiersH3M.cpp +++ b/lib/mapping/MapIdentifiersH3M.cpp @@ -12,8 +12,8 @@ #include "MapIdentifiersH3M.h" #include "../VCMI_Lib.h" -#include "../CTownHandler.h" -#include "../CHeroHandler.h" +#include "../entities/faction/CFaction.h" +#include "../entities/faction/CTownHandler.h" #include "../filesystem/Filesystem.h" #include "../mapObjectConstructors/AObjectTypeHandler.h" #include "../mapObjectConstructors/CObjectClassesHandler.h" diff --git a/lib/mapping/MapReaderH3M.cpp b/lib/mapping/MapReaderH3M.cpp index d2db92dff..21c2a8771 100644 --- a/lib/mapping/MapReaderH3M.cpp +++ b/lib/mapping/MapReaderH3M.cpp @@ -393,7 +393,7 @@ void MapReaderH3M::skipZero(size_t amount) #endif } -void MapReaderH3M::readResourses(TResources & resources) +void MapReaderH3M::readResources(TResources & resources) { for(int x = 0; x < features.resourcesCount; ++x) resources[x] = reader->readInt32(); @@ -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/mapping/MapReaderH3M.h b/lib/mapping/MapReaderH3M.h index 42b446ba5..d6312a80d 100644 --- a/lib/mapping/MapReaderH3M.h +++ b/lib/mapping/MapReaderH3M.h @@ -67,7 +67,7 @@ public: void skipUnused(size_t amount); void skipZero(size_t amount); - void readResourses(TResources & resources); + void readResources(TResources & resources); bool readBool(); diff --git a/lib/mapping/ObstacleProxy.cpp b/lib/mapping/ObstacleProxy.cpp index 88cd97f73..10a22d508 100644 --- a/lib/mapping/ObstacleProxy.cpp +++ b/lib/mapping/ObstacleProxy.cpp @@ -16,6 +16,9 @@ #include "../mapObjects/CGObjectInstance.h" #include "../mapObjects/ObjectTemplate.h" #include "../mapObjects/ObstacleSetHandler.h" +#include "../VCMI_Lib.h" + +#include VCMI_LIB_NAMESPACE_BEGIN @@ -52,7 +55,7 @@ void ObstacleProxy::sortObstacles() }); } -bool ObstacleProxy::prepareBiome(const ObstacleSetFilter & filter, CRandomGenerator & rand) +bool ObstacleProxy::prepareBiome(const ObstacleSetFilter & filter, vstd::RNG & rand) { possibleObstacles.clear(); @@ -227,7 +230,7 @@ bool ObstacleProxy::isProhibited(const rmg::Area& objArea) const return false; }; -int ObstacleProxy::getWeightedObjects(const int3 & tile, CRandomGenerator & rand, IGameCallback * cb, std::list & allObjects, std::vector> & weightedObjects) +int ObstacleProxy::getWeightedObjects(const int3 & tile, vstd::RNG & rand, IGameCallback * cb, std::list & allObjects, std::vector> & weightedObjects) { int maxWeight = std::numeric_limits::min(); for(auto & possibleObstacle : possibleObstacles) @@ -308,7 +311,7 @@ int ObstacleProxy::getWeightedObjects(const int3 & tile, CRandomGenerator & rand return maxWeight; } -std::set ObstacleProxy::createObstacles(CRandomGenerator & rand, IGameCallback * cb) +std::set ObstacleProxy::createObstacles(vstd::RNG & rand, IGameCallback * cb) { //reverse order, since obstacles begin in bottom-right corner, while the map coordinates begin in top-left auto blockedTiles = blockedArea.getTilesVector(); @@ -381,7 +384,7 @@ bool EditorObstaclePlacer::isInTheMap(const int3& tile) return map->isInTheMap(tile); } -std::set EditorObstaclePlacer::placeObstacles(CRandomGenerator & rand) +std::set EditorObstaclePlacer::placeObstacles(vstd::RNG & rand) { auto obstacles = createObstacles(rand, map->cb); finalInsertion(map->getEditManager(), obstacles); diff --git a/lib/mapping/ObstacleProxy.h b/lib/mapping/ObstacleProxy.h index 1bf4e4b72..b3dd2f35e 100644 --- a/lib/mapping/ObstacleProxy.h +++ b/lib/mapping/ObstacleProxy.h @@ -18,7 +18,6 @@ VCMI_LIB_NAMESPACE_BEGIN class CMapEditManager; class CGObjectInstance; class ObjectTemplate; -class CRandomGenerator; class IGameCallback; class ObstacleSetFilter; @@ -30,7 +29,7 @@ public: virtual ~ObstacleProxy() = default; void collectPossibleObstacles(TerrainId terrain); - bool prepareBiome(const ObstacleSetFilter & filter, CRandomGenerator & rand); + bool prepareBiome(const ObstacleSetFilter & filter, vstd::RNG & rand); void addBlockedTile(const int3 & tile); @@ -44,7 +43,7 @@ public: virtual void placeObject(rmg::Object & object, std::set & instances); - virtual std::set createObstacles(CRandomGenerator & rand, IGameCallback * cb); + virtual std::set createObstacles(vstd::RNG & rand, IGameCallback * cb); virtual bool isInTheMap(const int3& tile) = 0; @@ -53,7 +52,7 @@ public: virtual void postProcess(const rmg::Object& object) {}; protected: - int getWeightedObjects(const int3& tile, CRandomGenerator& rand, IGameCallback * cb, std::list& allObjects, std::vector>& weightedObjects); + int getWeightedObjects(const int3& tile, vstd::RNG& rand, IGameCallback * cb, std::list& allObjects, std::vector>& weightedObjects); void sortObstacles(); rmg::Area blockedArea; @@ -71,7 +70,7 @@ public: bool isInTheMap(const int3& tile) override; - std::set placeObstacles(CRandomGenerator& rand); + std::set placeObstacles(vstd::RNG& rand); private: CMap* map; diff --git a/lib/minizip/MiniZip64_Changes.txt b/lib/minizip/MiniZip64_Changes.txt index 13a1bd91a..375946811 100644 --- a/lib/minizip/MiniZip64_Changes.txt +++ b/lib/minizip/MiniZip64_Changes.txt @@ -1,5 +1,5 @@ -MiniZip 1.1 was derrived from MiniZip at version 1.01f +MiniZip 1.1 was derived from MiniZip at version 1.01f Change in 1.0 (Okt 2009) - **TODO - Add history** diff --git a/lib/minizip/ioapi.h b/lib/minizip/ioapi.h index 1bc77cfd7..401e87db7 100644 --- a/lib/minizip/ioapi.h +++ b/lib/minizip/ioapi.h @@ -90,7 +90,7 @@ #include "mz64conf.h" #endif -/* a type choosen by DEFINE */ +/* a type chosen by DEFINE */ #ifdef HAVE_64BIT_INT_CUSTOM typedef 64BIT_INT_CUSTOM_TYPE ZPOS64_T; #else diff --git a/lib/minizip/miniunz.c b/lib/minizip/miniunz.c index 3d65401be..b00f18de9 100644 --- a/lib/minizip/miniunz.c +++ b/lib/minizip/miniunz.c @@ -200,7 +200,7 @@ void do_help() " -l list files\n" \ " -d directory to extract into\n" \ " -o overwrite files without prompting\n" \ - " -p extract crypted file using password\n\n"); + " -p extract encrypted file using password\n\n"); } void Display64BitsSize(ZPOS64_T n, int size_char) @@ -259,7 +259,7 @@ int do_list(uf) if (file_info.uncompressed_size>0) ratio = (uLong)((file_info.compressed_size*100)/file_info.uncompressed_size); - /* display a '*' if the file is crypted */ + /* display a '*' if the file is encrypted */ if ((file_info.flag & 1) != 0) charCrypt='*'; diff --git a/lib/minizip/minizip.cbp b/lib/minizip/minizip.cbp deleted file mode 100644 index d464d11a4..000000000 --- a/lib/minizip/minizip.cbp +++ /dev/null @@ -1,86 +0,0 @@ - - - - - - diff --git a/lib/minizip/minizip.vcxproj b/lib/minizip/minizip.vcxproj deleted file mode 100644 index cab7d74a2..000000000 --- a/lib/minizip/minizip.vcxproj +++ /dev/null @@ -1,190 +0,0 @@ - - - - - Debug - Win32 - - - Debug - x64 - - - RD - Win32 - - - RD - x64 - - - - - - - - - - - - - - {AA3CC588-9D08-4178-A1E8-C71561E99723} - Win32Proj - minizip - 10.0 - - - - DynamicLibrary - true - v142 - Unicode - - - DynamicLibrary - true - v142 - Unicode - - - DynamicLibrary - false - v142 - true - Unicode - - - DynamicLibrary - false - v142 - true - Unicode - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - true - $(VCMI_Out)\ - - - true - $(VCMI_Out)\ - - - $(SolutionDir)..\include;$(IncludePath) - .. - $(SolutionDir)..\libs\$(PlatformShortName);$(VCMI_Out);$(LibraryPath) - - - $(SolutionDir)..\libs\$(PlatformShortName);$(VCMI_Out);$(LibraryPath) - $(SolutionDir)..\include;$(IncludePath) - $(VCMI_Out)\ - - - - - - Level3 - Disabled - MINIZIP_DLL;WIN32;_DEBUG;_WINDOWS;_USRDLL;%(PreprocessorDefinitions) - true - - - Windows - true - zlib.lib;%(AdditionalDependencies) - - - - - - - Level3 - Disabled - MINIZIP_DLL;WIN32;_DEBUG;_WINDOWS;_USRDLL;%(PreprocessorDefinitions) - true - - - Windows - true - zlib.lib;%(AdditionalDependencies) - - - - - Level3 - - - Full - - - true - MINIZIP_DLL;ZLIB_DLL;ZLIB_INTERNAL;_CRT_SECURE_NO_WARNINGS;%(PreprocessorDefinitions) - true - F:\Programowanie\VCMI\include;%(AdditionalIncludeDirectories) - true - - - Windows - true - true - true - ..\..\libs;..\.. - zlib.lib;%(AdditionalDependencies) - - - - - Level3 - - - Full - - - true - MINIZIP_DLL;ZLIB_DLL;ZLIB_INTERNAL;_CRT_SECURE_NO_WARNINGS;%(PreprocessorDefinitions) - true - F:\Programowanie\VCMI\include;%(AdditionalIncludeDirectories) - - - Windows - true - true - true - ..\..\libs;..\.. - zlib.lib;%(AdditionalDependencies) - - - - - - \ No newline at end of file diff --git a/lib/minizip/minizip.vcxproj.filters b/lib/minizip/minizip.vcxproj.filters deleted file mode 100644 index b64f388df..000000000 --- a/lib/minizip/minizip.vcxproj.filters +++ /dev/null @@ -1,39 +0,0 @@ - - - - - {4FC737F1-C7A5-4376-A066-2A32D752A2FF} - cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx - - - {93995380-89BD-4b04-88EB-625FBE52EBFB} - h;hpp;hxx;hm;inl;inc;xsd - - - {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} - rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms - - - - - Header Files - - - Header Files - - - Header Files - - - - - Source Files - - - Source Files - - - Source Files - - - \ No newline at end of file diff --git a/lib/minizip/unzip.c b/lib/minizip/unzip.c index f6261cbae..6ac19a574 100644 --- a/lib/minizip/unzip.c +++ b/lib/minizip/unzip.c @@ -53,8 +53,8 @@ Oct-2009 - Mathias Svensson - Fixed problem if uncompressed size was > 4G and compressed size was <4G should only read the compressed/uncompressed size from the Zip64 format if the size from normal header was 0xFFFFFFFF - Oct-2009 - Mathias Svensson - Applied some bug fixes from paches recived from Gilles Vollant - Oct-2009 - Mathias Svensson - Applied support to unzip files with compression mathod BZIP2 (bzip2 lib is required) + Oct-2009 - Mathias Svensson - Applied some bug fixes from patches received from Gilles Vollant + Oct-2009 - Mathias Svensson - Applied support to unzip files with compression method BZIP2 (bzip2 lib is required) Patch created by Daniel Borca Jan-2010 - back to unzip and minizip 1.0 name scheme, with compatibility layer @@ -153,7 +153,7 @@ typedef struct ZPOS64_T rest_read_compressed; /* number of byte to be decompressed */ ZPOS64_T rest_read_uncompressed;/*number of byte to be obtained after decomp*/ zlib_filefunc64_32_def z_filefunc; - voidpf filestream; /* io structore of the zipfile */ + voidpf filestream; /* io structure of the zipfile */ uLong compression_method; /* compression method (0==store) */ ZPOS64_T byte_before_the_zipfile;/* byte before the zipfile, (>0 for sfx)*/ int raw; @@ -166,7 +166,7 @@ typedef struct { zlib_filefunc64_32_def z_filefunc; int is64bitOpenFunction; - voidpf filestream; /* io structore of the zipfile */ + voidpf filestream; /* io structure of the zipfile */ unz_global_info64 gi; /* public global information */ ZPOS64_T byte_before_the_zipfile;/* byte before the zipfile, (>0 for sfx)*/ ZPOS64_T num_file; /* number of the current file in the zipfile*/ @@ -200,7 +200,7 @@ typedef struct /* =========================================================================== Read a byte from a gz_stream; update next_in and avail_in. Return EOF for end of file. - IN assertion: the stream s has been sucessfully opened for reading. + IN assertion: the stream s has been successfully opened for reading. */ @@ -380,10 +380,10 @@ local int strcmpcasenosensitive_internal (const char* fileName1, const char* fil /* Compare two filename (fileName1,fileName2). - If iCaseSenisivity = 1, comparision is case sensitivity (like strcmp) - If iCaseSenisivity = 2, comparision is not case sensitivity (like strcmpi + If iCaseSenisivity = 1, comparison is case sensitivity (like strcmp) + If iCaseSenisivity = 2, comparison is not case sensitivity (like strcmpi or strcasecmp) - If iCaseSenisivity = 0, case sensitivity is defaut of your operating system + If iCaseSenisivity = 0, case sensitivity is default of your operating system (like 1 on Unix, 2 on Windows) */ @@ -591,9 +591,9 @@ local unzFile unzOpenInternal (const void *path, uLong uL; uLong number_disk; /* number of the current dist, used for - spaning ZIP, unsupported, always 0*/ + spanning ZIP, unsupported, always 0*/ uLong number_disk_with_CD; /* number the the disk with central dir, used - for spaning ZIP, unsupported, always 0*/ + for spanning ZIP, unsupported, always 0*/ ZPOS64_T number_entry_CD; /* total number of entries in the central dir (same than number_entry on nospan) */ @@ -847,7 +847,7 @@ extern int MINIZIP_EXPORT unzGetGlobalInfo (unzFile file, unz_global_info* pglob return UNZ_OK; } /* - Translate date/time from Dos format to tm_unz (readable more easilty) + Translate date/time from Dos format to tm_unz (readable more easily) */ local void unz64local_DosDateToTmuDate (ZPOS64_T ulDosDate, tm_unz* ptm) { diff --git a/lib/minizip/unzip.h b/lib/minizip/unzip.h index 52bcc7ed5..b38561465 100644 --- a/lib/minizip/unzip.h +++ b/lib/minizip/unzip.h @@ -155,10 +155,10 @@ extern int MINIZIP_EXPORT unzStringFileNameCompare OF ((const char* fileName1, int iCaseSensitivity)); /* Compare two filename (fileName1,fileName2). - If iCaseSenisivity = 1, comparision is case sensitivity (like strcmp) - If iCaseSenisivity = 2, comparision is not case sensitivity (like strcmpi + If iCaseSenisivity = 1, comparison is case sensitivity (like strcmp) + If iCaseSenisivity = 2, comparison is not case sensitivity (like strcmpi or strcasecmp) - If iCaseSenisivity = 0, case sensitivity is defaut of your operating system + If iCaseSenisivity = 0, case sensitivity is default of your operating system (like 1 on Unix, 2 on Windows) */ diff --git a/lib/minizip/zip.c b/lib/minizip/zip.c index 93886fcac..1244d8f13 100644 --- a/lib/minizip/zip.c +++ b/lib/minizip/zip.c @@ -14,8 +14,8 @@ Oct-2009 - Mathias Svensson - Added Zip64 Support when creating new file archives Oct-2009 - Mathias Svensson - Did some code cleanup and refactoring to get better overview of some functions. Oct-2009 - Mathias Svensson - Added zipRemoveExtraInfoBlock to strip extra field data from its ZIP64 data - It is used when recreting zip archive with RAW when deleting items from a zip. - ZIP64 data is automaticly added to items that needs it, and existing ZIP64 data need to be removed. + It is used when recreating zip archive with RAW when deleting items from a zip. + ZIP64 data is automatically added to items that needs it, and existing ZIP64 data need to be removed. Oct-2009 - Mathias Svensson - Added support for BZIP2 as compression mode (bzip2 lib is required) Jan-2010 - back to unzip and minizip 1.0 name scheme, with compatibility layer @@ -52,7 +52,7 @@ /* compile with -Dlocal if your debugger can't find static symbols */ #ifndef VERSIONMADEBY -# define VERSIONMADEBY (0x0) /* platform depedent */ +# define VERSIONMADEBY (0x0) /* platform dependent */ #endif #ifndef Z_BUFSIZE @@ -121,7 +121,7 @@ typedef struct linkedlist_datablock_internal_s struct linkedlist_datablock_internal_s* next_datablock; uLong avail_in_this_block; uLong filled_in_this_block; - uLong unused; /* for future use and alignement */ + uLong unused; /* for future use and alignment */ unsigned char data[SIZEDATA_INDATABLOCK]; } linkedlist_datablock_internal; @@ -143,20 +143,20 @@ typedef struct uInt pos_in_buffered_data; /* last written byte in buffered_data */ ZPOS64_T pos_local_header; /* offset of the local header of the file - currenty writing */ + currently writing */ char* central_header; /* central header data for the current file */ uLong size_centralExtra; uLong size_centralheader; /* size of the central header for cur file */ uLong size_centralExtraFree; /* Extra bytes allocated to the centralheader but that are not used */ uLong flag; /* flag of the file currently writing */ - int method; /* compression method of file currenty wr.*/ + int method; /* compression method of file currently wr.*/ int raw; /* 1 for directly writing raw data */ Byte buffered_data[Z_BUFSIZE];/* buffer contain compressed data to be writ*/ uLong dosDate; uLong crc32; int encrypt; - int zip64; /* Add ZIP64 extened information in the extra field */ + int zip64; /* Add ZIP64 extended information in the extra field */ ZPOS64_T pos_zip64extrainfo; ZPOS64_T totalCompressedData; ZPOS64_T totalUncompressedData; @@ -170,13 +170,13 @@ typedef struct typedef struct { zlib_filefunc64_32_def z_filefunc; - voidpf filestream; /* io structore of the zipfile */ + voidpf filestream; /* io structure of the zipfile */ linkedlist_data central_dir;/* datablock with central dir in construction*/ int in_opened_file_inzip; /* 1 if a file in the zip is currently writ.*/ - curfile64_info ci; /* info on the file curretly writing */ + curfile64_info ci; /* info on the file currently writing */ ZPOS64_T begin_pos; /* position of the beginning of the zipfile */ - ZPOS64_T add_position_when_writting_offset; + ZPOS64_T add_position_when_writing_offset; ZPOS64_T number_entry; #ifndef NO_ADDFILEINEXISTINGZIP @@ -653,9 +653,9 @@ int LoadCentralDirectoryRecord(zip64_internal* pziinit) uLong uL; uLong number_disk; /* number of the current dist, used for - spaning ZIP, unsupported, always 0*/ + spanning ZIP, unsupported, always 0*/ uLong number_disk_with_CD; /* number the the disk with central dir, used - for spaning ZIP, unsupported, always 0*/ + for spanning ZIP, unsupported, always 0*/ ZPOS64_T number_entry; ZPOS64_T number_entry_CD; /* total number of entries in the central dir @@ -812,7 +812,7 @@ int LoadCentralDirectoryRecord(zip64_internal* pziinit) } byte_before_the_zipfile = central_pos - (offset_central_dir+size_central_dir); - pziinit->add_position_when_writting_offset = byte_before_the_zipfile; + pziinit->add_position_when_writing_offset = byte_before_the_zipfile; { ZPOS64_T size_central_dir_to_read = size_central_dir; @@ -880,7 +880,7 @@ extern zipFile MINIZIP_EXPORT zipOpen3 (const void *pathname, int append, zipcha ziinit.in_opened_file_inzip = 0; ziinit.ci.stream_initialised = 0; ziinit.number_entry = 0; - ziinit.add_position_when_writting_offset = 0; + ziinit.add_position_when_writing_offset = 0; init_linkedlist(&(ziinit.central_dir)); @@ -1169,7 +1169,7 @@ extern int MINIZIP_EXPORT zipOpenNewFileInZip4_64 (zipFile file, const char* fil if(zi->ci.pos_local_header >= 0xffffffff) zip64local_putValue_inmemory(zi->ci.central_header+42,(uLong)0xffffffff,4); else - zip64local_putValue_inmemory(zi->ci.central_header+42,(uLong)zi->ci.pos_local_header - zi->add_position_when_writting_offset,4); + zip64local_putValue_inmemory(zi->ci.central_header+42,(uLong)zi->ci.pos_local_header - zi->add_position_when_writing_offset,4); for (i=0;ici.central_header+SIZECENTRALHEADER+i) = *(filename+i); @@ -1760,7 +1760,7 @@ extern int MINIZIP_EXPORT zipCloseFileInZip (zipFile file) int Write_Zip64EndOfCentralDirectoryLocator(zip64_internal* zi, ZPOS64_T zip64eocd_pos_inzip) { int err = ZIP_OK; - ZPOS64_T pos = zip64eocd_pos_inzip - zi->add_position_when_writting_offset; + ZPOS64_T pos = zip64eocd_pos_inzip - zi->add_position_when_writing_offset; err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)ZIP64ENDLOCHEADERMAGIC,4); @@ -1813,7 +1813,7 @@ int Write_Zip64EndOfCentralDirectoryRecord(zip64_internal* zi, uLong size_centra if (err==ZIP_OK) /* offset of start of central directory with respect to the starting disk number */ { - ZPOS64_T pos = centraldir_pos_inzip - zi->add_position_when_writting_offset; + ZPOS64_T pos = centraldir_pos_inzip - zi->add_position_when_writing_offset; err = zip64local_putValue(&zi->z_filefunc,zi->filestream, (ZPOS64_T)pos,8); } return err; @@ -1854,13 +1854,13 @@ int Write_EndOfCentralDirectoryRecord(zip64_internal* zi, uLong size_centraldir, if (err==ZIP_OK) /* offset of start of central directory with respect to the starting disk number */ { - ZPOS64_T pos = centraldir_pos_inzip - zi->add_position_when_writting_offset; + ZPOS64_T pos = centraldir_pos_inzip - zi->add_position_when_writing_offset; if(pos >= 0xffffffff) { err = zip64local_putValue(&zi->z_filefunc,zi->filestream, (uLong)0xffffffff,4); } else - err = zip64local_putValue(&zi->z_filefunc,zi->filestream, (uLong)(centraldir_pos_inzip - zi->add_position_when_writting_offset),4); + err = zip64local_putValue(&zi->z_filefunc,zi->filestream, (uLong)(centraldir_pos_inzip - zi->add_position_when_writing_offset),4); } return err; @@ -1926,7 +1926,7 @@ extern int MINIZIP_EXPORT zipClose (zipFile file, const char* global_comment) } free_linkedlist(&(zi->central_dir)); - pos = centraldir_pos_inzip - zi->add_position_when_writting_offset; + pos = centraldir_pos_inzip - zi->add_position_when_writing_offset; if(pos >= 0xffffffff || zi->number_entry > 0xFFFF) { ZPOS64_T Zip64EOCDpos = ZTELL64(zi->z_filefunc,zi->filestream); diff --git a/lib/minizip/zip.h b/lib/minizip/zip.h index a7919c9e8..1784f1ac7 100644 --- a/lib/minizip/zip.h +++ b/lib/minizip/zip.h @@ -131,7 +131,7 @@ extern zipFile MINIZIP_EXPORT zipOpen64 OF((const void *pathname, int append)); /* Note : there is no delete function into a zipfile. If you want delete file into a zipfile, you must open a zipfile, and create another - Of couse, you can use RAW reading and writing to copy the file you did not want delte + Of couse, you can use RAW reading and writing to copy the file you did not want delete */ extern zipFile MINIZIP_EXPORT zipOpen2 OF((const char *pathname, 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 1cf7b7ba9..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 "../CGeneralTextHandler.h" -#include "../CStopWatch.h" #include "../GameSettings.h" -#include "../Languages.h" -#include "../MetaString.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" 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 childs 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->load(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->load(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 30eea7a38..e991459c8 100644 --- a/lib/modding/CModHandler.h +++ b/lib/modding/CModHandler.h @@ -12,70 +12,53 @@ VCMI_LIB_NAMESPACE_BEGIN class CModHandler; -class CModIndentifier; -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 erros 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 8690c17d0..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 "../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 102e037c7..380e98d18 100644 --- a/lib/modding/ContentTypeHandler.cpp +++ b/lib/modding/ContentTypeHandler.cpp @@ -11,20 +11,22 @@ #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 "../CGeneralTextHandler.h" -#include "../CHeroHandler.h" +#include "../CConfigHandler.h" +#include "../entities/faction/CTownHandler.h" +#include "../entities/hero/CHeroClassHandler.h" +#include "../entities/hero/CHeroHandler.h" +#include "../texts/CGeneralTextHandler.h" #include "../CSkillHandler.h" #include "../CStopWatch.h" -#include "../CTownHandler.h" -#include "../GameSettings.h" +#include "../IGameSettings.h" #include "../IHandlerBase.h" -#include "../Languages.h" #include "../ObstacleHandler.h" #include "../mapObjects/ObstacleSetHandler.h" #include "../RiverHandler.h" @@ -36,12 +38,13 @@ #include "../mapObjectConstructors/CObjectClassesHandler.h" #include "../rmg/CRmgTemplateStorage.h" #include "../spells/CSpellHandler.h" +#include "../VCMI_Lib.h" 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/IdentifierStorage.cpp b/lib/modding/IdentifierStorage.cpp index 09b0eed98..79009f7c3 100644 --- a/lib/modding/IdentifierStorage.cpp +++ b/lib/modding/IdentifierStorage.cpp @@ -190,6 +190,12 @@ void CIdentifierStorage::requestIdentifier(const JsonNode & name, const std::fun requestIdentifier(ObjectCallback::fromNameWithType(name.getModScope(), name.String(), callback, false)); } +void CIdentifierStorage::requestIdentifierOptional(const std::string & type, const JsonNode & name, const std::function & callback) const +{ + if (!name.isNull()) + requestIdentifier(type, name, callback); +} + void CIdentifierStorage::tryRequestIdentifier(const std::string & scope, const std::string & type, const std::string & name, const std::function & callback) const { requestIdentifier(ObjectCallback::fromNameAndType(scope, type, name, callback, true)); @@ -430,7 +436,7 @@ bool CIdentifierStorage::resolveIdentifier(const ObjectCallback & request) const return true; } - if (request.optional && identifiers.empty()) // failed to resolve optinal ID + if (request.optional && identifiers.empty()) // failed to resolve optional ID { return true; } diff --git a/lib/modding/IdentifierStorage.h b/lib/modding/IdentifierStorage.h index 6a657d715..108e9ecf0 100644 --- a/lib/modding/IdentifierStorage.h +++ b/lib/modding/IdentifierStorage.h @@ -84,6 +84,8 @@ public: void requestIdentifier(const std::string & type, const JsonNode & name, const std::function & callback) const; void requestIdentifier(const JsonNode & name, const std::function & callback) const; + void requestIdentifierOptional(const std::string & type, const JsonNode & name, const std::function & callback) const; + /// try to request ID. If ID with such name won't be loaded, callback function will not be called void tryRequestIdentifier(const std::string & scope, const std::string & type, const std::string & name, const std::function & callback) const; void tryRequestIdentifier(const std::string & type, const JsonNode & name, const std::function & callback) const; diff --git a/lib/modding/ModDescription.cpp b/lib/modding/ModDescription.cpp new file mode 100644 index 000000000..656036060 --- /dev/null +++ b/lib/modding/ModDescription.cpp @@ -0,0 +1,232 @@ +/* + * 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 +{ + if (!isInstalled() || isUpdateAvailable()) + 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/ModIncompatibility.h b/lib/modding/ModIncompatibility.h index 0d0ab6581..e21ad8bff 100644 --- a/lib/modding/ModIncompatibility.h +++ b/lib/modding/ModIncompatibility.h @@ -24,7 +24,7 @@ public: messageMissingMods = _ss.str(); } - ModIncompatibility(const ModList & _missingMods, ModList & _excessiveMods) + ModIncompatibility(const ModList & _missingMods, const ModList & _excessiveMods) : ModIncompatibility(_missingMods) { std::ostringstream _ss; diff --git a/lib/modding/ModManager.cpp b/lib/modding/ModManager.cpp new file mode 100644 index 000000000..ecd8f5423 --- /dev/null +++ b/lib/modding/ModManager.cpp @@ -0,0 +1,799 @@ +/* + * 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["presets"].Struct().empty()) + { + modConfig["activePreset"] = JsonNode("default"); + if(modConfig["activeMods"].isNull()) + createInitialPreset(); // new install + else + importInitialPreset(); // 1.5 format import + } + + auto allPresets = getAllPresets(); + if (!vstd::contains(allPresets, modConfig["activePreset"].String())) + modConfig["activePreset"] = JsonNode(allPresets.front()); +} + +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(); +} + +void ModsPresetState::createNewPreset(const std::string & presetName) +{ + if (modConfig["presets"][presetName].isNull()) + modConfig["presets"][presetName]["mods"].Vector().emplace_back("vcmi"); +} + +void ModsPresetState::deletePreset(const std::string & presetName) +{ + if (modConfig["presets"].Struct().size() < 2) + throw std::runtime_error("Unable to delete last preset!"); + + modConfig["presets"].Struct().erase(presetName); +} + +void ModsPresetState::activatePreset(const std::string & presetName) +{ + if (modConfig["presets"].Struct().count(presetName) == 0) + throw std::runtime_error("Unable to activate non-exinsting preset!"); + + modConfig["activePreset"].String() = presetName; +} + +void ModsPresetState::renamePreset(const std::string & oldPresetName, const std::string & newPresetName) +{ + if (oldPresetName == newPresetName) + throw std::runtime_error("Unable to rename preset to the same name!"); + + if (modConfig["presets"].Struct().count(oldPresetName) == 0) + throw std::runtime_error("Unable to rename non-existing last preset!"); + + if (modConfig["presets"].Struct().count(newPresetName) != 0) + throw std::runtime_error("Unable to rename preset - preset with such name already exists!"); + + modConfig["presets"][newPresetName] = modConfig["presets"][oldPresetName]; + modConfig["presets"].Struct().erase(oldPresetName); + + if (modConfig["activePreset"].String() == oldPresetName) + modConfig["activePreset"].String() = newPresetName; +} + +std::vector ModsPresetState::getAllPresets() const +{ + std::vector presets; + + for (const auto & preset : modConfig["presets"].Struct()) + presets.push_back(preset.first); + + return presets; +} + +std::string ModsPresetState::getActivePreset() const +{ + return modConfig["activePreset"].String(); +} + +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 (mod.getTopParentID().empty() || 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()); +} + +void ModManager::createNewPreset(const std::string & presetName) +{ + modsPreset->createNewPreset(presetName); + modsPreset->saveConfigurationState(); +} + +void ModManager::deletePreset(const std::string & presetName) +{ + modsPreset->deletePreset(presetName); + modsPreset->saveConfigurationState(); +} + +void ModManager::activatePreset(const std::string & presetName) +{ + modsPreset->activatePreset(presetName); + modsPreset->saveConfigurationState(); +} + +void ModManager::renamePreset(const std::string & oldPresetName, const std::string & newPresetName) +{ + modsPreset->renamePreset(oldPresetName, newPresetName); + modsPreset->saveConfigurationState(); +} + +std::vector ModManager::getAllPresets() const +{ + return modsPreset->getAllPresets(); +} + +std::string ModManager::getActivePreset() const +{ + return modsPreset->getActivePreset(); +} + +VCMI_LIB_NAMESPACE_END diff --git a/lib/modding/ModManager.h b/lib/modding/ModManager.h new file mode 100644 index 000000000..616e2632d --- /dev/null +++ b/lib/modding/ModManager.h @@ -0,0 +1,160 @@ +/* + * 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 createNewPreset(const std::string & presetName); + void deletePreset(const std::string & presetName); + void activatePreset(const std::string & presetName); + void renamePreset(const std::string & oldPresetName, const std::string & newPresetName); + + std::vector getAllPresets() const; + std::string getActivePreset() const; + + 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); + + void createNewPreset(const std::string & presetName); + void deletePreset(const std::string & presetName); + void activatePreset(const std::string & presetName); + void renamePreset(const std::string & oldPresetName, const std::string & newPresetName); + + std::vector getAllPresets() const; + std::string getActivePreset() const; +}; + +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/network/NetworkConnection.cpp b/lib/network/NetworkConnection.cpp index 5aa88b312..024a0214f 100644 --- a/lib/network/NetworkConnection.cpp +++ b/lib/network/NetworkConnection.cpp @@ -136,7 +136,7 @@ void NetworkConnection::setAsyncWritesEnabled(bool on) void NetworkConnection::sendPacket(const std::vector & message) { - std::lock_guard lock(writeMutex); + std::lock_guard lock(writeMutex); std::vector headerVector(sizeof(uint32_t)); uint32_t messageSize = message.size(); std::memcpy(headerVector.data(), &messageSize, sizeof(uint32_t)); @@ -148,7 +148,7 @@ void NetworkConnection::sendPacket(const std::vector & message) bool messageQueueEmpty = dataToSend.empty(); dataToSend.push_back(headerVector); - if (message.size() > 0) + if (!message.empty()) dataToSend.push_back(message); if (messageQueueEmpty) @@ -159,7 +159,7 @@ void NetworkConnection::sendPacket(const std::vector & message) { boost::system::error_code ec; boost::asio::write(*socket, boost::asio::buffer(headerVector), ec ); - if (message.size() > 0) + if (!message.empty()) boost::asio::write(*socket, boost::asio::buffer(message), ec ); } } @@ -177,7 +177,7 @@ void NetworkConnection::doSendData() void NetworkConnection::onDataSent(const boost::system::error_code & ec) { - std::lock_guard lock(writeMutex); + std::lock_guard lock(writeMutex); dataToSend.pop_front(); if (ec) { @@ -199,7 +199,11 @@ void NetworkConnection::close() { boost::system::error_code ec; socket->close(ec); +#if BOOST_VERSION >= 108700 + timer->cancel(); +#else timer->cancel(ec); +#endif //NOTE: ignoring error code, intended } diff --git a/lib/network/NetworkDefines.h b/lib/network/NetworkDefines.h index 6b86ff23a..22e90e899 100644 --- a/lib/network/NetworkDefines.h +++ b/lib/network/NetworkDefines.h @@ -15,7 +15,11 @@ VCMI_LIB_NAMESPACE_BEGIN +#if BOOST_VERSION >= 108700 +using NetworkContext = boost::asio::io_context; +#else using NetworkContext = boost::asio::io_service; +#endif using NetworkSocket = boost::asio::ip::tcp::socket; using NetworkAcceptor = boost::asio::ip::tcp::acceptor; using NetworkBuffer = boost::asio::streambuf; diff --git a/lib/network/NetworkInterface.h b/lib/network/NetworkInterface.h index 45e26bf9e..b5412820f 100644 --- a/lib/network/NetworkInterface.h +++ b/lib/network/NetworkInterface.h @@ -40,7 +40,7 @@ class DLL_LINKAGE INetworkServer : boost::noncopyable public: virtual ~INetworkServer() = default; - virtual void start(uint16_t port) = 0; + virtual uint16_t start(uint16_t port) = 0; }; /// Base interface that must be implemented by user of networking API to handle any connection callbacks diff --git a/lib/network/NetworkServer.cpp b/lib/network/NetworkServer.cpp index 110b2d0ca..f3b882052 100644 --- a/lib/network/NetworkServer.cpp +++ b/lib/network/NetworkServer.cpp @@ -19,16 +19,17 @@ NetworkServer::NetworkServer(INetworkServerListener & listener, const std::share { } -void NetworkServer::start(uint16_t port) +uint16_t NetworkServer::start(uint16_t port) { acceptor = std::make_shared(*io, boost::asio::ip::tcp::endpoint(boost::asio::ip::tcp::v4(), port)); - startAsyncAccept(); + return startAsyncAccept(); } -void NetworkServer::startAsyncAccept() +uint16_t NetworkServer::startAsyncAccept() { auto upcomingConnection = std::make_shared(*io); acceptor->async_accept(*upcomingConnection, [this, upcomingConnection](const auto & ec) { connectionAccepted(upcomingConnection, ec); }); + return acceptor->local_endpoint().port(); } void NetworkServer::connectionAccepted(std::shared_ptr upcomingConnection, const boost::system::error_code & ec) diff --git a/lib/network/NetworkServer.h b/lib/network/NetworkServer.h index 8fc0e8988..4cf96ad20 100644 --- a/lib/network/NetworkServer.h +++ b/lib/network/NetworkServer.h @@ -22,14 +22,14 @@ class NetworkServer : public INetworkConnectionListener, public INetworkServer INetworkServerListener & listener; void connectionAccepted(std::shared_ptr, const boost::system::error_code & ec); - void startAsyncAccept(); + uint16_t startAsyncAccept(); void onDisconnected(const std::shared_ptr & connection, const std::string & errorMessage) override; void onPacketReceived(const std::shared_ptr & connection, const std::vector & message) override; public: NetworkServer(INetworkServerListener & listener, const std::shared_ptr & context); - void start(uint16_t port) override; + uint16_t start(uint16_t port) override; }; VCMI_LIB_NAMESPACE_END diff --git a/lib/networkPacks/ArtifactLocation.h b/lib/networkPacks/ArtifactLocation.h index 42739083f..3864ebad3 100644 --- a/lib/networkPacks/ArtifactLocation.h +++ b/lib/networkPacks/ArtifactLocation.h @@ -37,6 +37,12 @@ struct ArtifactLocation , creature(creatureSlot) { } + ArtifactLocation(const ObjectInstanceID id, const std::optional creatureSlot, const ArtifactPosition & slot) + : artHolder(id) + , slot(slot) + , creature(creatureSlot) + { + } template void serialize(Handler & h) { diff --git a/lib/networkPacks/NetPackVisitor.h b/lib/networkPacks/NetPackVisitor.h index 02e2d287f..335f7caa1 100644 --- a/lib/networkPacks/NetPackVisitor.h +++ b/lib/networkPacks/NetPackVisitor.h @@ -13,6 +13,8 @@ #include "PacksForClientBattle.h" #include "PacksForServer.h" #include "PacksForLobby.h" +#include "SaveLocalState.h" +#include "SetRewardableConfiguration.h" #include "SetStackEffect.h" VCMI_LIB_NAMESPACE_BEGIN @@ -34,11 +36,14 @@ public: virtual void visitTurnTimeUpdate(TurnTimeUpdate & pack) {} virtual void visitGamePause(GamePause & pack) {} virtual void visitEntitiesChanged(EntitiesChanged & pack) {} + virtual void visitSetRewardableConfiguration(SetRewardableConfiguration & pack) {} + virtual void visitSetBankConfiguration(SetBankConfiguration & pack) {} virtual void visitSetResources(SetResources & pack) {} virtual void visitSetPrimSkill(SetPrimSkill & pack) {} 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) {} @@ -52,8 +57,6 @@ public: virtual void visitSetCommanderProperty(SetCommanderProperty & pack) {} virtual void visitAddQuest(AddQuest & pack) {} virtual void visitUpdateArtHandlerLists(UpdateArtHandlerLists & pack) {} - virtual void visitUpdateMapEvents(UpdateMapEvents & pack) {} - virtual void visitUpdateCastleEvents(UpdateCastleEvents & pack) {} virtual void visitChangeFormation(ChangeFormation & pack) {} virtual void visitRemoveObject(RemoveObject & pack) {} virtual void visitTryMoveHero(TryMoveHero & pack) {} @@ -77,8 +80,7 @@ public: virtual void visitBulkRebalanceStacks(BulkRebalanceStacks & pack) {} virtual void visitBulkSmartRebalanceStacks(BulkSmartRebalanceStacks & pack) {} virtual void visitPutArtifact(PutArtifact & pack) {} - virtual void visitEraseArtifact(EraseArtifact & pack) {} - virtual void visitMoveArtifact(MoveArtifact & pack) {} + virtual void visitEraseArtifact(BulkEraseArtifacts & pack) {} virtual void visitBulkMoveArtifacts(BulkMoveArtifacts & pack) {} virtual void visitAssembledArtifact(AssembledArtifact & pack) {} virtual void visitDisassembledArtifact(DisassembledArtifact & pack) {} @@ -126,7 +128,9 @@ public: virtual void visitBulkSmartSplitStack(BulkSmartSplitStack & pack) {} virtual void visitDisbandCreature(DisbandCreature & pack) {} 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) {} @@ -166,6 +170,7 @@ public: virtual void visitLobbyChangePlayerOption(LobbyChangePlayerOption & pack) {} virtual void visitLobbySetPlayer(LobbySetPlayer & pack) {} virtual void visitLobbySetPlayerName(LobbySetPlayerName & pack) {} + virtual void visitLobbySetPlayerHandicap(LobbySetPlayerHandicap & pack) {} virtual void visitLobbySetSimturns(LobbySetSimturns & pack) {} virtual void visitLobbySetTurnTime(LobbySetTurnTime & pack) {} virtual void visitLobbySetExtraOptions(LobbySetExtraOptions & pack) {} @@ -173,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/NetPacksBase.h b/lib/networkPacks/NetPacksBase.h index 093cc2cb1..0cc3b07e5 100644 --- a/lib/networkPacks/NetPacksBase.h +++ b/lib/networkPacks/NetPacksBase.h @@ -10,6 +10,7 @@ #pragma once #include "../constants/EntityIdentifiers.h" +#include "../serializer/Serializeable.h" VCMI_LIB_NAMESPACE_BEGIN @@ -18,7 +19,7 @@ class CConnection; class ICPackVisitor; -struct DLL_LINKAGE CPack +struct DLL_LINKAGE CPack : public Serializeable { /// Pointer to connection that pack received from /// Only set & used on server @@ -33,9 +34,6 @@ struct DLL_LINKAGE CPack throw std::runtime_error("CPack serialized... this should not happen!"); } - void applyGs(CGameState * gs) - {} - void visit(ICPackVisitor & cpackVisitor); protected: @@ -52,6 +50,8 @@ protected: struct DLL_LINKAGE CPackForClient : public CPack { + virtual void applyGs(CGameState * gs) = 0; + protected: void visitBasic(ICPackVisitor & cpackVisitor) override; }; diff --git a/lib/networkPacks/NetPacksLib.cpp b/lib/networkPacks/NetPacksLib.cpp index 1539b5ad1..a187f48e6 100644 --- a/lib/networkPacks/NetPacksLib.cpp +++ b/lib/networkPacks/NetPacksLib.cpp @@ -12,13 +12,14 @@ #include "PacksForClient.h" #include "PacksForClientBattle.h" #include "PacksForServer.h" +#include "SaveLocalState.h" +#include "SetRewardableConfiguration.h" #include "StackLocation.h" #include "PacksForLobby.h" #include "SetStackEffect.h" #include "NetPackVisitor.h" -#include "CGeneralTextHandler.h" +#include "texts/CGeneralTextHandler.h" #include "CArtHandler.h" -#include "CHeroHandler.h" #include "VCMI_Lib.h" #include "mapping/CMap.h" #include "spells/CSpellHandler.h" @@ -27,20 +28,24 @@ #include "gameState/TavernHeroesPool.h" #include "CStack.h" #include "battle/BattleInfo.h" -#include "CTownHandler.h" #include "mapping/CMapInfo.h" #include "StartInfo.h" #include "CPlayerState.h" #include "TerrainHandler.h" +#include "entities/building/CBuilding.h" +#include "entities/building/TownFortifications.h" +#include "mapObjects/CBank.h" #include "mapObjects/CGCreature.h" #include "mapObjects/CGMarket.h" +#include "mapObjects/TownBuildingInstance.h" #include "mapObjects/CGTownInstance.h" #include "mapObjects/CQuest.h" #include "mapObjects/MiscObjects.h" #include "mapObjectConstructors/AObjectTypeHandler.h" #include "mapObjectConstructors/CObjectClassesHandler.h" #include "campaign/CampaignState.h" -#include "GameSettings.h" +#include "IGameSettings.h" +#include "mapObjects/FlaggableMapObject.h" VCMI_LIB_NAMESPACE_BEGIN @@ -88,6 +93,12 @@ bool CLobbyPackToServer::isForServer() const return true; } +void SaveLocalState::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitSaveLocalState(*this); +} + + void PackageApplied::visitTyped(ICPackVisitor & visitor) { visitor.visitPackageApplied(*this); @@ -123,6 +134,16 @@ void EntitiesChanged::visitTyped(ICPackVisitor & visitor) visitor.visitEntitiesChanged(*this); } +void SetRewardableConfiguration::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitSetRewardableConfiguration(*this); +} + +void SetBankConfiguration::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitSetBankConfiguration(*this); +} + void SetResources::visitTyped(ICPackVisitor & visitor) { visitor.visitSetResources(*this); @@ -148,6 +169,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); @@ -213,16 +238,6 @@ void UpdateArtHandlerLists::visitTyped(ICPackVisitor & visitor) visitor.visitUpdateArtHandlerLists(*this); } -void UpdateMapEvents::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitUpdateMapEvents(*this); -} - -void UpdateCastleEvents::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitUpdateCastleEvents(*this); -} - void ChangeFormation::visitTyped(ICPackVisitor & visitor) { visitor.visitChangeFormation(*this); @@ -333,16 +348,11 @@ void PutArtifact::visitTyped(ICPackVisitor & visitor) visitor.visitPutArtifact(*this); } -void EraseArtifact::visitTyped(ICPackVisitor & visitor) +void BulkEraseArtifacts::visitTyped(ICPackVisitor & visitor) { visitor.visitEraseArtifact(*this); } -void MoveArtifact::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitMoveArtifact(*this); -} - void BulkMoveArtifacts::visitTyped(ICPackVisitor & visitor) { visitor.visitBulkMoveArtifacts(*this); @@ -583,11 +593,21 @@ void BuildStructure::visitTyped(ICPackVisitor & visitor) visitor.visitBuildStructure(*this); } +void VisitTownBuilding::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitVisitTownBuilding(*this); +} + void RazeStructure::visitTyped(ICPackVisitor & visitor) { visitor.visitRazeStructure(*this); } +void SpellResearch::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitSpellResearch(*this); +} + void RecruitCreatures::visitTyped(ICPackVisitor & visitor) { visitor.visitRecruitCreatures(*this); @@ -783,6 +803,11 @@ void LobbySetPlayerName::visitTyped(ICPackVisitor & visitor) visitor.visitLobbySetPlayerName(*this); } +void LobbySetPlayerHandicap::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitLobbySetPlayerHandicap(*this); +} + void LobbySetSimturns::visitTyped(ICPackVisitor & visitor) { visitor.visitLobbySetSimturns(*this); @@ -818,7 +843,12 @@ void LobbyPvPAction::visitTyped(ICPackVisitor & visitor) visitor.visitLobbyPvPAction(*this); } -void SetResources::applyGs(CGameState * gs) const +void LobbyDelete::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitLobbyDelete(*this); +} + +void SetResources::applyGs(CGameState *gs) { assert(player.isValidPlayer()); if(abs) @@ -833,14 +863,14 @@ void SetResources::applyGs(CGameState * gs) const gs->getPlayerState(player)->resources.positive(); } -void SetPrimSkill::applyGs(CGameState * gs) const +void SetPrimSkill::applyGs(CGameState *gs) { CGHeroInstance * hero = gs->getHero(id); assert(hero); hero->setPrimarySkill(which, val, abs); } -void SetSecSkill::applyGs(CGameState * gs) const +void SetSecSkill::applyGs(CGameState *gs) { CGHeroInstance *hero = gs->getHero(id); hero->setSecSkillLevel(which, val, abs); @@ -875,7 +905,7 @@ void SetCommanderProperty::applyGs(CGameState *gs) } } -void AddQuest::applyGs(CGameState * gs) const +void AddQuest::applyGs(CGameState *gs) { assert (vstd::contains(gs->players, player)); auto * vec = &gs->players[player].quests; @@ -885,28 +915,17 @@ void AddQuest::applyGs(CGameState * gs) const logNetwork->warn("Warning! Attempt to add duplicated quest"); } -void UpdateArtHandlerLists::applyGs(CGameState * gs) const +void UpdateArtHandlerLists::applyGs(CGameState *gs) { gs->allocatedArtifacts = allocatedArtifacts; } -void UpdateMapEvents::applyGs(CGameState * gs) const -{ - gs->map->events = events; -} - -void UpdateCastleEvents::applyGs(CGameState * gs) const -{ - auto * t = gs->getTown(town); - t->events = events; -} - -void ChangeFormation::applyGs(CGameState * gs) const +void ChangeFormation::applyGs(CGameState *gs) { gs->getHero(hid)->setFormation(formation); } -void HeroVisitCastle::applyGs(CGameState * gs) const +void HeroVisitCastle::applyGs(CGameState *gs) { CGHeroInstance *h = gs->getHero(hid); CGTownInstance *t = gs->getTown(tid); @@ -932,7 +951,17 @@ void ChangeSpells::applyGs(CGameState *gs) hero->removeSpellFromSpellbook(sid); } -void SetMana::applyGs(CGameState * gs) const +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); @@ -946,7 +975,7 @@ void SetMana::applyGs(CGameState * gs) const vstd::amax(hero->mana, 0); //not less than 0 } -void SetMovePoints::applyGs(CGameState * gs) const +void SetMovePoints::applyGs(CGameState *gs) { CGHeroInstance *hero = gs->getHero(hid); @@ -963,7 +992,7 @@ void FoWChange::applyGs(CGameState *gs) TeamState * team = gs->getPlayerTeam(player); auto & fogOfWarMap = team->fogOfWarMap; for(const int3 & t : tiles) - (*fogOfWarMap)[t.z][t.x][t.y] = mode != ETileVisibility::HIDDEN; + fogOfWarMap[t.z][t.x][t.y] = mode != ETileVisibility::HIDDEN; if (mode == ETileVisibility::HIDDEN) //do not hide too much { @@ -986,7 +1015,7 @@ void FoWChange::applyGs(CGameState *gs) } } for(const int3 & t : tilesRevealed) //probably not the most optimal solution ever - (*fogOfWarMap)[t.z][t.x][t.y] = 1; + fogOfWarMap[t.z][t.x][t.y] = 1; } } @@ -1030,39 +1059,39 @@ void ChangeObjPos::applyGs(CGameState *gs) return; } gs->map->removeBlockVisTiles(obj); - obj->pos = nPos + obj->getVisitableOffset(); + obj->setAnchorPos(nPos + obj->getVisitableOffset()); gs->map->addBlockVisTiles(obj); } -void ChangeObjectVisitors::applyGs(CGameState * gs) const +void ChangeObjectVisitors::applyGs(CGameState *gs) { switch (mode) { - case VISITOR_ADD: + case VISITOR_ADD_HERO: + gs->getPlayerTeam(gs->getHero(hero)->tempOwner)->scoutedObjects.insert(object); gs->getHero(hero)->visitedObjects.insert(object); gs->getPlayerState(gs->getHero(hero)->tempOwner)->visitedObjects.insert(object); break; - case VISITOR_ADD_TEAM: - { - TeamState *ts = gs->getPlayerTeam(gs->getHero(hero)->tempOwner); - for(const auto & color : ts->players) - { - gs->getPlayerState(color)->visitedObjects.insert(object); - } - } + case VISITOR_ADD_PLAYER: + gs->getPlayerTeam(gs->getHero(hero)->tempOwner)->scoutedObjects.insert(object); + for(const auto & color : gs->getPlayerTeam(gs->getHero(hero)->tempOwner)->players) + gs->getPlayerState(color)->visitedObjects.insert(object); + break; case VISITOR_CLEAR: + // remove visit info from all heroes, including those that are not present on map for (CGHeroInstance * hero : gs->map->allHeroes) - { if (hero) - { - hero->visitedObjects.erase(object); // remove visit info from all heroes, including those that are not present on map - } - } + hero->visitedObjects.erase(object); for(auto &elem : gs->players) - { elem.second.visitedObjects.erase(object); - } + + for(auto &elem : gs->teams) + elem.second.scoutedObjects.erase(object); + + break; + case VISITOR_SCOUTED: + gs->getPlayerTeam(gs->getHero(hero)->tempOwner)->scoutedObjects.insert(object); break; case VISITOR_GLOBAL: @@ -1071,13 +1100,10 @@ void ChangeObjectVisitors::applyGs(CGameState * gs) const gs->getPlayerState(gs->getHero(hero)->tempOwner)->visitedObjectsGlobal.insert({objectPtr->ID, objectPtr->subID}); break; } - case VISITOR_REMOVE: - gs->getHero(hero)->visitedObjects.erase(object); - break; } } -void ChangeArtifactsCostume::applyGs(CGameState * gs) const +void ChangeArtifactsCostume::applyGs(CGameState *gs) { auto & allCostumes = gs->getPlayerState(player)->costumesArtifacts; if(const auto & costume = allCostumes.find(costumeIdx); costume != allCostumes.end()) @@ -1086,7 +1112,7 @@ void ChangeArtifactsCostume::applyGs(CGameState * gs) const allCostumes.try_emplace(costumeIdx, costumeSet); } -void PlayerEndsGame::applyGs(CGameState * gs) const +void PlayerEndsGame::applyGs(CGameState *gs) { PlayerState *p = gs->getPlayerState(player); if(victoryLossCheckResult.victory()) @@ -1159,7 +1185,6 @@ void RemoveBonus::applyGs(CGameState *gs) void RemoveObject::applyGs(CGameState *gs) { - CGObjectInstance *obj = gs->getObjInstance(objectID); logGlobal->debug("removing object id=%d; address=%x; name=%s", objectID, (intptr_t)obj, obj->getObjectName()); //unblock tiles @@ -1168,14 +1193,22 @@ void RemoveObject::applyGs(CGameState *gs) if (initiator.isValidPlayer()) gs->getPlayerState(initiator)->destroyedObjects.insert(objectID); + if(obj->getOwner().isValidPlayer()) + { + gs->getPlayerState(obj->getOwner())->removeOwnedObject(obj); //object removed via map event or hero got beaten + + FlaggableMapObject* flaggableObject = dynamic_cast(obj); + if(flaggableObject) + { + flaggableObject->markAsDeleted(); + } + } + if(obj->ID == Obj::HERO) //remove beaten hero { auto * beatenHero = dynamic_cast(obj); assert(beatenHero); - PlayerState * p = gs->getPlayerState(beatenHero->tempOwner); gs->map->heroesOnMap -= beatenHero; - p->heroes -= beatenHero; - auto * siegeNode = beatenHero->whereShouldBeAttachedOnSiege(gs); @@ -1188,7 +1221,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) @@ -1325,7 +1358,7 @@ void TryMoveHero::applyGs(CGameState *gs) auto & fogOfWarMap = gs->getPlayerTeam(h->getOwner())->fogOfWarMap; for(const int3 & t : fowRevealed) - (*fogOfWarMap)[t.z][t.x][t.y] = 1; + fogOfWarMap[t.z][t.x][t.y] = 1; } void NewStructures::applyGs(CGameState *gs) @@ -1334,21 +1367,11 @@ void NewStructures::applyGs(CGameState *gs) for(const auto & id : bid) { - assert(t->town->buildings.at(id) != nullptr); - t->builtBuildings.insert(id); - t->updateAppearance(); - auto currentBuilding = t->town->buildings.at(id); - - if(currentBuilding->overrideBids.empty()) - continue; - - for(const auto & overrideBid : currentBuilding->overrideBids) - { - t->overriddenBuildings.insert(overrideBid); - t->deleteTownBonus(overrideBid); - } + assert(t->getTown()->buildings.at(id) != nullptr); + t->addBuilding(id); } - t->builded = builded; + t->updateAppearance(); + t->built = built; t->recreateBuildingsBonuses(); } @@ -1357,7 +1380,7 @@ void RazeStructures::applyGs(CGameState *gs) CGTownInstance *t = gs->getTown(tid); for(const auto & id : bid) { - t->builtBuildings.erase(id); + t->removeBuilding(id); t->updateAppearance(); } @@ -1365,14 +1388,14 @@ void RazeStructures::applyGs(CGameState *gs) t->recreateBuildingsBonuses(); } -void SetAvailableCreatures::applyGs(CGameState * gs) const +void SetAvailableCreatures::applyGs(CGameState *gs) { auto * dw = dynamic_cast(gs->getObjInstance(tid)); assert(dw); dw->creatures = creatures; } -void SetHeroesInTown::applyGs(CGameState * gs) const +void SetHeroesInTown::applyGs(CGameState *gs) { CGTownInstance *t = gs->getTown(tid); @@ -1401,7 +1424,7 @@ void SetHeroesInTown::applyGs(CGameState * gs) const } } -void HeroRecruited::applyGs(CGameState * gs) const +void HeroRecruited::applyGs(CGameState *gs) { CGHeroInstance *h = gs->heroesPool->takeHeroFromPool(hid); CGTownInstance *t = gs->getTown(tid); @@ -1420,7 +1443,7 @@ void HeroRecruited::applyGs(CGameState * gs) const h->setOwner(player); h->pos = tile; - h->initObj(gs->getRandomGenerator()); + h->updateAppearance(); if(h->id == ObjectInstanceID()) { @@ -1431,7 +1454,7 @@ void HeroRecruited::applyGs(CGameState * gs) const gs->map->objects[h->id.getNum()] = h; gs->map->heroesOnMap.emplace_back(h); - p->heroes.emplace_back(h); + p->addOwnedObject(h); h->attachTo(*p); gs->map->addBlockVisTiles(h); @@ -1439,7 +1462,7 @@ void HeroRecruited::applyGs(CGameState * gs) const t->setVisitingHero(h); } -void GiveHero::applyGs(CGameState * gs) const +void GiveHero::applyGs(CGameState *gs) { CGHeroInstance *h = gs->getHero(id); @@ -1460,13 +1483,13 @@ void GiveHero::applyGs(CGameState * gs) const 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())->heroes.emplace_back(h); + gs->getPlayerState(h->getOwner())->addOwnedObject(h); gs->map->addBlockVisTiles(h); h->inTownGarrison = false; @@ -1474,74 +1497,21 @@ void GiveHero::applyGs(CGameState * gs) const void NewObject::applyGs(CGameState *gs) { - TerrainId terrainType = ETerrainId::NONE; + newObject->id = ObjectInstanceID(static_cast(gs->map->objects.size())); - if (!gs->isInTheMap(targetPos)) - { - logGlobal->error("Attempt to create object outside map at %s!", targetPos.toString()); - return; - } - - const TerrainTile & t = gs->map->getTile(targetPos); - terrainType = t.terType->getId(); - - auto handler = VLC->objtypeh->getHandlerFor(ID, subID); - - CGObjectInstance * o = handler->create(gs->callback, nullptr); - handler->configureObject(o, gs->getRandomGenerator()); - assert(o->ID == this->ID); - - if (ID == Obj::MONSTER) //probably more options will be needed - { - //CStackInstance hlp; - auto * cre = dynamic_cast(o); - //cre->slots[0] = hlp; - assert(cre); - cre->notGrowingTeam = cre->neverFlees = false; - cre->character = 2; - cre->gainedArtifact = ArtifactID::NONE; - cre->identifier = -1; - cre->addToSlot(SlotID(0), new CStackInstance(subID.getNum(), -1)); //add placeholder stack - } - - assert(!handler->getTemplates(terrainType).empty()); - if (handler->getTemplates().empty()) - { - logGlobal->error("Attempt to create object (%d %d) with no templates!", ID, subID.getNum()); - return; - } - - if (!handler->getTemplates(terrainType).empty()) - o->appearance = handler->getTemplates(terrainType).front(); - else - o->appearance = handler->getTemplates().front(); - - o->id = ObjectInstanceID(static_cast(gs->map->objects.size())); - o->pos = targetPos + o->getVisitableOffset(); - - gs->map->objects.emplace_back(o); - gs->map->addBlockVisTiles(o); - o->initObj(gs->getRandomGenerator()); + gs->map->objects.emplace_back(newObject); + gs->map->addBlockVisTiles(newObject); gs->map->calculateGuardingGreaturePositions(); - createdObjectID = o->id; - - logGlobal->debug("Added object id=%d; address=%x; name=%s", o->id, (intptr_t)o, o->getObjectName()); + logGlobal->debug("Added object id=%d; name=%s", newObject->id, newObject->getObjectName()); } void NewArtifact::applyGs(CGameState *gs) { - assert(!vstd::contains(gs->map->artInstances, art)); - assert(!art->getParentNodes().size()); - assert(art->artType); - - art->setType(art->artType); - if(art->isCombined()) - { - for(const auto & part : art->artType->getConstituents()) - art->addPart(ArtifactUtils::createNewArtifactInstance(part), ArtifactPosition::PRE_FIRST); - } + auto art = ArtifactUtils::createArtifact(artId, spellId); gs->map->addNewArtifactInstance(art); + PutArtifact pa(art->getId(), ArtifactLocation(artHolder, pos), false); + pa.applyGs(gs); } const CStackInstance * StackLocation::getStack() @@ -1575,7 +1545,7 @@ struct GetBase } }; -void ChangeStackCount::applyGs(CGameState * gs) +void ChangeStackCount::applyGs(CGameState *gs) { auto * srcObj = gs->getArmyInstance(army); if(!srcObj) @@ -1587,7 +1557,7 @@ void ChangeStackCount::applyGs(CGameState * gs) srcObj->changeStackCount(slot, count); } -void SetStackType::applyGs(CGameState * gs) +void SetStackType::applyGs(CGameState *gs) { auto * srcObj = gs->getArmyInstance(army); if(!srcObj) @@ -1596,7 +1566,7 @@ void SetStackType::applyGs(CGameState * gs) srcObj->setStackType(slot, type); } -void EraseStack::applyGs(CGameState * gs) +void EraseStack::applyGs(CGameState *gs) { auto * srcObj = gs->getArmyInstance(army); if(!srcObj) @@ -1605,7 +1575,7 @@ void EraseStack::applyGs(CGameState * gs) srcObj->eraseStack(slot); } -void SwapStacks::applyGs(CGameState * gs) +void SwapStacks::applyGs(CGameState *gs) { auto * srcObj = gs->getArmyInstance(srcArmy); if(!srcObj) @@ -1630,7 +1600,7 @@ void InsertNewStack::applyGs(CGameState *gs) throw std::runtime_error("InsertNewStack: invalid army object " + std::to_string(army.getNum()) + ", possible game state corruption."); } -void RebalanceStacks::applyGs(CGameState * gs) +void RebalanceStacks::applyGs(CGameState *gs) { auto * srcObj = gs->getArmyInstance(srcArmy); if(!srcObj) @@ -1645,7 +1615,7 @@ void RebalanceStacks::applyGs(CGameState * gs) const CCreature * srcType = src.army->getCreature(src.slot); TQuantity srcCount = src.army->getStackCount(src.slot); - bool stackExp = VLC->settings()->getBoolean(EGameSettings::MODULE_STACK_EXPERIENCE); + bool stackExp = gs->getSettings().getBoolean(EGameSettings::MODULE_STACK_EXPERIENCE); if(srcCount == count) //moving whole stack { @@ -1659,30 +1629,31 @@ 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 cna be lost :/ + //else - artifact can be lost :/ else { - EraseArtifact ea; - ea.al = ArtifactLocation(dstHero->id, ArtifactPosition::CREATURE_SLOT); - ea.al.creature = dst.slot; + BulkEraseArtifacts ea; + ea.artHolder = dstHero->id; + ea.posPack.emplace_back(ArtifactPosition::CREATURE_SLOT); + ea.creature = dst.slot; 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) @@ -1735,13 +1706,13 @@ void RebalanceStacks::applyGs(CGameState * gs) CBonusSystemNode::treeHasChanged(); } -void BulkRebalanceStacks::applyGs(CGameState * gs) +void BulkRebalanceStacks::applyGs(CGameState *gs) { for(auto & move : moves) move.applyGs(gs); } -void BulkSmartRebalanceStacks::applyGs(CGameState * gs) +void BulkSmartRebalanceStacks::applyGs(CGameState *gs) { for(auto & move : moves) move.applyGs(gs); @@ -1752,62 +1723,58 @@ 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)); - art->putAt(*hero, al.slot); + assert(ArtifactUtils::checkIfSlotValid(*hero, al.slot)); + gs->map->putArtifactInstance(*hero, art, al.slot); } -void EraseArtifact::applyGs(CGameState *gs) +void BulkEraseArtifacts::applyGs(CGameState *gs) { - const auto artSet = gs->getArtSet(al.artHolder); + const auto artSet = gs->getArtSet(artHolder); assert(artSet); - const auto slot = artSet->getSlot(al.slot); - if(slot->locked) - { - logGlobal->debug("Erasing locked artifact: %s", slot->artifact->artType->getNameTranslated()); - DisassembledArtifact dis; - dis.al.artHolder = al.artHolder; - - for(auto & slotInfo : artSet->artifactsWorn) + + std::sort(posPack.begin(), posPack.end(), [](const ArtifactPosition & slot0, const ArtifactPosition & slot1) -> bool { - auto art = slotInfo.second.artifact; - if(art->isCombined() && art->isPart(slot->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()); - dis.applyGs(gs); - } - else + return slot0.num > slot1.num; + }); + + for(const auto & slot : posPack) { - logGlobal->debug("Erasing artifact %s", slot->artifact->artType->getNameTranslated()); + const auto slotInfo = artSet->getSlot(slot); + if(slotInfo->locked) + { + 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->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)->getType()->getNameTranslated()); + dis.applyGs(gs); + } + else + { + logGlobal->debug("Erasing artifact %s", slotInfo->artifact->getType()->getNameTranslated()); + } + gs->map->removeArtifactInstance(*artSet, slot); } - auto art = artSet->getArt(al.slot); - assert(art); - art->removeFrom(*artSet, al.slot); } -void MoveArtifact::applyGs(CGameState * gs) +void BulkMoveArtifacts::applyGs(CGameState *gs) { - auto srcHero = gs->getArtSet(src); - auto dstHero = gs->getArtSet(dst); - assert(srcHero); - assert(dstHero); - auto art = srcHero->getArt(src.slot); - assert(art && art->canBePutAt(dstHero, dst.slot)); - art->move(*srcHero, src.slot, *dstHero, dst.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) @@ -1818,20 +1785,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); } }; @@ -1852,72 +1815,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->getSlotByInstance(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) @@ -1927,14 +1889,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); } @@ -1943,7 +1905,7 @@ void HeroVisit::applyGs(CGameState *gs) { } -void SetAvailableArtifacts::applyGs(CGameState * gs) const +void SetAvailableArtifacts::applyGs(CGameState *gs) { if(id != ObjectInstanceID::NONE) { @@ -1972,39 +1934,34 @@ void NewTurn::applyGs(CGameState *gs) gs->globalEffects.reduceBonusDurations(Bonus::OneWeek); //TODO not really a single root hierarchy, what about bonuses placed elsewhere? [not an issue with H3 mechanics but in the future...] - for(const NewTurn::Hero & h : heroes) //give mana/movement point - { - CGHeroInstance *hero = gs->getHero(h.id); - if(!hero) - { - logGlobal->error("Hero %d not found in NewTurn::applyGs", h.id.getNum()); - continue; - } + for(auto & manaPack : heroesMana) + manaPack.applyGs(gs); - hero->setMovementPoints(h.move); - hero->mana = h.mana; - } + for(auto & movePack : heroesMovement) + movePack.applyGs(gs); gs->heroesPool->onNewDay(); - for(const auto & re : res) + for(auto & entry : playerIncome) { - assert(re.first.isValidPlayer()); - gs->getPlayerState(re.first)->resources = re.second; - gs->getPlayerState(re.first)->resources.amin(GameConstants::PLAYER_RESOURCES_CAP); + gs->getPlayerState(entry.first)->resources += entry.second; + gs->getPlayerState(entry.first)->resources.amin(GameConstants::PLAYER_RESOURCES_CAP); } - for(const auto & creatureSet : cres) //set available creatures in towns - creatureSet.second.applyGs(gs); + for(auto & creatureSet : availableCreatures) //set available creatures in towns + creatureSet.applyGs(gs); for(CGTownInstance* t : gs->map->towns) - t->builded = 0; + { + t->built = 0; + t->spellResearchCounterDay = 0; + } - if(gs->getDate(Date::DAY_OF_WEEK) == 1) - gs->updateRumor(); + if(newRumor) + gs->currentRumor = *newRumor; } -void SetObjectProperty::applyGs(CGameState * gs) const +void SetObjectProperty::applyGs(CGameState *gs) { CGObjectInstance *obj = gs->getObjInstance(id); if(!obj) @@ -2014,6 +1971,18 @@ void SetObjectProperty::applyGs(CGameState * gs) const } auto * cai = dynamic_cast(obj); + + if(what == ObjProperty::OWNER && obj->asOwnable()) + { + PlayerColor oldOwner = obj->getOwner(); + PlayerColor newOwner = identifier.as(); + if(oldOwner.isValidPlayer()) + gs->getPlayerState(oldOwner)->removeOwnedObject(obj);; + + if(newOwner.isValidPlayer()) + gs->getPlayerState(newOwner)->addOwnedObject(obj);; + } + if(what == ObjProperty::OWNER && cai) { if(obj->ID == Obj::TOWN) @@ -2025,17 +1994,13 @@ void SetObjectProperty::applyGs(CGameState * gs) const if(oldOwner.isValidPlayer()) { auto * state = gs->getPlayerState(oldOwner); - state->towns -= t; - - if(state->towns.empty()) + if(state->getTowns().empty()) state->daysWithoutCastle = 0; } if(identifier.as().isValidPlayer()) { - PlayerState * p = gs->getPlayerState(identifier.as()); - p->towns.emplace_back(t); - //reset counter before NewTurn to avoid no town message if game loaded at turn when one already captured + PlayerState * p = gs->getPlayerState(identifier.as()); if(p->daysWithoutCastle) p->daysWithoutCastle = std::nullopt; } @@ -2052,14 +2017,14 @@ void SetObjectProperty::applyGs(CGameState * gs) const } } -void HeroLevelUp::applyGs(CGameState * gs) const +void HeroLevelUp::applyGs(CGameState *gs) { auto * hero = gs->getHero(heroId); assert(hero); hero->levelUp(skills); } -void CommanderLevelUp::applyGs(CGameState * gs) const +void CommanderLevelUp::applyGs(CGameState *gs) { auto * hero = gs->getHero(heroId); assert(hero); @@ -2068,7 +2033,7 @@ void CommanderLevelUp::applyGs(CGameState * gs) const commander->levelUp(); } -void BattleStart::applyGs(CGameState * gs) const +void BattleStart::applyGs(CGameState *gs) { assert(battleID == gs->nextBattleID); @@ -2080,17 +2045,17 @@ void BattleStart::applyGs(CGameState * gs) const gs->nextBattleID = BattleID(gs->nextBattleID.getNum() + 1); } -void BattleNextRound::applyGs(CGameState * gs) const +void BattleNextRound::applyGs(CGameState *gs) { gs->getBattle(battleID)->nextRound(); } -void BattleSetActiveStack::applyGs(CGameState * gs) const +void BattleSetActiveStack::applyGs(CGameState *gs) { gs->getBattle(battleID)->nextTurn(stack); } -void BattleTriggerEffect::applyGs(CGameState * gs) const +void BattleTriggerEffect::applyGs(CGameState *gs) { CStack * st = gs->getBattle(battleID)->getStack(stackID); assert(st); @@ -2129,13 +2094,13 @@ void BattleTriggerEffect::applyGs(CGameState * gs) const } } -void BattleUpdateGateState::applyGs(CGameState * gs) const +void BattleUpdateGateState::applyGs(CGameState *gs) { if(gs->getBattle(battleID)) gs->getBattle(battleID)->si.gateState = state; } -void BattleCancelled::applyGs(CGameState * gs) const +void BattleCancelled::applyGs(CGameState *gs) { auto currentBattle = boost::range::find_if(gs->currentBattles, [&](const auto & battle) { @@ -2146,7 +2111,7 @@ void BattleCancelled::applyGs(CGameState * gs) const gs->currentBattles.erase(currentBattle); } -void BattleResultAccepted::applyGs(CGameState * gs) const +void BattleResultAccepted::applyGs(CGameState *gs) { // Remove any "until next battle" bonuses for(auto & res : heroResult) @@ -2155,7 +2120,7 @@ void BattleResultAccepted::applyGs(CGameState * gs) const res.hero->removeBonusesRecursive(Bonus::OneBattle); } - if(winnerSide != 2) + if(winnerSide != BattleSide::NONE) { // Grow up growing artifacts const auto hero = heroResult[winnerSide].hero; @@ -2174,12 +2139,12 @@ void BattleResultAccepted::applyGs(CGameState * gs) const } } - if(VLC->settings()->getBoolean(EGameSettings::MODULE_STACK_EXPERIENCE)) + if(gs->getSettings().getBoolean(EGameSettings::MODULE_STACK_EXPERIENCE)) { - if(heroResult[0].army) - heroResult[0].army->giveStackExp(heroResult[0].exp); - if(heroResult[1].army) - heroResult[1].army->giveStackExp(heroResult[1].exp); + if(heroResult[BattleSide::ATTACKER].army) + heroResult[BattleSide::ATTACKER].army->giveStackExp(heroResult[BattleSide::ATTACKER].exp); + if(heroResult[BattleSide::DEFENDER].army) + heroResult[BattleSide::DEFENDER].army->giveStackExp(heroResult[BattleSide::DEFENDER].exp); CBonusSystemNode::treeHasChanged(); } @@ -2212,7 +2177,7 @@ void BattleStackMoved::applyBattle(IBattleState * battleState) battleState->moveUnit(stack, tilesToMove.back()); } -void BattleStackAttacked::applyGs(CGameState * gs) +void BattleStackAttacked::applyGs(CGameState *gs) { applyBattle(gs->getBattle(battleID)); } @@ -2222,7 +2187,7 @@ void BattleStackAttacked::applyBattle(IBattleState * battleState) battleState->setUnitState(newState.id, newState.data, newState.healthDelta); } -void BattleAttack::applyGs(CGameState * gs) +void BattleAttack::applyGs(CGameState *gs) { CStack * attacker = gs->getBattle(battleID)->getStack(stackAttacking); assert(attacker); @@ -2277,25 +2242,21 @@ void StartAction::applyGs(CGameState *gs) st->waiting = false; st->defendingAnim = false; st->movedThisRound = true; + st->castSpellThisTurn = ba.actionType == EActionType::MONSTER_SPELL; break; } } else { if(ba.actionType == EActionType::HERO_SPELL) - gs->getBattle(battleID)->sides[ba.side].usedSpellsHistory.push_back(ba.spell); + gs->getBattle(battleID)->getSide(ba.side).usedSpellsHistory.push_back(ba.spell); } } -void BattleSpellCast::applyGs(CGameState * gs) const +void BattleSpellCast::applyGs(CGameState *gs) { - if(castByHero) - { - if(side < 2) - { - gs->getBattle(battleID)->sides[side].castSpellsCount++; - } - } + if(castByHero && side != BattleSide::NONE) + gs->getBattle(battleID)->getSide(side).castSpellsCount++; } void SetStackEffect::applyGs(CGameState *gs) @@ -2357,7 +2318,7 @@ void BattleUnitsChanged::applyBattle(IBattleState * battleState) } } -void BattleObstaclesChanged::applyGs(CGameState * gs) +void BattleObstaclesChanged::applyGs(CGameState *gs) { applyBattle(gs->getBattle(battleID)); } @@ -2388,7 +2349,7 @@ CatapultAttack::CatapultAttack() = default; CatapultAttack::~CatapultAttack() = default; -void CatapultAttack::applyGs(CGameState * gs) +void CatapultAttack::applyGs(CGameState *gs) { applyBattle(gs->getBattle(battleID)); } @@ -2404,7 +2365,7 @@ void CatapultAttack::applyBattle(IBattleState * battleState) if(!town) return; - if(town->fortLevel() == CGTownInstance::NONE) + if(town->fortificationsLevel().wallsHealth == 0) return; for(const auto & part : attackedParts) @@ -2414,7 +2375,7 @@ void CatapultAttack::applyBattle(IBattleState * battleState) } } -void BattleSetStackProperty::applyGs(CGameState * gs) const +void BattleSetStackProperty::applyGs(CGameState *gs) { CStack * stack = gs->getBattle(battleID)->getStack(stackID, false); switch(which) @@ -2429,7 +2390,7 @@ void BattleSetStackProperty::applyGs(CGameState * gs) const } case ENCHANTER_COUNTER: { - auto & counter = gs->getBattle(battleID)->sides[gs->getBattle(battleID)->whatSide(stack->unitOwner())].enchanterCounter; + auto & counter = gs->getBattle(battleID)->getSide(gs->getBattle(battleID)->whatSide(stack->unitOwner())).enchanterCounter; if(absolute) counter = val; else @@ -2455,7 +2416,7 @@ void BattleSetStackProperty::applyGs(CGameState * gs) const } } -void PlayerCheated::applyGs(CGameState * gs) const +void PlayerCheated::applyGs(CGameState *gs) { if(!player.isValidPlayer()) return; @@ -2465,43 +2426,76 @@ void PlayerCheated::applyGs(CGameState * gs) const gs->getPlayerState(player)->cheated = true; } -void PlayerStartsTurn::applyGs(CGameState * gs) const +void PlayerStartsTurn::applyGs(CGameState *gs) { //assert(gs->actingPlayers.count(player) == 0);//Legal - may happen after loading of deserialized map state gs->actingPlayers.insert(player); } -void PlayerEndsTurn::applyGs(CGameState * gs) const +void PlayerEndsTurn::applyGs(CGameState *gs) { assert(gs->actingPlayers.count(player) == 1); gs->actingPlayers.erase(player); } -void DaysWithoutTown::applyGs(CGameState * gs) const +void DaysWithoutTown::applyGs(CGameState *gs) { auto & playerState = gs->players[player]; playerState.daysWithoutCastle = daysWithoutCastle; } -void TurnTimeUpdate::applyGs(CGameState *gs) const +void TurnTimeUpdate::applyGs(CGameState *gs) { auto & playerState = gs->players[player]; playerState.turnTimer = turnTimer; } -void EntitiesChanged::applyGs(CGameState * gs) +void EntitiesChanged::applyGs(CGameState *gs) { for(const auto & change : changes) gs->updateEntity(change.metatype, change.entityIndex, change.data); } +void SetRewardableConfiguration::applyGs(CGameState *gs) +{ + auto * objectPtr = gs->getObjInstance(objectID); + + if (!buildingID.hasValue()) + { + auto * rewardablePtr = dynamic_cast(objectPtr); + assert(rewardablePtr); + rewardablePtr->configuration = configuration; + rewardablePtr->initializeGuards(); + } + else + { + auto * townPtr = dynamic_cast(objectPtr); + TownBuildingInstance * buildingPtr = nullptr; + + for (auto & building : townPtr->rewardableBuildings) + if (building.second->getBuildingType() == buildingID) + buildingPtr = building.second; + + auto * rewardablePtr = dynamic_cast(buildingPtr); + assert(rewardablePtr); + rewardablePtr->configuration = configuration; + } +} + +void SetBankConfiguration::applyGs(CGameState *gs) +{ + auto * objectPtr = gs->getObjInstance(objectID); + auto * bankPtr = dynamic_cast(objectPtr); + + assert(bankPtr); + + bankPtr->setConfig(configuration); +} + const CArtifactInstance * ArtSlotInfo::getArt() const { if(locked) - { - logNetwork->warn("ArtifactLocation::getArt: This location is locked!"); return nullptr; - } return artifact; } diff --git a/lib/networkPacks/ObjProperty.h b/lib/networkPacks/ObjProperty.h index 67029d375..1e94c24c6 100644 --- a/lib/networkPacks/ObjProperty.h +++ b/lib/networkPacks/ObjProperty.h @@ -43,11 +43,9 @@ enum class ObjProperty : int8_t //creature-bank specific BANK_DAYCOUNTER, - BANK_RESET, BANK_CLEAR, //object with reward - REWARD_RANDOMIZE, REWARD_SELECT, REWARD_CLEARED }; diff --git a/lib/networkPacks/PacksForClient.h b/lib/networkPacks/PacksForClient.h index 866c8a9d3..9a0b20f45 100644 --- a/lib/networkPacks/PacksForClient.h +++ b/lib/networkPacks/PacksForClient.h @@ -18,12 +18,13 @@ #include "ObjProperty.h" #include "../CCreatureSet.h" -#include "../MetaString.h" #include "../ResourceSet.h" #include "../TurnTimerInfo.h" #include "../gameState/EVictoryLossCheckResult.h" +#include "../gameState/RumorState.h" #include "../gameState/QuestInfo.h" #include "../gameState/TavernSlot.h" +#include "../gameState/GameStatistics.h" #include "../int3.h" #include "../mapping/CMapDefines.h" #include "../spells/ViewSpellInt.h" @@ -56,6 +57,7 @@ struct DLL_LINKAGE PackageApplied : public CPackForClient { } void visitTyped(ICPackVisitor & visitor) override; + void applyGs(CGameState *gs) override {} ui8 result = 0; //0 - something went wrong, request hasn't been realized; 1 - OK ui32 packType = 0; //type id of applied package @@ -80,6 +82,7 @@ struct DLL_LINKAGE SystemMessage : public CPackForClient SystemMessage() = default; void visitTyped(ICPackVisitor & visitor) override; + void applyGs(CGameState *gs) override {} MetaString text; @@ -99,6 +102,7 @@ struct DLL_LINKAGE PlayerBlocked : public CPackForClient PlayerColor player; void visitTyped(ICPackVisitor & visitor) override; + void applyGs(CGameState *gs) override {} template void serialize(Handler & h) { @@ -110,7 +114,7 @@ struct DLL_LINKAGE PlayerBlocked : public CPackForClient struct DLL_LINKAGE PlayerCheated : public CPackForClient { - void applyGs(CGameState * gs) const; + void applyGs(CGameState * gs) override; PlayerColor player; bool losingCheatCode = false; @@ -128,7 +132,7 @@ struct DLL_LINKAGE PlayerCheated : public CPackForClient struct DLL_LINKAGE TurnTimeUpdate : public CPackForClient { - void applyGs(CGameState * gs) const; + void applyGs(CGameState * gs) override; PlayerColor player; TurnTimerInfo turnTimer; @@ -142,7 +146,7 @@ struct DLL_LINKAGE TurnTimeUpdate : public CPackForClient struct DLL_LINKAGE PlayerStartsTurn : public Query { - void applyGs(CGameState * gs) const; + void applyGs(CGameState * gs) override; PlayerColor player; @@ -157,7 +161,7 @@ struct DLL_LINKAGE PlayerStartsTurn : public Query struct DLL_LINKAGE DaysWithoutTown : public CPackForClient { - void applyGs(CGameState * gs) const; + void applyGs(CGameState * gs) override; PlayerColor player; std::optional daysWithoutCastle; @@ -175,7 +179,7 @@ struct DLL_LINKAGE EntitiesChanged : public CPackForClient { std::vector changes; - void applyGs(CGameState * gs); + void applyGs(CGameState * gs) override; void visitTyped(ICPackVisitor & visitor) override; @@ -187,7 +191,7 @@ struct DLL_LINKAGE EntitiesChanged : public CPackForClient struct DLL_LINKAGE SetResources : public CPackForClient { - void applyGs(CGameState * gs) const; + void applyGs(CGameState * gs) override; void visitTyped(ICPackVisitor & visitor) override; @@ -205,7 +209,7 @@ struct DLL_LINKAGE SetResources : public CPackForClient struct DLL_LINKAGE SetPrimSkill : public CPackForClient { - void applyGs(CGameState * gs) const; + void applyGs(CGameState * gs) override; void visitTyped(ICPackVisitor & visitor) override; @@ -225,7 +229,7 @@ struct DLL_LINKAGE SetPrimSkill : public CPackForClient struct DLL_LINKAGE SetSecSkill : public CPackForClient { - void applyGs(CGameState * gs) const; + void applyGs(CGameState * gs) override; void visitTyped(ICPackVisitor & visitor) override; @@ -245,7 +249,7 @@ struct DLL_LINKAGE SetSecSkill : public CPackForClient struct DLL_LINKAGE HeroVisitCastle : public CPackForClient { - void applyGs(CGameState * gs) const; + void applyGs(CGameState * gs) override; void visitTyped(ICPackVisitor & visitor) override; @@ -268,7 +272,7 @@ struct DLL_LINKAGE HeroVisitCastle : public CPackForClient struct DLL_LINKAGE ChangeSpells : public CPackForClient { - void applyGs(CGameState * gs); + void applyGs(CGameState * gs) override; void visitTyped(ICPackVisitor & visitor) override; @@ -284,12 +288,39 @@ struct DLL_LINKAGE ChangeSpells : public CPackForClient } }; -struct DLL_LINKAGE SetMana : public CPackForClient +struct DLL_LINKAGE SetResearchedSpells : public CPackForClient { - void applyGs(CGameState * gs) const; + 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; + + void visitTyped(ICPackVisitor & visitor) override; + + SetMana() = default; + SetMana(ObjectInstanceID hid, si32 val, bool absolute) + : hid(hid) + , val(val) + , absolute(absolute) + {} + ObjectInstanceID hid; si32 val = 0; bool absolute = true; @@ -304,7 +335,14 @@ struct DLL_LINKAGE SetMana : public CPackForClient struct DLL_LINKAGE SetMovePoints : public CPackForClient { - void applyGs(CGameState * gs) const; + void applyGs(CGameState * gs) override; + + SetMovePoints() = default; + SetMovePoints(ObjectInstanceID hid, si32 val, bool absolute) + : hid(hid) + , val(val) + , absolute(absolute) + {} ObjectInstanceID hid; si32 val = 0; @@ -322,7 +360,7 @@ struct DLL_LINKAGE SetMovePoints : public CPackForClient struct DLL_LINKAGE FoWChange : public CPackForClient { - void applyGs(CGameState * gs); + void applyGs(CGameState * gs) override; std::unordered_set tiles; PlayerColor player; @@ -346,7 +384,7 @@ struct DLL_LINKAGE SetAvailableHero : public CPackForClient { army.clearSlots(); } - void applyGs(CGameState * gs); + void applyGs(CGameState * gs) override; TavernHeroSlot slotID; TavernSlotRole roleID; @@ -377,7 +415,7 @@ struct DLL_LINKAGE GiveBonus : public CPackForClient { } - void applyGs(CGameState * gs); + void applyGs(CGameState * gs) override; ETarget who = ETarget::OBJECT; VariantIdentifier id; @@ -396,7 +434,7 @@ struct DLL_LINKAGE GiveBonus : public CPackForClient struct DLL_LINKAGE ChangeObjPos : public CPackForClient { - void applyGs(CGameState * gs); + void applyGs(CGameState * gs) override; /// Object to move ObjectInstanceID objid; @@ -417,7 +455,7 @@ struct DLL_LINKAGE ChangeObjPos : public CPackForClient struct DLL_LINKAGE PlayerEndsTurn : public CPackForClient { - void applyGs(CGameState * gs) const; + void applyGs(CGameState * gs) override; PlayerColor player; @@ -431,10 +469,11 @@ struct DLL_LINKAGE PlayerEndsTurn : public CPackForClient struct DLL_LINKAGE PlayerEndsGame : public CPackForClient { - void applyGs(CGameState * gs) const; + void applyGs(CGameState * gs) override; PlayerColor player; EVictoryLossCheckResult victoryLossCheckResult; + StatisticDataSet statistic; void visitTyped(ICPackVisitor & visitor) override; @@ -442,12 +481,14 @@ struct DLL_LINKAGE PlayerEndsGame : public CPackForClient { h & player; h & victoryLossCheckResult; + if (h.version >= Handler::Version::STATISTICS_SCREEN) + h & statistic; } }; struct DLL_LINKAGE PlayerReinitInterface : public CPackForClient { - void applyGs(CGameState * gs); + void applyGs(CGameState * gs) override; std::vector players; ui8 playerConnectionId; //PLAYER_AI for AI player @@ -468,7 +509,7 @@ struct DLL_LINKAGE RemoveBonus : public CPackForClient { } - void applyGs(CGameState * gs); + void applyGs(CGameState * gs) override; GiveBonus::ETarget who; //who receives bonus VariantIdentifier whoID; @@ -495,7 +536,7 @@ struct DLL_LINKAGE SetCommanderProperty : public CPackForClient { enum ECommanderProperty { ALIVE, BONUS, SECONDARY_SKILL, EXPERIENCE, SPECIAL_SKILL }; - void applyGs(CGameState * gs); + void applyGs(CGameState * gs) override; ObjectInstanceID heroid; @@ -518,7 +559,7 @@ struct DLL_LINKAGE SetCommanderProperty : public CPackForClient struct DLL_LINKAGE AddQuest : public CPackForClient { - void applyGs(CGameState * gs) const; + void applyGs(CGameState * gs) override; PlayerColor player; QuestInfo quest; @@ -536,7 +577,7 @@ struct DLL_LINKAGE UpdateArtHandlerLists : public CPackForClient { std::map allocatedArtifacts; - void applyGs(CGameState * gs) const; + void applyGs(CGameState * gs) override; void visitTyped(ICPackVisitor & visitor) override; template void serialize(Handler & h) @@ -545,40 +586,12 @@ struct DLL_LINKAGE UpdateArtHandlerLists : public CPackForClient } }; -struct DLL_LINKAGE UpdateMapEvents : public CPackForClient -{ - std::list events; - - void applyGs(CGameState * gs) const; - void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h) - { - h & events; - } -}; - -struct DLL_LINKAGE UpdateCastleEvents : public CPackForClient -{ - ObjectInstanceID town; - std::list events; - - void applyGs(CGameState * gs) const; - void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h) - { - h & town; - h & events; - } -}; - struct DLL_LINKAGE ChangeFormation : public CPackForClient { ObjectInstanceID hid; EArmyFormation formation{}; - void applyGs(CGameState * gs) const; + void applyGs(CGameState * gs) override; void visitTyped(ICPackVisitor & visitor) override; template void serialize(Handler & h) @@ -597,7 +610,7 @@ struct DLL_LINKAGE RemoveObject : public CPackForClient { } - void applyGs(CGameState * gs); + void applyGs(CGameState * gs) override; void visitTyped(ICPackVisitor & visitor) override; /// ID of removed object @@ -615,7 +628,7 @@ struct DLL_LINKAGE RemoveObject : public CPackForClient struct DLL_LINKAGE TryMoveHero : public CPackForClient { - void applyGs(CGameState * gs); + void applyGs(CGameState * gs) override; enum EResult { @@ -656,11 +669,11 @@ struct DLL_LINKAGE TryMoveHero : public CPackForClient struct DLL_LINKAGE NewStructures : public CPackForClient { - void applyGs(CGameState * gs); + void applyGs(CGameState * gs) override; ObjectInstanceID tid; std::set bid; - si16 builded = 0; + si16 built = 0; void visitTyped(ICPackVisitor & visitor) override; @@ -668,13 +681,13 @@ struct DLL_LINKAGE NewStructures : public CPackForClient { h & tid; h & bid; - h & builded; + h & built; } }; struct DLL_LINKAGE RazeStructures : public CPackForClient { - void applyGs(CGameState * gs); + void applyGs(CGameState * gs) override; ObjectInstanceID tid; std::set bid; @@ -692,7 +705,7 @@ struct DLL_LINKAGE RazeStructures : public CPackForClient struct DLL_LINKAGE SetAvailableCreatures : public CPackForClient { - void applyGs(CGameState * gs) const; + void applyGs(CGameState * gs) override; ObjectInstanceID tid; std::vector > > creatures; @@ -708,7 +721,7 @@ struct DLL_LINKAGE SetAvailableCreatures : public CPackForClient struct DLL_LINKAGE SetHeroesInTown : public CPackForClient { - void applyGs(CGameState * gs) const; + void applyGs(CGameState * gs) override; ObjectInstanceID tid; //id of town ObjectInstanceID visiting; //id of visiting hero @@ -726,7 +739,7 @@ struct DLL_LINKAGE SetHeroesInTown : public CPackForClient struct DLL_LINKAGE HeroRecruited : public CPackForClient { - void applyGs(CGameState * gs) const; + void applyGs(CGameState * gs) override; HeroTypeID hid; //subID of hero ObjectInstanceID tid; @@ -748,7 +761,7 @@ struct DLL_LINKAGE HeroRecruited : public CPackForClient struct DLL_LINKAGE GiveHero : public CPackForClient { - void applyGs(CGameState * gs) const; + void applyGs(CGameState * gs) override; ObjectInstanceID id; //object id ObjectInstanceID boatId; @@ -771,6 +784,7 @@ struct DLL_LINKAGE OpenWindow : public Query ObjectInstanceID visitor; void visitTyped(ICPackVisitor & visitor) override; + void applyGs(CGameState *gs) override {} template void serialize(Handler & h) { @@ -783,37 +797,29 @@ struct DLL_LINKAGE OpenWindow : public Query struct DLL_LINKAGE NewObject : public CPackForClient { - void applyGs(CGameState * gs); + void applyGs(CGameState * gs) override; /// Object ID to create - MapObjectID ID; - /// Object secondary ID to create - MapObjectSubID subID; - /// Position of visitable tile of created object - int3 targetPos; + CGObjectInstance * newObject; /// Which player initiated creation of this object PlayerColor initiator; - ObjectInstanceID createdObjectID; //used locally, filled during applyGs - void visitTyped(ICPackVisitor & visitor) override; template void serialize(Handler & h) { - h & ID; - subID.serializeIdentifier(h, ID); - h & targetPos; + h & newObject; h & initiator; } }; struct DLL_LINKAGE SetAvailableArtifacts : public CPackForClient { - void applyGs(CGameState * gs) const; + void applyGs(CGameState * gs) override; //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; @@ -835,7 +841,7 @@ struct DLL_LINKAGE ChangeStackCount : CGarrisonOperationPack TQuantity count; bool absoluteValue; //if not -> count will be added (or subtracted if negative) - void applyGs(CGameState * gs); + void applyGs(CGameState * gs) override; void visitTyped(ICPackVisitor & visitor) override; @@ -854,7 +860,7 @@ struct DLL_LINKAGE SetStackType : CGarrisonOperationPack SlotID slot; CreatureID type; - void applyGs(CGameState * gs); + void applyGs(CGameState * gs) override; void visitTyped(ICPackVisitor & visitor) override; @@ -871,7 +877,7 @@ struct DLL_LINKAGE EraseStack : CGarrisonOperationPack ObjectInstanceID army; SlotID slot; - void applyGs(CGameState * gs); + void applyGs(CGameState * gs) override; void visitTyped(ICPackVisitor & visitor) override; template void serialize(Handler & h) @@ -888,7 +894,7 @@ struct DLL_LINKAGE SwapStacks : CGarrisonOperationPack SlotID srcSlot; SlotID dstSlot; - void applyGs(CGameState * gs); + void applyGs(CGameState * gs) override; void visitTyped(ICPackVisitor & visitor) override; template void serialize(Handler & h) @@ -907,7 +913,7 @@ struct DLL_LINKAGE InsertNewStack : CGarrisonOperationPack CreatureID type; TQuantity count = 0; - void applyGs(CGameState * gs); + void applyGs(CGameState * gs) override; void visitTyped(ICPackVisitor & visitor) override; template void serialize(Handler & h) @@ -929,7 +935,7 @@ struct DLL_LINKAGE RebalanceStacks : CGarrisonOperationPack TQuantity count; - void applyGs(CGameState * gs); + void applyGs(CGameState * gs) override; void visitTyped(ICPackVisitor & visitor) override; template void serialize(Handler & h) @@ -946,7 +952,7 @@ struct DLL_LINKAGE BulkRebalanceStacks : CGarrisonOperationPack { std::vector moves; - void applyGs(CGameState * gs); + void applyGs(CGameState * gs) override; void visitTyped(ICPackVisitor & visitor) override; template @@ -961,7 +967,7 @@ struct DLL_LINKAGE BulkSmartRebalanceStacks : CGarrisonOperationPack std::vector moves; std::vector changes; - void applyGs(CGameState * gs); + void applyGs(CGameState * gs) override; void visitTyped(ICPackVisitor & visitor) override; template @@ -979,73 +985,59 @@ struct DLL_LINKAGE CArtifactOperationPack : CPackForClient struct DLL_LINKAGE PutArtifact : CArtifactOperationPack { PutArtifact() = default; - explicit PutArtifact(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); + void applyGs(CGameState * gs) override; void visitTyped(ICPackVisitor & visitor) override; template void serialize(Handler & h) { h & al; h & askAssemble; - h & art; + h & id; } }; struct DLL_LINKAGE NewArtifact : public CArtifactOperationPack { - ConstTransitivePtr art; + ObjectInstanceID artHolder; + ArtifactID artId; + SpellID spellId; + ArtifactPosition pos; - void applyGs(CGameState * gs); + void applyGs(CGameState * gs) override; void visitTyped(ICPackVisitor & visitor) override; template void serialize(Handler & h) { - h & art; + h & artHolder; + h & artId; + h & spellId; + h & pos; } }; -struct DLL_LINKAGE EraseArtifact : CArtifactOperationPack +struct DLL_LINKAGE BulkEraseArtifacts : CArtifactOperationPack { - ArtifactLocation al; + ObjectInstanceID artHolder; + std::vector posPack; + std::optional creature; - void applyGs(CGameState * gs); + void applyGs(CGameState * gs) override; void visitTyped(ICPackVisitor & visitor) override; template void serialize(Handler & h) { - h & al; - } -}; - -struct DLL_LINKAGE MoveArtifact : CArtifactOperationPack -{ - MoveArtifact() = default; - MoveArtifact(const PlayerColor & interfaceOwner, const ArtifactLocation & src, const ArtifactLocation & dst, bool askAssemble = true) - : interfaceOwner(interfaceOwner), src(src), dst(dst), askAssemble(askAssemble) - { - } - PlayerColor interfaceOwner; - ArtifactLocation src; - ArtifactLocation dst; - bool askAssemble = true; - - void applyGs(CGameState * gs); - void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h) - { - h & interfaceOwner; - h & src; - h & dst; - h & askAssemble; + h & artHolder; + h & posPack; + h & creature; } }; @@ -1055,17 +1047,20 @@ struct DLL_LINKAGE BulkMoveArtifacts : CArtifactOperationPack { ArtifactPosition srcPos; ArtifactPosition dstPos; + bool askAssemble; LinkedSlots() = default; - LinkedSlots(const ArtifactPosition & srcPos, const ArtifactPosition & dstPos) + LinkedSlots(const ArtifactPosition & srcPos, const ArtifactPosition & dstPos, bool askAssemble = false) : srcPos(srcPos) , dstPos(dstPos) + , askAssemble(askAssemble) { } template void serialize(Handler & h) { h & srcPos; h & dstPos; + h & askAssemble; } }; @@ -1079,8 +1074,6 @@ struct DLL_LINKAGE BulkMoveArtifacts : CArtifactOperationPack : interfaceOwner(PlayerColor::NEUTRAL) , srcArtHolder(ObjectInstanceID::NONE) , dstArtHolder(ObjectInstanceID::NONE) - , swap(false) - , askAssemble(false) , srcCreature(std::nullopt) , dstCreature(std::nullopt) { @@ -1089,19 +1082,15 @@ struct DLL_LINKAGE BulkMoveArtifacts : CArtifactOperationPack : interfaceOwner(interfaceOwner) , srcArtHolder(srcArtHolder) , dstArtHolder(dstArtHolder) - , swap(swap) - , askAssemble(false) , srcCreature(std::nullopt) , dstCreature(std::nullopt) { } - void applyGs(CGameState * gs); + void applyGs(CGameState * gs) override; std::vector artsPack0; std::vector artsPack1; - bool swap; - bool askAssemble; void visitTyped(ICPackVisitor & visitor) override; @@ -1114,24 +1103,22 @@ struct DLL_LINKAGE BulkMoveArtifacts : CArtifactOperationPack h & dstArtHolder; h & srcCreature; h & dstCreature; - h & swap; - h & askAssemble; } }; struct DLL_LINKAGE AssembledArtifact : CArtifactOperationPack { - ArtifactLocation al; //where assembly will be put - const CArtifact * builtArt; + ArtifactLocation al; + ArtifactID artId; - void applyGs(CGameState * gs); + void applyGs(CGameState * gs) override; void visitTyped(ICPackVisitor & visitor) override; template void serialize(Handler & h) { h & al; - h & builtArt; + h & artId; } }; @@ -1139,7 +1126,7 @@ struct DLL_LINKAGE DisassembledArtifact : CArtifactOperationPack { ArtifactLocation al; - void applyGs(CGameState * gs); + void applyGs(CGameState * gs) override; void visitTyped(ICPackVisitor & visitor) override; @@ -1157,7 +1144,7 @@ struct DLL_LINKAGE HeroVisit : public CPackForClient bool starting; //false -> ending - void applyGs(CGameState * gs); + void applyGs(CGameState * gs) override; void visitTyped(ICPackVisitor & visitor) override; @@ -1170,48 +1157,6 @@ struct DLL_LINKAGE HeroVisit : public CPackForClient } }; -struct DLL_LINKAGE NewTurn : public CPackForClient -{ - enum weekType { NORMAL, DOUBLE_GROWTH, BONUS_GROWTH, DEITYOFFIRE, PLAGUE, NO_ACTION }; - - void applyGs(CGameState * gs); - - void visitTyped(ICPackVisitor & visitor) override; - - struct Hero - { - ObjectInstanceID id; //id is a general serial id - ui32 move; - ui32 mana; - template void serialize(Handler & h) - { - h & id; - h & move; - h & mana; - } - bool operator<(const Hero & h)const { return id < h.id; } - }; - - std::set heroes; //updates movement and mana points - std::map res; //player ID => resource value[res_id] - std::map cres;//creatures to be placed in towns - ui32 day = 0; - ui8 specialWeek = 0; //weekType - CreatureID creatureid; //for creature weeks - - NewTurn() = default; - - template void serialize(Handler & h) - { - h & heroes; - h & cres; - h & res; - h & day; - h & specialWeek; - h & creatureid; - } -}; - struct DLL_LINKAGE InfoWindow : public CPackForClient //103 - displays simple info window { EInfoWindowMode type = EInfoWindowMode::MODAL; @@ -1221,6 +1166,7 @@ struct DLL_LINKAGE InfoWindow : public CPackForClient //103 - displays simple i ui16 soundID = 0; void visitTyped(ICPackVisitor & visitor) override; + void applyGs(CGameState * gs) override {} template void serialize(Handler & h) { @@ -1233,9 +1179,42 @@ struct DLL_LINKAGE InfoWindow : public CPackForClient //103 - displays simple i InfoWindow() = default; }; +struct DLL_LINKAGE NewTurn : public CPackForClient +{ + void applyGs(CGameState * gs) override; + + void visitTyped(ICPackVisitor & visitor) override; + + ui32 day = 0; + CreatureID creatureid; //for creature weeks + EWeekType specialWeek = EWeekType::NORMAL; + + std::vector heroesMovement; + std::vector heroesMana; + std::vector availableCreatures; + std::map playerIncome; + std::optional newRumor; // only on new weeks + std::optional newWeekNotification; // only on new week + + NewTurn() = default; + + template void serialize(Handler & h) + { + h & day; + h & creatureid; + h & specialWeek; + h & heroesMovement; + h & heroesMana; + h & availableCreatures; + h & playerIncome; + h & newRumor; + h & newWeekNotification; + } +}; + struct DLL_LINKAGE SetObjectProperty : public CPackForClient { - void applyGs(CGameState * gs) const; + void applyGs(CGameState * gs) override; ObjectInstanceID id; ObjProperty what{}; @@ -1257,17 +1236,17 @@ struct DLL_LINKAGE ChangeObjectVisitors : public CPackForClient { enum VisitMode { - VISITOR_ADD, // mark hero as one that have visited this object - VISITOR_ADD_TEAM, // mark team as one that have visited this object - VISITOR_GLOBAL, // mark player as one that have visited object of this type - VISITOR_REMOVE, // unmark visitor, reversed to ADD - VISITOR_CLEAR // clear all visitors from this object (object reset) + VISITOR_ADD_HERO, // mark hero as one that have visited this object + VISITOR_ADD_PLAYER, // mark player as one that have visited this object instance + VISITOR_GLOBAL, // mark player as one that have visited object of this type + VISITOR_SCOUTED, // marks targeted team as having scouted this object + VISITOR_CLEAR, // clear all visitors from this object (object reset) }; VisitMode mode = VISITOR_CLEAR; // uses VisitMode enum ObjectInstanceID object; ObjectInstanceID hero; // note: hero owner will be also marked as "visited" this object - void applyGs(CGameState * gs) const; + void applyGs(CGameState * gs) override; void visitTyped(ICPackVisitor & visitor) override; @@ -1294,7 +1273,7 @@ struct DLL_LINKAGE ChangeArtifactsCostume : public CPackForClient uint32_t costumeIdx = 0; const PlayerColor player = PlayerColor::NEUTRAL; - void applyGs(CGameState * gs) const; + void applyGs(CGameState * gs) override; void visitTyped(ICPackVisitor & visitor) override; ChangeArtifactsCostume() = default; @@ -1320,7 +1299,7 @@ struct DLL_LINKAGE HeroLevelUp : public Query PrimarySkill primskill = PrimarySkill::ATTACK; std::vector skills; - void applyGs(CGameState * gs) const; + void applyGs(CGameState * gs) override; void visitTyped(ICPackVisitor & visitor) override; @@ -1341,7 +1320,7 @@ struct DLL_LINKAGE CommanderLevelUp : public Query std::vector skills; //0-5 - secondary skills, val-100 - special skill - void applyGs(CGameState * gs) const; + void applyGs(CGameState * gs) override; void visitTyped(ICPackVisitor & visitor) override; @@ -1388,6 +1367,7 @@ struct DLL_LINKAGE BlockingDialog : public Query BlockingDialog() = default; void visitTyped(ICPackVisitor & visitor) override; + void applyGs(CGameState * gs) override {} template void serialize(Handler & h) { @@ -1407,6 +1387,7 @@ struct DLL_LINKAGE GarrisonDialog : public Query bool removableUnits = false; void visitTyped(ICPackVisitor & visitor) override; + void applyGs(CGameState * gs) override {} template void serialize(Handler & h) { @@ -1425,6 +1406,7 @@ struct DLL_LINKAGE ExchangeDialog : public Query ObjectInstanceID hero2; void visitTyped(ICPackVisitor & visitor) override; + void applyGs(CGameState * gs) override {} template void serialize(Handler & h) { @@ -1450,6 +1432,7 @@ struct DLL_LINKAGE TeleportDialog : public Query bool impassable = false; void visitTyped(ICPackVisitor & visitor) override; + void applyGs(CGameState * gs) override {} template void serialize(Handler & h) { @@ -1470,6 +1453,7 @@ struct DLL_LINKAGE MapObjectSelectDialog : public Query std::vector objects; void visitTyped(ICPackVisitor & visitor) override; + void applyGs(CGameState * gs) override {} template void serialize(Handler & h) { @@ -1494,6 +1478,7 @@ struct DLL_LINKAGE AdvmapSpellCast : public CPackForClient protected: void visitTyped(ICPackVisitor & visitor) override; + void applyGs(CGameState * gs) override {} }; struct DLL_LINKAGE ShowWorldViewEx : public CPackForClient @@ -1503,6 +1488,8 @@ struct DLL_LINKAGE ShowWorldViewEx : public CPackForClient std::vector objectPositions; + void applyGs(CGameState * gs) override {} + template void serialize(Handler & h) { h & player; @@ -1523,6 +1510,7 @@ struct DLL_LINKAGE PlayerMessageClient : public CPackForClient { } void visitTyped(ICPackVisitor & visitor) override; + void applyGs(CGameState * gs) override {} PlayerColor player; std::string text; @@ -1541,6 +1529,7 @@ struct DLL_LINKAGE CenterView : public CPackForClient ui32 focusTime = 0; //ms void visitTyped(ICPackVisitor & visitor) override; + void applyGs(CGameState * gs) override {} template void serialize(Handler & h) { diff --git a/lib/networkPacks/PacksForClientBattle.h b/lib/networkPacks/PacksForClientBattle.h index 239a248da..fcaf85457 100644 --- a/lib/networkPacks/PacksForClientBattle.h +++ b/lib/networkPacks/PacksForClientBattle.h @@ -11,8 +11,8 @@ #include "NetPacksBase.h" #include "BattleChanges.h" -#include "../MetaString.h" #include "../battle/BattleAction.h" +#include "../texts/MetaString.h" class CClient; @@ -25,7 +25,7 @@ class BattleInfo; struct DLL_LINKAGE BattleStart : public CPackForClient { - void applyGs(CGameState * gs) const; + void applyGs(CGameState * gs) override; BattleID battleID = BattleID::NONE; BattleInfo * info = nullptr; @@ -42,7 +42,7 @@ struct DLL_LINKAGE BattleStart : public CPackForClient struct DLL_LINKAGE BattleNextRound : public CPackForClient { - void applyGs(CGameState * gs) const; + void applyGs(CGameState * gs) override; BattleID battleID = BattleID::NONE; @@ -57,7 +57,7 @@ struct DLL_LINKAGE BattleNextRound : public CPackForClient struct DLL_LINKAGE BattleSetActiveStack : public CPackForClient { - void applyGs(CGameState * gs) const; + void applyGs(CGameState * gs) override; BattleID battleID = BattleID::NONE; ui32 stack = 0; @@ -76,7 +76,7 @@ struct DLL_LINKAGE BattleSetActiveStack : public CPackForClient struct DLL_LINKAGE BattleCancelled: public CPackForClient { - void applyGs(CGameState * gs) const; + void applyGs(CGameState * gs) override; BattleID battleID = BattleID::NONE; @@ -89,7 +89,7 @@ struct DLL_LINKAGE BattleCancelled: public CPackForClient struct DLL_LINKAGE BattleResultAccepted : public CPackForClient { - void applyGs(CGameState * gs) const; + void applyGs(CGameState * gs) override; struct HeroBattleResults { @@ -109,8 +109,8 @@ struct DLL_LINKAGE BattleResultAccepted : public CPackForClient }; BattleID battleID = BattleID::NONE; - std::array heroResult; - ui8 winnerSide; + BattleSideArray heroResult; + BattleSide winnerSide; template void serialize(Handler & h) { @@ -127,12 +127,13 @@ struct DLL_LINKAGE BattleResult : public Query BattleID battleID = BattleID::NONE; EBattleResult result = EBattleResult::NORMAL; - ui8 winner = 2; //0 - attacker, 1 - defender, [2 - draw (should be possible?)] - std::map casualties[2]; //first => casualties of attackers - map crid => number - TExpType exp[2] = {0, 0}; //exp for attacker and defender + BattleSide winner = BattleSide::NONE; //0 - attacker, 1 - defender, [2 - draw (should be possible?)] + BattleSideArray> casualties; //first => casualties of attackers - map crid => number + BattleSideArray exp{0,0}; //exp for attacker and defender std::set artifacts; //artifacts taken from loser to winner - currently unused void visitTyped(ICPackVisitor & visitor) override; + void applyGs(CGameState *gs) override {} template void serialize(Handler & h) { @@ -140,8 +141,7 @@ struct DLL_LINKAGE BattleResult : public Query h & queryID; h & result; h & winner; - h & casualties[0]; - h & casualties[1]; + h & casualties; h & exp; h & artifacts; assert(battleID != BattleID::NONE); @@ -153,7 +153,7 @@ struct DLL_LINKAGE BattleLogMessage : public CPackForClient BattleID battleID = BattleID::NONE; std::vector lines; - void applyGs(CGameState * gs); + void applyGs(CGameState * gs) override; void applyBattle(IBattleState * battleState); void visitTyped(ICPackVisitor & visitor) override; @@ -174,7 +174,7 @@ struct DLL_LINKAGE BattleStackMoved : public CPackForClient int distance = 0; bool teleporting = false; - void applyGs(CGameState * gs); + void applyGs(CGameState * gs) override; void applyBattle(IBattleState * battleState); void visitTyped(ICPackVisitor & visitor) override; @@ -192,7 +192,7 @@ struct DLL_LINKAGE BattleStackMoved : public CPackForClient struct DLL_LINKAGE BattleUnitsChanged : public CPackForClient { - void applyGs(CGameState * gs); + void applyGs(CGameState * gs) override; void applyBattle(IBattleState * battleState); BattleID battleID = BattleID::NONE; @@ -268,7 +268,7 @@ struct BattleStackAttacked struct DLL_LINKAGE BattleAttack : public CPackForClient { - void applyGs(CGameState * gs); + void applyGs(CGameState * gs) override; BattleUnitsChanged attackerChanges; BattleID battleID = BattleID::NONE; @@ -336,7 +336,7 @@ struct DLL_LINKAGE StartAction : public CPackForClient { } void applyFirstCl(CClient * cl); - void applyGs(CGameState * gs); + void applyGs(CGameState * gs) override; BattleID battleID = BattleID::NONE; BattleAction ba; @@ -354,6 +354,7 @@ struct DLL_LINKAGE StartAction : public CPackForClient struct DLL_LINKAGE EndAction : public CPackForClient { void visitTyped(ICPackVisitor & visitor) override; + void applyGs(CGameState *gs) override {} BattleID battleID = BattleID::NONE; @@ -365,11 +366,11 @@ struct DLL_LINKAGE EndAction : public CPackForClient struct DLL_LINKAGE BattleSpellCast : public CPackForClient { - void applyGs(CGameState * gs) const; + void applyGs(CGameState * gs) override; BattleID battleID = BattleID::NONE; bool activeCast = true; - ui8 side = 0; //which hero did cast spell: 0 - attacker, 1 - defender + BattleSide side = BattleSide::NONE; //which hero did cast spell SpellID spellID; //id of spell ui8 manaGained = 0; //mana channeling ability BattleHex tile; //destination tile (may not be set in some global/mass spells @@ -400,7 +401,7 @@ struct DLL_LINKAGE BattleSpellCast : public CPackForClient struct DLL_LINKAGE StacksInjured : public CPackForClient { - void applyGs(CGameState * gs); + void applyGs(CGameState * gs) override; void applyBattle(IBattleState * battleState); BattleID battleID = BattleID::NONE; @@ -421,6 +422,7 @@ struct DLL_LINKAGE BattleResultsApplied : public CPackForClient BattleID battleID = BattleID::NONE; PlayerColor player1, player2; void visitTyped(ICPackVisitor & visitor) override; + void applyGs(CGameState *gs) override {} template void serialize(Handler & h) { @@ -433,7 +435,7 @@ struct DLL_LINKAGE BattleResultsApplied : public CPackForClient struct DLL_LINKAGE BattleObstaclesChanged : public CPackForClient { - void applyGs(CGameState * gs); + void applyGs(CGameState * gs) override; void applyBattle(IBattleState * battleState); BattleID battleID = BattleID::NONE; @@ -468,7 +470,7 @@ struct DLL_LINKAGE CatapultAttack : public CPackForClient CatapultAttack(); ~CatapultAttack() override; - void applyGs(CGameState * gs); + void applyGs(CGameState * gs) override; void applyBattle(IBattleState * battleState); BattleID battleID = BattleID::NONE; @@ -490,7 +492,7 @@ struct DLL_LINKAGE BattleSetStackProperty : public CPackForClient { enum BattleStackProperty { CASTS, ENCHANTER_COUNTER, UNBIND, CLONED, HAS_CLONE }; - void applyGs(CGameState * gs) const; + void applyGs(CGameState * gs) override; BattleID battleID = BattleID::NONE; int stackID = 0; @@ -515,7 +517,7 @@ protected: ///activated at the beginning of turn struct DLL_LINKAGE BattleTriggerEffect : public CPackForClient { - void applyGs(CGameState * gs) const; //effect + void applyGs(CGameState * gs) override; //effect BattleID battleID = BattleID::NONE; int stackID = 0; @@ -539,7 +541,7 @@ protected: struct DLL_LINKAGE BattleUpdateGateState : public CPackForClient { - void applyGs(CGameState * gs) const; + void applyGs(CGameState * gs) override; BattleID battleID = BattleID::NONE; EGateState state = EGateState::NONE; diff --git a/lib/networkPacks/PacksForLobby.h b/lib/networkPacks/PacksForLobby.h index 23ecbdfd3..c702935c2 100644 --- a/lib/networkPacks/PacksForLobby.h +++ b/lib/networkPacks/PacksForLobby.h @@ -11,8 +11,8 @@ #include "StartInfo.h" #include "NetPacksBase.h" -#include "../MetaString.h" #include "../serializer/ESerializationVersion.h" +#include "../texts/MetaString.h" class CServerHandler; class CVCMIServer; @@ -55,18 +55,7 @@ struct DLL_LINKAGE LobbyClientConnected : public CLobbyPackToPropagate h & clientId; h & hostClientId; - - try - { - if (h.version >= Handler::Version::RELEASE_152) - h & version; - else - version = ESerializationVersion::RELEASE_150; - } - catch (const std::runtime_error & e) - { - version = ESerializationVersion::RELEASE_150; - } + h & version; } }; @@ -155,12 +144,13 @@ struct DLL_LINKAGE LobbyStartGame : public CLobbyPackToPropagate template void serialize(Handler &h) { + if (!h.saving) + h.loadingGamestate = true; h & clientId; h & initializedStartInfo; - bool sps = h.smartPointerSerialization; - h.smartPointerSerialization = true; h & initializedGameState; - h.smartPointerSerialization = sps; + if (!h.saving) + h.loadingGamestate = false; } }; @@ -180,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; } }; @@ -284,6 +276,20 @@ struct DLL_LINKAGE LobbySetPlayerName : public CLobbyPackToServer } }; +struct DLL_LINKAGE LobbySetPlayerHandicap : public CLobbyPackToServer +{ + PlayerColor color = PlayerColor::CANNOT_DETERMINE; + Handicap handicap = Handicap(); + + void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler &h) + { + h & color; + h & handicap; + } +}; + struct DLL_LINKAGE LobbySetSimturns : public CLobbyPackToServer { SimturnsInfo simturnsInfo; @@ -362,7 +368,9 @@ struct DLL_LINKAGE LobbyPvPAction : public CLobbyPackToServer { enum EAction : ui8 { NONE, COIN, RANDOM_TOWN, RANDOM_TOWN_VS - } action = NONE; + }; + + EAction action = NONE; std::vector bannedTowns; @@ -375,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 9db178053..d72be5265 100644 --- a/lib/networkPacks/PacksForServer.h +++ b/lib/networkPacks/PacksForServer.h @@ -280,11 +280,54 @@ struct DLL_LINKAGE BuildStructure : public CPackForServer } }; +struct DLL_LINKAGE VisitTownBuilding : public CPackForServer +{ + VisitTownBuilding() = default; + VisitTownBuilding(const ObjectInstanceID & TID, const BuildingID BID) + : tid(TID) + , bid(BID) + { + } + ObjectInstanceID tid; + BuildingID bid; + + void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h) + { + h & static_cast(*this); + h & tid; + h & bid; + } +}; + 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/networkPacks/SetRewardableConfiguration.h b/lib/networkPacks/SetRewardableConfiguration.h new file mode 100644 index 000000000..cc1865f0b --- /dev/null +++ b/lib/networkPacks/SetRewardableConfiguration.h @@ -0,0 +1,51 @@ +/* + * SetRewardableConfiguration.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 "../rewardable/Configuration.h" +#include "../mapObjectConstructors/CBankInstanceConstructor.h" + +VCMI_LIB_NAMESPACE_BEGIN + +struct DLL_LINKAGE SetRewardableConfiguration : public CPackForClient +{ + void applyGs(CGameState * gs) override; + void visitTyped(ICPackVisitor & visitor) override; + + ObjectInstanceID objectID; + BuildingID buildingID; + Rewardable::Configuration configuration; + + template void serialize(Handler & h) + { + h & objectID; + h & buildingID; + h & configuration; + } +}; + +struct DLL_LINKAGE SetBankConfiguration : public CPackForClient +{ + void applyGs(CGameState * gs) override; + void visitTyped(ICPackVisitor & visitor) override; + + ObjectInstanceID objectID; + BankConfig configuration; + + template void serialize(Handler & h) + { + h & objectID; + h & configuration; + } +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/networkPacks/SetStackEffect.h b/lib/networkPacks/SetStackEffect.h index af6196353..484d94f4a 100644 --- a/lib/networkPacks/SetStackEffect.h +++ b/lib/networkPacks/SetStackEffect.h @@ -19,7 +19,7 @@ class IBattleState; struct DLL_LINKAGE SetStackEffect : public CPackForClient { - void applyGs(CGameState * gs); + void applyGs(CGameState * gs) override; void applyBattle(IBattleState * battleState); BattleID battleID = BattleID::NONE; diff --git a/lib/pathfinder/CGPathNode.h b/lib/pathfinder/CGPathNode.h index a850761b1..2598633cc 100644 --- a/lib/pathfinder/CGPathNode.h +++ b/lib/pathfinder/CGPathNode.h @@ -59,18 +59,22 @@ enum class EPathNodeAction : ui8 struct DLL_LINKAGE CGPathNode { + using TFibHeap = boost::heap::fibonacci_heap>>; using ELayer = EPathfindingLayer; + TFibHeap::handle_type pqHandle; + TFibHeap * pq; CGPathNode * theNodeBefore; + int3 coord; //coordinates ELayer layer; + + float cost; //total cost of the path to this tile measured in turns with fractions int moveRemains; //remaining movement points after hero reaches the tile ui8 turns; //how many turns we have to wait before reaching the tile - 0 means current turn - EPathAccessibility accessible; EPathNodeAction action; bool locked; - bool inPQ; CGPathNode() : coord(-1), @@ -89,9 +93,14 @@ struct DLL_LINKAGE CGPathNode cost = std::numeric_limits::max(); turns = 255; theNodeBefore = nullptr; - action = EPathNodeAction::UNKNOWN; - inPQ = false; pq = nullptr; + action = EPathNodeAction::UNKNOWN; + } + + STRONG_INLINE + bool inPQ() const + { + return pq != nullptr; } STRONG_INLINE @@ -109,7 +118,7 @@ struct DLL_LINKAGE CGPathNode bool getUpNode = value < cost; cost = value; // If the node is in the heap, update the heap. - if(inPQ && pq != nullptr) + if(inPQ()) { if(getUpNode) { @@ -155,14 +164,6 @@ struct DLL_LINKAGE CGPathNode return true; } - - using TFibHeap = boost::heap::fibonacci_heap>>; - - TFibHeap::handle_type pqHandle; - TFibHeap* pq; - -private: - float cost; //total cost of the path to this tile measured in turns with fractions }; struct DLL_LINKAGE CGPath diff --git a/lib/pathfinder/CPathfinder.cpp b/lib/pathfinder/CPathfinder.cpp index 397edfd5a..7a7fc1af6 100644 --- a/lib/pathfinder/CPathfinder.cpp +++ b/lib/pathfinder/CPathfinder.cpp @@ -82,9 +82,8 @@ CPathfinder::CPathfinder(CGameState * _gs, std::shared_ptr con void CPathfinder::push(CGPathNode * node) { - if(node && !node->inPQ) + if(node && !node->inPQ()) { - node->inPQ = true; node->pq = &this->pq; auto handle = pq.push(node); node->pqHandle = handle; @@ -96,14 +95,13 @@ CGPathNode * CPathfinder::topAndPop() auto * node = pq.top(); pq.pop(); - node->inPQ = false; node->pq = nullptr; return node; } void CPathfinder::calculatePaths() { - //logGlobal->info("Calculating paths for hero %s (adress %d) of player %d", hero->name, hero , hero->tempOwner); + //logGlobal->info("Calculating paths for hero %s (address %d) of player %d", hero->name, hero , hero->tempOwner); //initial tile - set cost on 0 and add to the queue std::vector initialNodes = config->nodeStorage->getInitialNodes(); @@ -175,7 +173,7 @@ void CPathfinder::calculatePaths() if(!hlp->isPatrolMovementAllowed(neighbour->coord)) continue; - /// Check transition without tile accessability rules + /// Check transition without tile accessibility rules if(source.node->layer != neighbour->layer && !isLayerTransitionPossible()) continue; @@ -266,8 +264,7 @@ TeleporterTilesVector CPathfinderHelper::getCastleGates(const PathNodeInfo & sou { TeleporterTilesVector allowedExits; - auto towns = getPlayerState(hero->tempOwner)->towns; - for(const auto & town : towns) + for(const auto & town : getPlayerState(hero->tempOwner)->getTowns()) { if(town->id != source.nodeObject->id && town->visitingHero == nullptr && town->hasBuilt(BuildingID::CASTLE_GATE, ETownType::INFERNO)) @@ -295,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 @@ -468,7 +465,7 @@ bool CPathfinderHelper::addTeleportOneWayRandom(const CGTeleport * obj) const bool CPathfinderHelper::addTeleportWhirlpool(const CGWhirlpool * obj) const { - return options.useTeleportWhirlpool && hasBonusOfType(BonusType::WHIRLPOOL_PROTECTION) && obj; + return options.useTeleportWhirlpool && (whirlpoolProtection || options.forceUseTeleportWhirlpool) && obj; } int CPathfinderHelper::movementPointsAfterEmbark(int movement, int basicCost, bool disembark) const @@ -508,6 +505,8 @@ CPathfinderHelper::CPathfinderHelper(CGameState * gs, const CGHeroInstance * Her updateTurnInfo(); initializePatrol(); + whirlpoolProtection = Hero->hasBonusOfType(BonusType::WHIRLPOOL_PROTECTION); + SpellID flySpell = SpellID::FLY; canCastFly = Hero->canCastThisSpell(flySpell.toSpell()); @@ -597,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); } @@ -663,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); @@ -704,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/CPathfinder.h b/lib/pathfinder/CPathfinder.h index 518244b6d..4791a450f 100644 --- a/lib/pathfinder/CPathfinder.h +++ b/lib/pathfinder/CPathfinder.h @@ -85,6 +85,7 @@ public: const PathfinderOptions & options; bool canCastFly; bool canCastWaterWalk; + bool whirlpoolProtection; CPathfinderHelper(CGameState * gs, const CGHeroInstance * Hero, const PathfinderOptions & Options); virtual ~CPathfinderHelper(); 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/PathfinderOptions.cpp b/lib/pathfinder/PathfinderOptions.cpp index 675731c8c..51dbdddb3 100644 --- a/lib/pathfinder/PathfinderOptions.cpp +++ b/lib/pathfinder/PathfinderOptions.cpp @@ -10,7 +10,8 @@ #include "StdInc.h" #include "PathfinderOptions.h" -#include "../GameSettings.h" +#include "../gameState/CGameState.h" +#include "../IGameSettings.h" #include "../VCMI_Lib.h" #include "NodeStorage.h" #include "PathfindingRules.h" @@ -18,28 +19,30 @@ VCMI_LIB_NAMESPACE_BEGIN -PathfinderOptions::PathfinderOptions() +PathfinderOptions::PathfinderOptions(const CGameInfoCallback * cb) : useFlying(true) , useWaterWalking(true) - , ignoreGuards(VLC->settings()->getBoolean(EGameSettings::PATHFINDER_IGNORE_GUARDS)) - , useEmbarkAndDisembark(VLC->settings()->getBoolean(EGameSettings::PATHFINDER_USE_BOAT)) - , useTeleportTwoWay(VLC->settings()->getBoolean(EGameSettings::PATHFINDER_USE_MONOLITH_TWO_WAY)) - , useTeleportOneWay(VLC->settings()->getBoolean(EGameSettings::PATHFINDER_USE_MONOLITH_ONE_WAY_UNIQUE)) - , useTeleportOneWayRandom(VLC->settings()->getBoolean(EGameSettings::PATHFINDER_USE_MONOLITH_ONE_WAY_RANDOM)) - , useTeleportWhirlpool(VLC->settings()->getBoolean(EGameSettings::PATHFINDER_USE_WHIRLPOOL)) - , originalFlyRules(VLC->settings()->getBoolean(EGameSettings::PATHFINDER_ORIGINAL_FLY_RULES)) + , ignoreGuards(cb->getSettings().getBoolean(EGameSettings::PATHFINDER_IGNORE_GUARDS)) + , useEmbarkAndDisembark(cb->getSettings().getBoolean(EGameSettings::PATHFINDER_USE_BOAT)) + , useTeleportTwoWay(cb->getSettings().getBoolean(EGameSettings::PATHFINDER_USE_MONOLITH_TWO_WAY)) + , useTeleportOneWay(cb->getSettings().getBoolean(EGameSettings::PATHFINDER_USE_MONOLITH_ONE_WAY_UNIQUE)) + , useTeleportOneWayRandom(cb->getSettings().getBoolean(EGameSettings::PATHFINDER_USE_MONOLITH_ONE_WAY_RANDOM)) + , useTeleportWhirlpool(cb->getSettings().getBoolean(EGameSettings::PATHFINDER_USE_WHIRLPOOL)) + , originalFlyRules(cb->getSettings().getBoolean(EGameSettings::PATHFINDER_ORIGINAL_FLY_RULES)) , useCastleGate(false) , lightweightFlyingMode(false) , oneTurnSpecialLayersLimit(true) , turnLimit(std::numeric_limits::max()) , canUseCast(false) , allowLayerTransitioningAfterBattle(false) + , forceUseTeleportWhirlpool(false) { } -PathfinderConfig::PathfinderConfig(std::shared_ptr nodeStorage, std::vector> rules): +PathfinderConfig::PathfinderConfig(std::shared_ptr nodeStorage, const CGameInfoCallback * callback, std::vector> rules): nodeStorage(std::move(nodeStorage)), - rules(std::move(rules)) + rules(std::move(rules)), + options(callback) { } @@ -57,7 +60,7 @@ std::vector> SingleHeroPathfinderConfig::build SingleHeroPathfinderConfig::~SingleHeroPathfinderConfig() = default; SingleHeroPathfinderConfig::SingleHeroPathfinderConfig(CPathsInfo & out, CGameState * gs, const CGHeroInstance * hero) - : PathfinderConfig(std::make_shared(out, hero), buildRuleSet()) + : PathfinderConfig(std::make_shared(out, hero), gs, buildRuleSet()) { pathfinderHelper = std::make_unique(gs, hero, options); } diff --git a/lib/pathfinder/PathfinderOptions.h b/lib/pathfinder/PathfinderOptions.h index 044511910..d7c39d4f5 100644 --- a/lib/pathfinder/PathfinderOptions.h +++ b/lib/pathfinder/PathfinderOptions.h @@ -16,7 +16,7 @@ class IPathfindingRule; class CPathfinderHelper; class CGameState; class CGHeroInstance; - +class CGameInfoCallback; struct PathNodeInfo; struct CPathsInfo; @@ -30,6 +30,7 @@ struct DLL_LINKAGE PathfinderOptions bool useTeleportOneWay; // One-way monoliths with one known exit only bool useTeleportOneWayRandom; // One-way monoliths with more than one known exit bool useTeleportWhirlpool; // Force enabled if hero protected or unaffected (have one stack of one creature) + bool forceUseTeleportWhirlpool; // Force enabled if hero protected or unaffected (have one stack of one creature) /// TODO: Find out with client and server code, merge with normal teleporters. /// Likely proper implementation would require some refactoring of CGTeleport. @@ -84,7 +85,7 @@ struct DLL_LINKAGE PathfinderOptions ///
bool allowLayerTransitioningAfterBattle; - PathfinderOptions(); + PathfinderOptions(const CGameInfoCallback * callback); }; class DLL_LINKAGE PathfinderConfig @@ -96,6 +97,7 @@ public: PathfinderConfig( std::shared_ptr nodeStorage, + const CGameInfoCallback * callback, std::vector> rules); virtual ~PathfinderConfig() = default; diff --git a/lib/pathfinder/PathfinderUtil.h b/lib/pathfinder/PathfinderUtil.h index fa5f5ae05..3e1f4cbaf 100644 --- a/lib/pathfinder/PathfinderUtil.h +++ b/lib/pathfinder/PathfinderUtil.h @@ -19,20 +19,20 @@ VCMI_LIB_NAMESPACE_BEGIN namespace PathfinderUtil { - using FoW = std::unique_ptr>; + using FoW = boost::multi_array; using ELayer = EPathfindingLayer; template EPathAccessibility evaluateAccessibility(const int3 & pos, const TerrainTile & tinfo, const FoW & fow, const PlayerColor player, const CGameState * gs) { - if(!(*fow)[pos.z][pos.x][pos.y]) + if(!fow[pos.z][pos.x][pos.y]) return EPathAccessibility::BLOCKED; switch(layer) { 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/pathfinder/TurnInfo.cpp b/lib/pathfinder/TurnInfo.cpp index 1f32139a6..b260efafa 100644 --- a/lib/pathfinder/TurnInfo.cpp +++ b/lib/pathfinder/TurnInfo.cpp @@ -41,7 +41,7 @@ TurnInfo::TurnInfo(const CGHeroInstance * Hero, const int turn): maxMovePointsWater(-1), turn(turn) { - bonuses = hero->getAllBonuses(Selector::days(turn), Selector::all, nullptr, ""); + bonuses = hero->getAllBonuses(Selector::days(turn), Selector::all, ""); bonusCache = std::make_unique(bonuses); nativeTerrain = hero->getNativeTerrain(); } @@ -144,7 +144,7 @@ void TurnInfo::updateHeroBonuses(BonusType type, const CSelector& sel) const bonusCache->pathfindingVal = bonuses->valOfBonuses(Selector::type()(BonusType::ROUGH_TERRAIN_DISCOUNT)); break; default: - bonuses = hero->getAllBonuses(Selector::days(turn), Selector::all, nullptr, ""); + bonuses = hero->getAllBonuses(Selector::days(turn), Selector::all, ""); } } diff --git a/lib/registerTypes/RegisterTypes.h b/lib/registerTypes/RegisterTypes.h deleted file mode 100644 index 2c2284452..000000000 --- a/lib/registerTypes/RegisterTypes.h +++ /dev/null @@ -1,28 +0,0 @@ -/* - * RegisterTypes.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 "RegisterTypesClientPacks.h" -#include "RegisterTypesLobbyPacks.h" -#include "RegisterTypesMapObjects.h" -#include "RegisterTypesServerPacks.h" - -VCMI_LIB_NAMESPACE_BEGIN - -template -void registerTypes(Serializer &s) -{ - registerTypesMapObjects(s); - registerTypesClientPacks(s); - registerTypesServerPacks(s); - registerTypesLobbyPacks(s); -} - -VCMI_LIB_NAMESPACE_END diff --git a/lib/registerTypes/RegisterTypesClientPacks.h b/lib/registerTypes/RegisterTypesClientPacks.h deleted file mode 100644 index 9bb4fb30d..000000000 --- a/lib/registerTypes/RegisterTypesClientPacks.h +++ /dev/null @@ -1,125 +0,0 @@ -/* - * RegisterTypesClientPacks.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 "../networkPacks/PacksForClient.h" -#include "../networkPacks/PacksForClientBattle.h" -#include "../networkPacks/SetStackEffect.h" - -VCMI_LIB_NAMESPACE_BEGIN - -template -void registerTypesClientPacks(Serializer &s) -{ - s.template registerType(); - - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - - s.template registerType(); - s.template registerType(); - s.template registerType(); -} - -VCMI_LIB_NAMESPACE_END diff --git a/lib/registerTypes/RegisterTypesLobbyPacks.h b/lib/registerTypes/RegisterTypesLobbyPacks.h deleted file mode 100644 index 6859f5d9f..000000000 --- a/lib/registerTypes/RegisterTypesLobbyPacks.h +++ /dev/null @@ -1,65 +0,0 @@ -/* - * RegisterTypesLobbyPacks.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 "../networkPacks/PacksForLobby.h" -#include "../gameState/CGameState.h" -#include "../campaign/CampaignState.h" -#include "../mapping/CMapInfo.h" -#include "../rmg/CMapGenOptions.h" -#include "../gameState/TavernHeroesPool.h" -#include "../gameState/CGameStateCampaign.h" -#include "../mapping/CMap.h" -#include "../TerrainHandler.h" -#include "../RiverHandler.h" -#include "../RoadHandler.h" - -VCMI_LIB_NAMESPACE_BEGIN - -template -void registerTypesLobbyPacks(Serializer &s) -{ - s.template registerType(); - s.template registerType(); - s.template registerType(); - - // Any client can sent - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - // Only host client send - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - // Only server send - s.template registerType(); - s.template registerType(); - - // For client with permissions - s.template registerType(); - // Only for host client - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); -} - -VCMI_LIB_NAMESPACE_END diff --git a/lib/registerTypes/RegisterTypesMapObjects.h b/lib/registerTypes/RegisterTypesMapObjects.h deleted file mode 100644 index 5e8d96f32..000000000 --- a/lib/registerTypes/RegisterTypesMapObjects.h +++ /dev/null @@ -1,139 +0,0 @@ -/* - * RegisterTypesMapObjects.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 "../mapObjectConstructors/CBankInstanceConstructor.h" -#include "../mapObjects/MapObjects.h" -#include "../mapObjects/CGCreature.h" -#include "../mapObjects/CGTownBuilding.h" -#include "../mapObjects/ObjectTemplate.h" -#include "../battle/BattleInfo.h" -#include "../battle/CObstacleInstance.h" -#include "../bonuses/Limiters.h" -#include "../bonuses/Updaters.h" -#include "../bonuses/Propagators.h" -#include "../CPlayerState.h" -#include "../CStack.h" -#include "../CHeroHandler.h" - -VCMI_LIB_NAMESPACE_BEGIN - -template -void registerTypesMapObjects(Serializer &s) -{ - ////////////////////////////////////////////////////////////////////////// - // Adventure map objects - ////////////////////////////////////////////////////////////////////////// - s.template registerType(); - - // Non-armed objects - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - - s.template registerType(); s.template registerType(); s.template registerType(); - - // Armed objects - s.template registerType(); s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); s.template registerType(); - s.template registerType(); - - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - - s.template registerType(); - s.template registerType(); - s.template registerType(); - //new types (other than netpacks) must register here - //order of type registration is critical for loading old savegames - - //Other object-related - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - - s.template registerType(); - - s.template registerType(); - s.template registerType(); - - //end of objects - - ////////////////////////////////////////////////////////////////////////// - // Bonus system - ////////////////////////////////////////////////////////////////////////// - //s.template registerType(); - s.template registerType(); - - // Limiters - //s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - -// s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - //s.template registerType(); //TODO - //s.template registerType(); - s.template registerType(); - s.template registerType(); - //s.template registerType(); - s.template registerType(); - - //s.template registerType(); - s.template registerType(); - - s.template registerType(); -} - -VCMI_LIB_NAMESPACE_END diff --git a/lib/registerTypes/RegisterTypesServerPacks.h b/lib/registerTypes/RegisterTypesServerPacks.h deleted file mode 100644 index de13eae8e..000000000 --- a/lib/registerTypes/RegisterTypesServerPacks.h +++ /dev/null @@ -1,58 +0,0 @@ -/* - * RegisterTypesServerPacks.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 "../networkPacks/PacksForServer.h" - -VCMI_LIB_NAMESPACE_BEGIN - -class BinarySerializer; -class BinaryDeserializer; -class CTypeList; - -template -void registerTypesServerPacks(Serializer &s) -{ - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); -} - -VCMI_LIB_NAMESPACE_END diff --git a/lib/rewardable/Configuration.cpp b/lib/rewardable/Configuration.cpp index 9eb95649a..e866d627d 100644 --- a/lib/rewardable/Configuration.cpp +++ b/lib/rewardable/Configuration.cpp @@ -103,6 +103,7 @@ void Rewardable::Configuration::serializeJson(JsonSerializeFormat & handler) handler.serializeStruct("resetParameters", resetParameters); handler.serializeBool("canRefuse", canRefuse); handler.serializeBool("showScoutedPreview", showScoutedPreview); + handler.serializeBool("coastVisitable", coastVisitable); handler.serializeInt("infoWindowType", infoWindowType); } diff --git a/lib/rewardable/Configuration.h b/lib/rewardable/Configuration.h index 9880ea547..e9e9f832f 100644 --- a/lib/rewardable/Configuration.h +++ b/lib/rewardable/Configuration.h @@ -11,9 +11,9 @@ #pragma once #include "Limiter.h" -#include "MetaString.h" #include "Reward.h" #include "../networkPacks/EInfoWindowMode.h" +#include "../texts/MetaString.h" VCMI_LIB_NAMESPACE_BEGIN @@ -35,7 +35,7 @@ enum ESelectMode { SELECT_FIRST, // first reward that matches limiters SELECT_PLAYER, // player can select from all allowed rewards - SELECT_RANDOM, // one random reward from all mathing limiters + SELECT_RANDOM, // one random reward from all matching limiters SELECT_ALL // grant all rewards that match limiters }; @@ -44,7 +44,8 @@ enum class EEventType EVENT_INVALID = 0, EVENT_FIRST_VISIT, EVENT_ALREADY_VISITED, - EVENT_NOT_AVAILABLE + EVENT_NOT_AVAILABLE, + EVENT_GUARDED }; constexpr std::array SelectModeString{"selectFirst", "selectPlayer", "selectRandom", "selectAll"}; @@ -143,7 +144,7 @@ struct DLL_LINKAGE Configuration /// how reward will be selected, uses ESelectMode enum ui8 selectMode = Rewardable::SELECT_FIRST; - /// contols who can visit an object, uses EVisitMode enum + /// controls who can visit an object, uses EVisitMode enum ui8 visitMode = Rewardable::VISIT_UNLIMITED; /// how and when should the object be reset @@ -155,12 +156,16 @@ struct DLL_LINKAGE Configuration /// Limiter that will be used to determine that object is visited. Only if visit mode is set to "limiter" Rewardable::Limiter visitLimiter; + std::string guardsLayout; + /// if true - player can refuse visiting an object (e.g. Tomb) bool canRefuse = false; /// if true - right-clicking object will show preview of object rewards bool showScoutedPreview = false; + bool coastVisitable = false; + /// if true - object info will shown in infobox (like resource pickup) EInfoWindowMode infoWindowType = EInfoWindowMode::AUTO; @@ -189,6 +194,13 @@ struct DLL_LINKAGE Configuration h & canRefuse; h & showScoutedPreview; h & infoWindowType; + if (h.version >= Handler::Version::REWARDABLE_BANKS) + { + h & coastVisitable; + h & guardsLayout; + } + else + coastVisitable = false; } }; diff --git a/lib/rewardable/Info.cpp b/lib/rewardable/Info.cpp index 5692e4986..8c8d321de 100644 --- a/lib/rewardable/Info.cpp +++ b/lib/rewardable/Info.cpp @@ -15,12 +15,13 @@ #include "Limiter.h" #include "Reward.h" -#include "../CGeneralTextHandler.h" +#include "../texts/CGeneralTextHandler.h" #include "../IGameCallback.h" #include "../json/JsonRandom.h" #include "../mapObjects/IObjectInterface.h" #include "../modding/IdentifierStorage.h" -#include "../CRandomGenerator.h" + +#include VCMI_LIB_NAMESPACE_BEGIN @@ -75,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; @@ -104,9 +105,10 @@ void Rewardable::Info::init(const JsonNode & objectConfig, const std::string & o loadString(parameters["visitedTooltip"], TextIdentifier(objectName, "visitedTooltip")); loadString(parameters["onVisitedMessage"], TextIdentifier(objectName, "onVisited")); loadString(parameters["onEmptyMessage"], TextIdentifier(objectName, "onEmpty")); + loadString(parameters["onGuardedMessage"], TextIdentifier(objectName, "onGuarded")); } -Rewardable::LimitersList Rewardable::Info::configureSublimiters(Rewardable::Configuration & object, CRandomGenerator & rng, IGameCallback * cb, const JsonNode & source) const +Rewardable::LimitersList Rewardable::Info::configureSublimiters(Rewardable::Configuration & object, vstd::RNG & rng, IGameCallback * cb, const JsonNode & source) const { Rewardable::LimitersList result; for (const auto & input : source.Vector()) @@ -121,7 +123,7 @@ Rewardable::LimitersList Rewardable::Info::configureSublimiters(Rewardable::Conf return result; } -void Rewardable::Info::configureLimiter(Rewardable::Configuration & object, CRandomGenerator & rng, IGameCallback * cb, Rewardable::Limiter & limiter, const JsonNode & source) const +void Rewardable::Info::configureLimiter(Rewardable::Configuration & object, vstd::RNG & rng, IGameCallback * cb, Rewardable::Limiter & limiter, const JsonNode & source) const { auto const & variables = object.variables.values; JsonRandom randomizer(cb); @@ -153,7 +155,7 @@ void Rewardable::Info::configureLimiter(Rewardable::Configuration & object, CRan limiter.noneOf = configureSublimiters(object, rng, cb, source["noneOf"] ); } -void Rewardable::Info::configureReward(Rewardable::Configuration & object, CRandomGenerator & rng, IGameCallback * cb, Rewardable::Reward & reward, const JsonNode & source) const +void Rewardable::Info::configureReward(Rewardable::Configuration & object, vstd::RNG & rng, IGameCallback * cb, Rewardable::Reward & reward, const JsonNode & source) const { auto const & variables = object.variables.values; JsonRandom randomizer(cb); @@ -173,6 +175,8 @@ void Rewardable::Info::configureReward(Rewardable::Configuration & object, CRand reward.removeObject = source["removeObject"].Bool(); reward.bonuses = randomizer.loadBonuses(source["bonuses"]); + reward.guards = randomizer.loadCreatures(source["guards"], rng, variables); + reward.primary = randomizer.loadPrimaries(source["primary"], rng, variables); reward.secondary = randomizer.loadSecondaries(source["secondary"], rng, variables); @@ -210,14 +214,14 @@ void Rewardable::Info::configureReward(Rewardable::Configuration & object, CRand } } -void Rewardable::Info::configureResetInfo(Rewardable::Configuration & object, CRandomGenerator & rng, Rewardable::ResetInfo & resetParameters, const JsonNode & source) const +void Rewardable::Info::configureResetInfo(Rewardable::Configuration & object, vstd::RNG & rng, Rewardable::ResetInfo & resetParameters, const JsonNode & source) const { resetParameters.period = static_cast(source["period"].Float()); resetParameters.visitors = source["visitors"].Bool(); resetParameters.rewards = source["rewards"].Bool(); } -void Rewardable::Info::configureVariables(Rewardable::Configuration & object, CRandomGenerator & rng, IGameCallback * cb, const JsonNode & source) const +void Rewardable::Info::configureVariables(Rewardable::Configuration & object, vstd::RNG & rng, IGameCallback * cb, const JsonNode & source) const { JsonRandom randomizer(cb); @@ -263,21 +267,69 @@ void Rewardable::Info::replaceTextPlaceholders(MetaString & target, const Variab void Rewardable::Info::replaceTextPlaceholders(MetaString & target, const Variables & variables, const VisitInfo & info) const { - for (const auto & artifact : info.reward.artifacts ) - target.replaceName(artifact); + if (!info.reward.guards.empty()) + { + replaceTextPlaceholders(target, variables); - for (const auto & spell : info.reward.spells ) - target.replaceName(spell); + CreatureID strongest = info.reward.guards.at(0).getId(); - for (const auto & secondary : info.reward.secondary ) - target.replaceName(secondary.first); + for (const auto & guard : info.reward.guards ) + { + if (strongest.toEntity(VLC)->getFightValue() < guard.getId().toEntity(VLC)->getFightValue()) + strongest = guard.getId(); + } + target.replaceNamePlural(strongest); // FIXME: use singular if only 1 such unit is in guards - replaceTextPlaceholders(target, variables); + MetaString loot; + + for (GameResID it : GameResID::ALL_RESOURCES()) + { + if (info.reward.resources[it] != 0) + { + loot.appendRawString("%d %s"); + loot.replaceNumber(info.reward.resources[it]); + loot.replaceName(it); + } + } + + for (const auto & artifact : info.reward.artifacts ) + { + loot.appendRawString("%s"); + loot.replaceName(artifact); + } + + for (const auto & spell : info.reward.spells ) + { + loot.appendRawString("%s"); + loot.replaceName(spell); + } + + for (const auto & secondary : info.reward.secondary ) + { + loot.appendRawString("%s"); + loot.replaceName(secondary.first); + } + + target.replaceRawString(loot.buildList()); + } + else + { + for (const auto & artifact : info.reward.artifacts ) + target.replaceName(artifact); + + for (const auto & spell : info.reward.spells ) + target.replaceName(spell); + + for (const auto & secondary : info.reward.secondary ) + target.replaceName(secondary.first); + + replaceTextPlaceholders(target, variables); + } } void Rewardable::Info::configureRewards( Rewardable::Configuration & object, - CRandomGenerator & rng, + vstd::RNG & rng, IGameCallback * cb, const JsonNode & source, Rewardable::EEventType event, @@ -298,7 +350,7 @@ void Rewardable::Info::configureRewards( { const JsonNode & preset = object.getPresetVariable("dice", diceID); if (preset.isNull()) - object.initVariable("dice", diceID, rng.getIntRange(0, 99)()); + object.initVariable("dice", diceID, rng.nextInt(0, 99)); else object.initVariable("dice", diceID, preset.Integer()); @@ -335,7 +387,7 @@ void Rewardable::Info::configureRewards( } } -void Rewardable::Info::configureObject(Rewardable::Configuration & object, CRandomGenerator & rng, IGameCallback * cb) const +void Rewardable::Info::configureObject(Rewardable::Configuration & object, vstd::RNG & rng, IGameCallback * cb) const { object.info.clear(); object.variables.values.clear(); @@ -377,10 +429,22 @@ void Rewardable::Info::configureObject(Rewardable::Configuration & object, CRand object.info.push_back(onEmpty); } + if (!parameters["onGuardedMessage"].isNull()) + { + Rewardable::VisitInfo onGuarded; + onGuarded.visitType = Rewardable::EEventType::EVENT_GUARDED; + onGuarded.message = loadMessage(parameters["onGuardedMessage"], TextIdentifier(objectTextID, "onGuarded")); + replaceTextPlaceholders(onGuarded.message, object.variables); + + object.info.push_back(onGuarded); + } + configureResetInfo(object, rng, object.resetParameters, parameters["resetParameters"]); object.canRefuse = parameters["canRefuse"].Bool(); object.showScoutedPreview = parameters["showScoutedPreview"].Bool(); + object.guardsLayout = parameters["guardsLayout"].String(); + object.coastVisitable = parameters["coastVisitable"].Bool(); if(parameters["showInInfobox"].isNull()) object.infoWindowType = EInfoWindowMode::AUTO; @@ -462,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 e2bb4322f..ac89bdc8d 100644 --- a/lib/rewardable/Info.h +++ b/lib/rewardable/Info.h @@ -15,13 +15,16 @@ VCMI_LIB_NAMESPACE_BEGIN -class CRandomGenerator; +namespace vstd +{ +class RNG; +} + class MetaString; class IGameCallback; namespace Rewardable { - struct Limiter; using LimitersList = std::vector>; struct Reward; @@ -39,14 +42,14 @@ class DLL_LINKAGE Info : public IObjectInfo void replaceTextPlaceholders(MetaString & target, const Variables & variables) const; void replaceTextPlaceholders(MetaString & target, const Variables & variables, const VisitInfo & info) const; - void configureVariables(Rewardable::Configuration & object, CRandomGenerator & rng, IGameCallback * cb, const JsonNode & source) const; - void configureRewards(Rewardable::Configuration & object, CRandomGenerator & rng, IGameCallback * cb, const JsonNode & source, Rewardable::EEventType mode, const std::string & textPrefix) const; + void configureVariables(Rewardable::Configuration & object, vstd::RNG & rng, IGameCallback * cb, const JsonNode & source) const; + void configureRewards(Rewardable::Configuration & object, vstd::RNG & rng, IGameCallback * cb, const JsonNode & source, Rewardable::EEventType mode, const std::string & textPrefix) const; - void configureLimiter(Rewardable::Configuration & object, CRandomGenerator & rng, IGameCallback * cb, Rewardable::Limiter & limiter, const JsonNode & source) const; - Rewardable::LimitersList configureSublimiters(Rewardable::Configuration & object, CRandomGenerator & rng, IGameCallback * cb, const JsonNode & source) const; + void configureLimiter(Rewardable::Configuration & object, vstd::RNG & rng, IGameCallback * cb, Rewardable::Limiter & limiter, const JsonNode & source) const; + Rewardable::LimitersList configureSublimiters(Rewardable::Configuration & object, vstd::RNG & rng, IGameCallback * cb, const JsonNode & source) const; - void configureReward(Rewardable::Configuration & object, CRandomGenerator & rng, IGameCallback * cb, Rewardable::Reward & info, const JsonNode & source) const; - void configureResetInfo(Rewardable::Configuration & object, CRandomGenerator & rng, Rewardable::ResetInfo & info, const JsonNode & source) const; + void configureReward(Rewardable::Configuration & object, vstd::RNG & rng, IGameCallback * cb, Rewardable::Reward & info, const JsonNode & source) const; + void configureResetInfo(Rewardable::Configuration & object, vstd::RNG & rng, Rewardable::ResetInfo & info, const JsonNode & source) const; public: const JsonNode & getParameters() const; @@ -65,7 +68,9 @@ public: bool givesBonuses() const override; - void configureObject(Rewardable::Configuration & object, CRandomGenerator & rng, IGameCallback * cb) const; + 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 2a6cb3b27..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)); @@ -157,7 +163,7 @@ void Rewardable::Interface::grantRewardAfterLevelup(IGameCallback * cb, const Re } for(const ArtifactID & art : info.reward.artifacts) - cb->giveHeroNewArtifact(hero, art.toArtifact(), ArtifactPosition::FIRST_AVAILABLE); + cb->giveHeroNewArtifact(hero, art, ArtifactPosition::FIRST_AVAILABLE); if(!info.reward.spells.empty()) { @@ -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,9 +199,9 @@ 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 splitted on map instance part and interface part + 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 3ff6980c9..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 { @@ -29,16 +29,31 @@ private: protected: - /// filters list of visit info and returns rewards that can be granted to current hero - std::vector getAvailableRewards(const CGHeroInstance * hero, Rewardable::EEventType event) const; - /// 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 + std::vector getAvailableRewards(const CGHeroInstance * hero, Rewardable::EEventType event) const; Rewardable::Configuration configuration; diff --git a/lib/rewardable/Limiter.cpp b/lib/rewardable/Limiter.cpp index 339f45415..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 @@ -107,7 +106,7 @@ bool Rewardable::Limiter::heroAllowed(const CGHeroInstance * hero) const if (canLearnSkills && !hero->canLearnSkill()) return false; - if(manaPercentage > 100 * hero->mana / hero->manaLimit()) + if (hero->manaLimit() != 0 && manaPercentage > 100 * hero->mana / hero->manaLimit()) return false; for(size_t i=0; igetArtPosCount(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); @@ -244,6 +261,7 @@ void Rewardable::Limiter::serializeJson(JsonSerializeFormat & handler) handler.serializeIdArray("colors", players); handler.serializeInt("manaPoints", manaPoints); handler.serializeIdArray("artifacts", artifacts); + handler.serializeIdArray("spells", spells); handler.enterArray("creatures").serializeStruct(creatures); handler.enterArray("primary").serializeArray(primary); { diff --git a/lib/rewardable/Limiter.h b/lib/rewardable/Limiter.h index 969bcd576..e95e6459a 100644 --- a/lib/rewardable/Limiter.h +++ b/lib/rewardable/Limiter.h @@ -12,6 +12,7 @@ #include "../GameConstants.h" #include "../ResourceSet.h" +#include "../serializer/Serializeable.h" VCMI_LIB_NAMESPACE_BEGIN @@ -26,7 +27,7 @@ using LimitersList = std::vector>; /// Limiters of rewards. Rewards will be granted to hero only if he satisfies requirements /// Note: for this is only a test - it won't remove anything from hero (e.g. artifacts or creatures) -struct DLL_LINKAGE Limiter final +struct DLL_LINKAGE Limiter final : public Serializeable { /// day of week, unused if 0, 1-7 will test for current day of week si32 dayOfWeek; 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 guards; + /// list of bonuses, e.g. morale/luck std::vector bonuses; @@ -126,6 +129,8 @@ struct DLL_LINKAGE Reward final h & removeObject; h & manaPercentage; h & movePercentage; + if (h.version >= Handler::Version::REWARDABLE_GUARDS) + h & guards; h & heroExperience; h & heroLevel; h & manaDiff; diff --git a/lib/rmg/CMapGenOptions.cpp b/lib/rmg/CMapGenOptions.cpp index 622bbdc3f..2a9169739 100644 --- a/lib/rmg/CMapGenOptions.cpp +++ b/lib/rmg/CMapGenOptions.cpp @@ -11,14 +11,16 @@ #include "StdInc.h" #include "CMapGenOptions.h" +#include "../entities/faction/CTownHandler.h" +#include "../entities/faction/CFaction.h" #include "../mapping/CMapHeader.h" #include "CRmgTemplateStorage.h" #include "CRmgTemplate.h" -#include "CRandomGenerator.h" #include "../VCMI_Lib.h" -#include "../CTownHandler.h" #include "serializer/JsonSerializeFormat.h" +#include + VCMI_LIB_NAMESPACE_BEGIN CMapGenOptions::CMapGenOptions() @@ -28,9 +30,6 @@ CMapGenOptions::CMapGenOptions() customizedPlayers(false) { initPlayersMap(); - setRoadEnabled(RoadId(Road::DIRT_ROAD), true); - setRoadEnabled(RoadId(Road::GRAVEL_ROAD), true); - setRoadEnabled(RoadId(Road::COBBLESTONE_ROAD), true); } si32 CMapGenOptions::getWidth() const @@ -487,7 +486,7 @@ void CMapGenOptions::setPlayerTeam(const PlayerColor & color, const TeamID & tea customizedPlayers = true; } -void CMapGenOptions::finalize(CRandomGenerator & rand) +void CMapGenOptions::finalize(vstd::RNG & rand) { logGlobal->info("RMG map: %dx%d, %s underground", getWidth(), getHeight(), getHasTwoLevels() ? "WITH" : "NO"); logGlobal->info("RMG settings: players %d, teams %d, computer players %d, computer teams %d, water %d, monsters %d", @@ -690,8 +689,7 @@ bool CMapGenOptions::checkOptions() const } else { - CRandomGenerator gen; - return getPossibleTemplate(gen) != nullptr; + return !getPossibleTemplates().empty(); } } @@ -750,7 +748,7 @@ std::vector CMapGenOptions::getPossibleTemplates() const return templates; } -const CRmgTemplate * CMapGenOptions::getPossibleTemplate(CRandomGenerator & rand) const +const CRmgTemplate * CMapGenOptions::getPossibleTemplate(vstd::RNG & rand) const { auto templates = getPossibleTemplates(); diff --git a/lib/rmg/CMapGenOptions.h b/lib/rmg/CMapGenOptions.h index 2055d58e5..3f151c435 100644 --- a/lib/rmg/CMapGenOptions.h +++ b/lib/rmg/CMapGenOptions.h @@ -11,11 +11,15 @@ #pragma once #include "../GameConstants.h" +#include "../serializer/Serializeable.h" #include "CRmgTemplate.h" VCMI_LIB_NAMESPACE_BEGIN -class CRandomGenerator; +namespace vstd +{ +class RNG; +} enum class EPlayerType { @@ -26,7 +30,7 @@ enum class EPlayerType /// The map gen options class holds values about general map generation settings /// e.g. the size of the map, the count of players,... -class DLL_LINKAGE CMapGenOptions +class DLL_LINKAGE CMapGenOptions : public Serializeable { public: /// The player settings class maps the player color, starting town and human player flag. @@ -73,10 +77,7 @@ public: h & startingTown; h & playerType; h & team; - if (h.version >= Handler::Version::RELEASE_143) - h & startingHero; - else - startingHero = HeroTypeID::RANDOM; + h & startingHero; } }; @@ -147,7 +148,7 @@ public: /// Finalizes the options. All random sizes for various properties will be overwritten by numbers from /// a random number generator by keeping the options in a valid state. Check options should return true, otherwise /// this function fails. - void finalize(CRandomGenerator & rand); + void finalize(vstd::RNG & rand); /// Returns false if there is no template available which fits to the currently selected options. bool checkOptions() const; @@ -165,7 +166,7 @@ private: PlayerColor getNextPlayerColor() const; void updateCompOnlyPlayers(); void updatePlayers(); - const CRmgTemplate * getPossibleTemplate(CRandomGenerator & rand) const; + const CRmgTemplate * getPossibleTemplate(vstd::RNG & rand) const; si32 width; si32 height; diff --git a/lib/rmg/CMapGenerator.cpp b/lib/rmg/CMapGenerator.cpp index 34ef5fe7d..f789b78c6 100644 --- a/lib/rmg/CMapGenerator.cpp +++ b/lib/rmg/CMapGenerator.cpp @@ -13,13 +13,15 @@ #include "../mapping/CMap.h" #include "../mapping/MapFormat.h" #include "../VCMI_Lib.h" -#include "../CGeneralTextHandler.h" +#include "../texts/CGeneralTextHandler.h" +#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 "../CTownHandler.h" -#include "../CHeroHandler.h" #include "../constants/StringConstants.h" #include "../filesystem/Filesystem.h" #include "CZonePlacer.h" @@ -32,15 +34,18 @@ #include "modificators/TreasurePlacer.h" #include "modificators/RoadPlacer.h" +#include +#include + VCMI_LIB_NAMESPACE_BEGIN CMapGenerator::CMapGenerator(CMapGenOptions& mapGenOptions, IGameCallback * cb, int RandomSeed) : mapGenOptions(mapGenOptions), randomSeed(RandomSeed), - monolithIndex(0) + monolithIndex(0), + rand(std::make_unique(RandomSeed)) { loadConfig(); - rand.setSeed(this->randomSeed); - mapGenOptions.finalize(rand); + mapGenOptions.finalize(*rand); map = std::make_unique(mapGenOptions, cb); placer = std::make_shared(*map); } @@ -100,8 +105,9 @@ const CMapGenOptions& CMapGenerator::getMapGenOptions() const void CMapGenerator::initQuestArtsRemaining() { //TODO: Move to QuestArtifactPlacer? - for (auto art : VLC->arth->objects) + for (auto artID : VLC->arth->getDefaultAllowed()) { + auto art = artID.toArtifact(); //Don't use parts of combined artifacts if (art->aClass == CArtifact::ART_TREASURE && VLC->arth->legalArtifact(art->getId()) && art->getPartOf().empty()) questArtifacts.push_back(art->getId()); @@ -115,7 +121,7 @@ std::unique_ptr CMapGenerator::generate() try { addHeaderInfo(); - map->initTiles(*this, rand); + map->initTiles(*this, *rand); Load::Progress::step(); initQuestArtsRemaining(); genZones(); @@ -136,44 +142,65 @@ std::unique_ptr CMapGenerator::generate() throw; } Load::Progress::finish(); + + map->mapInstance->creationDateTime = std::time(nullptr); + map->mapInstance->author = MetaString::createFromTextID("core.genrltxt.740"); + const auto * mapTemplate = mapGenOptions.getMapTemplate(); + if(mapTemplate) + map->mapInstance->mapVersion = MetaString::createFromRawString(mapTemplate->getName()); + 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() @@ -278,7 +305,7 @@ void CMapGenerator::addPlayerInfo() logGlobal->error("Not enough places in team for %s player", ((j == CPUONLY) ? "CPU" : "CPU or human")); assert (teamNumbers[j].size()); } - auto itTeam = RandomGeneratorUtil::nextItem(teamNumbers[j], rand); + auto itTeam = RandomGeneratorUtil::nextItem(teamNumbers[j], *rand); player.team = TeamID(*itTeam); teamNumbers[j].erase(itTeam); } @@ -298,8 +325,8 @@ void CMapGenerator::addPlayerInfo() void CMapGenerator::genZones() { - placer->placeZones(&rand); - placer->assignZones(&rand); + placer->placeZones(rand.get()); + placer->assignZones(rand.get()); logGlobal->info("Zones generated successfully"); } @@ -420,9 +447,9 @@ void CMapGenerator::fillZones() if (it.second->getType() != ETemplateZoneType::WATER) treasureZones.push_back(it.second); } - auto grailZone = *RandomGeneratorUtil::nextItem(treasureZones, rand); + auto grailZone = *RandomGeneratorUtil::nextItem(treasureZones, *rand); - map->getMap(this).grailPos = *RandomGeneratorUtil::nextItem(grailZone->freePaths()->getTiles(), rand); + map->getMap(this).grailPos = *RandomGeneratorUtil::nextItem(grailZone->freePaths()->getTiles(), *rand); map->getMap(this).reindexObjects(); logGlobal->info("Zones filled successfully"); @@ -438,11 +465,12 @@ 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); m.banWaterContent(); + m.overrideGameSettings(mapGenOptions.getMapTemplate()->getMapSettings()); } int CMapGenerator::getNextMonlithIndex() diff --git a/lib/rmg/CMapGenerator.h b/lib/rmg/CMapGenerator.h index e75d9fe3a..a635534b6 100644 --- a/lib/rmg/CMapGenerator.h +++ b/lib/rmg/CMapGenerator.h @@ -10,15 +10,12 @@ #pragma once -#include "../GameConstants.h" -#include "../CRandomGenerator.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; @@ -67,7 +64,7 @@ public: std::unique_ptr generate(); int getNextMonlithIndex(); - int getPrisonsRemaning() const; + int getPrisonsRemaining() const; std::shared_ptr getZonePlacer() const; const std::vector & getAllPossibleQuestArtifacts() const; const std::vector getAllPossibleHeroes() const; @@ -79,7 +76,7 @@ public: int getRandomSeed() const; private: - CRandomGenerator rand; + std::unique_ptr rand; int randomSeed; CMapGenOptions& mapGenOptions; Config config; @@ -94,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 ac7a491e3..9abaaed34 100644 --- a/lib/rmg/CRmgTemplate.cpp +++ b/lib/rmg/CRmgTemplate.cpp @@ -14,12 +14,12 @@ #include "CRmgTemplate.h" #include "Functions.h" -#include "../VCMI_Lib.h" -#include "../CTownHandler.h" #include "../TerrainHandler.h" -#include "../serializer/JsonSerializeFormat.h" -#include "../modding/ModScope.h" +#include "../VCMI_Lib.h" #include "../constants/StringConstants.h" +#include "../entities/faction/CTownHandler.h" +#include "../modding/ModScope.h" +#include "../serializer/JsonSerializeFormat.h" VCMI_LIB_NAMESPACE_BEGIN @@ -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()) { @@ -176,7 +177,7 @@ void ZoneOptions::setTerrainTypes(const std::set & value) std::set ZoneOptions::getDefaultTerrainTypes() const { std::set terrains; - for (auto terrain : VLC->terrainTypeHandler->objects) + for(const auto & terrain : VLC->terrainTypeHandler->objects) { if (terrain->isLand() && terrain->isPassable()) { @@ -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) @@ -463,7 +494,8 @@ void ZoneConnection::serializeJson(JsonSerializeFormat & handler) "guarded", "fictive", "repulsive", - "wide" + "wide", + "forcePortal" }; static const std::vector roadOptions = @@ -500,9 +532,12 @@ void ZoneConnection::serializeJson(JsonSerializeFormat & handler) using namespace rmg;//todo: remove +CRmgTemplate::~CRmgTemplate() = default; + CRmgTemplate::CRmgTemplate() : minSize(72, 72, 2), - maxSize(72, 72, 2) + maxSize(72, 72, 2), + mapSettings(std::make_unique()) { } @@ -568,7 +603,7 @@ const CRmgTemplate::Zones & CRmgTemplate::getZones() const const std::vector & CRmgTemplate::getConnectedZoneIds() const { - return connectedZoneIds; + return connections; } void CRmgTemplate::validate() const @@ -693,9 +728,18 @@ void CRmgTemplate::serializeJson(JsonSerializeFormat & handler) serializePlayers(handler, players, "players"); serializePlayers(handler, humanPlayers, "humans"); // TODO: Rename this parameter + *mapSettings = handler.getCurrent()["settings"]; + { 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); + } + } } { @@ -748,53 +792,34 @@ void CRmgTemplate::serializeJson(JsonSerializeFormat & handler) } } -std::set CRmgTemplate::inheritTerrainType(std::shared_ptr zone, uint32_t iteration /* = 0 */) +const JsonNode & CRmgTemplate::getMapSettings() const { - if (iteration >= 50) - { - logGlobal->error("Infinite recursion for terrain types detected in template %s", name); - return std::set(); - } - if (zone->getTerrainTypeLikeZone() != ZoneOptions::NO_ZONE) - { - iteration++; - const auto otherZone = zones.at(zone->getTerrainTypeLikeZone()); - zone->setTerrainTypes(inheritTerrainType(otherZone, iteration)); - } - //This implicitely excludes banned terrains - return zone->getTerrainTypes(); + return *mapSettings; } -std::map CRmgTemplate::inheritMineTypes(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 mine types detected in template %s", name); - return std::map(); + logGlobal->error("Infinite recursion for %s detected in template %s", propertyString, name); + return T(); } - if (zone->getMinesLikeZone() != ZoneOptions::NO_ZONE) + + if (((*zone).*inheritFrom)() != rmg::ZoneOptions::NO_ZONE) { iteration++; - const auto otherZone = zones.at(zone->getMinesLikeZone()); - zone->setMinesInfo(inheritMineTypes(otherZone, iteration)); + const auto otherZone = zones.at(((*zone).*inheritFrom)()); + T inheritedValue = inheritZoneProperty(otherZone, getter, setter, inheritFrom, propertyString, iteration); + ((*zone).*setter)(inheritedValue); } - 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() @@ -803,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()) { @@ -816,7 +861,7 @@ void CRmgTemplate::afterLoad() } } - for(const auto & connection : connectedZoneIds) + for(const auto & connection : connections) { auto id1 = connection.getZoneA(); auto id2 = connection.getZoneB(); @@ -837,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 = @@ -905,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 1d0382ad1..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 @@ -75,7 +80,8 @@ enum class EConnectionType GUARDED = 0, //default FICTIVE, REPULSIVE, - WIDE + WIDE, + FORCE_PORTAL }; enum class ERoadOption @@ -91,6 +97,8 @@ public: ZoneConnection(); + int getId() const; + void setId(int id); TRmgTemplateZoneId getZoneA() const; TRmgTemplateZoneId getZoneB() const; TRmgTemplateZoneId getOtherZoneId(TRmgTemplateZoneId id) const; @@ -102,6 +110,7 @@ public: friend bool operator==(const ZoneConnection &, const ZoneConnection &); private: + int id; TRmgTemplateZoneId zoneA; TRmgTemplateZoneId zoneB; int guardStrength; @@ -131,6 +140,9 @@ public: int castleCount; int townDensity; int castleDensity; + + // TODO: Copy from another zone once its randomized + TRmgTemplateZoneId sourceZone = NO_ZONE; }; ZoneOptions(); @@ -145,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); @@ -163,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(); @@ -182,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; @@ -210,12 +234,13 @@ protected: TRmgTemplateZoneId minesLikeZone; TRmgTemplateZoneId terrainTypeLikeZone; TRmgTemplateZoneId treasureLikeZone; + TRmgTemplateZoneId customObjectsLikeZone; }; } /// The CRmgTemplate describes a random map template. -class DLL_LINKAGE CRmgTemplate +class DLL_LINKAGE CRmgTemplate : boost::noncopyable { public: using Zones = std::map>; @@ -239,6 +264,7 @@ public: }; CRmgTemplate(); + ~CRmgTemplate(); bool matchesSize(const int3 & value) const; bool isWaterContentAllowed(EWaterContent::EWaterContent waterContent) const; @@ -254,6 +280,7 @@ public: const CPlayerCountRange & getHumanPlayers() const; std::pair getMapSizes() const; const Zones & getZones() const; + const JsonNode & getMapSettings() const; const std::vector & getConnectedZoneIds() const; void validate() const; /// Tests template on validity and throws exception on failure @@ -270,14 +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/CZonePlacer.cpp b/lib/rmg/CZonePlacer.cpp index f5de295f0..29b1acfc6 100644 --- a/lib/rmg/CZonePlacer.cpp +++ b/lib/rmg/CZonePlacer.cpp @@ -11,23 +11,24 @@ #include "StdInc.h" #include "CZonePlacer.h" -#include "../CRandomGenerator.h" -#include "../CTownHandler.h" #include "../TerrainHandler.h" +#include "../entities/faction/CFaction.h" +#include "../entities/faction/CTownHandler.h" #include "../mapping/CMap.h" #include "../mapping/CMapEditManager.h" +#include "../VCMI_Lib.h" #include "CMapGenOptions.h" #include "RmgMap.h" #include "Zone.h" #include "Functions.h" #include "PenroseTiling.h" +#include + VCMI_LIB_NAMESPACE_BEGIN //#define ZONE_PLACEMENT_LOG true -class CRandomGenerator; - CZonePlacer::CZonePlacer(RmgMap & map) : width(0), height(0), mapSize(0), gravityConstant(1e-3f), @@ -79,12 +80,21 @@ void CZonePlacer::findPathsBetweenZones() for (auto & connection : connectedZoneIds) { - if (connection.getConnectionType() == rmg::EConnectionType::REPULSIVE) + switch (connection.getConnectionType()) { //Do not consider virtual connections for graph distance - continue; + case rmg::EConnectionType::REPULSIVE: + case rmg::EConnectionType::FORCE_PORTAL: + continue; } auto neighbor = connection.getOtherZoneId(current); + + if (current == neighbor) + { + //Do not consider self-connections + continue; + } + if (!visited[neighbor]) { visited[neighbor] = true; @@ -96,7 +106,7 @@ void CZonePlacer::findPathsBetweenZones() } } -void CZonePlacer::placeOnGrid(CRandomGenerator* rand) +void CZonePlacer::placeOnGrid(vstd::RNG* rand) { auto zones = map.getZones(); assert(zones.size()); @@ -117,7 +127,7 @@ void CZonePlacer::placeOnGrid(CRandomGenerator* rand) auto getRandomEdge = [rand, gridSize](size_t& x, size_t& y) { - switch (rand->nextInt() % 4) + switch (rand->nextInt(0, 3) % 4) { case 0: x = 0; @@ -149,7 +159,7 @@ void CZonePlacer::placeOnGrid(CRandomGenerator* rand) else { //Random corner - if (rand->nextInt() % 2) + if (rand->nextInt(0, 1) == 1) { x = 0; } @@ -157,7 +167,7 @@ void CZonePlacer::placeOnGrid(CRandomGenerator* rand) { x = gridSize - 1; } - if (rand->nextInt() % 2) + if (rand->nextInt(0, 1) == 1) { y = 0; } @@ -175,8 +185,8 @@ void CZonePlacer::placeOnGrid(CRandomGenerator* rand) else { //One of 4 squares in the middle - x = (gridSize / 2) - 1 + rand->nextInt() % 2; - y = (gridSize / 2) - 1 + rand->nextInt() % 2; + x = (gridSize / 2) - 1 + rand->nextInt(0, 1); + y = (gridSize / 2) - 1 + rand->nextInt(0, 1); } break; case ETemplateZoneType::JUNCTION: @@ -307,7 +317,7 @@ float CZonePlacer::scaleForceBetweenZones(const std::shared_ptr zoneA, con } } -void CZonePlacer::placeZones(CRandomGenerator * rand) +void CZonePlacer::placeZones(vstd::RNG * rand) { logGlobal->info("Starting zone placement"); @@ -431,7 +441,7 @@ void CZonePlacer::placeZones(CRandomGenerator * rand) } } -void CZonePlacer::prepareZones(TZoneMap &zones, TZoneVector &zonesVector, const bool underground, CRandomGenerator * rand) +void CZonePlacer::prepareZones(TZoneMap &zones, TZoneVector &zonesVector, const bool underground, vstd::RNG * rand) { std::vector totalSize = { 0, 0 }; //make sure that sum of zone sizes on surface and uderground match size of the map @@ -551,8 +561,16 @@ void CZonePlacer::attractConnectedZones(TZoneMap & zones, TForceVector & forces, for (const auto & connection : zone.second->getConnections()) { - if (connection.getConnectionType() == rmg::EConnectionType::REPULSIVE) + switch (connection.getConnectionType()) { + //Do not consider virtual connections for graph distance + case rmg::EConnectionType::REPULSIVE: + case rmg::EConnectionType::FORCE_PORTAL: + continue; + } + if (connection.getZoneA() == connection.getZoneB()) + { + //Do not consider self-connections continue; } @@ -693,7 +711,7 @@ void CZonePlacer::moveOneZone(TZoneMap& zones, TForceVector& totalForces, TDista boost::sort(misplacedZones, [](const Misplacement& lhs, Misplacement& rhs) { - return lhs.first > rhs.first; //Largest dispalcement first + return lhs.first > rhs.first; //Largest displacement first }); #ifdef ZONE_PLACEMENT_LOG @@ -709,11 +727,19 @@ void CZonePlacer::moveOneZone(TZoneMap& zones, TForceVector& totalForces, TDista std::set connectedZones; for (const auto& connection : firstZone->getConnections()) { - //FIXME: Should we also exclude fictive connections? - if (connection.getConnectionType() != rmg::EConnectionType::REPULSIVE) + switch (connection.getConnectionType()) { - connectedZones.insert(connection.getOtherZoneId(firstZone->getId())); + //Do not consider virtual connections for graph distance + case rmg::EConnectionType::REPULSIVE: + case rmg::EConnectionType::FORCE_PORTAL: + continue; } + if (connection.getZoneA() == connection.getZoneB()) + { + //Do not consider self-connections + continue; + } + connectedZones.insert(connection.getOtherZoneId(firstZone->getId())); } auto level = firstZone->getCenter().z; @@ -823,7 +849,7 @@ float CZonePlacer::metric (const int3 &A, const int3 &B) const } -void CZonePlacer::assignZones(CRandomGenerator * rand) +void CZonePlacer::assignZones(vstd::RNG * rand) { logGlobal->info("Starting zone colouring"); @@ -973,7 +999,7 @@ void CZonePlacer::assignZones(CRandomGenerator * rand) { moveZoneToCenterOfMass(zone.second); - //TODO: similiar for islands + //TODO: similar for islands #define CREATE_FULL_UNDERGROUND true //consider linking this with water amount if (zone.second->isUnderground()) { diff --git a/lib/rmg/CZonePlacer.h b/lib/rmg/CZonePlacer.h index 2eaf429ce..43e3479df 100644 --- a/lib/rmg/CZonePlacer.h +++ b/lib/rmg/CZonePlacer.h @@ -16,9 +16,13 @@ VCMI_LIB_NAMESPACE_BEGIN +namespace vstd +{ +class RNG; +} + class CZoneGraph; class CMap; -class CRandomGenerator; class RmgMap; class Zone; @@ -34,19 +38,19 @@ public: explicit CZonePlacer(RmgMap & map); int3 cords(const float3 & f) const; float metric (const int3 &a, const int3 &b) const; - float getDistance(float distance) const; //additional scaling without 0 divison + float getDistance(float distance) const; //additional scaling without 0 division ~CZonePlacer() = default; - void placeZones(CRandomGenerator * rand); + void placeZones(vstd::RNG * rand); void findPathsBetweenZones(); - void placeOnGrid(CRandomGenerator* rand); + void placeOnGrid(vstd::RNG* rand); float scaleForceBetweenZones(const std::shared_ptr zoneA, const std::shared_ptr zoneB) const; - void assignZones(CRandomGenerator * rand); + void assignZones(vstd::RNG * rand); const TDistanceMap & getDistanceMap(); private: - void prepareZones(TZoneMap &zones, TZoneVector &zonesVector, const bool underground, CRandomGenerator * rand); + void prepareZones(TZoneMap &zones, TZoneVector &zonesVector, const bool underground, vstd::RNG * rand); void attractConnectedZones(TZoneMap & zones, TForceVector & forces, TDistanceVector & distances) const; void separateOverlappingZones(TZoneMap &zones, TForceVector &forces, TDistanceVector &overlaps); void moveOneZone(TZoneMap & zones, TForceVector & totalForces, TDistanceVector & distances, TDistanceVector & overlaps); @@ -54,7 +58,7 @@ private: private: int width; int height; - //metric coeficient + //metric coefficient float mapSize; float gravityConstant; diff --git a/lib/rmg/Functions.cpp b/lib/rmg/Functions.cpp index 4147aafb6..2d76d2f49 100644 --- a/lib/rmg/Functions.cpp +++ b/lib/rmg/Functions.cpp @@ -15,14 +15,32 @@ #include "TileInfo.h" #include "RmgPath.h" #include "../TerrainHandler.h" -#include "../CTownHandler.h" #include "../mapping/CMap.h" #include "../mapObjectConstructors/AObjectTypeHandler.h" #include "../mapObjectConstructors/CObjectClassesHandler.h" #include "../VCMI_Lib.h" +#include + VCMI_LIB_NAMESPACE_BEGIN +void replaceWithCurvedPath(rmg::Path & path, const Zone & zone, const int3 & src, bool onlyStraight) +{ + auto costFunction = rmg::Path::createCurvedCostFunction(zone.area()->getBorder()); + auto pathArea = zone.areaForRoads(); + rmg::Path curvedPath(pathArea); + curvedPath.connect(zone.freePaths().get()); + curvedPath = curvedPath.search(src, onlyStraight, costFunction); + if (curvedPath.valid()) + { + path = curvedPath; + } + else + { + logGlobal->warn("Failed to create curved path to %s", src.toString()); + } +} + rmg::Tileset collectDistantTiles(const Zone& zone, int distance) { uint32_t distanceSq = distance * distance; @@ -34,7 +52,7 @@ rmg::Tileset collectDistantTiles(const Zone& zone, int distance) return subarea.getTiles(); } -int chooseRandomAppearance(CRandomGenerator & generator, si32 ObjID, TerrainId terrain) +int chooseRandomAppearance(vstd::RNG & generator, si32 ObjID, TerrainId terrain) { auto factories = VLC->objtypeh->knownSubObjects(ObjID); vstd::erase_if(factories, [ObjID, &terrain](si32 f) diff --git a/lib/rmg/Functions.h b/lib/rmg/Functions.h index 0bcaee0d7..dfec5c3d7 100644 --- a/lib/rmg/Functions.h +++ b/lib/rmg/Functions.h @@ -19,7 +19,6 @@ class RmgMap; class ObjectManager; class ObjectTemplate; class CMapGenerator; -class CRandomGenerator; class rmgException : public std::exception { @@ -35,9 +34,11 @@ public: } }; +void replaceWithCurvedPath(rmg::Path & path, const Zone & zone, const int3 & src, bool onlyStraight = true); + rmg::Tileset collectDistantTiles(const Zone & zone, int distance); -int chooseRandomAppearance(CRandomGenerator & generator, si32 ObjID, TerrainId terrain); +int chooseRandomAppearance(vstd::RNG & generator, si32 ObjID, TerrainId terrain); VCMI_LIB_NAMESPACE_END 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/PenroseTiling.cpp b/lib/rmg/PenroseTiling.cpp index 80395573f..9df43f866 100644 --- a/lib/rmg/PenroseTiling.cpp +++ b/lib/rmg/PenroseTiling.cpp @@ -13,6 +13,8 @@ #include "StdInc.h" #include "PenroseTiling.h" +#include + VCMI_LIB_NAMESPACE_BEGIN @@ -143,7 +145,7 @@ void PenroseTiling::split(Triangle& p, std::vector& points, return; } -std::set PenroseTiling::generatePenroseTiling(size_t numZones, CRandomGenerator * rand) +std::set PenroseTiling::generatePenroseTiling(size_t numZones, vstd::RNG * rand) { float scale = 173.2f / (numZones * 1.5f + 20); float polyAngle = (2 * PI_CONSTANT) / POLY; @@ -181,7 +183,7 @@ std::set PenroseTiling::generatePenroseTiling(size_t numZones, CRandomG for (auto & point : points) { point = point + Point2D(0.5f, 0.5f); - }; + } // For 8XM8 map, only 650 out of 15971 points are in the range @@ -193,4 +195,4 @@ std::set PenroseTiling::generatePenroseTiling(size_t numZones, CRandomG return finalPoints; } -VCMI_LIB_NAMESPACE_END \ No newline at end of file +VCMI_LIB_NAMESPACE_END diff --git a/lib/rmg/PenroseTiling.h b/lib/rmg/PenroseTiling.h index 8b6ace8ed..18a8a3f40 100644 --- a/lib/rmg/PenroseTiling.h +++ b/lib/rmg/PenroseTiling.h @@ -11,12 +11,16 @@ #pragma once #include "../GameConstants.h" -#include "../CRandomGenerator.h" #include #include VCMI_LIB_NAMESPACE_BEGIN +namespace vstd +{ +class RNG; +} + using namespace boost::geometry; typedef std::array TIndices; @@ -66,11 +70,11 @@ public: const bool P2 = false; // Tiling type - std::set generatePenroseTiling(size_t numZones, CRandomGenerator * rand); + std::set generatePenroseTiling(size_t numZones, vstd::RNG * rand); private: void split(Triangle& p, std::vector& points, std::array, 5>& indices, uint32_t depth); }; -VCMI_LIB_NAMESPACE_END \ No newline at end of file +VCMI_LIB_NAMESPACE_END diff --git a/lib/rmg/RmgArea.h b/lib/rmg/RmgArea.h index 8b842900e..3c24160bc 100644 --- a/lib/rmg/RmgArea.h +++ b/lib/rmg/RmgArea.h @@ -58,7 +58,7 @@ namespace rmg int3 nearest(const Area & area) const; void clear(); - void assign(const Tileset tiles); //do not use reference to allow assigment of cached data + void assign(const Tileset tiles); //do not use reference to allow assignment of cached data void add(const int3 & tile); void erase(const int3 & tile); void unite(const Area & area); diff --git a/lib/rmg/RmgMap.cpp b/lib/rmg/RmgMap.cpp index c889e720a..5f6be7b7c 100644 --- a/lib/rmg/RmgMap.cpp +++ b/lib/rmg/RmgMap.cpp @@ -13,9 +13,10 @@ #include "TileInfo.h" #include "CMapGenOptions.h" #include "Zone.h" +#include "../entities/faction/CTownHandler.h" #include "../mapping/CMapEditManager.h" #include "../mapping/CMap.h" -#include "../CTownHandler.h" +#include "../VCMI_Lib.h" #include "modificators/ObjectManager.h" #include "modificators/RoadPlacer.h" #include "modificators/TreasurePlacer.h" @@ -83,7 +84,7 @@ void RmgMap::foreachDiagonalNeighbour(const int3 & pos, const std::functioninitTerrain(); diff --git a/lib/rmg/RmgMap.h b/lib/rmg/RmgMap.h index 6caff8236..24510dee5 100644 --- a/lib/rmg/RmgMap.h +++ b/lib/rmg/RmgMap.h @@ -17,7 +17,6 @@ VCMI_LIB_NAMESPACE_BEGIN class CMap; class CMapEditManager; -class CRandomGenerator; class TileInfo; class CMapGenOptions; class Zone; @@ -25,6 +24,11 @@ class CMapGenerator; class MapProxy; class playerInfo; +namespace vstd +{ +class RNG; +} + class RmgMap { public: @@ -79,7 +83,7 @@ public: void registerZone(FactionID faction); ui32 getZoneCount(FactionID faction); ui32 getTotalZoneCount() const; - void initTiles(CMapGenerator & generator, CRandomGenerator & rand); + void initTiles(CMapGenerator & generator, vstd::RNG & rand); void addModificators(); bool isAllowedSpell(const SpellID & sid) const; diff --git a/lib/rmg/RmgObject.cpp b/lib/rmg/RmgObject.cpp index b344b268b..08c5d1096 100644 --- a/lib/rmg/RmgObject.cpp +++ b/lib/rmg/RmgObject.cpp @@ -21,6 +21,8 @@ #include "Functions.h" #include "../TerrainHandler.h" +#include + VCMI_LIB_NAMESPACE_BEGIN using namespace rmg; @@ -85,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(); @@ -94,24 +96,24 @@ 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(CRandomGenerator & rng) +void Object::Instance::setAnyTemplate(vstd::RNG & rng) { auto templates = dObject.getObjectHandler()->getTemplates(); if(templates.empty()) @@ -122,7 +124,7 @@ void Object::Instance::setAnyTemplate(CRandomGenerator & rng) setPosition(getPosition(false)); } -void Object::Instance::setTemplate(TerrainId terrain, CRandomGenerator & rng) +void Object::Instance::setTemplate(TerrainId terrain, vstd::RNG & rng) { auto templates = dObject.getObjectHandler()->getMostSpecificTemplates(terrain); @@ -326,7 +328,7 @@ const rmg::Area & Object::getVisitableArea() const { for(const auto & i : dInstances) { - // FIXME: Account for bjects with multiple visitable tiles + // FIXME: Account for objects with multiple visitable tiles dVisitableCache.add(i.getVisitablePosition()); } } @@ -366,7 +368,7 @@ void Object::setPosition(const int3 & position) i.setPositionRaw(i.getPosition()); } -void Object::setTemplate(const TerrainId & terrain, CRandomGenerator & rng) +void Object::setTemplate(const TerrainId & terrain, vstd::RNG & rng) { for(auto& i : dInstances) i.setTemplate(terrain, rng); @@ -474,7 +476,7 @@ rmg::Area Object::Instance::getBorderAbove() const return borderAbove; } -void Object::Instance::finalize(RmgMap & map, CRandomGenerator & rng) +void Object::Instance::finalize(RmgMap & map, vstd::RNG & rng) { if(!map.isOnMap(getPosition(true))) throw rmgException(boost::str(boost::format("Position of object %d at %s is outside the map") % dObject.id % getPosition(true).toString())); @@ -482,7 +484,7 @@ void Object::Instance::finalize(RmgMap & map, CRandomGenerator & 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()) { @@ -495,12 +497,12 @@ void Object::Instance::finalize(RmgMap & map, CRandomGenerator & 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()) @@ -511,7 +513,7 @@ void Object::Instance::finalize(RmgMap & map, CRandomGenerator & rng) map.getMapProxy()->insertObject(&dObject); } -void Object::finalize(RmgMap & map, CRandomGenerator & rng) +void Object::finalize(RmgMap & map, vstd::RNG & rng) { if(dInstances.empty()) throw rmgException("Cannot finalize object without instances"); diff --git a/lib/rmg/RmgObject.h b/lib/rmg/RmgObject.h index ffdfb0a89..c15256fb2 100644 --- a/lib/rmg/RmgObject.h +++ b/lib/rmg/RmgObject.h @@ -16,8 +16,12 @@ VCMI_LIB_NAMESPACE_BEGIN +namespace vstd +{ +class RNG; +} + class CGObjectInstance; -class CRandomGenerator; class RmgMap; namespace rmg { @@ -39,8 +43,8 @@ public: bool isRemovable() const; const Area & getAccessibleArea() const; Area getBorderAbove() const; - void setTemplate(TerrainId terrain, CRandomGenerator &); //cache invalidation - void setAnyTemplate(CRandomGenerator &); //cache invalidation + void setTemplate(TerrainId terrain, vstd::RNG &); //cache invalidation + void setAnyTemplate(vstd::RNG &); //cache invalidation int3 getTopTile() const; int3 getPosition(bool isAbsolute = false) const; @@ -49,7 +53,7 @@ public: const CGObjectInstance & object() const; CGObjectInstance & object(); - void finalize(RmgMap & map, CRandomGenerator &); //cache invalidation + void finalize(RmgMap & map, vstd::RNG &); //cache invalidation void clear(); std::function onCleared; @@ -83,7 +87,7 @@ public: const int3 & getPosition() const; void setPosition(const int3 & position); - void setTemplate(const TerrainId & terrain, CRandomGenerator &); + void setTemplate(const TerrainId & terrain, vstd::RNG &); const Area & getArea() const; //lazy cache invalidation const int3 getVisibleTop() const; @@ -94,7 +98,7 @@ public: void setValue(uint32_t value); uint32_t getValue() const; - void finalize(RmgMap & map, CRandomGenerator &); + void finalize(RmgMap & map, vstd::RNG &); void clearCachedArea() const; void clear(); diff --git a/lib/rmg/RmgPath.cpp b/lib/rmg/RmgPath.cpp index 134f08267..b36628f98 100644 --- a/lib/rmg/RmgPath.cpp +++ b/lib/rmg/RmgPath.cpp @@ -116,8 +116,7 @@ Path Path::search(const Tileset & dst, bool straight, std::functioncontains(pos)) return; - float movementCost = moveCostFunction(currentNode, pos) + currentNode.dist2d(pos); - + float movementCost = moveCostFunction(currentNode, pos); float distance = distances[currentNode] + movementCost; //we prefer to use already free paths int bestDistanceSoFar = std::numeric_limits::max(); auto it = distances.find(pos); @@ -190,4 +189,21 @@ const Area & Path::getPathArea() const return dPath; } +Path::MoveCostFunction Path::createCurvedCostFunction(const Area & border) +{ + // Capture by value to ensure the Area object persists + return [border = border](const int3& src, const int3& dst) -> float + { + // Route main roads far from border + float ret = dst.dist2d(src); + float dist = border.distanceSqr(dst); + + if(dist > 1.0f) + { + ret /= dist; + } + return ret; + }; +} + VCMI_LIB_NAMESPACE_END diff --git a/lib/rmg/RmgPath.h b/lib/rmg/RmgPath.h index a76df8cfa..7707e1abb 100644 --- a/lib/rmg/RmgPath.h +++ b/lib/rmg/RmgPath.h @@ -21,7 +21,8 @@ namespace rmg class Path { public: - const static std::function DEFAULT_MOVEMENT_FUNCTION; + using MoveCostFunction = std::function; + const static MoveCostFunction DEFAULT_MOVEMENT_FUNCTION; Path(const Area & area); Path(const Area & area, const int3 & src); @@ -42,6 +43,7 @@ public: const Area & getPathArea() const; static Path invalid(); + static MoveCostFunction createCurvedCostFunction(const Area & border); private: diff --git a/lib/rmg/TileInfo.cpp b/lib/rmg/TileInfo.cpp index e1701a3b7..5bd4d3b55 100644 --- a/lib/rmg/TileInfo.cpp +++ b/lib/rmg/TileInfo.cpp @@ -26,7 +26,7 @@ float TileInfo::getNearestObjectDistance() const void TileInfo::setNearestObjectDistance(float value) { - nearestObjectDistance = std::max(0, value); //never negative (or unitialized) + nearestObjectDistance = std::max(0, value); //never negative (or uninitialized) } bool TileInfo::shouldBeBlocked() const { diff --git a/lib/rmg/Zone.cpp b/lib/rmg/Zone.cpp index 2bf28cae6..e40954c66 100644 --- a/lib/rmg/Zone.cpp +++ b/lib/rmg/Zone.cpp @@ -17,6 +17,10 @@ #include "RmgPath.h" #include "modificators/ObjectManager.h" +#include "../CRandomGenerator.h" + +#include + VCMI_LIB_NAMESPACE_BEGIN const std::function AREA_NO_FILTER = [](const int3 & t) @@ -24,16 +28,18 @@ const std::function AREA_NO_FILTER = [](const int3 & t) return true; }; -Zone::Zone(RmgMap & map, CMapGenerator & generator, CRandomGenerator & r) +Zone::Zone(RmgMap & map, CMapGenerator & generator, vstd::RNG & r) : finished(false) , townType(ETownType::NEUTRAL) , terrainType(ETerrainId::GRASS) , map(map) + , rand(std::make_unique(r.nextInt())) , generator(generator) { - rand.setSeed(r.nextInt()); } +Zone::~Zone() = default; + bool Zone::isUnderground() const { return getPos().z; @@ -115,6 +121,11 @@ ThreadSafeProxy Zone::areaUsed() const return ThreadSafeProxy(dAreaUsed, areaMutex); } +rmg::Area Zone::areaForRoads() const +{ + return areaPossible() + freePaths(); +} + void Zone::clearTiles() { Lock lock(areaMutex); @@ -132,7 +143,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); @@ -269,7 +280,7 @@ void Zone::fractalize() { if (treasureValue > 250) { - // A quater at max density - means more free space + // A quarter at max density - means more free space marginFactor = (0.6f + ((std::max(0, (600 - treasureValue))) / (600.f - 250)) * 0.4f); // Low value - dense obstacles @@ -293,7 +304,6 @@ void Zone::fractalize() logGlobal->trace("Zone %d: treasureValue %d blockDistance: %2.f, freeDistance: %2.f", getId(), treasureValue, blockDistance, freeDistance); Lock lock(areaMutex); - // FIXME: Do not access Area directly rmg::Area clearedTiles(dAreaFree); rmg::Area possibleTiles(dAreaPossible); @@ -342,6 +352,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 @@ -401,9 +421,9 @@ void Zone::initModificators() } } -CRandomGenerator& Zone::getRand() +vstd::RNG& Zone::getRand() { - return rand; + return *rand; } VCMI_LIB_NAMESPACE_END diff --git a/lib/rmg/Zone.h b/lib/rmg/Zone.h index 50fd789c2..5b18a6fe8 100644 --- a/lib/rmg/Zone.h +++ b/lib/rmg/Zone.h @@ -13,7 +13,6 @@ #include "../GameConstants.h" #include "float3.h" #include "../int3.h" -#include "../CRandomGenerator.h" #include "CRmgTemplate.h" #include "RmgArea.h" #include "RmgPath.h" @@ -28,7 +27,6 @@ VCMI_LIB_NAMESPACE_BEGIN class RmgMap; class CMapGenerator; class Modificator; -class CRandomGenerator; extern const std::function AREA_NO_FILTER; @@ -74,8 +72,9 @@ private: class Zone : public rmg::ZoneOptions { public: - Zone(RmgMap & map, CMapGenerator & generator, CRandomGenerator & rand); + Zone(RmgMap & map, CMapGenerator & generator, vstd::RNG & rand); Zone(const Zone &) = delete; + ~Zone(); void setOptions(const rmg::ZoneOptions & options); bool isUnderground() const; @@ -93,6 +92,8 @@ public: ThreadSafeProxy freePaths() const; ThreadSafeProxy areaUsed(); ThreadSafeProxy areaUsed() const; + + rmg::Area areaForRoads() const; void initFreeTiles(); void clearTiles(); @@ -127,14 +128,14 @@ public: void initModificators(); - CRandomGenerator & getRand(); + vstd::RNG & getRand(); public: mutable boost::recursive_mutex areaMutex; using Lock = boost::unique_lock; protected: CMapGenerator & generator; - CRandomGenerator rand; + std::unique_ptr rand; RmgMap & map; TModificators modificators; bool finished; @@ -142,7 +143,7 @@ protected: //placement info int3 pos; float3 center; - rmg::Area dArea; //irregular area assined to zone + rmg::Area dArea; //irregular area assigned to zone rmg::Area dAreaPossible; rmg::Area dAreaFree; //core paths of free tiles that all other objects will be linked to rmg::Area dAreaUsed; diff --git a/lib/rmg/float3.h b/lib/rmg/float3.h index ca8775c3a..2870bcd6f 100644 --- a/lib/rmg/float3.h +++ b/lib/rmg/float3.h @@ -27,16 +27,16 @@ public: // returns float3 with coordinates increased by corresponding coordinate of given float3 float3 operator+(const float3 & i) const { return float3(x + i.x, y + i.y, z + i.z); } - // returns float3 with coordinates increased by given numer + // returns float3 with coordinates increased by given number float3 operator+(const float i) const { return float3(x + i, y + i, z + (si32)i); } // returns float3 with coordinates decreased by corresponding coordinate of given float3 float3 operator-(const float3 & i) const { return float3(x - i.x, y - i.y, z - i.z); } - // returns float3 with coordinates decreased by given numer + // returns float3 with coordinates decreased by given number float3 operator-(const float i) const { return float3(x - i, y - i, z - (si32)i); } - // returns float3 with plane coordinates decreased by given numer + // returns float3 with plane coordinates decreased by given number float3 operator*(const float i) const {return float3(x * i, y * i, z);} - // returns float3 with plane coordinates decreased by given numer + // returns float3 with plane coordinates decreased by given number float3 operator/(const float i) const {return float3(x / i, y / i, z);} // returns opposite position diff --git a/lib/rmg/modificators/ConnectionsPlacer.cpp b/lib/rmg/modificators/ConnectionsPlacer.cpp index e6692e260..244959d1a 100644 --- a/lib/rmg/modificators/ConnectionsPlacer.cpp +++ b/lib/rmg/modificators/ConnectionsPlacer.cpp @@ -26,6 +26,8 @@ #include "WaterProxy.h" #include "TownPlacer.h" +#include + VCMI_LIB_NAMESPACE_BEGIN std::pair ConnectionsPlacer::lockZones(std::shared_ptr otherZone) @@ -53,6 +55,17 @@ void ConnectionsPlacer::process() { for (auto& c : dConnections) { + if (c.getZoneA() == c.getZoneB()) + { + // Zone can always be connected to itself, but only by monolith pair + RecursiveLock lock(externalAccessMutex); + if (!vstd::contains(dCompleted, c)) + { + placeMonolithConnection(c); + continue; + } + } + auto otherZone = map.getZones().at(c.getZoneB()); auto* cp = otherZone->getModificator(); @@ -72,6 +85,11 @@ void ConnectionsPlacer::process() } }; + diningPhilosophers([this](const rmg::ZoneConnection& c) + { + forcePortalConnection(c); + }); + diningPhilosophers([this](const rmg::ZoneConnection& c) { selfSideDirectConnection(c); @@ -113,10 +131,19 @@ void ConnectionsPlacer::otherSideConnection(const rmg::ZoneConnection & connecti dCompleted.push_back(connection); } +void ConnectionsPlacer::forcePortalConnection(const rmg::ZoneConnection & connection) +{ + // This should always succeed + if (connection.getConnectionType() == rmg::EConnectionType::FORCE_PORTAL) + { + placeMonolithConnection(connection); + } +} + 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); @@ -158,8 +185,8 @@ void ConnectionsPlacer::selfSideDirectConnection(const rmg::ZoneConnection & con return 1.f / (1.f + border.distanceSqr(d)); }; - auto ourArea = zone.areaPossible() + zone.freePaths(); - auto theirArea = otherZone->areaPossible() + otherZone->freePaths(); + auto ourArea = zone.areaForRoads(); + auto theirArea = otherZone->areaForRoads(); theirArea.add(potentialPos); rmg::Path ourPath(ourArea); rmg::Path theirPath(theirArea); @@ -251,24 +278,22 @@ void ConnectionsPlacer::selfSideDirectConnection(const rmg::ZoneConnection & con assert(zone.getModificator()); auto & manager = *zone.getModificator(); auto * monsterType = manager.chooseGuard(connection.getGuardStrength(), true); - + rmg::Area border(zone.area()->getBorder()); border.unite(otherZone->area()->getBorder()); - - auto costFunction = [&border](const int3 & s, const int3 & d) - { - return 1.f / (1.f + border.distanceSqr(d)); - }; - - auto ourArea = zone.areaPossible() + zone.freePaths(); - auto theirArea = otherZone->areaPossible() + otherZone->freePaths(); + + auto localCostFunction = rmg::Path::createCurvedCostFunction(zone.area()->getBorder()); + auto otherCostFunction = rmg::Path::createCurvedCostFunction(otherZone->area()->getBorder()); + + auto ourArea = zone.areaForRoads(); + auto theirArea = otherZone->areaForRoads(); theirArea.add(guardPos); rmg::Path ourPath(ourArea); rmg::Path theirPath(theirArea); ourPath.connect(zone.freePaths().get()); - ourPath = ourPath.search(guardPos, true, costFunction); + ourPath = ourPath.search(guardPos, true, localCostFunction); theirPath.connect(otherZone->freePaths().get()); - theirPath = theirPath.search(guardPos, true, costFunction); + theirPath = theirPath.search(guardPos, true, otherCostFunction); if(ourPath.valid() && theirPath.valid()) { @@ -300,10 +325,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; } @@ -391,11 +415,14 @@ void ConnectionsPlacer::selfSideIndirectConnection(const rmg::ZoneConnection & c if(path1.valid() && path2.valid()) { - zone.connectPath(path1); - otherZone->connectPath(path2); - manager.placeObject(rmgGate1, guarded1, true, allowRoad); managerOther.placeObject(rmgGate2, guarded2, true, allowRoad); + + replaceWithCurvedPath(path1, zone, rmgGate1.getVisitablePosition()); + replaceWithCurvedPath(path2, *otherZone, rmgGate2.getVisitablePosition()); + + zone.connectPath(path1); + otherZone->connectPath(path2); assert(otherZone->getModificator()); otherZone->getModificator()->otherSideConnection(connection); @@ -408,23 +435,30 @@ void ConnectionsPlacer::selfSideIndirectConnection(const rmg::ZoneConnection & c //4. place monoliths/portals if(!success) { - auto factory = VLC->objtypeh->getHandlerFor(Obj::MONOLITH_TWO_WAY, generator.getNextMonlithIndex()); - auto * teleport1 = factory->create(map.mapInstance->cb, nullptr); - auto * teleport2 = factory->create(map.mapInstance->cb, nullptr); - - RequiredObjectInfo obj1(teleport1, connection.getGuardStrength(), allowRoad); - RequiredObjectInfo obj2(teleport2, connection.getGuardStrength(), allowRoad); - zone.getModificator()->addRequiredObject(obj1); - otherZone->getModificator()->addRequiredObject(obj2); - - assert(otherZone->getModificator()); - otherZone->getModificator()->otherSideConnection(connection); - - success = true; + placeMonolithConnection(connection); } +} + +void ConnectionsPlacer::placeMonolithConnection(const rmg::ZoneConnection & connection) +{ + auto otherZoneId = (connection.getZoneA() == zone.getId() ? connection.getZoneB() : connection.getZoneA()); + auto & otherZone = map.getZones().at(otherZoneId); + + bool allowRoad = shouldGenerateRoad(connection); + + auto factory = VLC->objtypeh->getHandlerFor(Obj::MONOLITH_TWO_WAY, generator.getNextMonlithIndex()); + auto * teleport1 = factory->create(map.mapInstance->cb, nullptr); + auto * teleport2 = factory->create(map.mapInstance->cb, nullptr); + + RequiredObjectInfo obj1(teleport1, connection.getGuardStrength(), allowRoad); + RequiredObjectInfo obj2(teleport2, connection.getGuardStrength(), allowRoad); + zone.getModificator()->addRequiredObject(obj1); + otherZone->getModificator()->addRequiredObject(obj2); + + dCompleted.push_back(connection); - if(success) - dCompleted.push_back(connection); + assert(otherZone->getModificator()); + otherZone->getModificator()->otherSideConnection(connection); } void ConnectionsPlacer::collectNeighbourZones() @@ -444,7 +478,7 @@ void ConnectionsPlacer::collectNeighbourZones() bool ConnectionsPlacer::shouldGenerateRoad(const rmg::ZoneConnection& connection) const { return connection.getRoadOption() == rmg::ERoadOption::ROAD_TRUE || - (connection.getRoadOption() == rmg::ERoadOption::ROAD_RANDOM && zone.getRand().nextDouble() >= 0.5f); + (connection.getRoadOption() == rmg::ERoadOption::ROAD_RANDOM && zone.getRand().nextDouble(0, 1) >= 0.5f); } void ConnectionsPlacer::createBorder() diff --git a/lib/rmg/modificators/ConnectionsPlacer.h b/lib/rmg/modificators/ConnectionsPlacer.h index 0350d6d92..ad31609b1 100644 --- a/lib/rmg/modificators/ConnectionsPlacer.h +++ b/lib/rmg/modificators/ConnectionsPlacer.h @@ -23,7 +23,8 @@ public: void init() override; void addConnection(const rmg::ZoneConnection& connection); - + void placeMonolithConnection(const rmg::ZoneConnection& connection); + void forcePortalConnection(const rmg::ZoneConnection & connection); void selfSideDirectConnection(const rmg::ZoneConnection & connection); void selfSideIndirectConnection(const rmg::ZoneConnection & connection); void otherSideConnection(const rmg::ZoneConnection & connection); diff --git a/lib/rmg/modificators/MinePlacer.cpp b/lib/rmg/modificators/MinePlacer.cpp index b0dc77a9e..4dcc2b41a 100644 --- a/lib/rmg/modificators/MinePlacer.cpp +++ b/lib/rmg/modificators/MinePlacer.cpp @@ -22,6 +22,8 @@ #include "WaterAdopter.h" #include "../TileInfo.h" +#include + VCMI_LIB_NAMESPACE_BEGIN void MinePlacer::process() diff --git a/lib/rmg/modificators/ObjectDistributor.cpp b/lib/rmg/modificators/ObjectDistributor.cpp index 76a0ac0a6..76bae9e41 100644 --- a/lib/rmg/modificators/ObjectDistributor.cpp +++ b/lib/rmg/modificators/ObjectDistributor.cpp @@ -25,6 +25,8 @@ #include "../Functions.h" #include "../RmgObject.h" +#include + VCMI_LIB_NAMESPACE_BEGIN void ObjectDistributor::process() @@ -43,7 +45,6 @@ void ObjectDistributor::init() void ObjectDistributor::distributeLimitedObjects() { - ObjectInfo oi; auto zones = map.getZones(); for (auto primaryID : VLC->objtypeh->knownObjects()) @@ -79,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); @@ -157,7 +160,7 @@ void ObjectDistributor::distributePrisons() } } - size_t allowedPrisons = prisonHeroPlacer->getPrisonsRemaning(); + size_t allowedPrisons = prisonHeroPlacer->getPrisonsRemaining(); for (int i = zones.size() - 1; i >= 0; i--) { auto zone = zones[i].second; diff --git a/lib/rmg/modificators/ObjectManager.cpp b/lib/rmg/modificators/ObjectManager.cpp index 849a94998..13b8b3000 100644 --- a/lib/rmg/modificators/ObjectManager.cpp +++ b/lib/rmg/modificators/ObjectManager.cpp @@ -29,6 +29,8 @@ #include "../Functions.h" #include "../RmgObject.h" +#include + VCMI_LIB_NAMESPACE_BEGIN void ObjectManager::process() @@ -342,7 +344,7 @@ rmg::Path ObjectManager::placeAndConnectObject(const rmg::Area & searchArea, rmg { int3 pos; auto possibleArea = searchArea; - auto cachedArea = zone.areaPossible() + zone.freePaths(); + auto cachedArea = zone.areaForRoads(); while(true) { pos = findPlaceForObject(possibleArea, obj, weightFunction, optimizer); @@ -417,6 +419,9 @@ bool ObjectManager::createMonoliths() return false; } + // Once it can be created, replace with curved path + replaceWithCurvedPath(path, zone, rmgObject.getVisitablePosition()); + zone.connectPath(path); placeObject(rmgObject, guarded, true, objInfo.createRoad); } @@ -447,6 +452,11 @@ bool ObjectManager::createRequiredObjects() logGlobal->error("Failed to fill zone %d due to lack of space", zone.getId()); return false; } + if (objInfo.createRoad) + { + // Once valid path can be created, replace with curved path + replaceWithCurvedPath(path, zone, rmgObject.getVisitablePosition()); + } zone.connectPath(path); placeObject(rmgObject, guarded, true, objInfo.createRoad); @@ -481,7 +491,7 @@ bool ObjectManager::createRequiredObjects() [this, &rmgObject](const int3 & tile) { float dist = rmgObject.getArea().distanceSqr(zone.getPos()); - dist *= (dist > 12.f * 12.f) ? 10.f : 1.f; //tiles closer 12 are preferrable + dist *= (dist > 12.f * 12.f) ? 10.f : 1.f; //tiles closer 12 are preferable dist = 1000000.f - dist; //some big number return dist + map.getNearestObjectDistance(tile); }, guarded, false, OptimizeType::WEIGHT); @@ -725,13 +735,13 @@ CGCreature * ObjectManager::chooseGuard(si32 strength, bool zoneGuard) CreatureID creId = CreatureID::NONE; int amount = 0; std::vector possibleCreatures; - for(auto cre : VLC->creh->objects) + for(auto const & cre : VLC->creh->objects) { if(cre->special) 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 fc9892753..5447a256e 100644 --- a/lib/rmg/modificators/ObstaclePlacer.cpp +++ b/lib/rmg/modificators/ObstaclePlacer.cpp @@ -19,14 +19,13 @@ #include "RiverPlacer.h" #include "../RmgMap.h" #include "../CMapGenerator.h" -#include "../../CRandomGenerator.h" #include "../Functions.h" +#include "../../entities/faction/CFaction.h" #include "../../mapping/CMapEditManager.h" #include "../../mapping/CMap.h" #include "../../mapping/ObstacleProxy.h" #include "../../mapObjects/CGObjectInstance.h" #include "../../mapObjects/ObstacleSetHandler.h" -#include "../../CTownHandler.h" VCMI_LIB_NAMESPACE_BEGIN @@ -40,7 +39,7 @@ void ObstaclePlacer::process() ObstacleSetFilter filter(ObstacleSet::EObstacleType::INVALID, zone.getTerrainType(), - static_cast(zone.isUnderground()), + static_cast(zone.isUnderground()), faction->getId(), faction->alignment); @@ -154,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/PrisonHeroPlacer.cpp b/lib/rmg/modificators/PrisonHeroPlacer.cpp index d4787784d..9d801e098 100644 --- a/lib/rmg/modificators/PrisonHeroPlacer.cpp +++ b/lib/rmg/modificators/PrisonHeroPlacer.cpp @@ -17,7 +17,9 @@ #include "../../VCMI_Lib.h" #include "../../mapObjectConstructors/AObjectTypeHandler.h" #include "../../mapObjectConstructors/CObjectClassesHandler.h" -#include "../../mapObjects/MapObjects.h" +#include "../../mapObjects/MapObjects.h" + +#include VCMI_LIB_NAMESPACE_BEGIN @@ -43,7 +45,7 @@ void PrisonHeroPlacer::getAllowedHeroes() } } -int PrisonHeroPlacer::getPrisonsRemaning() const +int PrisonHeroPlacer::getPrisonsRemaining() const { return std::max(allowedHeroes.size() - reservedHeroes, 0); } @@ -51,7 +53,7 @@ int PrisonHeroPlacer::getPrisonsRemaning() const HeroTypeID PrisonHeroPlacer::drawRandomHero() { RecursiveLock lock(externalAccessMutex); - if (getPrisonsRemaning() > 0) + if (getPrisonsRemaining() > 0) { RandomGeneratorUtil::randomShuffle(allowedHeroes, zone.getRand()); HeroTypeID ret = allowedHeroes.back(); diff --git a/lib/rmg/modificators/PrisonHeroPlacer.h b/lib/rmg/modificators/PrisonHeroPlacer.h index 62c32d381..6fc379da6 100644 --- a/lib/rmg/modificators/PrisonHeroPlacer.h +++ b/lib/rmg/modificators/PrisonHeroPlacer.h @@ -15,8 +15,6 @@ VCMI_LIB_NAMESPACE_BEGIN -class CRandomGenerator; - class PrisonHeroPlacer : public Modificator { public: @@ -25,7 +23,7 @@ public: void process() override; void init() override; - int getPrisonsRemaning() const; + int getPrisonsRemaining() const; [[nodiscard]] HeroTypeID drawRandomHero(); void restoreDrawnHero(const HeroTypeID & hid); diff --git a/lib/rmg/modificators/QuestArtifactPlacer.cpp b/lib/rmg/modificators/QuestArtifactPlacer.cpp index 912ee58c9..31d9e5c05 100644 --- a/lib/rmg/modificators/QuestArtifactPlacer.cpp +++ b/lib/rmg/modificators/QuestArtifactPlacer.cpp @@ -17,7 +17,9 @@ #include "../../VCMI_Lib.h" #include "../../mapObjectConstructors/AObjectTypeHandler.h" #include "../../mapObjectConstructors/CObjectClassesHandler.h" -#include "../../mapObjects/MapObjects.h" +#include "../../mapObjects/MapObjects.h" + +#include VCMI_LIB_NAMESPACE_BEGIN @@ -95,7 +97,7 @@ void QuestArtifactPlacer::findZonesForQuestArts() logGlobal->trace("Number of nearby zones suitable for quest artifacts: %d", questArtZones.size()); } -void QuestArtifactPlacer::placeQuestArtifacts(CRandomGenerator & rand) +void QuestArtifactPlacer::placeQuestArtifacts(vstd::RNG & rand) { for (const auto & artifactToPlace : questArtifactsToPlace) { @@ -110,7 +112,7 @@ void QuestArtifactPlacer::placeQuestArtifacts(CRandomGenerator & 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. @@ -119,7 +121,7 @@ void QuestArtifactPlacer::placeQuestArtifacts(CRandomGenerator & 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/QuestArtifactPlacer.h b/lib/rmg/modificators/QuestArtifactPlacer.h index 28896b711..6800bcb48 100644 --- a/lib/rmg/modificators/QuestArtifactPlacer.h +++ b/lib/rmg/modificators/QuestArtifactPlacer.h @@ -15,8 +15,6 @@ VCMI_LIB_NAMESPACE_BEGIN -class CRandomGenerator; - class QuestArtifactPlacer : public Modificator { public: @@ -33,7 +31,7 @@ public: void rememberPotentialArtifactToReplace(CGObjectInstance* obj); CGObjectInstance * drawObjectToReplace(); std::vector getPossibleArtifactsToReplace() const; - void placeQuestArtifacts(CRandomGenerator & rand); + void placeQuestArtifacts(vstd::RNG & rand); void dropReplacedArtifact(CGObjectInstance* obj); size_t getMaxQuestArtifactCount() const; @@ -50,4 +48,4 @@ protected: std::vector questArtifacts; }; -VCMI_LIB_NAMESPACE_END \ No newline at end of file +VCMI_LIB_NAMESPACE_END diff --git a/lib/rmg/modificators/RiverPlacer.cpp b/lib/rmg/modificators/RiverPlacer.cpp index 0201ac008..9983d2011 100644 --- a/lib/rmg/modificators/RiverPlacer.cpp +++ b/lib/rmg/modificators/RiverPlacer.cpp @@ -20,12 +20,15 @@ #include "../../mapObjects/ObjectTemplate.h" #include "../../mapping/CMap.h" #include "../../mapping/CMapEditManager.h" +#include "../../VCMI_Lib.h" #include "../RmgPath.h" #include "ObjectManager.h" #include "ObstaclePlacer.h" #include "WaterProxy.h" #include "RoadPlacer.h" +#include + VCMI_LIB_NAMESPACE_BEGIN const int RIVER_DELTA_ID = 143; @@ -209,7 +212,7 @@ void RiverPlacer::preprocess() { auto river = VLC->terrainTypeHandler->getById(zone.getTerrainType())->river; auto & a = neighbourZonesTiles[connectedToWaterZoneId]; - auto availableArea = zone.areaPossible() + zone.freePaths(); + auto availableArea = zone.areaForRoads(); for(const auto & tileToProcess : availableArea.getTilesVector()) { int templateId = -1; diff --git a/lib/rmg/modificators/RoadPlacer.cpp b/lib/rmg/modificators/RoadPlacer.cpp index 27dc8eea1..a2a404766 100644 --- a/lib/rmg/modificators/RoadPlacer.cpp +++ b/lib/rmg/modificators/RoadPlacer.cpp @@ -21,6 +21,7 @@ #include "../../modding/IdentifierStorage.h" #include "../../modding/ModScope.h" #include "../../TerrainHandler.h" +#include "../../VCMI_Lib.h" VCMI_LIB_NAMESPACE_BEGIN @@ -148,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 db422ec00..e7cf8848b 100644 --- a/lib/rmg/modificators/RockFiller.cpp +++ b/lib/rmg/modificators/RockFiller.cpp @@ -19,7 +19,6 @@ #include "../CMapGenerator.h" #include "../Functions.h" #include "../../TerrainHandler.h" -#include "../../CRandomGenerator.h" #include "../lib/mapping/CMapEditManager.h" #include "../TileInfo.h" #include "../threadpool/MapProxy.h" @@ -73,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 e3f68cdce..bf51be7e8 100644 --- a/lib/rmg/modificators/RockPlacer.cpp +++ b/lib/rmg/modificators/RockPlacer.cpp @@ -18,8 +18,8 @@ #include "../CMapGenerator.h" #include "../Functions.h" #include "../../TerrainHandler.h" -#include "../../CRandomGenerator.h" #include "../../mapping/CMapEditManager.h" +#include "../../VCMI_Lib.h" #include "../TileInfo.h" VCMI_LIB_NAMESPACE_BEGIN @@ -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/TerrainPainter.cpp b/lib/rmg/modificators/TerrainPainter.cpp index 5d88871ee..8bd6bb949 100644 --- a/lib/rmg/modificators/TerrainPainter.cpp +++ b/lib/rmg/modificators/TerrainPainter.cpp @@ -20,7 +20,10 @@ #include "../RmgMap.h" #include "../../VCMI_Lib.h" #include "../../TerrainHandler.h" -#include "../../CTownHandler.h" +#include "../../entities/faction/CFaction.h" +#include "../../entities/faction/CTownHandler.h" + +#include VCMI_LIB_NAMESPACE_BEGIN @@ -76,7 +79,7 @@ void TerrainPainter::initTerrainType() { //Fill with all terain types by default { - for (auto terrain : VLC->terrainTypeHandler->objects) + for (const auto & terrain : VLC->terrainTypeHandler->objects) { if (terrain->isLand() && terrain->isPassable()) { diff --git a/lib/rmg/modificators/TownPlacer.cpp b/lib/rmg/modificators/TownPlacer.cpp index 8f64db118..1ab68779d 100644 --- a/lib/rmg/modificators/TownPlacer.cpp +++ b/lib/rmg/modificators/TownPlacer.cpp @@ -12,6 +12,7 @@ #include "TownPlacer.h" #include "../CMapGenerator.h" #include "../RmgMap.h" +#include "../../entities/faction/CTownHandler.h" #include "../../mapObjectConstructors/AObjectTypeHandler.h" #include "../../mapObjectConstructors/CObjectClassesHandler.h" #include "../../mapObjects/CGTownInstance.h" @@ -27,6 +28,8 @@ #include "WaterAdopter.h" #include "../TileInfo.h" +#include + VCMI_LIB_NAMESPACE_BEGIN void TownPlacer::process() @@ -76,20 +79,18 @@ void TownPlacer::placeTowns(ObjectManager & manager) CGTownInstance * town = dynamic_cast(townFactory->create(map.mapInstance->cb, nullptr)); town->tempOwner = player; - town->builtBuildings.insert(BuildingID::FORT); - town->builtBuildings.insert(BuildingID::DEFAULT); + town->addBuilding(BuildingID::FORT); + town->addBuilding(BuildingID::DEFAULT); - for(auto spell : VLC->spellh->objects) //add all regular spells to town - { - if(!spell->isSpecial() && !spell->isCreatureAbility()) - town->possibleSpells.push_back(spell->id); - } + + for(auto spellID : VLC->spellh->getDefaultAllowed()) //add all regular spells to town + town->possibleSpells.push_back(spellID); auto position = placeMainTown(manager, *town); totalTowns++; //register MAIN town of zone only - map.registerZone(town->getFaction()); + map.registerZone(town->getFactionID()); if(player.isValidPlayer()) //configure info for owning player { @@ -202,20 +203,17 @@ void TownPlacer::addNewTowns(int count, bool hasFort, const PlayerColor & player town->tempOwner = player; if (hasFort) - town->builtBuildings.insert(BuildingID::FORT); - town->builtBuildings.insert(BuildingID::DEFAULT); + town->addBuilding(BuildingID::FORT); + town->addBuilding(BuildingID::DEFAULT); - for(auto spell : VLC->spellh->objects) //add all regular spells to town - { - if(!spell->isSpecial() && !spell->isCreatureAbility()) - town->possibleSpells.push_back(spell->id); - } + for(auto spellID : VLC->spellh->getDefaultAllowed()) //add all regular spells to town + town->possibleSpells.push_back(spellID); if(totalTowns <= 0) { //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 127c4d99c..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" @@ -33,17 +35,33 @@ #include "../../mapping/CMap.h" #include "../../mapping/CMapEditManager.h" +#include + 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); @@ -60,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)) @@ -81,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()) @@ -117,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()) @@ -141,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()); @@ -155,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 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}; @@ -194,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; @@ -206,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 * @@ -221,12 +273,10 @@ void TreasurePlacer::addAllPossibleObjects() auto * obj = dynamic_cast(factory->create(map.mapInstance->cb, nullptr)); std::vector out; - for(auto spell : VLC->spellh->objects) //spellh size appears to be greater (?) + for(auto spellID : VLC->spellh->getDefaultAllowed()) { - if(map.isAllowedSpell(spell->id) && spell->getLevel() == i + 1) - { - out.push_back(spell->id); - } + if(map.isAllowedSpell(spellID) && spellID.toSpell()->getLevel() == i + 1) + out.push_back(spellID); } auto * a = ArtifactUtils::createScroll(*RandomGeneratorUtil::nextItem(out, zone.getRand())); obj->storedArtifact = a; @@ -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++) { @@ -354,10 +394,10 @@ void TreasurePlacer::addAllPossibleObjects() auto * obj = dynamic_cast(factory->create(map.mapInstance->cb, nullptr)); std::vector spells; - for(auto spell : VLC->spellh->objects) + for(auto spellID : VLC->spellh->getDefaultAllowed()) { - if(map.isAllowedSpell(spell->id) && spell->getLevel() == i) - spells.push_back(spell.get()); + if(map.isAllowedSpell(spellID) && spellID.toSpell()->getLevel() == i) + spells.push_back(spellID.toSpell()); } RandomGeneratorUtil::randomShuffle(spells, zone.getRand()); @@ -387,10 +427,10 @@ void TreasurePlacer::addAllPossibleObjects() auto * obj = dynamic_cast(factory->create(map.mapInstance->cb, nullptr)); std::vector spells; - for(auto spell : VLC->spellh->objects) + for(auto spellID : VLC->spellh->getDefaultAllowed()) { - if(map.isAllowedSpell(spell->id) && spell->hasSchool(SpellSchool(i))) - spells.push_back(spell.get()); + if(map.isAllowedSpell(spellID) && spellID.toSpell()->hasSchool(SpellSchool(i))) + spells.push_back(spellID.toSpell()); } RandomGeneratorUtil::randomShuffle(spells, zone.getRand()); @@ -419,10 +459,10 @@ void TreasurePlacer::addAllPossibleObjects() auto * obj = dynamic_cast(factory->create(map.mapInstance->cb, nullptr)); std::vector spells; - for(auto spell : VLC->spellh->objects) + for(auto spellID : VLC->spellh->getDefaultAllowed()) { - if(map.isAllowedSpell(spell->id)) - spells.push_back(spell.get()); + if(map.isAllowedSpell(spellID)) + spells.push_back(spellID.toSpell()); } RandomGeneratorUtil::randomShuffle(spells, zone.getRand()); @@ -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; @@ -523,7 +568,7 @@ void TreasurePlacer::addAllPossibleObjects() } static const int seerLevels = std::min(generator.getConfig().questValues.size(), generator.getConfig().questRewardValues.size()); - for(int i = 0; i < seerLevels; i++) //seems that code for exp and gold reward is similiar + for(int i = 0; i < seerLevels; i++) //seems that code for exp and gold reward is similar { int randomAppearance = chooseRandomAppearance(zone.getRand(), Obj::SEER_HUT, zone.getTerrainType()); @@ -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; @@ -651,7 +725,7 @@ std::vector TreasurePlacer::prepareTreasurePile(const CTreasureInfo if (currentValue >= minValue) { // 50% chance to end right here - if (zone.getRand().nextInt() & 1) + if (zone.getRand().nextInt(0, 1) == 1) 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,17 +973,21 @@ 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 maxPileGenerationAttemps = 2; - for (ui32 attempt = 0; attempt < maxPileGenerationAttemps; attempt++) + const ui32 maxPileGenerationAttempts = 2; + for (ui32 attempt = 0; attempt < maxPileGenerationAttempts; attempt++) { auto rmgObject = constructTreasurePile(treasurePileInfos, attempt == maxAttempts); if (rmgObject.instances().empty()) { - // Restore once if all attemps failed - if (attempt == (maxPileGenerationAttemps - 1)) + // Restore once if all attempts failed + if (attempt == (maxPileGenerationAttempts - 1)) { restoreZoneLimits(treasurePileInfos); } @@ -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 8c6a6c316..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,22 +20,7 @@ class CGObjectInstance; class ObjectManager; class RmgMap; class CMapGenerator; -class CRandomGenerator; - -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 { @@ -46,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); @@ -60,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; @@ -68,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/WaterAdopter.cpp b/lib/rmg/modificators/WaterAdopter.cpp index bdb985890..642a2c5aa 100644 --- a/lib/rmg/modificators/WaterAdopter.cpp +++ b/lib/rmg/modificators/WaterAdopter.cpp @@ -23,6 +23,8 @@ #include "ConnectionsPlacer.h" #include "../TileInfo.h" +#include + VCMI_LIB_NAMESPACE_BEGIN void WaterAdopter::process() @@ -133,13 +135,13 @@ void WaterAdopter::createWater(EWaterContent::EWaterContent waterContent) waterArea.subtract(noWaterArea); - //start filtering of narrow places and coast atrifacts + //start filtering of narrow places and coast artifacts rmg::Area waterAdd; for(int coastId = 1; coastId <= coastIdMax; ++coastId) { for(const auto & tile : reverseDistanceMap[coastId]) { - //collect neighbout water tiles + //collect neighbour water tiles auto collectionLambda = [this](const int3 & t, std::set & outCollection) { if(waterArea.contains(t)) diff --git a/lib/rmg/modificators/WaterProxy.cpp b/lib/rmg/modificators/WaterProxy.cpp index c9678f406..5172910fa 100644 --- a/lib/rmg/modificators/WaterProxy.cpp +++ b/lib/rmg/modificators/WaterProxy.cpp @@ -30,6 +30,8 @@ #include "WaterAdopter.h" #include "../RmgArea.h" +#include + VCMI_LIB_NAMESPACE_BEGIN void WaterProxy::process() @@ -49,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 @@ -64,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); @@ -264,9 +266,9 @@ bool WaterProxy::placeBoat(Zone & land, const Lake & lake, bool createRoad, Rout rmg::Object rmgObject(*boat); rmgObject.setTemplate(zone.getTerrainType(), zone.getRand()); - auto waterAvailable = zone.areaPossible() + zone.freePaths(); + auto waterAvailable = zone.areaForRoads(); rmg::Area coast = lake.neighbourZones.at(land.getId()); //having land tiles - coast.intersect(land.areaPossible() + land.freePaths()); //having only available land tiles + coast.intersect(land.areaForRoads()); //having only available land tiles auto boardingPositions = coast.getSubarea([&waterAvailable, this](const int3 & tile) //tiles where boarding is possible { //We don't want place boat right to any land object, especiallly the zone guard @@ -330,10 +332,10 @@ bool WaterProxy::placeShipyard(Zone & land, const Lake & lake, si32 guard, bool rmgObject.setTemplate(land.getTerrainType(), zone.getRand()); bool guarded = manager->addGuard(rmgObject, guard); - auto waterAvailable = zone.areaPossible() + zone.freePaths(); + auto waterAvailable = zone.areaForRoads(); waterAvailable.intersect(lake.area); rmg::Area coast = lake.neighbourZones.at(land.getId()); //having land tiles - coast.intersect(land.areaPossible() + land.freePaths()); //having only available land tiles + coast.intersect(land.areaForRoads()); //having only available land tiles auto boardingPositions = coast.getSubarea([&waterAvailable](const int3 & tile) //tiles where boarding is possible { rmg::Area a({tile}); diff --git a/lib/rmg/threadpool/MapProxy.cpp b/lib/rmg/threadpool/MapProxy.cpp index af872e2c5..1f29b1bd3 100644 --- a/lib/rmg/threadpool/MapProxy.cpp +++ b/lib/rmg/threadpool/MapProxy.cpp @@ -10,6 +10,7 @@ #include "MapProxy.h" #include "../../TerrainHandler.h" +#include "../../VCMI_Lib.h" VCMI_LIB_NAMESPACE_BEGIN @@ -36,21 +37,21 @@ void MapProxy::removeObject(CGObjectInstance * obj) map.getEditManager()->removeObject(obj); } -void MapProxy::drawTerrain(CRandomGenerator & generator, std::vector & tiles, TerrainId terrain) +void MapProxy::drawTerrain(vstd::RNG & generator, std::vector & tiles, TerrainId terrain) { Lock lock(mx); map.getEditManager()->getTerrainSelection().setSelection(tiles); map.getEditManager()->drawTerrain(terrain, map.getDecorationsPercentage(), &generator); } -void MapProxy::drawRivers(CRandomGenerator & generator, std::vector & tiles, TerrainId terrain) +void MapProxy::drawRivers(vstd::RNG & generator, std::vector & tiles, TerrainId terrain) { Lock lock(mx); map.getEditManager()->getTerrainSelection().setSelection(tiles); map.getEditManager()->drawRiver(VLC->terrainTypeHandler->getById(terrain)->river, &generator); } -void MapProxy::drawRoads(CRandomGenerator & generator, std::vector & tiles, RoadId roadType) +void MapProxy::drawRoads(vstd::RNG & generator, std::vector & tiles, RoadId roadType) { Lock lock(mx); map.getEditManager()->getTerrainSelection().setSelection(tiles); diff --git a/lib/rmg/threadpool/MapProxy.h b/lib/rmg/threadpool/MapProxy.h index 9cb265f8f..5158a97fa 100644 --- a/lib/rmg/threadpool/MapProxy.h +++ b/lib/rmg/threadpool/MapProxy.h @@ -28,9 +28,9 @@ public: void insertObjects(std::set& objects); void removeObject(CGObjectInstance* obj); - void drawTerrain(CRandomGenerator & generator, std::vector & tiles, TerrainId terrain); - void drawRivers(CRandomGenerator & generator, std::vector & tiles, TerrainId terrain); - void drawRoads(CRandomGenerator & generator, std::vector & tiles, RoadId roadType); + void drawTerrain(vstd::RNG & generator, std::vector & tiles, TerrainId terrain); + void drawRivers(vstd::RNG & generator, std::vector & tiles, TerrainId terrain); + void drawRoads(vstd::RNG & generator, std::vector & tiles, RoadId roadType); private: mutable boost::shared_mutex mx; diff --git a/lib/serializer/BinaryDeserializer.cpp b/lib/serializer/BinaryDeserializer.cpp index ab8330b58..94630bec8 100644 --- a/lib/serializer/BinaryDeserializer.cpp +++ b/lib/serializer/BinaryDeserializer.cpp @@ -9,18 +9,13 @@ */ #include "StdInc.h" #include "BinaryDeserializer.h" -#include "../registerTypes/RegisterTypes.h" VCMI_LIB_NAMESPACE_BEGIN BinaryDeserializer::BinaryDeserializer(IBinaryReader * r): CLoaderBase(r) { - saving = false; version = Version::NONE; - smartPointerSerialization = true; reverseEndianness = false; - - registerTypes(*this); } VCMI_LIB_NAMESPACE_END diff --git a/lib/serializer/BinaryDeserializer.h b/lib/serializer/BinaryDeserializer.h index 0042b57d4..9db041b2e 100644 --- a/lib/serializer/BinaryDeserializer.h +++ b/lib/serializer/BinaryDeserializer.h @@ -10,7 +10,7 @@ #pragma once #include "CSerializer.h" -#include "CTypeList.h" +#include "SerializerReflection.h" #include "ESerializationVersion.h" #include "../mapObjects/CGHeroInstance.h" @@ -35,75 +35,50 @@ public: /// Main class for deserialization of classes from binary form /// Effectively revesed version of BinarySerializer -class DLL_LINKAGE BinaryDeserializer : public CLoaderBase +class BinaryDeserializer : public CLoaderBase { - template - struct LoadIfStackInstance + template + static bool loadIfStackInstance(T &data) { - static bool invoke(Ser &s, T &data) - { + return false; + } + + template + bool loadIfStackInstance(const CStackInstance* &data) + { + CArmedInstance * armyPtr = nullptr; + ObjectInstanceID armyID; + SlotID slot; + load(armyID); + load(slot); + + if (armyID == ObjectInstanceID::NONE) return false; - } - }; - template - struct LoadIfStackInstance - { - static bool invoke(Ser &s, CStackInstance* &data) + if(reader->smartVectorMembersSerialization) { - CArmedInstance *armedObj; - SlotID slot; - s.load(armedObj); - s.load(slot); - if(slot != SlotID::COMMANDER_SLOT_PLACEHOLDER) - { - assert(armedObj->hasStackAtSlot(slot)); - data = armedObj->stacks[slot]; - } - else - { - auto * hero = dynamic_cast(armedObj); - assert(hero); - assert(hero->commander); - data = hero->commander; - } - return true; + if(const auto *info = reader->getVectorizedTypeInfo()) + armyPtr = reader->getVectorItemFromId(*info, armyID); } - }; - template - struct ClassObjectCreator - { - static T *invoke(IGameCallback *cb) + if(slot != SlotID::COMMANDER_SLOT_PLACEHOLDER) { - static_assert(!std::is_base_of_v, "Cannot call new upon map objects!"); - static_assert(!std::is_abstract_v, "Cannot call new upon abstract classes!"); - return new T(); + assert(armyPtr->hasStackAtSlot(slot)); + data = armyPtr->stacks[slot]; } - }; - - template - struct ClassObjectCreator>> - { - static T *invoke(IGameCallback *cb) + else { - throw std::runtime_error("Something went really wrong during deserialization. Attempted creating an object of an abstract class " + std::string(typeid(T).name())); + auto * hero = dynamic_cast(armyPtr); + assert(hero); + assert(hero->commander); + data = hero->commander; } - }; + return true; + } - template - struct ClassObjectCreator && !std::is_abstract_v>> + STRONG_INLINE uint32_t readAndCheckLength() { - static T *invoke(IGameCallback *cb) - { - static_assert(!std::is_abstract_v, "Cannot call new upon abstract classes!"); - return new T(cb); - } - }; - - STRONG_INLINE ui32 readAndCheckLength() - { - ui32 length; + uint32_t length; load(length); //NOTE: also used for h3m's embedded in campaigns, so it may be quite large in some cases (e.g. XXL maps with multiple objects) if(length > 1000000) @@ -114,40 +89,6 @@ class DLL_LINKAGE BinaryDeserializer : public CLoaderBase return length; } - template class CPointerLoader; - - class IPointerLoader - { - public: - virtual void * loadPtr(CLoaderBase &ar, IGameCallback * cb, ui32 pid) const =0; //data is pointer to the ACTUAL POINTER - virtual ~IPointerLoader() = default; - - template static IPointerLoader *getApplier(const Type * t = nullptr) - { - return new CPointerLoader(); - } - }; - - template - class CPointerLoader : public IPointerLoader - { - public: - void * loadPtr(CLoaderBase &ar, IGameCallback * cb, ui32 pid) const override //data is pointer to the ACTUAL POINTER - { - auto & s = static_cast(ar); - - //create new object under pointer - Type * ptr = ClassObjectCreator::invoke(cb); //does new npT or throws for abstract classes - s.ptrAllocated(ptr, pid); - - ptr->serialize(s); - - return static_cast(ptr); - } - }; - - CApplier applier; - int write(const void * data, unsigned size); public: @@ -156,13 +97,20 @@ public: bool reverseEndianness; //if source has different endianness than us, we reverse bytes Version version; - std::map loadedPointers; - std::map> loadedSharedPointers; + std::vector loadedStrings; + std::map loadedPointers; + std::map> loadedSharedPointers; IGameCallback * cb = nullptr; - bool smartPointerSerialization; - bool saving; + static constexpr bool trackSerializedPointers = true; + static constexpr bool saving = false; + bool loadingGamestate = false; - BinaryDeserializer(IBinaryReader * r); + bool hasFeature(Version what) const + { + return version >= what; + }; + + DLL_LINKAGE BinaryDeserializer(IBinaryReader * r); template BinaryDeserializer & operator&(T & t) @@ -171,12 +119,56 @@ public: return * this; } - template < class T, typename std::enable_if_t < std::is_fundamental_v && !std::is_same_v, int > = 0 > + int64_t loadEncodedInteger() + { + uint64_t valueUnsigned = 0; + uint_fast8_t offset = 0; + + for (;;) + { + uint8_t byteValue; + load(byteValue); + + if ((byteValue & 0x80) != 0) + { + valueUnsigned |= static_cast(byteValue & 0x7f) << offset; + offset += 7; + } + else + { + valueUnsigned |= static_cast(byteValue & 0x3f) << offset; + bool isNegative = (byteValue & 0x40) != 0; + if (isNegative) + return -static_cast(valueUnsigned); + else + return valueUnsigned; + } + } + } + + template < class T, typename std::enable_if_t < std::is_floating_point_v, int > = 0 > void load(T &data) { this->read(static_cast(&data), sizeof(data), reverseEndianness); } + template < class T, typename std::enable_if_t < std::is_integral_v && !std::is_same_v, int > = 0 > + void load(T &data) + { + if constexpr (sizeof(T) == 1) + { + this->read(static_cast(&data), sizeof(data), reverseEndianness); + } + else + { + static_assert(!std::is_same_v, "Serialization of unsigned 64-bit value may not work in some cases"); + if (hasFeature(Version::COMPACT_INTEGER_SERIALIZATION)) + data = loadEncodedInteger(); + else + this->read(static_cast(&data), sizeof(data), reverseEndianness); + } + } + template < typename T, typename std::enable_if_t < is_serializeable::value, int > = 0 > void load(T &data) { @@ -188,15 +180,20 @@ public: template < typename T, typename std::enable_if_t < std::is_array_v, int > = 0 > void load(T &data) { - ui32 size = std::size(data); - for(ui32 i = 0; i < size; i++) + uint32_t size = std::size(data); + for(uint32_t i = 0; i < size; i++) load(data[i]); } + void load(Version &data) + { + this->read(static_cast(&data), sizeof(data), reverseEndianness); + } + template < typename T, typename std::enable_if_t < std::is_enum_v, int > = 0 > void load(T &data) { - si32 read; + int32_t read; load( read ); data = static_cast(read); } @@ -204,7 +201,7 @@ public: template < typename T, typename std::enable_if_t < std::is_same_v, int > = 0 > void load(T &data) { - ui8 read; + uint8_t read; load( read ); data = static_cast(read); } @@ -212,18 +209,18 @@ public: template , int > = 0> void load(std::vector &data) { - ui32 length = readAndCheckLength(); + uint32_t length = readAndCheckLength(); data.resize(length); - for(ui32 i=0;i, int > = 0> void load(std::deque & data) { - ui32 length = readAndCheckLength(); + uint32_t length = readAndCheckLength(); data.resize(length); - for(ui32 i = 0; i < length; i++) + for(uint32_t i = 0; i < length; i++) load(data[i]); } @@ -238,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 @@ -276,13 +254,13 @@ public: if(reader->sendStackInstanceByIds) { - bool gotLoaded = LoadIfStackInstance::invoke(* this, data); + bool gotLoaded = loadIfStackInstance(data); if(gotLoaded) return; } - ui32 pid = 0xffffffff; //pointer id (or maybe rather pointee id) - if(smartPointerSerialization) + uint32_t pid = 0xffffffff; //pointer id (or maybe rather pointee id) + if(trackSerializedPointers) { load( pid ); //get the id auto i = loadedPointers.find(pid); //lookup @@ -291,45 +269,43 @@ public: { // We already got this pointer // Cast it in case we are loading it to a non-first base pointer - data = static_cast(i->second); + data = dynamic_cast(i->second); return; } } //get type id - ui16 tid; + uint16_t tid; load( tid ); + typedef typename std::remove_pointer_t npT; + typedef typename std::remove_const_t ncpT; if(!tid) { - typedef typename std::remove_pointer_t npT; - typedef typename std::remove_const_t ncpT; data = ClassObjectCreator::invoke(cb); ptrAllocated(data, pid); load(*data); } else { - auto * app = applier.getApplier(tid); + auto * app = CSerializationApplier::getInstance().getApplier(tid); if(app == nullptr) { logGlobal->error("load %d %d - no loader exists", tid, pid); data = nullptr; return; } - data = static_cast(app->loadPtr(*this, cb, pid)); + auto dataNonConst = dynamic_cast(app->createPtr(*this, cb)); + data = dataNonConst; + ptrAllocated(data, pid); + app->loadPtr(*this, cb, dataNonConst); } } template - void ptrAllocated(const T *ptr, ui32 pid) + void ptrAllocated(T *ptr, uint32_t pid) { - if(smartPointerSerialization && pid != 0xffffffff) - loadedPointers[pid] = (void*)ptr; //add loaded pointer to our lookup map; cast is to avoid errors with const T* pt - } - - template void registerType(const Base * b = nullptr, const Derived * d = nullptr) - { - applier.registerType(b, d); + if(trackSerializedPointers && pid != 0xffffffff) + loadedPointers[pid] = const_cast(dynamic_cast(ptr)); //add loaded pointer to our lookup map; cast is to avoid errors with const T* pt } template @@ -339,7 +315,7 @@ public: NonConstT *internalPtr; load(internalPtr); - void * internalPtrDerived = static_cast(internalPtr); + const auto * internalPtrDerived = static_cast(internalPtr); if(internalPtr) { @@ -354,7 +330,7 @@ public: { auto hlp = std::shared_ptr(internalPtr); data = hlp; - loadedSharedPointers[internalPtrDerived] = std::static_pointer_cast(hlp); + loadedSharedPointers[internalPtrDerived] = std::static_pointer_cast(hlp); } } else @@ -386,16 +362,16 @@ public: template void load(std::array &data) { - for(ui32 i = 0; i < N; i++) + for(uint32_t i = 0; i < N; i++) load( data[i] ); } template void load(std::set &data) { - ui32 length = readAndCheckLength(); + uint32_t length = readAndCheckLength(); data.clear(); T ins; - for(ui32 i=0;i void load(std::unordered_set &data) { - ui32 length = readAndCheckLength(); + uint32_t length = readAndCheckLength(); data.clear(); T ins; - for(ui32 i=0;i void load(std::list &data) { - ui32 length = readAndCheckLength(); + uint32_t length = readAndCheckLength(); data.clear(); T ins; - for(ui32 i=0;i - void load(std::map &data) + void load(std::unordered_map &data) { - ui32 length = readAndCheckLength(); + uint32_t length = readAndCheckLength(); data.clear(); T1 key; - for(ui32 i=0;i + void load(std::map &data) + { + uint32_t length = readAndCheckLength(); + data.clear(); + T1 key; + for(uint32_t i=0;iread(static_cast(data.data()), length, false); + if (hasFeature(Version::COMPACT_STRING_SERIALIZATION)) + { + int32_t length; + load(length); + + if (length < 0) + { + int32_t stringID = -length - 1; // -1, -2 ... -> 0, 1 ... + data = loadedStrings[stringID]; + } + if (length == 0) + { + data = {}; + } + if (length > 0) + { + data.resize(length); + this->read(static_cast(data.data()), length, false); + loadedStrings.push_back(data); + } + } + else + { + uint32_t length = readAndCheckLength(); + data.resize(length); + this->read(static_cast(data.data()), length, false); + } } template void load(std::variant & data) { - si32 which; + int32_t which; load( which ); assert(which < sizeof...(TN)); @@ -469,7 +482,7 @@ public: template void load(std::optional & data) { - ui8 present; + uint8_t present; load( present ); if(present) { @@ -487,16 +500,16 @@ public: template void load(boost::multi_array & data) { - ui32 length = readAndCheckLength(); - ui32 x; - ui32 y; - ui32 z; + uint32_t length = readAndCheckLength(); + uint32_t x; + uint32_t y; + uint32_t z; load(x); load(y); load(z); data.resize(boost::extents[x][y][z]); assert(length == data.num_elements()); //x*y*z should be equal to number of elements - for(ui32 i = 0; i < length; i++) + for(uint32_t i = 0; i < length; i++) load(data.data()[i]); } template diff --git a/lib/serializer/BinarySerializer.cpp b/lib/serializer/BinarySerializer.cpp index 8ba41babd..76d358100 100644 --- a/lib/serializer/BinarySerializer.cpp +++ b/lib/serializer/BinarySerializer.cpp @@ -9,15 +9,11 @@ */ #include "StdInc.h" #include "BinarySerializer.h" -#include "../registerTypes/RegisterTypes.h" VCMI_LIB_NAMESPACE_BEGIN BinarySerializer::BinarySerializer(IBinaryWriter * w): CSaverBase(w) { - saving=true; - smartPointerSerialization = true; - registerTypes(*this); } VCMI_LIB_NAMESPACE_END diff --git a/lib/serializer/BinarySerializer.h b/lib/serializer/BinarySerializer.h index 5a73d4b38..851d53836 100644 --- a/lib/serializer/BinarySerializer.h +++ b/lib/serializer/BinarySerializer.h @@ -11,7 +11,9 @@ #include "CSerializer.h" #include "CTypeList.h" +#include "SerializerReflection.h" #include "ESerializationVersion.h" +#include "Serializeable.h" #include "../mapObjects/CArmedInstance.h" VCMI_LIB_NAMESPACE_BEGIN @@ -23,7 +25,7 @@ protected: public: CSaverBase(IBinaryWriter * w): writer(w){}; - inline void write(const void * data, unsigned size) + void write(const void * data, unsigned size) { writer->write(reinterpret_cast(data), size); }; @@ -34,7 +36,7 @@ public: /// Primitives: copy memory into underlying stream (defined in CSaverBase) /// Containers: custom overloaded method that decouples class into primitives /// VCMI Classes: recursively serialize them via ClassName::serialize( BinarySerializer &, int version) call -class DLL_LINKAGE BinarySerializer : public CSaverBase +class BinarySerializer : public CSaverBase { template struct VariantVisitorSaver @@ -51,81 +53,51 @@ class DLL_LINKAGE BinarySerializer : public CSaverBase } }; - template - struct SaveIfStackInstance + template + bool saveIfStackInstance(const T &data) { - static bool invoke(Ser &s, const T &data) - { - return false; - } - }; + return false; + } - template - struct SaveIfStackInstance + template + bool saveIfStackInstance(const CStackInstance* const &data) { - static bool invoke(Ser &s, const CStackInstance* const &data) - { - assert(data->armyObj); - SlotID slot; + assert(data->armyObj); - if(data->getNodeType() == CBonusSystemNode::COMMANDER) - slot = SlotID::COMMANDER_SLOT_PLACEHOLDER; - else - slot = data->armyObj->findStack(data); + SlotID slot; - assert(slot != SlotID()); - s & data->armyObj & slot; + if(data->getNodeType() == CBonusSystemNode::COMMANDER) + slot = SlotID::COMMANDER_SLOT_PLACEHOLDER; + else + slot = data->armyObj->findStack(data); + + assert(slot != SlotID()); + save(data->armyObj->id); + save(slot); + + if (data->armyObj->id != ObjectInstanceID::NONE) return true; - } - }; - - template class CPointerSaver; - - class CBasicPointerSaver - { - public: - virtual void savePtr(CSaverBase &ar, const void *data) const =0; - virtual ~CBasicPointerSaver(){} - - template static CBasicPointerSaver *getApplier(const T * t=nullptr) - { - return new CPointerSaver(); - } - }; - - - template - class CPointerSaver : public CBasicPointerSaver - { - public: - void savePtr(CSaverBase &ar, const void *data) const override - { - auto & s = static_cast(ar); - const T *ptr = static_cast(data); - - //T is most derived known type, it's time to call actual serialize - const_cast(ptr)->serialize(s); - } - }; - - CApplier applier; + else + return false; + } public: using Version = ESerializationVersion; - std::map savedPointers; + std::map savedStrings; + std::map savedPointers; Version version = Version::CURRENT; - bool smartPointerSerialization; - bool saving; + static constexpr bool trackSerializedPointers = true; + static constexpr bool saving = true; + bool loadingGamestate = false; - BinarySerializer(IBinaryWriter * w); - - template - void registerType(const Base * b = nullptr, const Derived * d = nullptr) + bool hasFeature(Version what) const { - applier.registerType(b, d); - } + return version >= what; + }; + + DLL_LINKAGE BinarySerializer(IBinaryWriter * w); template BinarySerializer & operator&(const T & t) @@ -134,32 +106,72 @@ public: return * this; } + void saveEncodedInteger(int64_t value) + { + uint64_t valueUnsigned = std::abs(value); + + while (valueUnsigned > 0x3f) + { + uint8_t byteValue = (valueUnsigned & 0x7f) | 0x80; + valueUnsigned = valueUnsigned >> 7; + save(byteValue); + } + + uint8_t lastByteValue = valueUnsigned & 0x3f; + if (value < 0) + lastByteValue |= 0x40; + + save(lastByteValue); + } + template < typename T, typename std::enable_if_t < std::is_same_v, int > = 0 > void save(const T &data) { - ui8 writ = static_cast(data); + uint8_t writ = static_cast(data); save(writ); } - template < class T, typename std::enable_if_t < std::is_fundamental_v && !std::is_same_v, int > = 0 > + template < class T, typename std::enable_if_t < std::is_floating_point_v, int > = 0 > void save(const T &data) { // save primitive - simply dump binary data to output this->write(static_cast(&data), sizeof(data)); } + template < class T, typename std::enable_if_t < std::is_integral_v && !std::is_same_v, int > = 0 > + void save(const T &data) + { + if constexpr (sizeof(T) == 1) + { + // save primitive - simply dump binary data to output + this->write(static_cast(&data), sizeof(data)); + } + else + { + if (hasFeature(Version::COMPACT_INTEGER_SERIALIZATION)) + saveEncodedInteger(data); + else + this->write(static_cast(&data), sizeof(data)); + } + } + + void save(const Version &data) + { + this->write(static_cast(&data), sizeof(data)); + } + template < typename T, typename std::enable_if_t < std::is_enum_v, int > = 0 > void save(const T &data) { - si32 writ = static_cast(data); + int32_t writ = static_cast(data); *this & writ; } template < typename T, typename std::enable_if_t < std::is_array_v, int > = 0 > void save(const T &data) { - ui32 size = std::size(data); - for(ui32 i=0; i < size; i++) + uint32_t size = std::size(data); + for(uint32_t i=0; i < size; i++) *this & data[i]; } @@ -174,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) @@ -205,16 +204,16 @@ public: if(writer->sendStackInstanceByIds) { - const bool gotSaved = SaveIfStackInstance::invoke(*this, data); + const bool gotSaved = saveIfStackInstance(data); if(gotSaved) return; } - if(smartPointerSerialization) + if(trackSerializedPointers) { // We might have an object that has multiple inheritance and store it via the non-first base pointer. // Therefore, all pointers need to be normalized to the actual object address. - const void * actualPointer = static_cast(data); + const auto * actualPointer = static_cast(data); auto i = savedPointers.find(actualPointer); if(i != savedPointers.end()) { @@ -224,7 +223,7 @@ public: } //give id to this pointer - ui32 pid = (ui32)savedPointers.size(); + uint32_t pid = savedPointers.size(); savedPointers[actualPointer] = pid; save(pid); } @@ -236,7 +235,7 @@ public: if(!tid) save(*data); //if type is unregistered simply write all data in a standard way else - applier.getApplier(tid)->savePtr(*this, static_cast(data)); //call serializer specific for our real type + CSerializationApplier::getInstance().getApplier(tid)->savePtr(*this, static_cast(data)); //call serializer specific for our real type } template < typename T, typename std::enable_if_t < is_serializeable::value, int > = 0 > @@ -271,30 +270,30 @@ public: template , int > = 0> void save(const std::vector &data) { - ui32 length = (ui32)data.size(); + uint32_t length = data.size(); *this & length; - for(ui32 i=0;i, int > = 0> void save(const std::deque & data) { - ui32 length = (ui32)data.size(); + uint32_t length = data.size(); *this & length; - for(ui32 i = 0; i < length; i++) + for(uint32_t i = 0; i < length; i++) save(data[i]); } template void save(const std::array &data) { - for(ui32 i=0; i < N; i++) + for(uint32_t i=0; i < N; i++) save(data[i]); } template void save(const std::set &data) { auto & d = const_cast &>(data); - ui32 length = (ui32)d.size(); + uint32_t length = d.size(); save(length); for(auto i = d.begin(); i != d.end(); i++) save(*i); @@ -303,7 +302,7 @@ public: void save(const std::unordered_set &data) { auto & d = const_cast &>(data); - ui32 length = (ui32)d.size(); + uint32_t length = d.size(); *this & length; for(auto i = d.begin(); i != d.end(); i++) save(*i); @@ -312,16 +311,47 @@ public: void save(const std::list &data) { auto & d = const_cast &>(data); - ui32 length = (ui32)d.size(); + uint32_t length = d.size(); *this & length; for(auto i = d.begin(); i != d.end(); i++) save(*i); } + void save(const std::string &data) { - save(ui32(data.length())); - this->write(static_cast(data.data()), data.size()); + if (hasFeature(Version::COMPACT_STRING_SERIALIZATION)) + { + if (data.empty()) + { + save(static_cast(0)); + return; + } + + auto it = savedStrings.find(data); + + if (it == savedStrings.end()) + { + save(static_cast(data.length())); + this->write(static_cast(data.data()), data.size()); + + // -1, -2... + int32_t newStringID = -1 - savedStrings.size(); + + savedStrings[data] = newStringID; + } + else + { + int32_t index = it->second; + save(index); + } + } + else + { + save(static_cast(data.length())); + this->write(static_cast(data.data()), data.size()); + } } + template void save(const std::pair &data) { @@ -329,9 +359,19 @@ public: save(data.second); } template + void save(const std::unordered_map &data) + { + *this & static_cast(data.size()); + for(auto i = data.begin(); i != data.end(); i++) + { + save(i->first); + save(i->second); + } + } + template void save(const std::map &data) { - *this & ui32(data.size()); + *this & static_cast(data.size()); for(auto i = data.begin(); i != data.end(); i++) { save(i->first); @@ -341,7 +381,7 @@ public: template void save(const std::multimap &data) { - *this & ui32(data.size()); + *this & static_cast(data.size()); for(auto i = data.begin(); i != data.end(); i++) { save(i->first); @@ -351,7 +391,7 @@ public: template void save(const std::variant & data) { - si32 which = data.index(); + int32_t which = data.index(); save(which); VariantVisitorSaver visitor(*this); @@ -362,26 +402,26 @@ public: { if(data) { - save((ui8)1); + save(static_cast(1)); save(*data); } else { - save((ui8)0); + save(static_cast(0)); } } template void save(const boost::multi_array &data) { - ui32 length = data.num_elements(); + uint32_t length = data.num_elements(); *this & length; auto shape = data.shape(); - ui32 x = shape[0]; - ui32 y = shape[1]; - ui32 z = shape[2]; + uint32_t x = shape[0]; + uint32_t y = shape[1]; + uint32_t z = shape[2]; *this & x & y & z; - for(ui32 i = 0; i < length; i++) + for(uint32_t i = 0; i < length; i++) save(data.data()[i]); } template diff --git a/lib/serializer/CLoadFile.cpp b/lib/serializer/CLoadFile.cpp index 8a99d0212..b79951f68 100644 --- a/lib/serializer/CLoadFile.cpp +++ b/lib/serializer/CLoadFile.cpp @@ -29,6 +29,7 @@ int CLoadFile::read(std::byte * data, unsigned size) void CLoadFile::openNextFile(const boost::filesystem::path & fname, ESerializationVersion minimalVersion) { + serializer.loadingGamestate = true; assert(!serializer.reverseEndianness); assert(minimalVersion <= ESerializationVersion::CURRENT); diff --git a/lib/serializer/CSerializer.cpp b/lib/serializer/CSerializer.cpp index 72d54ecd3..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" @@ -25,14 +25,8 @@ void CSerializer::addStdVecItems(CGameState *gs, LibClasses *lib) { registerVectoredType(&gs->map->objects, [](const CGObjectInstance &obj){ return obj.id; }); - registerVectoredType(&lib->heroh->objects, - [](const CHero &h){ return h.getId(); }); registerVectoredType(&gs->map->allHeroes, - [](const CGHeroInstance &h){ return h.type->getId(); }); - registerVectoredType(&lib->creh->objects, - [](const CCreature &cre){ return cre.getId(); }); - registerVectoredType(&lib->arth->objects, - [](const CArtifact &art){ return art.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/CTypeList.cpp b/lib/serializer/CTypeList.cpp index 8c75517ca..4ef55ab11 100644 --- a/lib/serializer/CTypeList.cpp +++ b/lib/serializer/CTypeList.cpp @@ -10,7 +10,7 @@ #include "StdInc.h" #include "CTypeList.h" -#include "../registerTypes/RegisterTypes.h" +#include "RegisterTypes.h" VCMI_LIB_NAMESPACE_BEGIN diff --git a/lib/serializer/CTypeList.h b/lib/serializer/CTypeList.h index df0eec7bc..32738adb7 100644 --- a/lib/serializer/CTypeList.h +++ b/lib/serializer/CTypeList.h @@ -36,22 +36,15 @@ public: return registry; } - template - void registerType() - { - registerType(); - registerType(); - } - template - void registerType() + void registerType(uint16_t index) { const std::type_info & typeInfo = typeid(T); if (typeInfos.count(typeInfo.name()) != 0) return; - typeInfos[typeInfo.name()] = typeInfos.size() + 1; + typeInfos[typeInfo.name()] = index; } template @@ -69,37 +62,4 @@ public: } }; -/// Wrapper over CTypeList. Allows execution of templated class T for any type -/// that was resgistered for this applier -template -class CApplier : boost::noncopyable -{ - std::map> apps; - - template - void addApplier(ui16 ID) - { - if(!apps.count(ID)) - { - RegisteredType * rtype = nullptr; - apps[ID].reset(T::getApplier(rtype)); - } - } - -public: - T * getApplier(ui16 ID) - { - if(!apps.count(ID)) - throw std::runtime_error("No applier found."); - return apps[ID].get(); - } - - template - void registerType(const Base * b = nullptr, const Derived * d = nullptr) - { - addApplier(CTypeList::getInstance().getTypeID(nullptr)); - addApplier(CTypeList::getInstance().getTypeID(nullptr)); - } -}; - VCMI_LIB_NAMESPACE_END diff --git a/lib/serializer/Connection.cpp b/lib/serializer/Connection.cpp index c428a6044..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,17 +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; @@ -101,7 +102,9 @@ 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; } @@ -132,7 +135,6 @@ void CConnection::enterLobbyConnectionMode() deserializer->loadedPointers.clear(); serializer->savedPointers.clear(); disableSmartVectorMemberSerialization(); - disableSmartPointerSerialization(); disableStackSendingByID(); } @@ -144,24 +146,11 @@ void CConnection::setCallback(IGameCallback * cb) void CConnection::enterGameplayConnectionMode(CGameState * gs) { enableStackSendingByID(); - disableSmartPointerSerialization(); setCallback(gs->callback); enableSmartVectorMemberSerializatoin(gs); } -void CConnection::disableSmartPointerSerialization() -{ - deserializer->smartPointerSerialization = false; - serializer->smartPointerSerialization = false; -} - -void CConnection::enableSmartPointerSerialization() -{ - deserializer->smartPointerSerialization = true; - serializer->smartPointerSerialization = true; -} - void CConnection::disableSmartVectorMemberSerialization() { packReader->smartVectorMembersSerialization = false; diff --git a/lib/serializer/Connection.h b/lib/serializer/Connection.h index 889bbbf81..b62849894 100644 --- a/lib/serializer/Connection.h +++ b/lib/serializer/Connection.h @@ -38,8 +38,6 @@ class DLL_LINKAGE CConnection : boost::noncopyable void disableStackSendingByID(); void enableStackSendingByID(); - void disableSmartPointerSerialization(); - void enableSmartPointerSerialization(); void disableSmartVectorMemberSerialization(); void enableSmartVectorMemberSerializatoin(CGameState * gs); @@ -53,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 4dc66edcb..a5b374f9b 100644 --- a/lib/serializer/ESerializationVersion.h +++ b/lib/serializer/ESerializationVersion.h @@ -31,25 +31,45 @@ enum class ESerializationVersion : int32_t { NONE = 0, - MINIMAL = 831, - - RELEASE_143, // 832 +text container in campaigns, +starting hero in RMG options - HAS_EXTRA_OPTIONS, // 833 +extra options struct as part of startinfo - DESTROYED_OBJECTS, // 834 +list of objects destroyed by player - CAMPAIGN_MAP_TRANSLATIONS, // 835 +campaigns include translations for its maps - JSON_FLAGS, // 836 json uses new format for flags - MANA_LIMIT, // 837 change MANA_PER_KNOWLEGDE to percentage - BONUS_META_STRING, // 838 bonuses use MetaString instead of std::string for descriptions - TURN_TIMERS_STATE, // 839 current state of turn timers is serialized - ARTIFACT_COSTUMES, // 840 swappable artifacts set added - - RELEASE_150 = ARTIFACT_COSTUMES, // for convenience + RELEASE_150 = 840, + MINIMAL = RELEASE_150, VOTING_SIMTURNS, // 841 - allow modification of simturns duration via vote REMOVE_TEXT_CONTAINER_SIZE_T, // 842 Fixed serialization of size_t from text containers BANK_UNIT_PLACEMENT, // 843 Banks have unit placement flag - RELEASE_152 = BANK_UNIT_PLACEMENT, + RELEASE_156 = BANK_UNIT_PLACEMENT, - CURRENT = BANK_UNIT_PLACEMENT + COMPACT_STRING_SERIALIZATION, // 844 - optimized serialization of previously encountered strings + COMPACT_INTEGER_SERIALIZATION, // 845 - serialize integers in forms similar to protobuf + REMOVE_FOG_OF_WAR_POINTER, // 846 - fog of war is serialized as reference instead of pointer + SIMPLE_TEXT_CONTAINER_SERIALIZATION, // 847 - text container is serialized using common routine instead of custom approach + MAP_FORMAT_ADDITIONAL_INFOS, // 848 - serialize new infos in map format + REMOVE_LIB_RNG, // 849 - removed random number generators from library classes + HIGHSCORE_PARAMETERS, // 850 - saves parameter for campaign + PLAYER_HANDICAP, // 851 - player handicap selection at game start + STATISTICS, // 852 - removed random number generators from library classes + CAMPAIGN_REGIONS, // 853 - configurable campaign regions + EVENTS_PLAYER_SET, // 854 - map & town events use std::set instead of bitmask to store player list + NEW_TOWN_BUILDINGS, // 855 - old bonusing buildings have been removed + STATISTICS_SCREEN, // 856 - extent statistic functions + NEW_MARKETS, // 857 - reworked market classes + PLAYER_STATE_OWNED_OBJECTS, // 858 - player state stores all owned objects in a single list + SAVE_COMPATIBILITY_FIXES, // 859 - implementation of previoulsy postponed changes to serialization + CHRONICLES_SUPPORT, // 860 - support for heroes chronicles + PER_MAP_GAME_SETTINGS, // 861 - game settings are now stored per-map + 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 + 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 new file mode 100644 index 000000000..c3dab0559 --- /dev/null +++ b/lib/serializer/RegisterTypes.h @@ -0,0 +1,300 @@ +/* + * RegisterTypes.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 "../CPlayerState.h" +#include "../CStack.h" +#include "../battle/BattleInfo.h" +#include "../battle/CObstacleInstance.h" +#include "../bonuses/Limiters.h" +#include "../bonuses/Propagators.h" +#include "../bonuses/Updaters.h" +#include "../campaign/CampaignState.h" +#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" + +VCMI_LIB_NAMESPACE_BEGIN + +/// This method defines all types that are part of Serializeable hieararchy and can be serialized as their base type +/// Each class is registered with a unique index that is used to determine correct type on deserialization +/// For example, if CGHeroInstance is serialized as pointer to CGObjectInstance serializer will write type index for CGHeroInstance, followed by CGHeroInstance::serialize() call +/// Similarly, on deserialize, game will look up type index of object that was serialized as this CGObjectInstance and will load it as CGHeroInstance instead +/// Meaning, these type indexes must NEVER change. +/// If type is removed please only remove corresponding type, without adjusting indexes of following types +/// NOTE: when removing type please consider potential save compatibility handling +/// Similarly, when adding new type make sure to add it to the very end of this list with new type index +template +void registerTypes(Serializer &s) +{ + static_assert(std::is_abstract_v, "If this type is no longer abstract consider registering it for serialization with ID 1"); + static_assert(std::is_abstract_v, "If this type is no longer abstract consider registering it for serialization with ID 3"); + static_assert(std::is_abstract_v, "If this type is no longer abstract consider registering it for serialization with ID 11"); + static_assert(std::is_abstract_v, "If this type is no longer abstract consider registering it for serialization with ID 29"); + static_assert(std::is_abstract_v, "If this type is no longer abstract consider registering it for serialization with ID 83"); + static_assert(std::is_abstract_v, "If this type is no longer abstract consider registering it for serialization with ID 153"); + static_assert(std::is_abstract_v, "If this type is no longer abstract consider registering it for serialization with ID 161"); + static_assert(std::is_abstract_v, "If this type is no longer abstract consider registering it for serialization with ID 168"); + + s.template registerType(2); + s.template registerType(4); + s.template registerType(5); + s.template registerType(6); + s.template registerType(7); + s.template registerType(8); + s.template registerType(9); + s.template registerType(10); + s.template registerType(12); + s.template registerType(13); + s.template registerType(14); + s.template registerType(15); + s.template registerType(16); + s.template registerType(17); + s.template registerType(18); + s.template registerType(19); + s.template registerType(20); + s.template registerType(21); + s.template registerType(22); + s.template registerType(23); + s.template registerType(24); + s.template registerType(25); + s.template registerType(26); + s.template registerType(27); + s.template registerType(28); + s.template registerType(30); + s.template registerType(31); + s.template registerType(32); + s.template registerType(33); + s.template registerType(34); + s.template registerType(35); + s.template registerType(36); + s.template registerType(37); + s.template registerType(38); + s.template registerType(39); + s.template registerType(40); + s.template registerType(41); + s.template registerType(42); + s.template registerType(43); + s.template registerType(44); + s.template registerType(45); + s.template registerType(46); + s.template registerType(47); + s.template registerType(48); + s.template registerType(49); + s.template registerType(50); + s.template registerType(51); + s.template registerType(52); + s.template registerType(53); + s.template registerType(56); + s.template registerType(57); + s.template registerType(58); + s.template registerType(59); + s.template registerType(60); + s.template registerType(61); + s.template registerType(62); + s.template registerType(63); + s.template registerType(64); + s.template registerType(65); + s.template registerType(66); + s.template registerType(67); + s.template registerType(68); + s.template registerType(69); + s.template registerType(70); + s.template registerType(71); + s.template registerType(72); + s.template registerType(73); + s.template registerType(74); + s.template registerType(75); + s.template registerType(76); + s.template registerType(77); + s.template registerType(78); + s.template registerType(79); + s.template registerType(80); + s.template registerType(82); + s.template registerType(84); + s.template registerType(85); + s.template registerType(86); + s.template registerType(87); + s.template registerType(88); + s.template registerType(89); + s.template registerType(90); + s.template registerType(91); + s.template registerType(92); + s.template registerType(93); + s.template registerType(94); + s.template registerType(95); + s.template registerType(96); + s.template registerType(97); + s.template registerType(98); + s.template registerType(99); + s.template registerType(100); + s.template registerType(101); + s.template registerType(102); + s.template registerType(103); + s.template registerType(104); + s.template registerType(105); + s.template registerType(106); + s.template registerType(107); + s.template registerType(108); + s.template registerType(109); + s.template registerType(110); + s.template registerType(111); + s.template registerType(112); + s.template registerType(113); + s.template registerType(114); + s.template registerType(115); + s.template registerType(116); + s.template registerType(117); + s.template registerType(118); + s.template registerType(119); + s.template registerType(120); + s.template registerType(121); + s.template registerType(122); + s.template registerType(123); + s.template registerType(124); + s.template registerType(125); + s.template registerType(126); + s.template registerType(127); + s.template registerType(128); + s.template registerType(129); + s.template registerType(130); + s.template registerType(131); + s.template registerType(132); + s.template registerType(133); + s.template registerType(134); + s.template registerType(135); + s.template registerType(136); + s.template registerType(137); + s.template registerType(138); + s.template registerType(139); + s.template registerType(140); + s.template registerType(141); + s.template registerType(142); + s.template registerType(143); + s.template registerType(144); + s.template registerType(145); + s.template registerType(146); + s.template registerType(147); + s.template registerType(148); + s.template registerType(149); + s.template registerType(150); + s.template registerType(151); + s.template registerType(152); + s.template registerType(154); + s.template registerType(155); + s.template registerType(156); + s.template registerType(157); + s.template registerType(158); + s.template registerType(159); + s.template registerType(160); + s.template registerType(162); + s.template registerType(163); + s.template registerType(164); + s.template registerType(165); + s.template registerType(166); + s.template registerType(167); + s.template registerType(169); + s.template registerType(170); + s.template registerType(171); + s.template registerType(172); + s.template registerType(173); + s.template registerType(174); + s.template registerType(175); + s.template registerType(176); + s.template registerType(177); + s.template registerType(178); + s.template registerType(179); + s.template registerType(180); + s.template registerType(181); + s.template registerType(182); + s.template registerType(183); + s.template registerType(184); + s.template registerType(185); + s.template registerType(186); + s.template registerType(187); + s.template registerType(188); + s.template registerType(189); + s.template registerType(190); + s.template registerType(191); + s.template registerType(192); + s.template registerType(193); + s.template registerType(194); + s.template registerType(195); + s.template registerType(196); + s.template registerType(197); + s.template registerType(198); + s.template registerType(199); + s.template registerType(200); + s.template registerType(201); + s.template registerType(202); + s.template registerType(203); + s.template registerType(204); + s.template registerType(205); + s.template registerType(206); + s.template registerType(207); + s.template registerType(208); + s.template registerType(209); + s.template registerType(210); + s.template registerType(211); + s.template registerType(212); + s.template registerType(213); + s.template registerType(214); + s.template registerType(215); + s.template registerType(216); + s.template registerType(217); + s.template registerType(218); + s.template registerType(219); + s.template registerType(220); + s.template registerType(221); + s.template registerType(222); + s.template registerType(223); + s.template registerType(224); + s.template registerType(225); + s.template registerType(226); + s.template registerType(227); + s.template registerType(228); + s.template registerType(229); + s.template registerType(230); + s.template registerType(231); + s.template registerType(232); + s.template registerType(233); + s.template registerType(234); + s.template registerType(235); + s.template registerType(236); + s.template registerType(237); + 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/Serializeable.h b/lib/serializer/Serializeable.h new file mode 100644 index 000000000..a0ab5927a --- /dev/null +++ b/lib/serializer/Serializeable.h @@ -0,0 +1,21 @@ +/* + * Serializeable.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 + +// Tag class that acts as base for all classes that can be serialized by pointer +class Serializeable +{ +public: + virtual ~Serializeable() = default; +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/serializer/SerializerReflection.cpp b/lib/serializer/SerializerReflection.cpp new file mode 100644 index 000000000..ecb3427b8 --- /dev/null +++ b/lib/serializer/SerializerReflection.cpp @@ -0,0 +1,106 @@ +/* + * SerializerReflection.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 "SerializerReflection.h" + +#include "BinaryDeserializer.h" +#include "BinarySerializer.h" + +#include "RegisterTypes.h" + +#include "../GameSettings.h" +#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" + +VCMI_LIB_NAMESPACE_BEGIN + +template +class SerializerReflection final : public ISerializerReflection +{ +public: + Serializeable * createPtr(BinaryDeserializer &ar, IGameCallback * cb) const override + { + return ClassObjectCreator::invoke(cb); + } + + void loadPtr(BinaryDeserializer &ar, IGameCallback * cb, Serializeable * data) const override + { + auto * realPtr = dynamic_cast(data); + realPtr->serialize(ar); + } + + void savePtr(BinarySerializer &s, const Serializeable *data) const override + { + const Type *ptr = dynamic_cast(data); + const_cast(ptr)->serialize(s); + } +}; + +template +class SerializerCompatibility : public ISerializerReflection +{ +public: + Serializeable * createPtr(BinaryDeserializer &ar, IGameCallback * cb) const override + { + return ClassObjectCreator::invoke(cb); + } + + void savePtr(BinarySerializer &s, const Serializeable *data) const override + { + throw std::runtime_error("Illegal call to savePtr - this type should not be used for serialization!"); + } +}; + +class SerializerCompatibilityBonusingBuilding final : public SerializerCompatibility +{ + void loadPtr(BinaryDeserializer &ar, IGameCallback * cb, Serializeable * data) const override + { + auto * realPtr = dynamic_cast(data); + realPtr->serialize(ar); + } +}; + +class SerializerCompatibilityArtifactsAltar final : public SerializerCompatibility +{ + void loadPtr(BinaryDeserializer &ar, IGameCallback * cb, Serializeable * data) const override + { + auto * realPtr = dynamic_cast(data); + realPtr->serializeArtifactsAltar(ar); + } +}; + +template +void CSerializationApplier::registerType(uint16_t ID) +{ + assert(!apps.count(ID)); + apps[ID].reset(new SerializerReflection); +} + +CSerializationApplier::CSerializationApplier() +{ + registerTypes(*this); + + apps[54].reset(new SerializerCompatibilityBonusingBuilding); + apps[55].reset(new SerializerCompatibilityBonusingBuilding); + apps[81].reset(new SerializerCompatibilityArtifactsAltar); +} + +CSerializationApplier & CSerializationApplier::getInstance() +{ + static CSerializationApplier registry; + return registry; +} + +VCMI_LIB_NAMESPACE_END diff --git a/lib/serializer/SerializerReflection.h b/lib/serializer/SerializerReflection.h new file mode 100644 index 000000000..d325cb915 --- /dev/null +++ b/lib/serializer/SerializerReflection.h @@ -0,0 +1,70 @@ +/* + * SerializerReflection.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 IGameCallback; +class Serializeable; +class GameCallbackHolder; +class BinaryDeserializer; +class BinarySerializer; +class GameCallbackHolder; + +template +struct ClassObjectCreator +{ + static T *invoke(IGameCallback *cb) + { + static_assert(!std::is_base_of_v, "Cannot call new upon map objects!"); + static_assert(!std::is_abstract_v, "Cannot call new upon abstract classes!"); + return new T(); + } +}; + +template +struct ClassObjectCreator>> +{ + static T *invoke(IGameCallback *cb) + { + static_assert(!std::is_abstract_v, "Cannot call new upon abstract classes!"); + return new T(cb); + } +}; + +class ISerializerReflection +{ +public: + virtual Serializeable * createPtr(BinaryDeserializer &ar, IGameCallback * cb) const =0; + virtual void loadPtr(BinaryDeserializer &ar, IGameCallback * cb, Serializeable * data) const =0; + virtual void savePtr(BinarySerializer &ar, const Serializeable *data) const =0; + virtual ~ISerializerReflection() = default; +}; + +class DLL_LINKAGE CSerializationApplier : boost::noncopyable +{ + std::map> apps; + + CSerializationApplier(); +public: + ISerializerReflection * getApplier(uint16_t ID) + { + if(!apps.count(ID)) + throw std::runtime_error("No applier found."); + return apps[ID].get(); + } + + template + void registerType(uint16_t index); + + static CSerializationApplier & getInstance(); +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/spells/AdventureSpellMechanics.cpp b/lib/spells/AdventureSpellMechanics.cpp index 2cd65a1b6..e197e2cd5 100644 --- a/lib/spells/AdventureSpellMechanics.cpp +++ b/lib/spells/AdventureSpellMechanics.cpp @@ -17,14 +17,15 @@ #include "../CGameInfoCallback.h" #include "../CPlayerState.h" -#include "../CRandomGenerator.h" -#include "../GameSettings.h" +#include "../IGameSettings.h" #include "../mapObjects/CGHeroInstance.h" #include "../mapObjects/CGTownInstance.h" #include "../mapObjects/MiscObjects.h" #include "../mapping/CMap.h" #include "../networkPacks/PacksForClient.h" +#include + VCMI_LIB_NAMESPACE_BEGIN ///AdventureSpellMechanics @@ -104,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; @@ -135,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); @@ -187,13 +188,13 @@ ESpellCastResult SummonBoatMechanics::applyAdventureEffects(SpellCastEnvironment const auto schoolLevel = parameters.caster->getSpellSchoolLevel(owner); //check if spell works at all - if(env->getRNG()->getInt64Range(0, 99)() >= owner->getLevelPower(schoolLevel)) //power is % chance of success + if(env->getRNG()->nextInt(0, 99) >= owner->getLevelPower(schoolLevel)) //power is % chance of success { InfoWindow iw; 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; } @@ -208,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; @@ -225,24 +226,19 @@ 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 { - NewObject no; - no.ID = Obj::BOAT; - no.subID = BoatId::NECROPOLIS; - no.targetPos = summonPos; - no.initiator = parameters.caster->getCasterOwner(); - env->apply(&no); + env->createBoat(summonPos, BoatId::NECROPOLIS, parameters.caster->getCasterOwner()); } return ESpellCastResult::OK; } @@ -280,13 +276,13 @@ ESpellCastResult ScuttleBoatMechanics::applyAdventureEffects(SpellCastEnvironmen { const auto schoolLevel = parameters.caster->getSpellSchoolLevel(owner); //check if spell works at all - if(env->getRNG()->getInt64Range(0, 99)() >= owner->getLevelPower(schoolLevel)) //power is % chance of success + if(env->getRNG()->nextInt(0, 99) >= owner->getLevelPower(schoolLevel)) //power is % chance of success { InfoWindow iw; 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; } @@ -295,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; } @@ -324,7 +320,7 @@ bool DimensionDoorMechanics::canBeCastImpl(spells::Problem & problem, const CGam int castsAlreadyPerformedThisTurn = caster->getHeroCaster()->getBonuses(Selector::source(BonusSource::SPELL_EFFECT, BonusSourceID(owner->id)), Selector::all, cachingStr.str())->size(); int castsLimit = owner->getLevelPower(schoolLevel); - bool isTournamentRulesLimitEnabled = VLC->settings()->getBoolean(EGameSettings::DIMENSION_DOOR_TOURNAMENT_RULES_LIMIT); + bool isTournamentRulesLimitEnabled = cb->getSettings().getBoolean(EGameSettings::DIMENSION_DOOR_TOURNAMENT_RULES_LIMIT); if(isTournamentRulesLimitEnabled) { int3 mapSize = cb->getMapSize(); @@ -351,7 +347,7 @@ bool DimensionDoorMechanics::canBeCastAtImpl(spells::Problem & problem, const CG if(!cb->isInTheMap(pos)) return false; - if(VLC->settings()->getBoolean(EGameSettings::DIMENSION_DOOR_ONLY_TO_UNCOVERED_TILES)) + if(cb->getSettings().getBoolean(EGameSettings::DIMENSION_DOOR_ONLY_TO_UNCOVERED_TILES)) { if(!cb->isVisible(pos, caster->getCasterOwner())) return false; @@ -371,14 +367,14 @@ bool DimensionDoorMechanics::canBeCastAtImpl(spells::Problem & problem, const CG if(!isInScreenRange(casterPosition, pos)) return false; - if(VLC->settings()->getBoolean(EGameSettings::DIMENSION_DOOR_EXPOSES_TERRAIN_TYPE)) + if(cb->getSettings().getBoolean(EGameSettings::DIMENSION_DOOR_EXPOSES_TERRAIN_TYPE)) { if(!dest->isClear(curr)) return false; } else { - if (dest->blocked) + if (dest->blocked()) return false; } @@ -400,18 +396,18 @@ ESpellCastResult DimensionDoorMechanics::applyAdventureEffects(SpellCastEnvironm iw.player = parameters.caster->getCasterOwner(); // tile is either blocked or not possible to move (e.g. water <-> land) - if(VLC->settings()->getBoolean(EGameSettings::DIMENSION_DOOR_FAILURE_SPENDS_POINTS)) + if(env->getCb()->getSettings().getBoolean(EGameSettings::DIMENSION_DOOR_FAILURE_SPENDS_POINTS)) { // 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; } } @@ -419,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()); @@ -427,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; } @@ -475,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; } } @@ -543,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; } @@ -572,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); } } @@ -591,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; } @@ -602,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; } @@ -647,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; } @@ -673,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) { @@ -696,9 +692,9 @@ std::vector TownPortalMechanics::getPossibleTowns(SpellC for(const auto & color : team->players) { - for(auto currTown : env->getCb()->getPlayerState(color)->towns) + for(auto currTown : env->getCb()->getPlayerState(color)->getTowns()) { - ret.push_back(currTown.get()); + ret.push_back(currTown); } } return ret; @@ -735,13 +731,13 @@ ESpellCastResult ViewMechanics::applyAdventureEffects(SpellCastEnvironment * env { ObjectPosInfo posInfo(obj); - if((*fowMap)[posInfo.pos.z][posInfo.pos.x][posInfo.pos.y] == 0) + if(fowMap[posInfo.pos.z][posInfo.pos.x][posInfo.pos.y] == 0) pack.objectPositions.push_back(posInfo); } } pack.showTerrain = showTerrain(spellLevel); - env->apply(&pack); + env->apply(pack); return ESpellCastResult::OK; } diff --git a/lib/spells/AdventureSpellMechanics.h b/lib/spells/AdventureSpellMechanics.h index 02f8c01aa..76438a3bc 100644 --- a/lib/spells/AdventureSpellMechanics.h +++ b/lib/spells/AdventureSpellMechanics.h @@ -21,7 +21,7 @@ enum class ESpellCastResult OK, // cast successful CANCEL, // cast failed but it is not an error, no mana has been spent PENDING, - ERROR// error occured, for example invalid request from player + ERROR// error occurred, for example invalid request from player }; class AdventureSpellMechanics : public IAdventureSpellMechanics diff --git a/lib/spells/BattleSpellMechanics.cpp b/lib/spells/BattleSpellMechanics.cpp index 8ad548bfa..81a7f1a1b 100644 --- a/lib/spells/BattleSpellMechanics.cpp +++ b/lib/spells/BattleSpellMechanics.cpp @@ -19,7 +19,8 @@ #include "../networkPacks/PacksForClientBattle.h" #include "../networkPacks/SetStackEffect.h" #include "../CStack.h" -#include "../CRandomGenerator.h" + +#include VCMI_LIB_NAMESPACE_BEGIN @@ -192,16 +193,16 @@ bool BattleSpellMechanics::canBeCast(Problem & problem) const return adaptProblem(ESpellCastProblem::ADVMAP_SPELL_INSTEAD_OF_BATTLE_SPELL, problem); const PlayerColor player = caster->getCasterOwner(); - const auto side = battle()->playerToSide(player); + const BattleSide side = battle()->playerToSide(player); - if(!side) + if(side == BattleSide::NONE) return adaptProblem(ESpellCastProblem::INVALID, problem); //effect like Recanter's Cloak. Blocks also passive casting. //TODO: check creature abilities to block //TODO: check any possible caster - if(battle()->battleMaxSpellLevel(side.value()) < getSpellLevel() || battle()->battleMinSpellLevel(side.value()) > getSpellLevel()) + if(battle()->battleMaxSpellLevel(side) < getSpellLevel() || battle()->battleMinSpellLevel(side) > getSpellLevel()) return adaptProblem(ESpellCastProblem::SPELL_LEVEL_LIMIT_EXCEEDED, problem); return effects->applicable(problem, this); @@ -216,19 +217,27 @@ 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 (mainTarget && mainTarget == caster) + 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); @@ -250,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)); @@ -283,7 +292,7 @@ void BattleSpellMechanics::cast(ServerCallback * server, const Target & target) const CGHeroInstance * otherHero = nullptr; { //check it there is opponent hero - const ui8 otherSide = battle()->otherSide(casterSide); + const BattleSide otherSide = battle()->otherSide(casterSide); if(battle()->battleHasHero(otherSide)) otherHero = battle()->battleGetFightingHero(otherSide); @@ -344,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); @@ -366,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) @@ -377,15 +386,13 @@ void BattleSpellMechanics::beforeCast(BattleSpellCast & sc, vstd::RNG & rng, con std::vector resisted; - auto rangeGen = rng.getInt64Range(0, 99); - auto filterResisted = [&, this](const battle::Unit * unit) -> bool { if(isNegativeSpell() && isMagicalEffect()) { //magic resistance const int prob = std::min(unit->magicResistance(), 100); //probability of resistance in % - if(rangeGen() < prob) + if(rng.nextInt(0, 99) < prob) return true; } return false; @@ -484,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 @@ -506,62 +513,14 @@ std::set BattleSpellMechanics::spellRangeInHexes(BattleHex centralHex using namespace SRSLPraserHelpers; std::set ret; - std::string rng = owner->getLevelInfo(getRangeLevel()).range + ','; //copy + artificial comma for easier handling + std::vector rng = owner->getLevelInfo(getRangeLevel()).range; - if(rng.size() >= 2 && rng[0] != 'X') //there is at least one hex in range (+artificial comma) + for(auto & elem : rng) { - std::string number1; - std::string number2; - int beg = 0; - int end = 0; - bool readingFirst = true; - for(auto & elem : rng) - { - if(std::isdigit(elem) ) //reading number - { - if(readingFirst) - number1 += elem; - else - number2 += elem; - } - else if(elem == ',') //comma - { - //calculating variables - if(readingFirst) - { - beg = std::stoi(number1); - number1 = ""; - } - else - { - end = std::stoi(number2); - number2 = ""; - } - //obtaining new hexes - std::set curLayer; - if(readingFirst) - { - curLayer = getInRange(centralHex, beg, beg); - } - else - { - curLayer = getInRange(centralHex, beg, end); - readingFirst = true; - } - //adding obtained hexes - for(const auto & curLayer_it : curLayer) - { - ret.insert(curLayer_it); - } - - } - else if(elem == '-') //dash - { - beg = std::stoi(number1); - number1 = ""; - readingFirst = false; - } - } + std::set curLayer = getInRange(centralHex, elem, elem); + //adding obtained hexes + for(const auto & curLayer_it : curLayer) + ret.insert(curLayer_it); } return ret; diff --git a/lib/spells/BattleSpellMechanics.h b/lib/spells/BattleSpellMechanics.h index 29ad70a94..a910154d4 100644 --- a/lib/spells/BattleSpellMechanics.h +++ b/lib/spells/BattleSpellMechanics.h @@ -33,7 +33,7 @@ public: /// Returns false if spell can not be cast at all, e.g. due to not having any possible target on battlefield bool canBeCast(Problem & problem) const override; - /// Returns false if spell can not be cast at specifid target + /// Returns false if spell can not be cast at specified target bool canBeCastAt(const Target & target, Problem & problem) const override; // TODO: ??? (what's the difference compared to applyEffects?) diff --git a/lib/spells/BonusCaster.cpp b/lib/spells/BonusCaster.cpp index 720082112..a2d8523b1 100644 --- a/lib/spells/BonusCaster.cpp +++ b/lib/spells/BonusCaster.cpp @@ -12,13 +12,12 @@ #include "BonusCaster.h" #include +#include -#include "../MetaString.h" #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 679f1ec62..79619ac51 100644 --- a/lib/spells/CSpellHandler.cpp +++ b/lib/spells/CSpellHandler.cpp @@ -17,7 +17,6 @@ #include -#include "../CGeneralTextHandler.h" #include "../filesystem/Filesystem.h" #include "../constants/StringConstants.h" @@ -28,9 +27,11 @@ #include "../json/JsonBonus.h" #include "../json/JsonUtils.h" #include "../mapObjects/CGHeroInstance.h" //todo: remove -#include "../serializer/CSerializer.h" #include "../modding/IdentifierStorage.h" #include "../modding/ModUtility.h" +#include "../serializer/CSerializer.h" +#include "../texts/CLegacyConfigParser.h" +#include "../texts/CGeneralTextHandler.h" #include "ISpellMechanics.h" @@ -78,6 +79,8 @@ CSpell::CSpell(): combat(false), creatureAbility(false), castOnSelf(false), + castOnlyOnSelf(false), + castWithoutSkip(false), positiveness(ESpellPositiveness::NEUTRAL), defaultProbability(0), rising(false), @@ -199,6 +202,11 @@ std::string CSpell::getJsonKey() const return modScope + ':' + identifier; } +std::string CSpell::getModScope() const +{ + return modScope; +} + int32_t CSpell::getIndex() const { return id.toEnum(); @@ -292,6 +300,16 @@ bool CSpell::canCastOnSelf() const return castOnSelf; } +bool CSpell::canCastOnlyOnSelf() const +{ + return castOnlyOnSelf; +} + +bool CSpell::canCastWithoutSkip() const +{ + return castWithoutSkip; +} + const std::string & CSpell::getIconImmune() const { return iconImmune; @@ -414,6 +432,10 @@ int64_t CSpell::adjustRawDamage(const spells::Caster * caster, const battle::Uni ret *= 100 + bearer->valOfBonuses(BonusType::MORE_DAMAGE_FROM_SPELL, BonusSubtypeID(id)); ret /= 100; } + + //invincible + if(bearer->hasBonusOfType(BonusType::INVINCIBLE)) + ret = 0; } ret = caster->getSpellBonus(this, ret, affectedCreature); return ret; @@ -522,6 +544,7 @@ void CSpell::serializeJson(JsonSerializeFormat & handler) ///CSpell::AnimationInfo CSpell::AnimationItem::AnimationItem() : verticalPosition(VerticalPosition::TOP), + transparency(1), pause(0) { @@ -556,7 +579,7 @@ CSpell::TargetInfo::TargetInfo(const CSpell * spell, const int level, spells::Mo const auto & levelInfo = spell->getLevelInfo(level); smart = levelInfo.smartTarget; - massive = levelInfo.range == "X"; + massive = levelInfo.range.empty(); clearAffected = levelInfo.clearAffected; clearTarget = levelInfo.clearTarget; } @@ -676,7 +699,65 @@ const std::vector & CSpellHandler::getTypeNames() const return typeNames; } -CSpell * CSpellHandler::loadFromJson(const std::string & scope, const JsonNode & json, const std::string & identifier, size_t index) +std::vector CSpellHandler::spellRangeInHexes(std::string input) const +{ + std::set ret; + std::string rng = input + ','; //copy + artificial comma for easier handling + + if(rng.size() >= 2 && std::tolower(rng[0]) != 'x') //there is at least one hex in range (+artificial comma) + { + std::string number1; + std::string number2; + int beg = 0; + int end = 0; + bool readingFirst = true; + for(auto & elem : rng) + { + if(std::isdigit(elem) ) //reading number + { + if(readingFirst) + number1 += elem; + else + number2 += elem; + } + else if(elem == ',') //comma + { + //calculating variables + if(readingFirst) + { + beg = std::stoi(number1); + number1 = ""; + } + else + { + end = std::stoi(number2); + number2 = ""; + } + //obtaining new hexes + std::set curLayer; + if(readingFirst) + { + ret.insert(beg); + } + else + { + for(int i = beg; i <= end; ++i) + ret.insert(i); + } + } + else if(elem == '-') //dash + { + beg = std::stoi(number1); + number1 = ""; + readingFirst = false; + } + } + } + + return std::vector(ret.begin(), ret.end()); +} + +std::shared_ptr CSpellHandler::loadFromJson(const std::string & scope, const JsonNode & json, const std::string & identifier, size_t index) { assert(identifier.find(':') == std::string::npos); assert(!scope.empty()); @@ -685,7 +766,7 @@ CSpell * CSpellHandler::loadFromJson(const std::string & scope, const JsonNode & SpellID id(static_cast(index)); - auto * spell = new CSpell(); + auto spell = std::make_shared(); spell->id = id; spell->identifier = identifier; spell->modScope = scope; @@ -703,7 +784,7 @@ CSpell * CSpellHandler::loadFromJson(const std::string & scope, const JsonNode & 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()); @@ -715,6 +796,8 @@ CSpell * CSpellHandler::loadFromJson(const std::string & scope, const JsonNode & } 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()); @@ -883,10 +966,15 @@ CSpell * CSpellHandler::loadFromJson(const std::string & scope, const JsonNode & 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); @@ -923,14 +1011,14 @@ CSpell * CSpellHandler::loadFromJson(const std::string & scope, const JsonNode & 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()); levelObject.smartTarget = levelNode["targetModifier"]["smart"].Bool(); levelObject.clearTarget = levelNode["targetModifier"]["clearTarget"].Bool(); levelObject.clearAffected = levelNode["targetModifier"]["clearAffected"].Bool(); - levelObject.range = levelNode["range"].String(); + levelObject.range = spellRangeInHexes(levelNode["range"].String()); for(const auto & elem : levelNode["effects"].Struct()) { diff --git a/lib/spells/CSpellHandler.h b/lib/spells/CSpellHandler.h index f8826e23d..77e69ff47 100644 --- a/lib/spells/CSpellHandler.h +++ b/lib/spells/CSpellHandler.h @@ -67,12 +67,6 @@ public: ///resource name AnimationPath resourceName; - - template void serialize(Handler & h) - { - h & minimumAngle; - h & resourceName; - } }; struct AnimationItem @@ -80,17 +74,10 @@ public: AnimationPath resourceName; std::string effectName; VerticalPosition verticalPosition; + float transparency; int pause; AnimationItem(); - - template void serialize(Handler & h) - { - h & resourceName; - h & effectName; - h & verticalPosition; - h & pause; - } }; using TAnimation = AnimationItem; @@ -111,14 +98,6 @@ public: ///use selectProjectile to access std::vector projectile; - template void serialize(Handler & h) - { - h & projectile; - h & hit; - h & cast; - h & affect; - } - AnimationPath selectProjectile(const double angle) const; } animationInfo; @@ -132,27 +111,13 @@ public: bool smartTarget = true; bool clearTarget = false; bool clearAffected = false; - std::string range = "0"; + std::vector range = { 0 }; //TODO: remove these two when AI will understand special effects std::vector> effects; //deprecated std::vector> cumulativeEffects; //deprecated JsonNode battleEffects; - - template void serialize(Handler & h) - { - h & cost; - h & power; - h & AIValue; - h & smartTarget; - h & range; - h & effects; - h & cumulativeEffects; - h & clearTarget; - h & clearAffected; - h & battleEffects; - } }; /** \brief Low level accessor. Don`t use it if absolutely necessary @@ -203,6 +168,8 @@ public: bool hasSchool(SpellSchool school) const override; bool canCastOnSelf() const override; + bool canCastOnlyOnSelf() const override; + bool canCastWithoutSkip() const override; /** * Calls cb for each school this spell belongs to @@ -228,6 +195,7 @@ public: int32_t getIndex() const override; int32_t getIconIndex() const override; std::string getJsonKey() const override; + std::string getModScope() const override; SpellID getId() const override; std::string getNameTextID() const override; @@ -331,6 +299,8 @@ 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 std::unique_ptr mechanics;//(!) do not serialize @@ -341,6 +311,8 @@ bool DLL_LINKAGE isInScreenRange(const int3 ¢er, const int3 &pos); //for spe class DLL_LINKAGE CSpellHandler: public CHandlerBase { + std::vector spellRangeInHexes(std::string rng) const; + public: ///IHandler base std::vector loadLegacyData() override; @@ -355,7 +327,7 @@ public: protected: const std::vector & getTypeNames() const override; - CSpell * loadFromJson(const std::string & scope, const JsonNode & json, const std::string & identifier, size_t index) override; + std::shared_ptr loadFromJson(const std::string & scope, const JsonNode & json, const std::string & identifier, size_t index) override; }; VCMI_LIB_NAMESPACE_END diff --git a/lib/spells/ISpellMechanics.cpp b/lib/spells/ISpellMechanics.cpp index 6e260347f..24f40b067 100644 --- a/lib/spells/ISpellMechanics.cpp +++ b/lib/spells/ISpellMechanics.cpp @@ -11,7 +11,6 @@ #include "StdInc.h" #include "ISpellMechanics.h" -#include "../CRandomGenerator.h" #include "../VCMI_Lib.h" #include "../bonuses/Bonus.h" @@ -37,10 +36,11 @@ #include "CSpellHandler.h" -#include "../CHeroHandler.h"//todo: remove #include "../IGameCallback.h"//todo: remove #include "../BattleFieldHandler.h" +#include + VCMI_LIB_NAMESPACE_BEGIN namespace spells @@ -268,11 +268,9 @@ void BattleCast::cast(ServerCallback * server, Target target) const std::string magicMirrorCacheStr = "type_MAGIC_MIRROR"; static const auto magicMirrorSelector = Selector::type()(BonusType::MAGIC_MIRROR); - auto rangeGen = server->getRNG()->getInt64Range(0, 99); - const int mirrorChance = mainTarget->valOfBonuses(magicMirrorSelector, magicMirrorCacheStr); - if(rangeGen() < mirrorChance) + if(server->getRNG()->nextInt(0, 99) < mirrorChance) { auto mirrorTargets = cb->battleGetUnitsIf([this](const battle::Unit * unit) { @@ -398,7 +396,7 @@ std::unique_ptr ISpellMechanicsFactory::get(const CSpell ///Mechanics Mechanics::Mechanics() : caster(nullptr), - casterSide(0) + casterSide(BattleSide::NONE) { } @@ -414,8 +412,7 @@ BaseMechanics::BaseMechanics(const IBattleCast * event): { caster = event->getCaster(); - //FIXME: do not crash on invalid side - casterSide = cb->playerToSide(caster->getCasterOwner()).value(); + casterSide = cb->playerToSide(caster->getCasterOwner()); { auto value = event->getSpellLevel(); diff --git a/lib/spells/ISpellMechanics.h b/lib/spells/ISpellMechanics.h index 0f9dc9646..35fedbf72 100644 --- a/lib/spells/ISpellMechanics.h +++ b/lib/spells/ISpellMechanics.h @@ -22,7 +22,6 @@ VCMI_LIB_NAMESPACE_BEGIN struct Query; class IBattleState; -class CRandomGenerator; class CreatureService; class CMap; class CGameInfoCallback; @@ -59,6 +58,7 @@ public: virtual const CMap * getMap() const = 0; virtual const CGameInfoCallback * getCb() const = 0; + virtual void createBoat(const int3 & visitablePosition, BoatId type, PlayerColor initiator) = 0; virtual bool moveHero(ObjectInstanceID hid, int3 dst, EMovementMode mode) = 0; //TODO: remove virtual void genericQuery(Query * request, PlayerColor color, std::function)> callback) = 0;//TODO: type safety on query, use generic query packet when implemented @@ -252,7 +252,7 @@ public: const Caster * caster; - ui8 casterSide; + BattleSide casterSide; protected: Mechanics(); diff --git a/lib/spells/Problem.cpp b/lib/spells/Problem.cpp index ed93028cd..90dcc7fe8 100644 --- a/lib/spells/Problem.cpp +++ b/lib/spells/Problem.cpp @@ -10,6 +10,8 @@ #include "StdInc.h" #include "Problem.h" +#include "../texts/MetaString.h" + VCMI_LIB_NAMESPACE_BEGIN namespace spells diff --git a/lib/spells/Problem.h b/lib/spells/Problem.h index aca7062f6..c3d850cb3 100644 --- a/lib/spells/Problem.h +++ b/lib/spells/Problem.h @@ -12,8 +12,6 @@ #include -#include "../MetaString.h" - VCMI_LIB_NAMESPACE_BEGIN namespace spells diff --git a/lib/spells/effects/Catapult.cpp b/lib/spells/effects/Catapult.cpp index 167aaabd6..28ec7998e 100644 --- a/lib/spells/effects/Catapult.cpp +++ b/lib/spells/effects/Catapult.cpp @@ -18,9 +18,11 @@ #include "../../battle/CBattleInfoCallback.h" #include "../../battle/Unit.h" #include "../../mapObjects/CGTownInstance.h" +#include "../../entities/building/TownFortifications.h" #include "../../networkPacks/PacksForClientBattle.h" #include "../../serializer/JsonSerializeFormat.h" -#include "../../CRandomGenerator.h" + +#include VCMI_LIB_NAMESPACE_BEGIN @@ -38,7 +40,7 @@ bool Catapult::applicable(Problem & problem, const Mechanics * m) const return m->adaptProblem(ESpellCastProblem::NO_APPROPRIATE_TARGET, problem); } - if(CGTownInstance::NONE == town->fortLevel()) + if(town->fortificationsLevel().wallsHealth == 0) { return m->adaptProblem(ESpellCastProblem::NO_APPROPRIATE_TARGET, problem); } @@ -102,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); } @@ -118,7 +120,7 @@ void Catapult::applyTargeted(ServerCallback * server, const Mechanics * m, const auto actualTarget = EWallPart::INVALID; if ( m->battle()->isWallPartAttackable(desiredTarget) && - server->getRNG()->getInt64Range(0, 99)() < getCatapultHitChance(desiredTarget)) + server->getRNG()->nextInt(0, 99) < getCatapultHitChance(desiredTarget)) { actualTarget = desiredTarget; } @@ -142,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); } } @@ -172,7 +174,7 @@ int Catapult::getRandomDamage (ServerCallback * server) const { std::array damageChances = { noDmg, hit, crit }; //dmgChance[i] - chance for doing i dmg when hit is successful int totalChance = std::accumulate(damageChances.begin(), damageChances.end(), 0); - int damageRandom = server->getRNG()->getInt64Range(0, totalChance - 1)(); + int damageRandom = server->getRNG()->nextInt(0, totalChance - 1); int dealtDamage = 0; //calculating dealt damage @@ -226,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 491fe5023..03f35ff9d 100644 --- a/lib/spells/effects/Clone.cpp +++ b/lib/spells/effects/Clone.cpp @@ -43,7 +43,7 @@ void Clone::apply(ServerCallback * server, const Mechanics * m, const EffectTarg if(clonedStack->getCount() < 1) continue; - auto hex = m->battle()->getAvaliableHex(clonedStack->creatureId(), m->casterSide, clonedStack->getPosition()); + auto hex = m->battle()->getAvailableHex(clonedStack->creatureId(), m->casterSide, clonedStack->getPosition()); if(!hex.isValid()) { @@ -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 425607008..0e9297329 100644 --- a/lib/spells/effects/Damage.cpp +++ b/lib/spells/effects/Damage.cpp @@ -14,13 +14,12 @@ #include "../CSpellHandler.h" #include "../ISpellMechanics.h" -#include "../../MetaString.h" #include "../../CStack.h" #include "../../battle/IBattleState.h" #include "../../battle/CBattleInfoCallback.h" #include "../../networkPacks/PacksForClientBattle.h" -#include "../../CGeneralTextHandler.h" -#include "../../Languages.h" +#include "../../texts/CGeneralTextHandler.h" +#include "../../texts/Languages.h" #include "../../serializer/JsonSerializeFormat.h" #include @@ -81,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 db14326d1..b86a87d1a 100644 --- a/lib/spells/effects/DemonSummon.cpp +++ b/lib/spells/effects/DemonSummon.cpp @@ -65,7 +65,7 @@ void DemonSummon::apply(ServerCallback * server, const Mechanics * m, const Effe continue; } - auto hex = m->battle()->getAvaliableHex(targetStack->creatureId(), m->casterSide, targetStack->getPosition()); + auto hex = m->battle()->getAvailableHex(targetStack->creatureId(), m->casterSide, targetStack->getPosition()); if(!hex.isValid()) { @@ -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 8ad649f9a..41fedab42 100644 --- a/lib/spells/effects/Dispel.cpp +++ b/lib/spells/effects/Dispel.cpp @@ -16,7 +16,6 @@ #include "../ISpellMechanics.h" -#include "../../MetaString.h" #include "../../battle/IBattleState.h" #include "../../battle/CBattleInfoCallback.h" #include "../../battle/Unit.h" @@ -66,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 @@ -97,7 +96,7 @@ std::shared_ptr Dispel::getBonuses(const Mechanics * m, const b if(!sourceSpell) return false;//error - //Special case: DISRUPTING_RAY and ACID_BREATH_DEFENSE are "immune" to dispell + //Special case: DISRUPTING_RAY and ACID_BREATH_DEFENSE are "immune" to dispel //Other even PERMANENT effects can be removed (f.e. BIND) if(sourceSpell->getIndex() == SpellID::DISRUPTING_RAY || sourceSpell->getIndex() == SpellID::ACID_BREATH_DEFENSE) return false; diff --git a/lib/spells/effects/Heal.cpp b/lib/spells/effects/Heal.cpp index 815d6888c..ce5f13cf5 100644 --- a/lib/spells/effects/Heal.cpp +++ b/lib/spells/effects/Heal.cpp @@ -13,7 +13,6 @@ #include "Registry.h" #include "../ISpellMechanics.h" -#include "../../MetaString.h" #include "../../battle/IBattleState.h" #include "../../battle/CUnitState.h" #include "../../battle/CBattleInfoCallback.h" @@ -43,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 45531d8b1..889644b88 100644 --- a/lib/spells/effects/Moat.cpp +++ b/lib/spells/effects/Moat.cpp @@ -14,10 +14,12 @@ #include "Registry.h" #include "../ISpellMechanics.h" +#include "../../entities/building/CBuilding.h" #include "../../mapObjects/CGTownInstance.h" #include "../../bonuses/Limiters.h" #include "../../battle/IBattleState.h" #include "../../battle/CBattleInfoCallback.h" +#include "../../entities/building/TownFortifications.h" #include "../../json/JsonBonus.h" #include "../../serializer/JsonSerializeFormat.h" #include "../../networkPacks/PacksForClient.h" @@ -84,9 +86,9 @@ void Moat::convertBonus(const Mechanics * m, std::vector & converted) con //Moat battlefield effect is always permanent nb.duration = BonusDuration::ONE_BATTLE; - if(m->battle()->battleGetDefendedTown() && m->battle()->battleGetSiegeLevel() >= CGTownInstance::CITADEL) + 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 @@ -108,7 +110,7 @@ void Moat::apply(ServerCallback * server, const Mechanics * m, const EffectTarge { assert(m->isMassive()); assert(m->battle()->battleGetDefendedTown()); - if(m->isMassive() && m->battle()->battleGetSiegeLevel() >= CGTownInstance::CITADEL) + if(m->isMassive() && m->battle()->battleGetFortifications().hasMoat) { EffectTarget moat; placeObstacles(server, m, moat); @@ -120,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); } } } @@ -133,7 +135,7 @@ void Moat::placeObstacles(ServerCallback * server, const Mechanics * m, const Ef BattleObstaclesChanged pack; pack.battleID = m->battle()->getBattle()->getBattleID(); - auto all = m->battle()->battleGetAllObstacles(BattlePerspective::ALL_KNOWING); + auto all = m->battle()->battleGetAllObstacles(BattleSide::ALL_KNOWING); int obstacleIdToGive = 1; for(auto & one : all) @@ -169,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 b9dbe3b0b..40c22a088 100644 --- a/lib/spells/effects/Obstacle.cpp +++ b/lib/spells/effects/Obstacle.cpp @@ -16,9 +16,11 @@ #include "../../battle/IBattleState.h" #include "../../battle/CBattleInfoCallback.h" +#include "../../entities/building/TownFortifications.h" #include "../../networkPacks/PacksForClientBattle.h" #include "../../serializer/JsonSerializeFormat.h" -#include "../../CRandomGenerator.h" + +#include VCMI_LIB_NAMESPACE_BEGIN @@ -101,11 +103,11 @@ void Obstacle::adjustAffectedHexes(std::set & hexes, const Mechanics for(auto & destination : effectTarget) { - for(const auto & trasformation : options.shape) + for(const auto & transformation : options.shape) { BattleHex hex = destination.hexValue; - for(auto direction : trasformation) + for(auto direction : transformation) hex.moveInDirection(direction, false); if(hex.isValid()) @@ -134,10 +136,10 @@ bool Obstacle::applicable(Problem & problem, const Mechanics * m, const EffectTa for(const auto & destination : target) { - for(const auto & trasformation : options.shape) + for(const auto & transformation : options.shape) { BattleHex hex = destination.hexValue; - for(auto direction : trasformation) + for(auto direction : transformation) hex.moveInDirection(direction, false); if(!isHexAvailable(m->battle(), hex, requiresClearTiles)) @@ -238,7 +240,7 @@ bool Obstacle::isHexAvailable(const CBattleInfoCallback * cb, const BattleHex & if(i->obstacleType != CObstacleInstance::MOAT) return false; - if(cb->battleGetSiegeLevel() != 0) + if(cb->battleGetFortifications().wallsHealth != 0) { EWallPart part = cb->battleHexToWallPart(hex); @@ -273,7 +275,7 @@ void Obstacle::placeObstacles(ServerCallback * server, const Mechanics * m, cons BattleObstaclesChanged pack; pack.battleID = m->battle()->getBattle()->getBattleID(); - auto all = m->battle()->battleGetAllObstacles(BattlePerspective::ALL_KNOWING); + auto all = m->battle()->battleGetAllObstacles(BattleSide::ALL_KNOWING); int obstacleIdToGive = 1; for(auto & one : all) @@ -324,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/Obstacle.h b/lib/spells/effects/Obstacle.h index 1dd256e1a..b6410aed4 100644 --- a/lib/spells/effects/Obstacle.h +++ b/lib/spells/effects/Obstacle.h @@ -66,7 +66,7 @@ private: bool passable = false; int32_t turnsRemaining = -1; - std::array sideOptions; + BattleSideArray sideOptions; static bool isHexAvailable(const CBattleInfoCallback * cb, const BattleHex & hex, const bool mustBeClear); static bool noRoomToPlace(Problem & problem, const Mechanics * m); diff --git a/lib/spells/effects/RemoveObstacle.cpp b/lib/spells/effects/RemoveObstacle.cpp index f24b0e881..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) @@ -90,7 +90,7 @@ std::set RemoveObstacle::getTargets(const Mechanics * std::set possibleTargets; if(m->isMassive() || alwaysMassive) { - for(const auto & obstacle : m->battle()->battleGetAllObstacles(BattlePerspective::ALL_KNOWING)) + for(const auto & obstacle : m->battle()->battleGetAllObstacles(BattleSide::ALL_KNOWING)) if(canRemove(obstacle.get())) possibleTargets.insert(obstacle.get()); } 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 dc0dfc153..e4d51bd5b 100644 --- a/lib/spells/effects/Summon.cpp +++ b/lib/spells/effects/Summon.cpp @@ -13,13 +13,11 @@ #include "Registry.h" #include "../ISpellMechanics.h" -#include "../../MetaString.h" #include "../../battle/CBattleInfoCallback.h" #include "../../battle/BattleInfo.h" #include "../../battle/Unit.h" #include "../../serializer/JsonSerializeFormat.h" #include "../../CCreatureHandler.h" -#include "../../CHeroHandler.h" #include "../../mapObjects/CGHeroInstance.h" #include "../../networkPacks/PacksForClientBattle.h" @@ -80,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); @@ -160,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 @@ -192,7 +190,7 @@ EffectTarget Summon::transformTarget(const Mechanics * m, const Target & aimPoin if(sameSummoned.empty() || !summonSameUnit) { - BattleHex hex = m->battle()->getAvaliableHex(creature, m->casterSide); + BattleHex hex = m->battle()->getAvailableHex(creature, m->casterSide); if(!hex.isValid()) logGlobal->error("No free space to summon creature!"); else diff --git a/lib/spells/effects/Teleport.cpp b/lib/spells/effects/Teleport.cpp index 4d29e9483..cffe3c2f1 100644 --- a/lib/spells/effects/Teleport.cpp +++ b/lib/spells/effects/Teleport.cpp @@ -15,6 +15,7 @@ #include "../../battle/IBattleState.h" #include "../../battle/CBattleInfoCallback.h" #include "../../battle/Unit.h" +#include "../../entities/building/TownFortifications.h" #include "../../networkPacks/PacksForClientBattle.h" #include "../../serializer/JsonSerializeFormat.h" @@ -61,10 +62,10 @@ bool Teleport::applicable(Problem & problem, const Mechanics * m, const EffectTa if(!targetUnit) return m->adaptProblem(ESpellCastProblem::WRONG_SPELL_TARGET, problem); - if(!targetHex.isValid() || !m->battle()->getAccesibility(targetUnit).accessible(targetHex, targetUnit)) + if(!targetHex.isValid() || !m->battle()->getAccessibility(targetUnit).accessible(targetHex, targetUnit)) return m->adaptProblem(ESpellCastProblem::WRONG_SPELL_TARGET, problem); - if(m->battle()->battleGetSiegeLevel() && !(isWallPassable && isMoatPassable)) + if(m->battle()->battleGetFortifications().wallsHealth > 0 && !(isWallPassable && isMoatPassable)) { return !m->battle()->battleHasPenaltyOnLine(target[0].hexValue, target[1].hexValue, !isWallPassable, !isMoatPassable); } @@ -84,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 a90c903d7..694f5628d 100644 --- a/lib/spells/effects/Timed.cpp +++ b/lib/spells/effects/Timed.cpp @@ -13,7 +13,6 @@ #include "Registry.h" #include "../ISpellMechanics.h" -#include "../../MetaString.h" #include "../../battle/IBattleState.h" #include "../../battle/CBattleInfoCallback.h" #include "../../battle/Unit.h" @@ -206,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 3d1be5842..3598fbbb8 100644 --- a/lib/spells/effects/UnitEffect.cpp +++ b/lib/spells/effects/UnitEffect.cpp @@ -164,10 +164,9 @@ EffectTarget UnitEffect::transformTargetByRange(const Mechanics * m, const Targe if(m->alwaysHitFirstTarget()) { + //TODO: examine if adjustments needed related to INVINCIBLE bonus if(!aimPoint.empty() && aimPoint.front().unitValue) targets.insert(aimPoint.front().unitValue); - else - logGlobal->error("Spell-like attack with no primary target."); } EffectTarget effectTarget; @@ -198,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/CGeneralTextHandler.cpp b/lib/texts/CGeneralTextHandler.cpp similarity index 50% rename from lib/CGeneralTextHandler.cpp rename to lib/texts/CGeneralTextHandler.cpp index cea2b392f..2f3cff5e8 100644 --- a/lib/CGeneralTextHandler.cpp +++ b/lib/texts/CGeneralTextHandler.cpp @@ -8,23 +8,17 @@ * */ #include "StdInc.h" -#include "CGeneralTextHandler.h" +#include "texts/CGeneralTextHandler.h" +#include "CLegacyConfigParser.h" #include "CConfigHandler.h" -#include "GameSettings.h" +#include "IGameSettings.h" #include "Languages.h" -#include "TextOperations.h" -#include "VCMIDirs.h" -#include "VCMI_Lib.h" -#include "filesystem/Filesystem.h" -#include "mapObjects/CQuest.h" -#include "modding/CModHandler.h" -#include "serializer/JsonSerializeFormat.h" +#include "../filesystem/Filesystem.h" +#include "../mapObjects/CQuest.h" VCMI_LIB_NAMESPACE_BEGIN -std::recursive_mutex TextLocalizationContainer::globalTextMutex; - /// Detects language and encoding of H3 text files based on matching against pregenerated footprints of H3 file void CGeneralTextHandler::detectInstallParameters() { @@ -113,361 +107,6 @@ void CGeneralTextHandler::detectInstallParameters() encoding->String() = Languages::getLanguageOptions(knownLanguages[bestIndex]).encoding; } -//Helper for string -> float conversion -class LocaleWithComma: public std::numpunct -{ -protected: - char do_decimal_point() const override - { - return ','; - } -}; - -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; - - data.reset(new char[input->getSize()]); - input->read(reinterpret_cast(data.get()), input->getSize()); - - curr = data.get(); - end = curr + input->getSize(); -} - -std::string CLegacyConfigParser::extractQuotedPart() -{ - assert(*curr == '\"'); - - curr++; // skip quote - char * begin = curr; - - while (curr != end && *curr != '\"' && *curr != '\t') - curr++; - - return std::string(begin, curr++); //increment curr to close quote -} - -std::string CLegacyConfigParser::extractQuotedString() -{ - assert(*curr == '\"'); - - std::string ret; - while (true) - { - ret += extractQuotedPart(); - - // double quote - add it to string and continue quoted part - if (curr < end && *curr == '\"') - { - ret += '\"'; - } - //extract normal part - else if(curr < end && *curr != '\t' && *curr != '\r') - { - char * begin = curr; - - while (curr < end && *curr != '\t' && *curr != '\r' && *curr != '\"')//find end of string or next quoted part start - curr++; - - ret += std::string(begin, curr); - - if(curr>=end || *curr != '\"') - return ret; - } - else // end of string - return ret; - } -} - -std::string CLegacyConfigParser::extractNormalString() -{ - char * begin = curr; - - while (curr < end && *curr != '\t' && *curr != '\r')//find end of string - curr++; - - return std::string(begin, curr); -} - -std::string CLegacyConfigParser::readRawString() -{ - if (curr >= end || *curr == '\n') - return ""; - - std::string ret; - - if (*curr == '\"') - ret = extractQuotedString();// quoted text - find closing quote - else - ret = extractNormalString();//string without quotes - copy till \t or \r - - curr++; - return ret; -} - -std::string CLegacyConfigParser::readString() -{ - // do not convert strings that are already in ASCII - this will only slow down loading process - std::string str = readRawString(); - if (TextOperations::isValidASCII(str)) - return str; - return TextOperations::toUnicode(str, fileEncoding); -} - -float CLegacyConfigParser::readNumber() -{ - std::string input = readRawString(); - - std::istringstream stream(input); - - if(input.find(',') != std::string::npos) // code to handle conversion with comma as decimal separator - stream.imbue(std::locale(std::locale(), new LocaleWithComma())); - - float result; - if ( !(stream >> result) ) - return 0; - return result; -} - -bool CLegacyConfigParser::isNextEntryEmpty() const -{ - char * nextSymbol = curr; - while (nextSymbol < end && *nextSymbol == ' ') - nextSymbol++; //find next meaningfull symbol - - return nextSymbol >= end || *nextSymbol == '\n' || *nextSymbol == '\r' || *nextSymbol == '\t'; -} - -bool CLegacyConfigParser::endLine() -{ - while (curr < end && *curr != '\n') - readString(); - - curr++; - - return curr < end; -} - -void TextLocalizationContainer::registerStringOverride(const std::string & modContext, const std::string & language, const TextIdentifier & UID, const std::string & localized) -{ - 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; -} - -void TextLocalizationContainer::addSubContainer(const TextLocalizationContainer & container) -{ - std::lock_guard globalLock(globalTextMutex); - - assert(!vstd::contains(subContainers, &container)); - subContainers.push_back(&container); -} - -void TextLocalizationContainer::removeSubContainer(const TextLocalizationContainer & container) -{ - std::lock_guard globalLock(globalTextMutex); - - assert(vstd::contains(subContainers, &container)); - - subContainers.erase(std::remove(subContainers.begin(), subContainers.end(), &container), subContainers.end()); -} - -const std::string & TextLocalizationContainer::deserialize(const TextIdentifier & identifier) const -{ - std::lock_guard globalLock(globalTextMutex); - - if(stringsLocalizations.count(identifier.get()) == 0) - { - for(auto containerIter = subContainers.rbegin(); containerIter != subContainers.rend(); ++containerIter) - if((*containerIter)->identifierExists(identifier)) - return (*containerIter)->deserialize(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; -} - -void TextLocalizationContainer::registerString(const std::string & modContext, const TextIdentifier & UID, const std::string & localized, const std::string & language) -{ - std::lock_guard globalLock(globalTextMutex); - - assert(!modContext.empty()); - assert(!Languages::getLanguageOptions(language).identifier.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? - - if(stringsLocalizations.count(UID.get()) > 0) - { - auto & value = stringsLocalizations[UID.get()]; - value.baseLanguage = language; - value.baseValue = localized; - } - else - { - StringState value; - value.baseLanguage = language; - value.baseValue = localized; - value.modContext = modContext; - - 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) -{ - for(const auto & node : config.Struct()) - registerStringOverride(modContext, language, node.first, node.second.String()); -} - -bool TextLocalizationContainer::identifierExists(const TextIdentifier & UID) const -{ - std::lock_guard globalLock(globalTextMutex); - - return stringsLocalizations.count(UID.get()); -} - -void TextLocalizationContainer::exportAllTexts(std::map> & storage) const -{ - std::lock_guard globalLock(globalTextMutex); - - for (auto const & subContainer : subContainers) - subContainer->exportAllTexts(storage); - - for (auto const & entry : stringsLocalizations) - { - std::string textToWrite; - std::string modName = entry.second.modContext; - - if (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; - - storage[modName][entry.first] = textToWrite; - } -} - -std::string TextLocalizationContainer::getModLanguage(const std::string & modContext) -{ - if (modContext == "core") - return CGeneralTextHandler::getInstalledLanguage(); - return VLC->modh->getModLanguage(modContext); -} - -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; - } -} - -TextContainerRegistrable::TextContainerRegistrable() -{ - VLC->generaltexth->addSubContainer(*this); -} - -TextContainerRegistrable::~TextContainerRegistrable() -{ - VLC->generaltexth->removeSubContainer(*this); -} - -TextContainerRegistrable::TextContainerRegistrable(const TextContainerRegistrable & other) - : TextLocalizationContainer(other) -{ - VLC->generaltexth->addSubContainer(*this); -} - -TextContainerRegistrable::TextContainerRegistrable(TextContainerRegistrable && other) noexcept - :TextLocalizationContainer(other) -{ - VLC->generaltexth->addSubContainer(*this); -} - void CGeneralTextHandler::readToVector(const std::string & sourceID, const std::string & sourceName) { CLegacyConfigParser parser(TextPath::builtin(sourceName)); @@ -481,21 +120,16 @@ void CGeneralTextHandler::readToVector(const std::string & sourceID, const std:: } CGeneralTextHandler::CGeneralTextHandler(): - victoryConditions(*this, "core.vcdesc" ), - lossCondtions (*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" ), @@ -505,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" ); @@ -532,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(); @@ -664,11 +292,6 @@ CGeneralTextHandler::CGeneralTextHandler(): scenariosCountPerCampaign.push_back(region); } } - if (VLC->settings()->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 @@ -712,7 +335,7 @@ std::string CGeneralTextHandler::getInstalledEncoding() std::vector CGeneralTextHandler::findStringsWithPrefix(const std::string & prefix) { - std::lock_guard globalLock(globalTextMutex); + std::lock_guard globalLock(globalTextMutex); std::vector result; for(const auto & entry : stringsLocalizations) diff --git a/lib/texts/CGeneralTextHandler.h b/lib/texts/CGeneralTextHandler.h new file mode 100644 index 000000000..428eea1b0 --- /dev/null +++ b/lib/texts/CGeneralTextHandler.h @@ -0,0 +1,100 @@ +/* + * CGeneralTextHandler.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 "TextLocalizationContainer.h" + +VCMI_LIB_NAMESPACE_BEGIN + +class CGeneralTextHandler; + +/// Small wrapper that provides text access API compatible with old code +class DLL_LINKAGE LegacyTextContainer +{ + CGeneralTextHandler & owner; + std::string basePath; + +public: + LegacyTextContainer(CGeneralTextHandler & owner, std::string basePath); + std::string operator [](size_t index) const; +}; + +/// Small wrapper that provides help text access API compatible with old code +class DLL_LINKAGE LegacyHelpContainer +{ + CGeneralTextHandler & owner; + std::string basePath; + +public: + LegacyHelpContainer(CGeneralTextHandler & owner, std::string basePath); + std::pair operator[](size_t index) const; +}; + +/// Handles all text-related data in game +class DLL_LINKAGE CGeneralTextHandler: public TextLocalizationContainer +{ + void readToVector(const std::string & sourceID, const std::string & sourceName); + + /// number of scenarios in specific campaign. TODO: move to a better location + std::vector scenariosCountPerCampaign; + +public: + LegacyTextContainer allTexts; + + LegacyTextContainer arraytxt; + LegacyTextContainer primarySkillNames; + LegacyTextContainer jktexts; + LegacyTextContainer heroscrn; + LegacyTextContainer overview;//text for Kingdom Overview window + LegacyTextContainer capColors; //names of player colors with first letter capitalized ("Red",...) + LegacyTextContainer turnDurations; //turn durations for pregame (1 Minute ... Unlimited) + + //towns + LegacyTextContainer tcommands; //texts for town screen, + LegacyTextContainer hcommands; // town hall screen + LegacyTextContainer fcommands; // fort screen + LegacyTextContainer tavernInfo; + + LegacyHelpContainer zelp; + + //objects + LegacyTextContainer advobtxt; + LegacyTextContainer restypes; //names of resources + LegacyTextContainer seerEmpty; + LegacyTextContainer seerNames; + LegacyTextContainer tentColors; + + //sec skills + LegacyTextContainer levels; + + std::vector findStringsWithPrefix(const std::string & prefix); + + int32_t pluralText(int32_t textIndex, int32_t count) const; + + size_t getCampaignLength(size_t campaignID) const; + + CGeneralTextHandler(); + CGeneralTextHandler(const CGeneralTextHandler&) = delete; + CGeneralTextHandler operator=(const CGeneralTextHandler&) = delete; + + /// Attempts to detect encoding & language of H3 files + static void detectInstallParameters(); + + /// Returns name of language preferred by user + static std::string getPreferredLanguage(); + + /// Returns name of language of Heroes III text files + static std::string getInstalledLanguage(); + + /// Returns name of encoding of Heroes III text files + static std::string getInstalledEncoding(); +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/texts/CLegacyConfigParser.cpp b/lib/texts/CLegacyConfigParser.cpp new file mode 100644 index 000000000..90eb9a206 --- /dev/null +++ b/lib/texts/CLegacyConfigParser.cpp @@ -0,0 +1,158 @@ +/* + * CLegacyConfigParser.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 "CLegacyConfigParser.h" + +#include "TextOperations.h" +#include "Languages.h" + +#include "../VCMI_Lib.h" +#include "filesystem/Filesystem.h" +#include "../modding/CModHandler.h" + +VCMI_LIB_NAMESPACE_BEGIN + +//Helper for string -> float conversion +class LocaleWithComma: public std::numpunct +{ +protected: + char do_decimal_point() const override + { + return ','; + } +}; + +CLegacyConfigParser::CLegacyConfigParser(const TextPath & resource) +{ + auto input = CResourceHandler::get()->load(resource); + fileEncoding = VLC->modh->findResourceEncoding(resource); + + data.reset(new char[input->getSize()]); + input->read(reinterpret_cast(data.get()), input->getSize()); + + curr = data.get(); + end = curr + input->getSize(); +} + +std::string CLegacyConfigParser::extractQuotedPart() +{ + assert(*curr == '\"'); + + curr++; // skip quote + const char * begin = curr; + + while (curr != end && *curr != '\"' && *curr != '\t') + curr++; + + return std::string(begin, curr++); //increment curr to close quote +} + +std::string CLegacyConfigParser::extractQuotedString() +{ + assert(*curr == '\"'); + + std::string ret; + while (true) + { + ret += extractQuotedPart(); + + // double quote - add it to string and continue quoted part + if (curr < end && *curr == '\"') + { + ret += '\"'; + } + //extract normal part + else if(curr < end && *curr != '\t' && *curr != '\r') + { + const char * begin = curr; + + while (curr < end && *curr != '\t' && *curr != '\r' && *curr != '\"')//find end of string or next quoted part start + curr++; + + ret += std::string(begin, curr); + + if(curr>=end || *curr != '\"') + return ret; + } + else // end of string + return ret; + } +} + +std::string CLegacyConfigParser::extractNormalString() +{ + const char * begin = curr; + + while (curr < end && *curr != '\t' && *curr != '\r')//find end of string + curr++; + + return std::string(begin, curr); +} + +std::string CLegacyConfigParser::readRawString() +{ + if (curr >= end || *curr == '\n') + return ""; + + std::string ret; + + if (*curr == '\"') + ret = extractQuotedString();// quoted text - find closing quote + else + ret = extractNormalString();//string without quotes - copy till \t or \r + + curr++; + return ret; +} + +std::string CLegacyConfigParser::readString() +{ + // do not convert strings that are already in ASCII - this will only slow down loading process + std::string str = readRawString(); + if (TextOperations::isValidASCII(str)) + return str; + return TextOperations::toUnicode(str, fileEncoding); +} + +float CLegacyConfigParser::readNumber() +{ + std::string input = readRawString(); + + std::istringstream stream(input); + + if(input.find(',') != std::string::npos) // code to handle conversion with comma as decimal separator + stream.imbue(std::locale(std::locale(), new LocaleWithComma())); + + float result; + if ( !(stream >> result) ) + return 0; + return result; +} + +bool CLegacyConfigParser::isNextEntryEmpty() const +{ + const char * nextSymbol = curr; + while (nextSymbol < end && *nextSymbol == ' ') + nextSymbol++; //find next meaningful symbol + + return nextSymbol >= end || *nextSymbol == '\n' || *nextSymbol == '\r' || *nextSymbol == '\t'; +} + +bool CLegacyConfigParser::endLine() +{ + while (curr < end && *curr != '\n') + readString(); + + curr++; + + return curr < end; +} + +VCMI_LIB_NAMESPACE_END diff --git a/lib/texts/CLegacyConfigParser.h b/lib/texts/CLegacyConfigParser.h new file mode 100644 index 000000000..41665e0d5 --- /dev/null +++ b/lib/texts/CLegacyConfigParser.h @@ -0,0 +1,61 @@ +/* + * CLegacyConfigParser.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 "filesystem/ResourcePath.h" + +VCMI_LIB_NAMESPACE_BEGIN + +/// Parser for any text files from H3 +class DLL_LINKAGE CLegacyConfigParser +{ + std::string fileEncoding; + + std::unique_ptr data; + const char * curr; + const char * end; + + /// extracts part of quoted string. + std::string extractQuotedPart(); + + /// extracts quoted string. Any end of lines are ignored, double-quote is considered as "escaping" + std::string extractQuotedString(); + + /// extracts non-quoted string + std::string extractNormalString(); + + /// reads "raw" string without encoding conversion + std::string readRawString(); + +public: + /// read one entry from current line. Return ""/0 if end of line reached + std::string readString(); + float readNumber(); + + template + std::vector readNumArray(size_t size) + { + std::vector ret; + ret.reserve(size); + while (size--) + ret.push_back(readNumber()); + return ret; + } + + /// returns true if next entry is empty + bool isNextEntryEmpty() const; + + /// end current line + bool endLine(); + + explicit CLegacyConfigParser(const TextPath & URI); +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/Languages.h b/lib/texts/Languages.h similarity index 80% rename from lib/Languages.h rename to lib/texts/Languages.h index 52dd1f96f..52b13b851 100644 --- a/lib/Languages.h +++ b/lib/texts/Languages.h @@ -63,6 +63,9 @@ struct Options /// primary IETF language tag std::string tagIETF; + /// ISO 639-2 (B) language code + std::string tagISO2; + /// DateTime format std::string dateTimeFormat; @@ -74,23 +77,23 @@ inline const auto & getLanguageList() { static const std::array languages { { - { "czech", "Czech", "Čeština", "CP1250", "cs", "%d.%m.%Y %T", EPluralForms::CZ_3 }, - { "chinese", "Chinese", "简体中文", "GBK", "zh", "%F %T", EPluralForms::VI_1 }, // Note: actually Simplified Chinese - { "english", "English", "English", "CP1252", "en", "%F %T", EPluralForms::EN_2 }, // English uses international date/time format here - { "finnish", "Finnish", "Suomi", "CP1252", "fi", "%d.%m.%Y %T", EPluralForms::EN_2 }, - { "french", "French", "Français", "CP1252", "fr", "%d/%m/%Y %T", EPluralForms::FR_2 }, - { "german", "German", "Deutsch", "CP1252", "de", "%d.%m.%Y %T", EPluralForms::EN_2 }, - { "hungarian", "Hungarian", "Magyar", "CP1250", "hu", "%Y. %m. %d. %T", EPluralForms::EN_2 }, - { "italian", "Italian", "Italiano", "CP1250", "it", "%d/%m/%Y %T", EPluralForms::EN_2 }, - { "korean", "Korean", "한국어", "CP949", "ko", "%F %T", EPluralForms::VI_1 }, - { "polish", "Polish", "Polski", "CP1250", "pl", "%d.%m.%Y %T", EPluralForms::PL_3 }, - { "portuguese", "Portuguese", "Português", "CP1252", "pt", "%d/%m/%Y %T", EPluralForms::EN_2 }, // Note: actually Brazilian Portuguese - { "russian", "Russian", "Русский", "CP1251", "ru", "%d.%m.%Y %T", EPluralForms::UK_3 }, - { "spanish", "Spanish", "Español", "CP1252", "es", "%d/%m/%Y %T", EPluralForms::EN_2 }, - { "swedish", "Swedish", "Svenska", "CP1252", "sv", "%F %T", EPluralForms::EN_2 }, - { "turkish", "Turkish", "Türkçe", "CP1254", "tr", "%d.%m.%Y %T", EPluralForms::EN_2 }, - { "ukrainian", "Ukrainian", "Українська", "CP1251", "uk", "%d.%m.%Y %T", EPluralForms::UK_3 }, - { "vietnamese", "Vietnamese", "Tiếng Việt", "UTF-8", "vi", "%d/%m/%Y %T", EPluralForms::VI_1 }, // Fan translation uses special encoding + { "czech", "Czech", "Čeština", "CP1250", "cs", "cze", "%d.%m.%Y %H:%M", EPluralForms::CZ_3 }, + { "chinese", "Chinese", "简体中文", "GBK", "zh", "chi", "%Y-%m-%d %H:%M", EPluralForms::VI_1 }, // Note: actually Simplified Chinese + { "english", "English", "English", "CP1252", "en", "eng", "%Y-%m-%d %H:%M", EPluralForms::EN_2 }, // English uses international date/time format here + { "finnish", "Finnish", "Suomi", "CP1252", "fi", "fin", "%d.%m.%Y %H:%M", EPluralForms::EN_2, }, + { "french", "French", "Français", "CP1252", "fr", "fre", "%d/%m/%Y %H:%M", EPluralForms::FR_2, }, + { "german", "German", "Deutsch", "CP1252", "de", "ger", "%d.%m.%Y %H:%M", EPluralForms::EN_2, }, + { "hungarian", "Hungarian", "Magyar", "CP1250", "hu", "hun", "%Y. %m. %d. %H:%M", EPluralForms::EN_2 }, + { "italian", "Italian", "Italiano", "CP1250", "it", "ita", "%d/%m/%Y %H:%M", EPluralForms::EN_2 }, + { "korean", "Korean", "한국어", "CP949", "ko", "kor", "%Y-%m-%d %H:%M", EPluralForms::VI_1 }, + { "polish", "Polish", "Polski", "CP1250", "pl", "pol", "%d.%m.%Y %H:%M", EPluralForms::PL_3 }, + { "portuguese", "Portuguese", "Português", "CP1252", "pt", "por", "%d/%m/%Y %H:%M", EPluralForms::EN_2 }, // Note: actually Brazilian Portuguese + { "russian", "Russian", "Русский", "CP1251", "ru", "rus", "%d.%m.%Y %H:%M", EPluralForms::UK_3 }, + { "spanish", "Spanish", "Español", "CP1252", "es", "spa", "%d/%m/%Y %H:%M", EPluralForms::EN_2 }, + { "swedish", "Swedish", "Svenska", "CP1252", "sv", "swe", "%Y-%m-%d %H:%M", EPluralForms::EN_2 }, + { "turkish", "Turkish", "Türkçe", "CP1254", "tr", "tur", "%d.%m.%Y %H:%M", EPluralForms::EN_2 }, + { "ukrainian", "Ukrainian", "Українська", "CP1251", "uk", "ukr", "%d.%m.%Y %H:%M", EPluralForms::UK_3 }, + { "vietnamese", "Vietnamese", "Tiếng Việt", "UTF-8", "vi", "vie", "%d/%m/%Y %H:%M", 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/MetaString.cpp b/lib/texts/MetaString.cpp similarity index 95% rename from lib/MetaString.cpp rename to lib/texts/MetaString.cpp index 010122452..0bb7304ec 100644 --- a/lib/MetaString.cpp +++ b/lib/texts/MetaString.cpp @@ -13,7 +13,8 @@ #include "CArtHandler.h" #include "CCreatureHandler.h" #include "CCreatureSet.h" -#include "CGeneralTextHandler.h" +#include "entities/faction/CFaction.h" +#include "texts/CGeneralTextHandler.h" #include "CSkillHandler.h" #include "GameConstants.h" #include "VCMI_Lib.h" @@ -367,6 +368,11 @@ void MetaString::appendName(const CreatureID & id, TQuantity count) appendNamePlural(id); } +void MetaString::appendName(const GameResID& id) +{ + appendTextID(TextIdentifier("core.restypes", id.getNum()).get()); +} + void MetaString::appendNameSingular(const CreatureID & id) { appendTextID(id.toEntity(VLC)->getNameSingularTextID()); @@ -382,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()); @@ -427,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/MetaString.h b/lib/texts/MetaString.h similarity index 93% rename from lib/MetaString.h rename to lib/texts/MetaString.h index 86e91d0cc..e13b55709 100644 --- a/lib/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; @@ -49,7 +50,7 @@ private: REPLACE_TEXTID_STRING, REPLACE_NUMBER, REPLACE_POSITIVE_NUMBER, - APPEND_EOL + APPEND_EOL }; std::vector message; @@ -80,6 +81,7 @@ public: void appendName(const SpellID& id); void appendName(const PlayerColor& id); void appendName(const CreatureID & id, TQuantity count); + void appendName(const GameResID& id); void appendNameSingular(const CreatureID & id); void appendNamePlural(const CreatureID & id); void appendEOL(); @@ -88,7 +90,7 @@ public: void replaceLocalString(EMetaText type, ui32 serial); /// Replaces first '%s' placeholder in string with specified fixed, untranslated string void replaceRawString(const std::string & txt); - /// Repalces first '%s' placeholder with string ID that will be translated in output + /// Replaces first '%s' placeholder with string ID that will be translated in output void replaceTextID(const std::string & value); /// Replaces first '%d' placeholder in string with specified number void replaceNumber(int64_t txt); @@ -96,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/TextIdentifier.h b/lib/texts/TextIdentifier.h new file mode 100644 index 000000000..44ad2a21a --- /dev/null +++ b/lib/texts/TextIdentifier.h @@ -0,0 +1,42 @@ +/* + * TextIdentifier.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 TextIdentifier +{ + std::string identifier; +public: + const std::string & get() const + { + return identifier; + } + + TextIdentifier(const char * id): + identifier(id) + {} + + TextIdentifier(const std::string & id): + identifier(id) + {} + + template + TextIdentifier(const std::string & id, size_t index, T... rest): + TextIdentifier(id + '.' + std::to_string(index), rest...) + {} + + template + TextIdentifier(const std::string & id, const std::string & id2, T... rest): + TextIdentifier(id + '.' + id2, rest...) + {} +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/texts/TextLocalizationContainer.cpp b/lib/texts/TextLocalizationContainer.cpp new file mode 100644 index 000000000..e22e7d3c4 --- /dev/null +++ b/lib/texts/TextLocalizationContainer.cpp @@ -0,0 +1,213 @@ +/* + * TextLocalizationContainer.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 "TextLocalizationContainer.h" + +#include "texts/CGeneralTextHandler.h" +#include "Languages.h" +#include "TextOperations.h" + +#include "../VCMI_Lib.h" +#include "../json/JsonNode.h" +#include "../modding/CModHandler.h" + +VCMI_LIB_NAMESPACE_BEGIN + +std::recursive_mutex TextLocalizationContainer::globalTextMutex; + +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()); + + // 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()]; + + // 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) +{ + std::lock_guard globalLock(globalTextMutex); + + assert(!vstd::contains(subContainers, &container)); + subContainers.push_back(&container); +} + +void TextLocalizationContainer::removeSubContainer(const TextLocalizationContainer & container) +{ + std::lock_guard globalLock(globalTextMutex); + + assert(vstd::contains(subContainers, &container)); + + subContainers.erase(std::remove(subContainers.begin(), subContainers.end(), &container), subContainers.end()); +} + +const std::string & TextLocalizationContainer::translateString(const TextIdentifier & identifier) const +{ + std::lock_guard globalLock(globalTextMutex); + + if(stringsLocalizations.count(identifier.get()) == 0) + { + for(auto containerIter = subContainers.rbegin(); containerIter != subContainers.rend(); ++containerIter) + if((*containerIter)->identifierExists(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()); + return entry.translatedText; +} + +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(!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 || 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.translatedText = localized; + value.identifierModContext = identifierModContext; + value.baseStringModContext = localizedStringModContext; + } + else + { + StringState value; + value.translatedText = localized; + value.identifierModContext = identifierModContext; + value.baseStringModContext = localizedStringModContext; + + stringsLocalizations[UID.get()] = value; + } +} + +void TextLocalizationContainer::loadTranslationOverrides(const std::string & modContext, const std::string & language, const JsonNode & config) +{ + for(const auto & node : config.Struct()) + registerStringOverride(modContext, node.first, node.second.String(), language); +} + +bool TextLocalizationContainer::identifierExists(const TextIdentifier & UID) const +{ + std::lock_guard globalLock(globalTextMutex); + + return stringsLocalizations.count(UID.get()); +} + +void TextLocalizationContainer::exportAllTexts(std::map> & storage, bool onlyMissing) const +{ + std::lock_guard globalLock(globalTextMutex); + + for (auto const & subContainer : subContainers) + subContainer->exportAllTexts(storage, onlyMissing); + + for (auto const & entry : stringsLocalizations) + { + if (onlyMissing && entry.second.overriden) + continue; + + 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('.')); + + boost::range::replace(modName, '.', '_'); + + textToWrite = entry.second.translatedText; + + if (!textToWrite.empty()) + storage[modName][entry.first] = textToWrite; + } +} + +std::string TextLocalizationContainer::getModLanguage(const std::string & modContext) +{ + if (modContext == "core") + return CGeneralTextHandler::getInstalledLanguage(); + return VLC->modh->getModLanguage(modContext); +} + +void TextLocalizationContainer::jsonSerialize(JsonNode & dest) const +{ + std::lock_guard globalLock(globalTextMutex); + + for(auto & s : stringsLocalizations) + dest.Struct()[s.first].String() = s.second.translatedText; +} + +TextContainerRegistrable::TextContainerRegistrable() +{ + VLC->generaltexth->addSubContainer(*this); +} + +TextContainerRegistrable::~TextContainerRegistrable() +{ + VLC->generaltexth->removeSubContainer(*this); +} + +TextContainerRegistrable::TextContainerRegistrable(const TextContainerRegistrable & other) + : TextLocalizationContainer(other) +{ + VLC->generaltexth->addSubContainer(*this); +} + +TextContainerRegistrable::TextContainerRegistrable(TextContainerRegistrable && other) noexcept + :TextLocalizationContainer(other) +{ + VLC->generaltexth->addSubContainer(*this); +} + + +VCMI_LIB_NAMESPACE_END diff --git a/lib/texts/TextLocalizationContainer.h b/lib/texts/TextLocalizationContainer.h new file mode 100644 index 000000000..523335c57 --- /dev/null +++ b/lib/texts/TextLocalizationContainer.h @@ -0,0 +1,150 @@ +/* + * TextLocalizationContainer.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 "TextIdentifier.h" + +VCMI_LIB_NAMESPACE_BEGIN + +class JsonNode; + +class DLL_LINKAGE TextLocalizationContainer +{ +protected: + static std::recursive_mutex globalTextMutex; + + struct StringState + { + /// Human-readable string that was added on registration + std::string translatedText; + + /// ID of mod that created this string + 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 & translatedText; + h & identifierModContext; + h & baseStringModContext; + } + }; + + /// map identifier -> localization + std::unordered_map stringsLocalizations; + + std::vector subContainers; + + /// add selected string to internal storage as high-priority strings + void registerStringOverride(const std::string & modContext, const TextIdentifier & UID, const std::string & localized, const std::string & language); + + std::string getModLanguage(const std::string & modContext); + + // returns true if identifier with such name was registered, even if not translated to current language + bool identifierExists(const TextIdentifier & UID) const; + +public: + /// Loads translation from provided json + /// Any entries loaded by this will have priority over texts registered normally + 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 & 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 translateString(id); + } + + /// converts identifier into user-readable string + 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, bool onlyMissing) const; + + /// Add or override subcontainer which can store identifiers + void addSubContainer(const TextLocalizationContainer & container); + + /// Remove subcontainer with give name + void removeSubContainer(const TextLocalizationContainer & container); + + void jsonSerialize(JsonNode & dest) const; + + template + void serialize(Handler & h) + { + std::lock_guard globalLock(globalTextMutex); + + if (h.version >= Handler::Version::SIMPLE_TEXT_CONTAINER_SERIALIZATION) + { + h & stringsLocalizations; + } + else + { + std::string key; + int64_t sz = stringsLocalizations.size(); + + if (h.version >= Handler::Version::REMOVE_TEXT_CONTAINER_SIZE_T) + { + int64_t size = sz; + h & size; + sz = size; + } + else + { + h & sz; + } + + if(h.saving) + { + for(auto & s : stringsLocalizations) + { + key = s.first; + h & key; + h & s.second; + } + } + else + { + for(size_t i = 0; i < sz; ++i) + { + h & key; + h & stringsLocalizations[key]; + } + } + } + } +}; + +class DLL_LINKAGE TextContainerRegistrable : public TextLocalizationContainer +{ +public: + TextContainerRegistrable(); + ~TextContainerRegistrable(); + + TextContainerRegistrable(const TextContainerRegistrable & other); + TextContainerRegistrable(TextContainerRegistrable && other) noexcept; + + TextContainerRegistrable& operator=(const TextContainerRegistrable & b) = default; +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/TextOperations.cpp b/lib/texts/TextOperations.cpp similarity index 99% rename from lib/TextOperations.cpp rename to lib/texts/TextOperations.cpp index c152be4f1..ed46d9080 100644 --- a/lib/TextOperations.cpp +++ b/lib/texts/TextOperations.cpp @@ -10,7 +10,7 @@ #include "StdInc.h" #include "TextOperations.h" -#include "CGeneralTextHandler.h" +#include "texts/CGeneralTextHandler.h" #include "Languages.h" #include "CConfigHandler.h" diff --git a/lib/TextOperations.h b/lib/texts/TextOperations.h similarity index 97% rename from lib/TextOperations.h rename to lib/texts/TextOperations.h index 8b2b1ecba..f549bc317 100644 --- a/lib/TextOperations.h +++ b/lib/texts/TextOperations.h @@ -11,7 +11,7 @@ VCMI_LIB_NAMESPACE_BEGIN -/// Namespace that provides utilites for unicode support (UTF-8) +/// Namespace that provides utilities for unicode support (UTF-8) namespace TextOperations { /// returns 32-bit UTF codepoint for UTF-8 character symbol @@ -24,7 +24,7 @@ namespace TextOperations size_t DLL_LINKAGE getUnicodeCharacterSize(char firstByte); /// test if character is a valid UTF-8 symbol - /// maxSize - maximum number of bytes this symbol may consist from ( = remainer of string) + /// maxSize - maximum number of bytes this symbol may consist from ( = remainder of string) bool DLL_LINKAGE isValidUnicodeCharacter(const char * character, size_t maxSize); /// returns true if text contains valid ASCII-string @@ -72,8 +72,6 @@ namespace TextOperations DLL_LINKAGE std::string getCurrentFormattedTimeLocal(std::chrono::seconds timeOffset = {}); }; - - template inline std::string TextOperations::formatMetric(Arithmetic number, int maxDigits) { diff --git a/lobby/LobbyServer.cpp b/lobby/LobbyServer.cpp index 2a09e3d02..692a2681b 100644 --- a/lobby/LobbyServer.cpp +++ b/lobby/LobbyServer.cpp @@ -12,11 +12,11 @@ #include "LobbyDatabase.h" -#include "../lib/Languages.h" -#include "../lib/TextOperations.h" #include "../lib/json/JsonFormatException.h" #include "../lib/json/JsonNode.h" #include "../lib/json/JsonUtils.h" +#include "../lib/texts/Languages.h" +#include "../lib/texts/TextOperations.h" #include #include @@ -213,7 +213,7 @@ static JsonNode loadLobbyGameRoomToJson(const LobbyGameRoom & gameRoom) jsonEntry["playerLimit"].Integer() = gameRoom.playerLimit; jsonEntry["ageSeconds"].Integer() = gameRoom.age.count(); if (!gameRoom.modsJson.empty()) // not present in match history - jsonEntry["mods"] = JsonNode(reinterpret_cast(gameRoom.modsJson.data()), gameRoom.modsJson.size()); + jsonEntry["mods"] = JsonNode(reinterpret_cast(gameRoom.modsJson.data()), gameRoom.modsJson.size(), ""); for(const auto & account : gameRoom.participants) jsonEntry["participants"].Vector().push_back(loadLobbyAccountToJson(account)); @@ -348,7 +348,7 @@ JsonNode LobbyServer::parseAndValidateMessage(const std::vector & mes JsonNode json; try { - JsonNode jsonTemp(message.data(), message.size()); + JsonNode jsonTemp(message.data(), message.size(), ""); json = std::move(jsonTemp); } catch (const JsonFormatException & e) @@ -595,7 +595,7 @@ void LobbyServer::receiveClientLogin(const NetworkConnectionPtr & connection, co auto clientCookieStatus = database->getAccountCookieStatus(accountID, accountCookie); if(clientCookieStatus == LobbyCookieStatus::INVALID) - return sendOperationFailed(connection, "Authentification failure"); + return sendOperationFailed(connection, "Authentication failure"); database->updateAccountLoginTime(accountID); database->setAccountOnline(accountID, true); @@ -611,7 +611,7 @@ void LobbyServer::receiveClientLogin(const NetworkConnectionPtr & connection, co sendRecentChatHistory(connection, "global", language); // send active game rooms list to new account - // and update acount list to everybody else including new account + // and update account list to everybody else including new account broadcastActiveAccounts(); sendMessage(connection, prepareActiveGameRooms()); sendMatchesHistory(connection); diff --git a/mapeditor/Animation.cpp b/mapeditor/Animation.cpp index 16019a65a..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; } @@ -599,7 +585,7 @@ void Animation::init() std::unique_ptr textData(new ui8[stream->getSize()]); stream->read(textData.get(), stream->getSize()); - const JsonNode config(reinterpret_cast(textData.get()), stream->getSize()); + const JsonNode config(reinterpret_cast(textData.get()), stream->getSize(), resID.getName()); initFromJson(config); } @@ -711,14 +697,6 @@ void Animation::duplicateImage(const size_t sourceGroup, const size_t sourceFram load(index, targetGroup); } -void Animation::setCustom(std::string filename, size_t frame, size_t group) -{ - if(source[group].size() <= frame) - source[group].resize(frame+1); - source[group][frame]["file"].String() = filename; - //FIXME: update image if already loaded -} - std::shared_ptr Animation::getImage(size_t frame, size_t group, bool verbose) const { auto groupIter = images.find(group); diff --git a/mapeditor/Animation.h b/mapeditor/Animation.h index 2e4556413..253e68981 100644 --- a/mapeditor/Animation.h +++ b/mapeditor/Animation.h @@ -68,9 +68,6 @@ public: // adjust the color of the animation, used in battle spell effects, e.g. Cloned objects - //add custom surface to the selected position. - void setCustom(std::string filename, size_t frame, size_t group = 0); - std::shared_ptr getImage(size_t frame, size_t group = 0, bool verbose = true) const; //all available frames diff --git a/mapeditor/CMakeLists.txt b/mapeditor/CMakeLists.txt index 109d08623..dd1f49493 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 @@ -28,11 +26,16 @@ set(editor_SRCS mapcontroller.cpp validator.cpp inspector/inspector.cpp - inspector/townbulidingswidget.cpp + inspector/townbuildingswidget.cpp + inspector/towneventdialog.cpp + inspector/towneventswidget.cpp + inspector/townspellswidget.cpp inspector/armywidget.cpp inspector/messagewidget.cpp inspector/rewardswidget.cpp inspector/questwidget.cpp + inspector/heroartifactswidget.cpp + inspector/artifactwidget.cpp inspector/heroskillswidget.cpp inspector/herospellwidget.cpp inspector/PickObjectDelegate.cpp @@ -42,8 +45,6 @@ set(editor_SRCS set(editor_HEADERS StdInc.h - launcherdirs.h - jsonutils.h mainwindow.h BitmapHandler.h maphandler.h @@ -69,16 +70,22 @@ set(editor_HEADERS mapcontroller.h validator.h inspector/inspector.h - inspector/townbulidingswidget.h + inspector/townbuildingswidget.h + inspector/towneventdialog.h + inspector/towneventswidget.h + inspector/townspellswidget.h inspector/armywidget.h inspector/messagewidget.h inspector/rewardswidget.h inspector/questwidget.h + inspector/heroartifactswidget.h + inspector/artifactwidget.h inspector/heroskillswidget.h inspector/herospellwidget.h inspector/PickObjectDelegate.h inspector/portraitwidget.h resourceExtractor/ResourceConverter.h + mapeditorroles.h ) set(editor_FORMS @@ -97,11 +104,16 @@ set(editor_FORMS playersettings.ui playerparams.ui validator.ui - inspector/townbulidingswidget.ui + inspector/townbuildingswidget.ui + inspector/towneventdialog.ui + inspector/towneventswidget.ui + inspector/townspellswidget.ui inspector/armywidget.ui inspector/messagewidget.ui inspector/rewardswidget.ui inspector/questwidget.ui + inspector/heroartifactswidget.ui + inspector/artifactwidget.ui inspector/heroskillswidget.ui inspector/herospellwidget.ui inspector/portraitwidget.ui @@ -186,6 +198,7 @@ endif() target_sources(vcmieditor PRIVATE ${editor_SRCS} ${editor_HEADERS} + ${editor_FORMS} ${editor_RESOURCES} ) @@ -210,7 +223,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 73e80b2b3..33dfb8b4d 100644 --- a/mapeditor/graphics.cpp +++ b/mapeditor/graphics.cpp @@ -26,14 +26,14 @@ #include "../lib/CThreadHelper.h" #include "../lib/VCMI_Lib.h" #include "../CCallback.h" -#include "../lib/CGeneralTextHandler.h" +#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; @@ -48,9 +48,9 @@ void Graphics::loadPaletteAndColors() for(int i = 0; i < 256; ++i) { QColor col; - col.setRed(pals[startPoint++]); - col.setGreen(pals[startPoint++]); - col.setBlue(pals[startPoint++]); + col.setRed(std::clamp(static_cast(pals[startPoint++]), 0, 255)); + col.setGreen(std::clamp(static_cast(pals[startPoint++]), 0, 255)); + col.setBlue(std::clamp(static_cast(pals[startPoint++]), 0, 255)); col.setAlpha(255); startPoint++; playerColorPalette[i] = col.rgba(); @@ -124,7 +124,7 @@ void Graphics::load() void Graphics::loadHeroAnimations() { - for(auto & elem : VLC->heroclassesh->objects) + for(const auto & elem : VLC->heroclassesh->objects) { for(auto templ : VLC->objtypeh->getHandlerFor(Obj::HERO, elem->getIndex())->getTemplates()) { diff --git a/mapeditor/icons/document-open-recent.png b/mapeditor/icons/document-open-recent.png new file mode 100644 index 000000000..3c42b2a42 Binary files /dev/null and b/mapeditor/icons/document-open-recent.png differ diff --git a/mapeditor/inspector/armywidget.cpp b/mapeditor/inspector/armywidget.cpp index cadd580f3..efaf0adbc 100644 --- a/mapeditor/inspector/armywidget.cpp +++ b/mapeditor/inspector/armywidget.cpp @@ -36,7 +36,7 @@ ArmyWidget::ArmyWidget(CArmedInstance & a, QWidget *parent) : for(int c = 0; c < VLC->creh->objects.size(); ++c) { - auto creature = VLC->creh->objects[c]; + auto const & creature = VLC->creh->objects[c]; uiSlots[i]->insertItem(c + 1, creature->getNamePluralTranslated().c_str()); uiSlots[i]->setItemData(c + 1, creature->getIndex()); } diff --git a/mapeditor/inspector/artifactwidget.cpp b/mapeditor/inspector/artifactwidget.cpp new file mode 100644 index 000000000..3723eb428 --- /dev/null +++ b/mapeditor/inspector/artifactwidget.cpp @@ -0,0 +1,63 @@ +/* + * herosspellwidget.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 "artifactwidget.h" +#include "ui_artifactwidget.h" +#include "inspector.h" +#include "../../lib/ArtifactUtils.h" +#include "../../lib/constants/StringConstants.h" + +ArtifactWidget::ArtifactWidget(CArtifactFittingSet & fittingSet, QWidget * parent) : + QDialog(parent), + ui(new Ui::ArtifactWidget), + fittingSet(fittingSet) +{ + ui->setupUi(this); + + connect(ui->saveButton, &QPushButton::clicked, this, [this]() + { + emit saveArtifact(ui->artifact->currentData().toInt(), ArtifactPosition(ui->possiblePositions->currentData().toInt())); + close(); + }); + connect(ui->cancelButton, &QPushButton::clicked, this, &ArtifactWidget::close); + connect(ui->possiblePositions, static_cast (&QComboBox::currentIndexChanged), this, &ArtifactWidget::fillArtifacts); + + std::vector possiblePositions; + for(const auto & slot : ArtifactUtils::allWornSlots()) + { + if(fittingSet.isPositionFree(slot)) + { + ui->possiblePositions->addItem(QString::fromStdString(NArtifactPosition::namesHero[slot.num]), slot.num); + } + } + ui->possiblePositions->addItem(QString::fromStdString(NArtifactPosition::backpack), ArtifactPosition::BACKPACK_START); + fillArtifacts(); + + +} + +void ArtifactWidget::fillArtifacts() +{ + ui->artifact->clear(); + auto currentSlot = ui->possiblePositions->currentData().toInt(); + for (const auto& art : VLC->arth->getDefaultAllowed()) + { + auto artifact = art.toArtifact(); + // forbid spell scroll for now as require special handling + if (artifact->canBePutAt(&fittingSet, currentSlot, true) && artifact->getId() != ArtifactID::SPELL_SCROLL) { + ui->artifact->addItem(QString::fromStdString(artifact->getNameTranslated()), QVariant::fromValue(artifact->getIndex())); + } + } +} + +ArtifactWidget::~ArtifactWidget() +{ + delete ui; +} diff --git a/mapeditor/inspector/artifactwidget.h b/mapeditor/inspector/artifactwidget.h new file mode 100644 index 000000000..d2a474240 --- /dev/null +++ b/mapeditor/inspector/artifactwidget.h @@ -0,0 +1,35 @@ +/* + * ArtifactWidget.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 "../../lib/mapObjects/CGHeroInstance.h" + +namespace Ui { +class ArtifactWidget; +} + +class ArtifactWidget : public QDialog +{ + Q_OBJECT + +public: + explicit ArtifactWidget(CArtifactFittingSet & fittingSet, QWidget * parent = nullptr); + ~ArtifactWidget(); + +signals: + void saveArtifact(int32_t artifactIndex, ArtifactPosition slot); + private slots: + void fillArtifacts(); + +private: + Ui::ArtifactWidget * ui; + CArtifactFittingSet & fittingSet; +}; diff --git a/mapeditor/inspector/artifactwidget.ui b/mapeditor/inspector/artifactwidget.ui new file mode 100644 index 000000000..e9c62b0ce --- /dev/null +++ b/mapeditor/inspector/artifactwidget.ui @@ -0,0 +1,92 @@ + + + ArtifactWidget + + + Qt::WindowModal + + + + 0 + 0 + 400 + 150 + + + + + 400 + 150 + + + + + 400 + 150 + + + + Artifact + + + + + 10 + 10 + 381 + 80 + + + + + + + Artifact + + + + + + + + + + + + + Equip where: + + + + + + + + + 190 + 100 + 93 + 28 + + + + Save + + + + + + 290 + 100 + 93 + 28 + + + + Cancel + + + + + + diff --git a/mapeditor/inspector/heroartifactswidget.cpp b/mapeditor/inspector/heroartifactswidget.cpp new file mode 100644 index 000000000..2c1c9d687 --- /dev/null +++ b/mapeditor/inspector/heroartifactswidget.cpp @@ -0,0 +1,144 @@ +/* + * herosspellwidget.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 "artifactwidget.h" +#include "heroartifactswidget.h" +#include "ui_heroartifactswidget.h" +#include "inspector.h" +#include "mapeditorroles.h" +#include "../../lib/ArtifactUtils.h" +#include "../../lib/constants/StringConstants.h" + +HeroArtifactsWidget::HeroArtifactsWidget(CGHeroInstance & h, QWidget * parent) : + QDialog(parent), + ui(new Ui::HeroArtifactsWidget), + hero(h), + fittingSet(CArtifactFittingSet(h)) +{ + ui->setupUi(this); +} + +HeroArtifactsWidget::~HeroArtifactsWidget() +{ + delete ui; +} + +void HeroArtifactsWidget::on_addButton_clicked() +{ + ArtifactWidget artifactWidget{ fittingSet, this }; + connect(&artifactWidget, &ArtifactWidget::saveArtifact, this, &HeroArtifactsWidget::onSaveArtifact); + artifactWidget.exec(); +} + +void HeroArtifactsWidget::on_removeButton_clicked() +{ + auto row = ui->artifacts->currentRow(); + if (row == -1) + { + return; + } + + auto slot = ui->artifacts->item(row, Column::SLOT)->data(MapEditorRoles::ArtifactSlotRole).toInt(); + fittingSet.removeArtifact(ArtifactPosition(slot)); + ui->artifacts->removeRow(row); +} + +void HeroArtifactsWidget::onSaveArtifact(int32_t artifactIndex, ArtifactPosition slot) +{ + auto artifact = ArtifactUtils::createArtifact(VLC->arth->getByIndex(artifactIndex)->getId()); + fittingSet.putArtifact(slot, artifact); + addArtifactToTable(artifactIndex, slot); +} + +void HeroArtifactsWidget::addArtifactToTable(int32_t artifactIndex, ArtifactPosition slot) +{ + auto artifact = VLC->arth->getByIndex(artifactIndex); + auto * itemArtifact = new QTableWidgetItem; + itemArtifact->setText(QString::fromStdString(artifact->getNameTranslated())); + itemArtifact->setData(MapEditorRoles::ArtifactIDRole, QVariant::fromValue(artifact->getIndex())); + + auto * itemSlot = new QTableWidgetItem; + auto slotText = ArtifactUtils::isSlotBackpack(slot) ? NArtifactPosition::backpack : NArtifactPosition::namesHero[slot.num]; + itemSlot->setData(MapEditorRoles::ArtifactSlotRole, QVariant::fromValue(slot.num)); + itemSlot->setText(QString::fromStdString(slotText)); + + ui->artifacts->insertRow(ui->artifacts->rowCount()); + ui->artifacts->setItem(ui->artifacts->rowCount() - 1, Column::ARTIFACT, itemArtifact); + ui->artifacts->setItem(ui->artifacts->rowCount() - 1, Column::SLOT, itemSlot); +} + +void HeroArtifactsWidget::obtainData() +{ + std::vector combinedArtifactsParts; + for (const auto & [artPosition, artSlotInfo] : fittingSet.artifactsWorn) + { + addArtifactToTable(VLC->arth->getById(artSlotInfo.artifact->getTypeId())->getIndex(), artPosition); + } + for (const auto & art : hero.artifactsInBackpack) + { + addArtifactToTable(VLC->arth->getById(art.artifact->getTypeId())->getIndex(), ArtifactPosition::BACKPACK_START); + } +} + +void HeroArtifactsWidget::commitChanges() +{ + while(!hero.artifactsWorn.empty()) + { + hero.removeArtifact(hero.artifactsWorn.begin()->first); + } + + while(!hero.artifactsInBackpack.empty()) + { + hero.removeArtifact(ArtifactPosition::BACKPACK_START + static_cast(hero.artifactsInBackpack.size()) - 1); + } + + for(const auto & [artPosition, artSlotInfo] : fittingSet.artifactsWorn) + { + hero.putArtifact(artPosition, artSlotInfo.artifact); + } + + for(const auto & art : fittingSet.artifactsInBackpack) + { + hero.putArtifact(ArtifactPosition::BACKPACK_START + static_cast(hero.artifactsInBackpack.size()), art.artifact); + } +} + +HeroArtifactsDelegate::HeroArtifactsDelegate(CGHeroInstance & h) : QStyledItemDelegate(), hero(h) +{ +} + +QWidget * HeroArtifactsDelegate::createEditor(QWidget * parent, const QStyleOptionViewItem & option, const QModelIndex & index) const +{ + return new HeroArtifactsWidget(hero, parent); +} + +void HeroArtifactsDelegate::setEditorData(QWidget * editor, const QModelIndex & index) const +{ + if (auto * ed = qobject_cast(editor)) + { + ed->obtainData(); + } + else + { + QStyledItemDelegate::setEditorData(editor, index); + } +} + +void HeroArtifactsDelegate::setModelData(QWidget * editor, QAbstractItemModel * model, const QModelIndex & index) const +{ + if (auto * ed = qobject_cast(editor)) + { + ed->commitChanges(); + } + else + { + QStyledItemDelegate::setModelData(editor, model, index); + } +} diff --git a/mapeditor/inspector/heroartifactswidget.h b/mapeditor/inspector/heroartifactswidget.h new file mode 100644 index 000000000..807a37ce5 --- /dev/null +++ b/mapeditor/inspector/heroartifactswidget.h @@ -0,0 +1,65 @@ +/* + * heroartifactswidget.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 "../../lib/mapObjects/CGHeroInstance.h" + +namespace Ui { +class HeroArtifactsWidget; +} + +class HeroArtifactsWidget : public QDialog +{ + Q_OBJECT + +public: + explicit HeroArtifactsWidget(CGHeroInstance &, QWidget *parent = nullptr); + ~HeroArtifactsWidget(); + + void obtainData(); + void commitChanges(); + +private slots: + void onSaveArtifact(int32_t artifactIndex, ArtifactPosition slot); + + void on_addButton_clicked(); + + void on_removeButton_clicked(); + +private: + enum Column + { + SLOT, ARTIFACT + }; + Ui::HeroArtifactsWidget * ui; + + CGHeroInstance & hero; + CArtifactFittingSet fittingSet; + + void addArtifactToTable(int32_t artifactIndex, ArtifactPosition slot); + +}; + +class HeroArtifactsDelegate : public QStyledItemDelegate +{ + Q_OBJECT +public: + using QStyledItemDelegate::QStyledItemDelegate; + + HeroArtifactsDelegate(CGHeroInstance &); + + QWidget * createEditor(QWidget * parent, const QStyleOptionViewItem & option, const QModelIndex & index) const override; + void setEditorData(QWidget * editor, const QModelIndex & index) const override; + void setModelData(QWidget * editor, QAbstractItemModel * model, const QModelIndex & index) const override; + +private: + CGHeroInstance & hero; +}; diff --git a/mapeditor/inspector/heroartifactswidget.ui b/mapeditor/inspector/heroartifactswidget.ui new file mode 100644 index 000000000..c3d326618 --- /dev/null +++ b/mapeditor/inspector/heroartifactswidget.ui @@ -0,0 +1,144 @@ + + + HeroArtifactsWidget + + + Qt::NonModal + + + + 0 + 0 + 480 + 635 + + + + + 0 + 0 + + + + + 480 + 480 + + + + Artifacts + + + true + + + + 10 + + + 5 + + + + + 0 + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + true + + + + 90 + 0 + + + + Add + + + + + + + true + + + + 90 + 0 + + + + Remove + + + + + + + + + true + + + Qt::ScrollBarAlwaysOff + + + QAbstractScrollArea::AdjustToContents + + + QAbstractItemView::NoEditTriggers + + + QAbstractItemView::SingleSelection + + + QAbstractItemView::SelectRows + + + true + + + 120 + + + true + + + false + + + 26 + + + + Slot + + + + + Artifact + + + + + + + + + diff --git a/mapeditor/inspector/heroskillswidget.cpp b/mapeditor/inspector/heroskillswidget.cpp index d1254e4b8..79b7fd2e5 100644 --- a/mapeditor/inspector/heroskillswidget.cpp +++ b/mapeditor/inspector/heroskillswidget.cpp @@ -34,7 +34,7 @@ HeroSkillsWidget::HeroSkillsWidget(CGHeroInstance & h, QWidget *parent) : ui->labelKnowledge->setText(QString::fromStdString(NPrimarySkill::names[3])); auto * delegate = new InspectorDelegate; - for(auto s : VLC->skillh->objects) + for(auto const & s : VLC->skillh->objects) delegate->options.push_back({QString::fromStdString(s->getNameTranslated()), QVariant::fromValue(s->getId().getNum())}); ui->skills->setItemDelegateForColumn(0, delegate); @@ -67,10 +67,10 @@ void HeroSkillsWidget::on_checkBox_toggled(bool checked) void HeroSkillsWidget::obtainData() { - ui->attack->setValue(hero.getPrimSkillLevel(PrimarySkill::ATTACK)); - ui->defence->setValue(hero.getPrimSkillLevel(PrimarySkill::DEFENSE)); - ui->power->setValue(hero.getPrimSkillLevel(PrimarySkill::SPELL_POWER)); - ui->knowledge->setValue(hero.getPrimSkillLevel(PrimarySkill::KNOWLEDGE)); + ui->attack->setValue(hero.getBasePrimarySkillValue(PrimarySkill::ATTACK)); + ui->defence->setValue(hero.getBasePrimarySkillValue(PrimarySkill::DEFENSE)); + ui->power->setValue(hero.getBasePrimarySkillValue(PrimarySkill::SPELL_POWER)); + ui->knowledge->setValue(hero.getBasePrimarySkillValue(PrimarySkill::KNOWLEDGE)); if(!hero.secSkills.empty() && hero.secSkills.front().first.getNum() == -1) return; diff --git a/mapeditor/inspector/herospellwidget.cpp b/mapeditor/inspector/herospellwidget.cpp index 063bb90ee..6fe3a49ef 100644 --- a/mapeditor/inspector/herospellwidget.cpp +++ b/mapeditor/inspector/herospellwidget.cpp @@ -44,14 +44,16 @@ void HeroSpellWidget::obtainData() void HeroSpellWidget::initSpellLists() { QListWidget * spellLists[] = { ui->spellList1, ui->spellList2, ui->spellList3, ui->spellList4, ui->spellList5 }; - auto spells = VLC->spellh->objects; + for (int i = 0; i < GameConstants::SPELL_LEVELS; i++) { - std::vector> spellsByLevel; - auto getSpellsByLevel = [i](auto spell) { - return spell->getLevel() == i + 1 && !spell->isSpecial() && !spell->isCreatureAbility(); - }; - vstd::copy_if(spells, std::back_inserter(spellsByLevel), getSpellsByLevel); + std::vector spellsByLevel; + for (auto const & spellID : VLC->spellh->getDefaultAllowed()) + { + if (spellID.toSpell()->getLevel() == i + 1) + spellsByLevel.push_back(spellID.toSpell()); + } + spellLists[i]->clear(); for (auto spell : spellsByLevel) { @@ -130,4 +132,4 @@ void HeroSpellDelegate::setModelData(QWidget * editor, QAbstractItemModel * mode { QStyledItemDelegate::setModelData(editor, model, index); } -} \ No newline at end of file +} diff --git a/mapeditor/inspector/inspector.cpp b/mapeditor/inspector/inspector.cpp index b5de5ccf4..3201e46d8 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" @@ -20,11 +21,14 @@ #include "../lib/mapping/CMap.h" #include "../lib/constants/StringConstants.h" -#include "townbulidingswidget.h" +#include "townbuildingswidget.h" +#include "towneventswidget.h" +#include "townspellswidget.h" #include "armywidget.h" #include "messagewidget.h" #include "rewardswidget.h" #include "questwidget.h" +#include "heroartifactswidget.h" #include "heroskillswidget.h" #include "herospellwidget.h" #include "portraitwidget.h" @@ -57,7 +61,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); @@ -88,6 +92,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) @@ -105,7 +123,7 @@ void Initializer::initialize(CGShipyard * o) o->tempOwner = defaultPlayer; } -void Initializer::initialize(CGLighthouse * o) +void Initializer::initialize(FlaggableMapObject * o) { if(!o) return; @@ -136,25 +154,23 @@ void Initializer::initialize(CGHeroInstance * o) o->subID = 0; o->tempOwner = PlayerColor::NEUTRAL; } - + if(o->ID == Obj::HERO) { - for(auto t : VLC->heroh->objects) + 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()); } } @@ -164,17 +180,19 @@ void Initializer::initialize(CGTownInstance * o) const std::vector castleLevels{"village", "fort", "citadel", "castle", "capitol"}; int lvl = vstd::find_pos(castleLevels, o->appearance->stringID); - o->builtBuildings.insert(BuildingID::DEFAULT); - if(lvl > -1) o->builtBuildings.insert(BuildingID::TAVERN); - if(lvl > 0) o->builtBuildings.insert(BuildingID::FORT); - if(lvl > 1) o->builtBuildings.insert(BuildingID::CITADEL); - if(lvl > 2) o->builtBuildings.insert(BuildingID::CASTLE); - if(lvl > 3) o->builtBuildings.insert(BuildingID::CAPITOL); + o->addBuilding(BuildingID::DEFAULT); + if(lvl > -1) o->addBuilding(BuildingID::TAVERN); + if(lvl > 0) o->addBuilding(BuildingID::FORT); + if(lvl > 1) o->addBuilding(BuildingID::CITADEL); + if(lvl > 2) o->addBuilding(BuildingID::CASTLE); + if(lvl > 3) o->addBuilding(BuildingID::CAPITOL); - for(auto 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); + } } } @@ -185,7 +203,7 @@ void Initializer::initialize(CGArtifact * o) if(o->ID == Obj::SPELL_SCROLL) { std::vector out; - for(auto spell : VLC->spellh->objects) //spellh size appears to be greater (?) + for(auto const & spell : VLC->spellh->objects) //spellh size appears to be greater (?) { if(VLC->spellh->getDefaultAllowed().count(spell->id) != 0) { @@ -243,7 +261,7 @@ void Inspector::updateProperties(CGDwelling * o) } } -void Inspector::updateProperties(CGLighthouse * o) +void Inspector::updateProperties(FlaggableMapObject * o) { if(!o) return; @@ -302,7 +320,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; @@ -316,21 +334,31 @@ void Inspector::updateProperties(CGHeroInstance * o) auto * delegate = new HeroSkillsDelegate(*o); addProperty("Skills", PropertyEditorPlaceholder(), delegate, false); addProperty("Spells", PropertyEditorPlaceholder(), new HeroSpellDelegate(*o), false); + addProperty("Artifacts", PropertyEditorPlaceholder(), new HeroArtifactsDelegate(*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); } } @@ -342,6 +370,8 @@ void Inspector::updateProperties(CGTownInstance * o) auto * delegate = new TownBuildingsDelegate(*o); addProperty("Buildings", PropertyEditorPlaceholder(), delegate, false); + addProperty("Spells", PropertyEditorPlaceholder(), new TownSpellsDelegate(*o), false); + addProperty("Events", PropertyEditorPlaceholder(), new TownEventsDelegate(*o, controller), false); } void Inspector::updateProperties(CGArtifact * o) @@ -357,7 +387,7 @@ void Inspector::updateProperties(CGArtifact * o) if(spellId != SpellID::NONE) { auto * delegate = new InspectorDelegate; - for(auto spell : VLC->spellh->objects) + for(auto const & spell : VLC->spellh->objects) { if(controller.map()->allowedSpells.count(spell->id) != 0) delegate->options.push_back({QObject::tr(spell->getNameTranslated().c_str()), QVariant::fromValue(int(spell->getId()))}); @@ -464,14 +494,12 @@ void Inspector::updateProperties() return; table->setRowCount(0); //cleanup table - addProperty("Indentifier", obj); + addProperty("Identifier", obj); addProperty("ID", obj->ID.getNum()); addProperty("SubID", obj->subID); addProperty("InstanceName", obj->instanceName); - addProperty("TypeName", obj->typeName); - addProperty("SubTypeName", obj->subTypeName); - if(!dynamic_cast(obj)) + if(obj->ID != Obj::HERO_PLACEHOLDER && !dynamic_cast(obj)) { auto factory = VLC->objtypeh->getHandlerFor(obj->ID, obj->subID); addProperty("IsStatic", factory->isStaticObject()); @@ -491,7 +519,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); @@ -539,7 +567,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); @@ -552,7 +580,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; } @@ -699,18 +727,22 @@ void Inspector::setProperty(CGHeroInstance * o, const QString & key, const QVari if(key == "Hero type") { - for(auto t : VLC->heroh->objects) + 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 1bb962555..1f03d5601 100644 --- a/mapeditor/inspector/inspector.h +++ b/mapeditor/inspector/inspector.h @@ -17,10 +17,10 @@ #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/CGeneralTextHandler.h" +#include "../lib/texts/CGeneralTextHandler.h" #include "../lib/ResourceSet.h" -#include "../lib/MetaString.h" #define DECLARE_OBJ_TYPE(x) void initialize(x*); #define DECLARE_OBJ_PROPERTY_METHODS(x) \ @@ -49,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); @@ -79,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); @@ -178,4 +178,4 @@ class OwnerDelegate : public InspectorDelegate Q_OBJECT public: OwnerDelegate(MapController & controller, bool addNeutral = true); -}; \ No newline at end of file +}; 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 982138741..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), @@ -48,48 +52,48 @@ QuestWidget::QuestWidget(MapController & _controller, CQuest & _sh, QWidget *par } //fill artifacts - for(int i = 0; i < controller.map()->allowedArtifact.size(); ++i) + for(const auto & artifactPtr : VLC->arth->objects) { - auto * item = new QListWidgetItem(QString::fromStdString(VLC->artifacts()->getByIndex(i)->getNameTranslated())); - item->setData(Qt::UserRole, QVariant::fromValue(i)); + auto artifactIndex = artifactPtr->getIndex(); + auto * item = new QListWidgetItem(QString::fromStdString(artifactPtr->getNameTranslated())); item->setFlags(item->flags() | Qt::ItemIsUserCheckable); item->setCheckState(Qt::Unchecked); - if(controller.map()->allowedArtifact.count(i) == 0) + if(controller.map()->allowedArtifact.count(artifactIndex) == 0) item->setFlags(item->flags() & ~Qt::ItemIsEnabled); ui->lArtifacts->addItem(item); } //fill spells - for(int i = 0; i < controller.map()->allowedSpells.size(); ++i) + for(const auto & spellPtr : VLC->spellh->objects) { - auto * item = new QListWidgetItem(QString::fromStdString(VLC->spells()->getByIndex(i)->getNameTranslated())); - item->setData(Qt::UserRole, QVariant::fromValue(i)); + auto spellIndex = spellPtr->getIndex(); + auto * item = new QListWidgetItem(QString::fromStdString(spellPtr->getNameTranslated())); item->setFlags(item->flags() | Qt::ItemIsUserCheckable); item->setCheckState(Qt::Unchecked); - if(controller.map()->allowedSpells.count(i) == 0) + if(controller.map()->allowedSpells.count(spellIndex) == 0) item->setFlags(item->flags() & ~Qt::ItemIsEnabled); ui->lSpells->addItem(item); } //fill skills - ui->lSkills->setRowCount(controller.map()->allowedAbilities.size()); - for(int i = 0; i < controller.map()->allowedAbilities.size(); ++i) + ui->lSkills->setRowCount(VLC->skillh->objects.size()); + for(const auto & skillPtr : VLC->skillh->objects) { - auto * item = new QTableWidgetItem(QString::fromStdString(VLC->skills()->getByIndex(i)->getNameTranslated())); - item->setData(Qt::UserRole, QVariant::fromValue(i)); + auto skillIndex = skillPtr->getIndex(); + auto * item = new QTableWidgetItem(QString::fromStdString(skillPtr->getNameTranslated())); auto * widget = new QComboBox; - for(auto & s : NSecondarySkill::levels) + for(const auto & s : NSecondarySkill::levels) widget->addItem(QString::fromStdString(s)); - if(controller.map()->allowedAbilities.count(i) == 0) + if(controller.map()->allowedAbilities.count(skillIndex) == 0) { item->setFlags(item->flags() & ~Qt::ItemIsEnabled); widget->setEnabled(false); } - ui->lSkills->setItem(i, 0, item); - ui->lSkills->setCellWidget(i, 1, widget); + ui->lSkills->setItem(skillIndex, 0, item); + ui->lSkills->setCellWidget(skillIndex, 1, widget); } //fill creatures @@ -156,7 +160,7 @@ void QuestWidget::obtainData() for(auto i : quest.mission.artifacts) ui->lArtifacts->item(VLC->artifacts()->getById(i)->getIndex())->setCheckState(Qt::Checked); for(auto i : quest.mission.spells) - ui->lArtifacts->item(VLC->spells()->getById(i)->getIndex())->setCheckState(Qt::Checked); + ui->lSpells->item(VLC->spells()->getById(i)->getIndex())->setCheckState(Qt::Checked); for(auto & i : quest.mission.secondary) { int index = VLC->skills()->getById(i.first)->getIndex(); @@ -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 new file mode 100644 index 000000000..4bf95c5c8 --- /dev/null +++ b/mapeditor/inspector/townbuildingswidget.cpp @@ -0,0 +1,363 @@ +/* + * townbuildingswidget.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 "townbuildingswidget.h" +#include "ui_townbuildingswidget.h" +#include "mapeditorroles.h" +#include "../lib/entities/building/CBuilding.h" +#include "../lib/entities/faction/CTownHandler.h" +#include "../lib/texts/CGeneralTextHandler.h" + +std::string defaultBuildingIdConversion(BuildingID bId) +{ + switch(bId) + { + case BuildingID::DEFAULT: return "DEFAULT"; + case BuildingID::MAGES_GUILD_1: return "MAGES_GUILD_1"; + case BuildingID::MAGES_GUILD_2: return "MAGES_GUILD_2"; + case BuildingID::MAGES_GUILD_3: return "MAGES_GUILD_3"; + case BuildingID::MAGES_GUILD_4: return "MAGES_GUILD_4"; + case BuildingID::MAGES_GUILD_5: return "MAGES_GUILD_5"; + case BuildingID::TAVERN: return "TAVERN"; + case BuildingID::SHIPYARD: return "SHIPYARD"; + case BuildingID::FORT: return "FORT"; + case BuildingID::CITADEL: return "CITADEL"; + case BuildingID::CASTLE: return "CASTLE"; + case BuildingID::VILLAGE_HALL: return "VILLAGE_HALL"; + case BuildingID::TOWN_HALL: return "TOWN_HALL"; + case BuildingID::CITY_HALL: return "CITY_HALL"; + case BuildingID::CAPITOL: return "CAPITOL"; + case BuildingID::MARKETPLACE: return "MARKETPLACE"; + case BuildingID::RESOURCE_SILO: return "RESOURCE_SILO"; + case BuildingID::BLACKSMITH: return "BLACKSMITH"; + case BuildingID::SPECIAL_1: return "SPECIAL_1"; + case BuildingID::SPECIAL_2: return "SPECIAL_2"; + case BuildingID::SPECIAL_3: return "SPECIAL_3"; + case BuildingID::SPECIAL_4: return "SPECIAL_4"; + case BuildingID::HORDE_1: return "HORDE_1"; + case BuildingID::HORDE_1_UPGR: return "HORDE_1_UPGR"; + case BuildingID::HORDE_2: return "HORDE_2"; + case BuildingID::HORDE_2_UPGR: return "HORDE_2_UPGR"; + case BuildingID::SHIP: return "SHIP"; + case BuildingID::GRAIL: return "GRAIL"; + case BuildingID::EXTRA_TOWN_HALL: return "EXTRA_TOWN_HALL"; + case BuildingID::EXTRA_CITY_HALL: return "EXTRA_CITY_HALL"; + case BuildingID::EXTRA_CAPITOL: return "EXTRA_CAPITOL"; + case BuildingID::DWELL_LVL_1: return "DWELL_LVL_1"; + case BuildingID::DWELL_LVL_2: return "DWELL_LVL_2"; + case BuildingID::DWELL_LVL_3: return "DWELL_LVL_3"; + case BuildingID::DWELL_LVL_4: return "DWELL_LVL_4"; + case BuildingID::DWELL_LVL_5: return "DWELL_LVL_5"; + case BuildingID::DWELL_LVL_6: return "DWELL_LVL_6"; + case BuildingID::DWELL_LVL_7: return "DWELL_LVL_7"; + case BuildingID::DWELL_LVL_8: return "DWELL_LVL_8"; + case BuildingID::DWELL_LVL_1_UP: return "DWELL_LVL_1_UP"; + case BuildingID::DWELL_LVL_2_UP: return "DWELL_LVL_2_UP"; + case BuildingID::DWELL_LVL_3_UP: return "DWELL_LVL_3_UP"; + case BuildingID::DWELL_LVL_4_UP: return "DWELL_LVL_4_UP"; + case BuildingID::DWELL_LVL_5_UP: return "DWELL_LVL_5_UP"; + case BuildingID::DWELL_LVL_6_UP: return "DWELL_LVL_6_UP"; + case BuildingID::DWELL_LVL_7_UP: return "DWELL_LVL_7_UP"; + case BuildingID::DWELL_LVL_8_UP: return "DWELL_LVL_8_UP"; + default: + return "UNKNOWN"; + } +} + +QStandardItem * getBuildingParentFromTreeModel(const CBuilding * building, const QStandardItemModel & model) +{ + QStandardItem * parent = nullptr; + std::vector stack(1); + do + { + auto pindex = stack.back(); + stack.pop_back(); + auto rowCount = model.rowCount(pindex); + for (int i = 0; i < rowCount; ++i) + { + QModelIndex index = model.index(i, 0, pindex); + if (building->upgrade.getNum() == model.itemFromIndex(index)->data(MapEditorRoles::BuildingIDRole).toInt()) + { + parent = model.itemFromIndex(index); + break; + } + if (model.hasChildren(index)) + stack.push_back(index); + } + } while(!parent && !stack.empty()); + return parent; +} + +QVariantList getBuildingVariantsFromModel(const QStandardItemModel & model, int modelColumn, Qt::CheckState checkState) +{ + QVariantList result; + std::vector stack(1); + do + { + auto pindex = stack.back(); + stack.pop_back(); + auto rowCount = model.rowCount(pindex); + for (int i = 0; i < rowCount; ++i) + { + QModelIndex index = model.index(i, modelColumn, pindex); + auto * item = model.itemFromIndex(index); + if(item && item->checkState() == checkState) + result.push_back(item->data(MapEditorRoles::BuildingIDRole)); + index = model.index(i, 0, pindex); + if (model.hasChildren(index)) + stack.push_back(index); + } + } while(!stack.empty()); + + return result; +} + + + +TownBuildingsWidget::TownBuildingsWidget(CGTownInstance & t, QWidget *parent) : + town(t), + QDialog(parent), + ui(new Ui::TownBuildingsWidget) +{ + ui->setupUi(this); + ui->treeView->setModel(&model); + //ui->treeView->setColumnCount(3); + model.setHorizontalHeaderLabels(QStringList() << tr("Type") << tr("Enabled") << tr("Built")); + connect(&model, &QStandardItemModel::itemChanged, this, &TownBuildingsWidget::onItemChanged); + //setAttribute(Qt::WA_DeleteOnClose); +} + +TownBuildingsWidget::~TownBuildingsWidget() +{ + delete ui; +} + +QStandardItem * TownBuildingsWidget::addBuilding(const CTown & ctown, int bId, std::set & remaining) +{ + BuildingID buildingId(bId); + const CBuilding * building = ctown.buildings.at(buildingId); + if(!building) + { + remaining.erase(bId); + return nullptr; + } + + QString name = QString::fromStdString(building->getNameTranslated()); + + if(name.isEmpty()) + name = QString::fromStdString(defaultBuildingIdConversion(buildingId)); + + QList checks; + + checks << new QStandardItem(name); + checks.back()->setData(bId, MapEditorRoles::BuildingIDRole); + + checks << new QStandardItem; + checks.back()->setCheckable(true); + checks.back()->setCheckState(town.forbiddenBuildings.count(buildingId) ? Qt::Unchecked : Qt::Checked); + checks.back()->setData(bId, MapEditorRoles::BuildingIDRole); + + checks << new QStandardItem; + checks.back()->setCheckable(true); + checks.back()->setCheckState(town.hasBuilt(buildingId) ? Qt::Checked : Qt::Unchecked); + checks.back()->setData(bId, MapEditorRoles::BuildingIDRole); + + if(building->getBase() == buildingId) + { + model.appendRow(checks); + } + else + { + QStandardItem * parent = getBuildingParentFromTreeModel(building, model); + + if(!parent) + parent = addBuilding(ctown, building->upgrade.getNum(), remaining); + + if(!parent) + { + remaining.erase(bId); + return nullptr; + } + + parent->appendRow(checks); + } + + remaining.erase(bId); + return checks.front(); +} + +void TownBuildingsWidget::addBuildings(const CTown & ctown) +{ + auto buildings = ctown.getAllBuildings(); + while(!buildings.empty()) + { + addBuilding(ctown, *buildings.begin(), buildings); + } + ui->treeView->resizeColumnToContents(0); + ui->treeView->resizeColumnToContents(1); + ui->treeView->resizeColumnToContents(2); +} + +std::set TownBuildingsWidget::getBuildingsFromModel(int modelColumn, Qt::CheckState checkState) +{ + auto buildingVariants = getBuildingVariantsFromModel(model, modelColumn, checkState); + std::set result; + for (const auto & buildingId : buildingVariants) + { + result.insert(buildingId.toInt()); + } + return result; +} + +std::set TownBuildingsWidget::getForbiddenBuildings() +{ + return getBuildingsFromModel(Column::ENABLED, Qt::Unchecked); +} + +std::set TownBuildingsWidget::getBuiltBuildings() +{ + return getBuildingsFromModel(Column::BUILT, Qt::Checked); +} + +void TownBuildingsWidget::on_treeView_expanded(const QModelIndex &index) +{ + ui->treeView->resizeColumnToContents(0); +} + +void TownBuildingsWidget::on_treeView_collapsed(const QModelIndex &index) +{ + ui->treeView->resizeColumnToContents(0); +} + +void TownBuildingsWidget::on_buildAll_clicked() +{ + setAllRowsColumnCheckState(Column::BUILT, Qt::Checked); +} + +void TownBuildingsWidget::on_demolishAll_clicked() +{ + setAllRowsColumnCheckState(Column::BUILT, Qt::Unchecked); +} + +void TownBuildingsWidget::on_enableAll_clicked() +{ + setAllRowsColumnCheckState(Column::ENABLED, Qt::Checked); +} + +void TownBuildingsWidget::on_disableAll_clicked() +{ + setAllRowsColumnCheckState(Column::ENABLED, Qt::Unchecked); +} + + +void TownBuildingsWidget::setRowColumnCheckState(const QStandardItem * item, Column column, Qt::CheckState checkState) { + auto sibling = item->model()->sibling(item->row(), column, item->index()); + model.itemFromIndex(sibling)->setCheckState(checkState); +} + +void TownBuildingsWidget::setAllRowsColumnCheckState(Column column, Qt::CheckState checkState) +{ + std::vector stack(1); + do + { + auto parentIndex = stack.back(); + stack.pop_back(); + auto rowCount = model.rowCount(parentIndex); + for (int i = 0; i < rowCount; ++i) + { + QModelIndex index = model.index(i, column, parentIndex); + if (auto* item = model.itemFromIndex(index)) + item->setCheckState(checkState); + index = model.index(i, 0, parentIndex); + if (model.hasChildren(index)) + stack.push_back(index); + } + } while(!stack.empty()); +} + +void TownBuildingsWidget::onItemChanged(const QStandardItem * item) { + disconnect(&model, &QStandardItemModel::itemChanged, this, &TownBuildingsWidget::onItemChanged); + auto rowFirstColumnIndex = item->model()->sibling(item->row(), Column::TYPE, item->index()); + QStandardItem * nextRow = model.itemFromIndex(rowFirstColumnIndex); + if (item->checkState() == Qt::Checked) { + while (nextRow) { + setRowColumnCheckState(nextRow, Column(item->column()), Qt::Checked); + if (item->column() == Column::BUILT) { + setRowColumnCheckState(nextRow, Column::ENABLED, Qt::Checked); + } + nextRow = nextRow->parent(); + + } + } + else if (item->checkState() == Qt::Unchecked) { + std::vector stack; + stack.push_back(nextRow); + do + { + nextRow = stack.back(); + stack.pop_back(); + setRowColumnCheckState(nextRow, Column(item->column()), Qt::Unchecked); + if (item->column() == Column::ENABLED) { + setRowColumnCheckState(nextRow, Column::BUILT, Qt::Unchecked); + } + if (nextRow->hasChildren()) { + for (int i = 0; i < nextRow->rowCount(); ++i) { + stack.push_back(nextRow->child(i, Column::TYPE)); + } + } + + } while(!stack.empty()); + } + connect(&model, &QStandardItemModel::itemChanged, this, &TownBuildingsWidget::onItemChanged); +} + +TownBuildingsDelegate::TownBuildingsDelegate(CGTownInstance & t): town(t), QStyledItemDelegate() +{ +} + +QWidget * TownBuildingsDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const +{ + return new TownBuildingsWidget(town, parent); +} + +void TownBuildingsDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const +{ + if(auto * ed = qobject_cast(editor)) + { + auto * ctown = town.getTown(); + if(!ctown) + ctown = VLC->townh->randomTown; + if(!ctown) + throw std::runtime_error("No Town defined for type selected"); + + ed->addBuildings(*ctown); + } + else + { + QStyledItemDelegate::setEditorData(editor, index); + } +} + +void TownBuildingsDelegate::setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const +{ + if(auto * ed = qobject_cast(editor)) + { + town.forbiddenBuildings = ed->getForbiddenBuildings(); + town.removeAllBuildings(); + for(const auto & building : ed->getBuiltBuildings()) + town.addBuilding(building); + } + else + { + QStyledItemDelegate::setModelData(editor, model, index); + } +} + + diff --git a/mapeditor/inspector/townbulidingswidget.h b/mapeditor/inspector/townbuildingswidget.h similarity index 64% rename from mapeditor/inspector/townbulidingswidget.h rename to mapeditor/inspector/townbuildingswidget.h index 81f441a98..2b3cf1dd2 100644 --- a/mapeditor/inspector/townbulidingswidget.h +++ b/mapeditor/inspector/townbuildingswidget.h @@ -14,21 +14,29 @@ #include "../lib/mapObjects/CGTownInstance.h" namespace Ui { -class TownBulidingsWidget; +class TownBuildingsWidget; } std::string defaultBuildingIdConversion(BuildingID bId); -class TownBulidingsWidget : public QDialog +QStandardItem * getBuildingParentFromTreeModel(const CBuilding * building, const QStandardItemModel & model); + +QVariantList getBuildingVariantsFromModel(const QStandardItemModel & model, int modelColumn, Qt::CheckState checkState); + +class TownBuildingsWidget : public QDialog { Q_OBJECT QStandardItem * addBuilding(const CTown & ctown, int bId, std::set & remaining); public: - explicit TownBulidingsWidget(CGTownInstance &, QWidget *parent = nullptr); - ~TownBulidingsWidget(); - + enum Column + { + TYPE, ENABLED, BUILT + }; + explicit TownBuildingsWidget(CGTownInstance &, QWidget *parent = nullptr); + ~TownBuildingsWidget(); + void addBuildings(const CTown & ctown); std::set getForbiddenBuildings(); std::set getBuiltBuildings(); @@ -38,10 +46,22 @@ private slots: void on_treeView_collapsed(const QModelIndex &index); + void on_buildAll_clicked(); + + void on_demolishAll_clicked(); + + void on_enableAll_clicked(); + + void on_disableAll_clicked(); + + void onItemChanged(const QStandardItem * item); + private: std::set getBuildingsFromModel(int modelColumn, Qt::CheckState checkState); - - Ui::TownBulidingsWidget *ui; + void setRowColumnCheckState(const QStandardItem * item, Column column, Qt::CheckState checkState); + void setAllRowsColumnCheckState(Column column, Qt::CheckState checkState); + + Ui::TownBuildingsWidget *ui; CGTownInstance & town; mutable QStandardItemModel model; }; diff --git a/mapeditor/inspector/townbulidingswidget.ui b/mapeditor/inspector/townbuildingswidget.ui similarity index 55% rename from mapeditor/inspector/townbulidingswidget.ui rename to mapeditor/inspector/townbuildingswidget.ui index f9eece29d..472093960 100644 --- a/mapeditor/inspector/townbulidingswidget.ui +++ b/mapeditor/inspector/townbuildingswidget.ui @@ -1,7 +1,7 @@ - TownBulidingsWidget - + TownBuildingsWidget + Qt::NonModal @@ -9,7 +9,7 @@ 0 0 - 480 + 580 280 @@ -21,7 +21,7 @@ - 480 + 580 280 @@ -45,6 +45,38 @@ + + + + + + Build all + + + + + + + Demolish all + + + + + + + Enable all + + + + + + + Disable all + + + + + diff --git a/mapeditor/inspector/townbulidingswidget.cpp b/mapeditor/inspector/townbulidingswidget.cpp deleted file mode 100644 index b4f4c87d3..000000000 --- a/mapeditor/inspector/townbulidingswidget.cpp +++ /dev/null @@ -1,256 +0,0 @@ -/* - * townbuildingswidget.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 "townbulidingswidget.h" -#include "ui_townbulidingswidget.h" -#include "../lib/CGeneralTextHandler.h" - -std::string defaultBuildingIdConversion(BuildingID bId) -{ - switch(bId) - { - case BuildingID::DEFAULT: return "DEFAULT"; - case BuildingID::MAGES_GUILD_1: return "MAGES_GUILD_1"; - case BuildingID::MAGES_GUILD_2: return "MAGES_GUILD_2"; - case BuildingID::MAGES_GUILD_3: return "MAGES_GUILD_3"; - case BuildingID::MAGES_GUILD_4: return "MAGES_GUILD_4"; - case BuildingID::MAGES_GUILD_5: return "MAGES_GUILD_5"; - case BuildingID::TAVERN: return "TAVERN"; - case BuildingID::SHIPYARD: return "SHIPYARD"; - case BuildingID::FORT: return "FORT"; - case BuildingID::CITADEL: return "CITADEL"; - case BuildingID::CASTLE: return "CASTLE"; - case BuildingID::VILLAGE_HALL: return "VILLAGE_HALL"; - case BuildingID::TOWN_HALL: return "TOWN_HALL"; - case BuildingID::CITY_HALL: return "CITY_HALL"; - case BuildingID::CAPITOL: return "CAPITOL"; - case BuildingID::MARKETPLACE: return "MARKETPLACE"; - case BuildingID::RESOURCE_SILO: return "RESOURCE_SILO"; - case BuildingID::BLACKSMITH: return "BLACKSMITH"; - case BuildingID::SPECIAL_1: return "SPECIAL_1"; - case BuildingID::SPECIAL_2: return "SPECIAL_2"; - case BuildingID::SPECIAL_3: return "SPECIAL_3"; - case BuildingID::SPECIAL_4: return "SPECIAL_4"; - case BuildingID::HORDE_1: return "HORDE_1"; - case BuildingID::HORDE_1_UPGR: return "HORDE_1_UPGR"; - case BuildingID::HORDE_2: return "HORDE_2"; - case BuildingID::HORDE_2_UPGR: return "HORDE_2_UPGR"; - case BuildingID::SHIP: return "SHIP"; - case BuildingID::GRAIL: return "GRAIL"; - case BuildingID::EXTRA_TOWN_HALL: return "EXTRA_TOWN_HALL"; - case BuildingID::EXTRA_CITY_HALL: return "EXTRA_CITY_HALL"; - case BuildingID::EXTRA_CAPITOL: return "EXTRA_CAPITOL"; - case BuildingID::DWELL_LVL_1: return "DWELL_LVL_1"; - case BuildingID::DWELL_LVL_2: return "DWELL_LVL_2"; - case BuildingID::DWELL_LVL_3: return "DWELL_LVL_3"; - case BuildingID::DWELL_LVL_4: return "DWELL_LVL_4"; - case BuildingID::DWELL_LVL_5: return "DWELL_LVL_5"; - case BuildingID::DWELL_LVL_6: return "DWELL_LVL_6"; - case BuildingID::DWELL_LVL_7: return "DWELL_LVL_7"; - case BuildingID::DWELL_LVL_1_UP: return "DWELL_LVL_1_UP"; - case BuildingID::DWELL_LVL_2_UP: return "DWELL_LVL_2_UP"; - case BuildingID::DWELL_LVL_3_UP: return "DWELL_LVL_3_UP"; - case BuildingID::DWELL_LVL_4_UP: return "DWELL_LVL_4_UP"; - case BuildingID::DWELL_LVL_5_UP: return "DWELL_LVL_5_UP"; - case BuildingID::DWELL_LVL_6_UP: return "DWELL_LVL_6_UP"; - case BuildingID::DWELL_LVL_7_UP: return "DWELL_LVL_7_UP"; - default: - return "UNKNOWN"; - } -} - -TownBulidingsWidget::TownBulidingsWidget(CGTownInstance & t, QWidget *parent) : - town(t), - QDialog(parent), - ui(new Ui::TownBulidingsWidget) -{ - ui->setupUi(this); - ui->treeView->setModel(&model); - //ui->treeView->setColumnCount(3); - model.setHorizontalHeaderLabels(QStringList() << QStringLiteral("Type") << QStringLiteral("Enabled") << QStringLiteral("Built")); - - //setAttribute(Qt::WA_DeleteOnClose); -} - -TownBulidingsWidget::~TownBulidingsWidget() -{ - delete ui; -} - -QStandardItem * TownBulidingsWidget::addBuilding(const CTown & ctown, int bId, std::set & remaining) -{ - BuildingID buildingId(bId); - const CBuilding * building = ctown.buildings.at(buildingId); - if(!building) - { - remaining.erase(bId); - return nullptr; - } - - QString name = tr(building->getNameTranslated().c_str()); - - if(name.isEmpty()) - name = QString::fromStdString(defaultBuildingIdConversion(buildingId)); - - QList checks; - - checks << new QStandardItem(name); - checks.back()->setData(bId, Qt::UserRole); - - checks << new QStandardItem; - checks.back()->setCheckable(true); - checks.back()->setCheckState(town.forbiddenBuildings.count(buildingId) ? Qt::Unchecked : Qt::Checked); - checks.back()->setData(bId, Qt::UserRole); - - checks << new QStandardItem; - checks.back()->setCheckable(true); - checks.back()->setCheckState(town.builtBuildings.count(buildingId) ? Qt::Checked : Qt::Unchecked); - checks.back()->setData(bId, Qt::UserRole); - - if(building->getBase() == buildingId) - { - model.appendRow(checks); - } - else - { - QStandardItem * parent = nullptr; - std::vector stack; - stack.push_back(QModelIndex()); - while(!parent && !stack.empty()) - { - auto pindex = stack.back(); - stack.pop_back(); - for(int i = 0; i < model.rowCount(pindex); ++i) - { - QModelIndex index = model.index(i, 0, pindex); - if(building->upgrade.getNum() == model.itemFromIndex(index)->data(Qt::UserRole).toInt()) - { - parent = model.itemFromIndex(index); - break; - } - if(model.hasChildren(index)) - stack.push_back(index); - } - } - - if(!parent) - parent = addBuilding(ctown, building->upgrade.getNum(), remaining); - - if(!parent) - { - remaining.erase(bId); - return nullptr; - } - - parent->appendRow(checks); - } - - remaining.erase(bId); - return checks.front(); -} - -void TownBulidingsWidget::addBuildings(const CTown & ctown) -{ - auto buildings = ctown.getAllBuildings(); - while(!buildings.empty()) - { - addBuilding(ctown, *buildings.begin(), buildings); - } - ui->treeView->resizeColumnToContents(0); - ui->treeView->resizeColumnToContents(1); - ui->treeView->resizeColumnToContents(2); -} - -std::set TownBulidingsWidget::getBuildingsFromModel(int modelColumn, Qt::CheckState checkState) -{ - std::set result; - std::vector stack; - stack.push_back(QModelIndex()); - while(!stack.empty()) - { - auto pindex = stack.back(); - stack.pop_back(); - for(int i = 0; i < model.rowCount(pindex); ++i) - { - QModelIndex index = model.index(i, modelColumn, pindex); - if(auto * item = model.itemFromIndex(index)) - if(item->checkState() == checkState) - result.emplace(item->data(Qt::UserRole).toInt()); - index = model.index(i, 0, pindex); //children are linked to first column of the model - if(model.hasChildren(index)) - stack.push_back(index); - } - } - - return result; -} - -std::set TownBulidingsWidget::getForbiddenBuildings() -{ - return getBuildingsFromModel(1, Qt::Unchecked); -} - -std::set TownBulidingsWidget::getBuiltBuildings() -{ - return getBuildingsFromModel(2, Qt::Checked); -} - -void TownBulidingsWidget::on_treeView_expanded(const QModelIndex &index) -{ - ui->treeView->resizeColumnToContents(0); -} - -void TownBulidingsWidget::on_treeView_collapsed(const QModelIndex &index) -{ - ui->treeView->resizeColumnToContents(0); -} - - -TownBuildingsDelegate::TownBuildingsDelegate(CGTownInstance & t): town(t), QStyledItemDelegate() -{ -} - -QWidget * TownBuildingsDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const -{ - return new TownBulidingsWidget(town, parent); -} - -void TownBuildingsDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const -{ - if(auto * ed = qobject_cast(editor)) - { - auto * ctown = town.town; - if(!ctown) - ctown = VLC->townh->randomTown; - if(!ctown) - throw std::runtime_error("No Town defined for type selected"); - - ed->addBuildings(*ctown); - } - else - { - QStyledItemDelegate::setEditorData(editor, index); - } -} - -void TownBuildingsDelegate::setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const -{ - if(auto * ed = qobject_cast(editor)) - { - town.forbiddenBuildings = ed->getForbiddenBuildings(); - town.builtBuildings = ed->getBuiltBuildings(); - } - else - { - QStyledItemDelegate::setModelData(editor, model, index); - } -} - - diff --git a/mapeditor/inspector/towneventdialog.cpp b/mapeditor/inspector/towneventdialog.cpp new file mode 100644 index 000000000..475c42edc --- /dev/null +++ b/mapeditor/inspector/towneventdialog.cpp @@ -0,0 +1,296 @@ +/* + * towneventdialog.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 "townbuildingswidget.h" +#include "towneventdialog.h" +#include "ui_towneventdialog.h" +#include "mapeditorroles.h" +#include "../mapsettings/eventsettings.h" +#include "../../lib/entities/building/CBuilding.h" +#include "../../lib/entities/faction/CTownHandler.h" +#include "../../lib/constants/NumericConstants.h" +#include "../../lib/constants/StringConstants.h" + +static const int FIRST_DAY_FOR_EVENT = 1; +static const int LAST_DAY_FOR_EVENT = 999; +static const int MAXIMUM_EVENT_REPEAT_AFTER = 999; + +static const int MAXIMUM_GOLD_CHANGE = 999999; +static const int MAXIMUM_RESOURCE_CHANGE = 999; +static const int GOLD_STEP = 100; +static const int RESOURCE_STEP = 1; + +static const int MAXIMUM_CREATURES_CHANGE = 999999; + +TownEventDialog::TownEventDialog(CGTownInstance & t, QListWidgetItem * item, QWidget * parent) : + QDialog(parent), + ui(new Ui::TownEventDialog), + town(t), + townEventListItem(item) +{ + ui->setupUi(this); + + ui->buildingsTree->setModel(&buildingsModel); + + params = townEventListItem->data(MapEditorRoles::TownEventRole).toMap(); + ui->eventFirstOccurrence->setMinimum(FIRST_DAY_FOR_EVENT); + ui->eventFirstOccurrence->setMaximum(LAST_DAY_FOR_EVENT); + ui->eventRepeatAfter->setMaximum(MAXIMUM_EVENT_REPEAT_AFTER); + ui->eventNameText->setText(params.value("name").toString()); + 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()+1); + ui->eventRepeatAfter->setValue(params.value("nextOccurrence").toInt()); + + initPlayers(); + initResources(); + initBuildings(); + initCreatures(); +} + +TownEventDialog::~TownEventDialog() +{ + delete ui; +} + +void TownEventDialog::initPlayers() +{ + auto playerList = params.value("players").toList(); + for (int i = 0; i < PlayerColor::PLAYER_LIMIT_I; ++i) + { + bool isAffected = playerList.contains(toQString(PlayerColor(i))); + auto * item = new QListWidgetItem(QString::fromStdString(GameConstants::PLAYER_COLOR_NAMES[i])); + item->setData(MapEditorRoles::PlayerIDRole, QVariant::fromValue(i)); + item->setCheckState(isAffected ? Qt::Checked : Qt::Unchecked); + ui->playersAffected->addItem(item); + } +} + +void TownEventDialog::initResources() +{ + ui->resourcesTable->setRowCount(GameConstants::RESOURCE_QUANTITY); + auto resourcesMap = params.value("resources").toMap(); + for (int i = 0; i < GameConstants::RESOURCE_QUANTITY; ++i) + { + auto name = QString::fromStdString(GameConstants::RESOURCE_NAMES[i]); + auto * item = new QTableWidgetItem(); + item->setFlags(item->flags() & ~Qt::ItemIsEditable); + item->setText(name); + ui->resourcesTable->setItem(i, 0, item); + + int val = resourcesMap.value(name).toInt(); + auto * edit = new QSpinBox(ui->resourcesTable); + edit->setMaximum(i == GameResID::GOLD ? MAXIMUM_GOLD_CHANGE : MAXIMUM_RESOURCE_CHANGE); + edit->setMinimum(i == GameResID::GOLD ? -MAXIMUM_GOLD_CHANGE : -MAXIMUM_RESOURCE_CHANGE); + edit->setSingleStep(i == GameResID::GOLD ? GOLD_STEP : RESOURCE_STEP); + edit->setValue(val); + + ui->resourcesTable->setCellWidget(i, 1, edit); + } +} + +void TownEventDialog::initBuildings() +{ + auto * ctown = town.getTown(); + if (!ctown) + ctown = VLC->townh->randomTown; + if (!ctown) + throw std::runtime_error("No Town defined for type selected"); + auto allBuildings = ctown->getAllBuildings(); + while (!allBuildings.empty()) + { + addBuilding(*ctown, *allBuildings.begin(), allBuildings); + } + ui->buildingsTree->resizeColumnToContents(0); + + connect(&buildingsModel, &QStandardItemModel::itemChanged, this, &TownEventDialog::onItemChanged); +} + +QStandardItem * TownEventDialog::addBuilding(const CTown& ctown, BuildingID buildingId, std::set& remaining) +{ + auto bId = buildingId.num; + const CBuilding * building = ctown.buildings.at(buildingId); + + QString name = QString::fromStdString(building->getNameTranslated()); + + if (name.isEmpty()) + name = QString::fromStdString(defaultBuildingIdConversion(buildingId)); + + QList checks; + + checks << new QStandardItem(name); + checks.back()->setData(bId, MapEditorRoles::BuildingIDRole); + + checks << new QStandardItem; + checks.back()->setCheckable(true); + checks.back()->setCheckState(params["buildings"].toList().contains(bId) ? Qt::Checked : Qt::Unchecked); + checks.back()->setData(bId, MapEditorRoles::BuildingIDRole); + + if (building->getBase() == buildingId) + { + buildingsModel.appendRow(checks); + } + else + { + QStandardItem * parent = getBuildingParentFromTreeModel(building, buildingsModel); + + if (!parent) + parent = addBuilding(ctown, building->upgrade.getNum(), remaining); + + parent->appendRow(checks); + } + + remaining.erase(bId); + return checks.front(); +} + +void TownEventDialog::initCreatures() +{ + auto creatures = params.value("creatures").toList(); + auto * ctown = town.getTown(); + if (!ctown) + ui->creaturesTable->setRowCount(GameConstants::CREATURES_PER_TOWN); + else + ui->creaturesTable->setRowCount(ctown->creatures.size()); + + for (int i = 0; i < ui->creaturesTable->rowCount(); ++i) + { + QString creatureNames; + if (!ctown) + { + creatureNames.append(tr("Creature level %1 / Creature level %1 Upgrade").arg(i + 1)); + } + else + { + auto creaturesOnLevel = ctown->creatures.at(i); + for (auto& creature : creaturesOnLevel) + { + auto cre = VLC->creatures()->getById(creature); + auto creatureName = QString::fromStdString(cre->getNameSingularTranslated()); + creatureNames.append(creatureNames.isEmpty() ? creatureName : " / " + creatureName); + } + } + auto * item = new QTableWidgetItem(); + item->setFlags(item->flags() & ~Qt::ItemIsEditable); + item->setText(creatureNames); + ui->creaturesTable->setItem(i, 0, item); + + auto creatureNumber = creatures.size() > i ? creatures.at(i).toInt() : 0; + auto * edit = new QSpinBox(ui->creaturesTable); + edit->setValue(creatureNumber); + edit->setMaximum(MAXIMUM_CREATURES_CHANGE); + ui->creaturesTable->setCellWidget(i, 1, edit); + + } + ui->creaturesTable->resizeColumnToContents(0); +} + +void TownEventDialog::on_TownEventDialog_finished(int result) +{ + QVariantMap descriptor; + descriptor["name"] = ui->eventNameText->text(); + 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()-1); + descriptor["nextOccurrence"] = QVariant::fromValue(ui->eventRepeatAfter->value()); + descriptor["players"] = playersToVariant(); + descriptor["resources"] = resourcesToVariant(); + descriptor["buildings"] = buildingsToVariant(); + descriptor["creatures"] = creaturesToVariant(); + + townEventListItem->setData(MapEditorRoles::TownEventRole, descriptor); + auto itemText = tr("Day %1 - %2").arg(ui->eventFirstOccurrence->value(), 3).arg(ui->eventNameText->text()); + townEventListItem->setText(itemText); +} + +QVariant TownEventDialog::playersToVariant() +{ + QVariantList players; + for (int i = 0; i < ui->playersAffected->count(); ++i) + { + auto * item = ui->playersAffected->item(i); + if (item->checkState() == Qt::Checked) + players.push_back(toQString(PlayerColor(i))); + } + return QVariant::fromValue(players); +} + +QVariantMap TownEventDialog::resourcesToVariant() +{ + auto res = params.value("resources").toMap(); + for (int i = 0; i < GameConstants::RESOURCE_QUANTITY; ++i) + { + auto * itemType = ui->resourcesTable->item(i, 0); + auto * itemQty = static_cast (ui->resourcesTable->cellWidget(i, 1)); + + res[itemType->text()] = QVariant::fromValue(itemQty->value()); + } + return res; +} + +QVariantList TownEventDialog::buildingsToVariant() +{ + return getBuildingVariantsFromModel(buildingsModel, 1, Qt::Checked); +} + +QVariantList TownEventDialog::creaturesToVariant() +{ + QVariantList creaturesList; + for (int i = 0; i < ui->creaturesTable->rowCount(); ++i) + { + auto * item = static_cast(ui->creaturesTable->cellWidget(i, 1)); + creaturesList.push_back(item->value()); + } + return creaturesList; +} + +void TownEventDialog::on_okButton_clicked() +{ + close(); +} + +void TownEventDialog::setRowColumnCheckState(const QStandardItem * item, int column, Qt::CheckState checkState) { + auto sibling = item->model()->sibling(item->row(), column, item->index()); + buildingsModel.itemFromIndex(sibling)->setCheckState(checkState); +} + +void TownEventDialog::onItemChanged(const QStandardItem * item) +{ + disconnect(&buildingsModel, &QStandardItemModel::itemChanged, this, &TownEventDialog::onItemChanged); + auto rowFirstColumnIndex = item->model()->sibling(item->row(), 0, item->index()); + QStandardItem * nextRow = buildingsModel.itemFromIndex(rowFirstColumnIndex); + if (item->checkState() == Qt::Checked) { + while (nextRow) { + setRowColumnCheckState(nextRow,item->column(), Qt::Checked); + nextRow = nextRow->parent(); + + } + } + else if (item->checkState() == Qt::Unchecked) { + std::vector stack; + stack.push_back(nextRow); + do + { + nextRow = stack.back(); + stack.pop_back(); + setRowColumnCheckState(nextRow, item->column(), Qt::Unchecked); + if (nextRow->hasChildren()) { + for (int i = 0; i < nextRow->rowCount(); ++i) { + stack.push_back(nextRow->child(i, 0)); + } + } + + } while(!stack.empty()); + } + connect(&buildingsModel, &QStandardItemModel::itemChanged, this, &TownEventDialog::onItemChanged); +} diff --git a/mapeditor/inspector/towneventdialog.h b/mapeditor/inspector/towneventdialog.h new file mode 100644 index 000000000..635939a36 --- /dev/null +++ b/mapeditor/inspector/towneventdialog.h @@ -0,0 +1,53 @@ +/* + * towneventdialog.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 "../StdInc.h" +#include +#include "../lib/mapObjects/CGTownInstance.h" + +namespace Ui { + class TownEventDialog; +} + +class TownEventDialog : public QDialog +{ + Q_OBJECT + +public: + explicit TownEventDialog(CGTownInstance & town, QListWidgetItem * item, QWidget * parent); + ~TownEventDialog(); + + +private slots: + void onItemChanged(const QStandardItem * item); + void on_TownEventDialog_finished(int result); + void on_okButton_clicked(); + void setRowColumnCheckState(const QStandardItem * item, int column, Qt::CheckState checkState); + +private: + void initPlayers(); + void initResources(); + void initBuildings(); + void initCreatures(); + + QVariant playersToVariant(); + QVariantMap resourcesToVariant(); + QVariantList buildingsToVariant(); + QVariantList creaturesToVariant(); + + QStandardItem * addBuilding(const CTown & ctown, BuildingID bId, std::set & remaining); + + Ui::TownEventDialog * ui; + CGTownInstance & town; + QListWidgetItem * townEventListItem; + QMap params; + QStandardItemModel buildingsModel; +}; diff --git a/mapeditor/inspector/towneventdialog.ui b/mapeditor/inspector/towneventdialog.ui new file mode 100644 index 000000000..9d698afc2 --- /dev/null +++ b/mapeditor/inspector/towneventdialog.ui @@ -0,0 +1,266 @@ + + + TownEventDialog + + + Qt::ApplicationModal + + + + 0 + 0 + 693 + 525 + + + + + 0 + 0 + + + + Town event + + + + 0 + + + 3 + + + 3 + + + + + 0 + + + + General + + + + + 9 + 9 + 511 + 351 + + + + + + + Event name + + + + + + + Type event message text + + + + + + + + + 10 + 370 + 511 + 61 + + + + + + + + + Day of first occurrence + + + + + + + + + + + + + + Repeat after (0 = no repeat) + + + + + + + + + + + + + + 529 + 9 + 141 + 421 + + + + + + + Affected players + + + + + + + + 0 + 0 + + + + + 200 + 16777215 + + + + + + + + affects human + + + + + + + + + affects AI + + + + + + + + + + + Resources + + + + + 10 + 10 + 661 + 421 + + + + + 0 + 0 + + + + 2 + + + false + + + false + + + + + + + + Buildings + + + + + 10 + 10 + 661 + 421 + + + + QAbstractItemView::NoEditTriggers + + + false + + + + + + Creatures + + + + + 10 + 10 + 661 + 421 + + + + 7 + + + 2 + + + false + + + false + + + + + + + + + + + + + + + + + + OK + + + + + + + + diff --git a/mapeditor/inspector/towneventswidget.cpp b/mapeditor/inspector/towneventswidget.cpp new file mode 100644 index 000000000..963a893f1 --- /dev/null +++ b/mapeditor/inspector/towneventswidget.cpp @@ -0,0 +1,177 @@ +/* + * towneventswidget.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 "towneventswidget.h" +#include "ui_towneventswidget.h" +#include "towneventdialog.h" +#include "mapeditorroles.h" +#include "mapsettings/eventsettings.h" +#include "../../lib/constants/NumericConstants.h" +#include "../../lib/constants/StringConstants.h" + +TownEventsWidget::TownEventsWidget(CGTownInstance & town, QWidget * parent) : + QDialog(parent), + ui(new Ui::TownEventsWidget), + town(town) +{ + ui->setupUi(this); +} + +TownEventsWidget::~TownEventsWidget() +{ + delete ui; +} + +QVariant toVariant(const std::set & buildings) +{ + QVariantList result; + for (auto b : buildings) + result.push_back(QVariant::fromValue(b.num)); + return result; +} + +QVariant toVariant(const std::vector & creatures) +{ + QVariantList result; + for (auto c : creatures) + result.push_back(QVariant::fromValue(c)); + return result; +} + +std::set buildingsFromVariant(const QVariant& v) +{ + std::set result; + for (const auto & r : v.toList()) { + result.insert(BuildingID(r.toInt())); + } + return result; +} + +std::vector creaturesFromVariant(const QVariant& v) +{ + std::vector result; + for (const auto & r : v.toList()) { + result.push_back(r.toInt()); + } + return result; +} + +QVariant toVariant(const CCastleEvent& event) +{ + QVariantMap result; + result["name"] = QString::fromStdString(event.name); + result["message"] = QString::fromStdString(event.message.toString()); + result["players"] = toVariant(event.players); + result["humanAffected"] = QVariant::fromValue(event.humanAffected); + result["computerAffected"] = QVariant::fromValue(event.computerAffected); + result["firstOccurrence"] = QVariant::fromValue(event.firstOccurrence); + result["nextOccurrence"] = QVariant::fromValue(event.nextOccurrence); + result["resources"] = toVariant(event.resources); + result["buildings"] = toVariant(event.buildings); + result["creatures"] = toVariant(event.creatures); + + return QVariant(result); +} + +CCastleEvent eventFromVariant(CMapHeader& map, const CGTownInstance& town, const QVariant& variant) +{ + CCastleEvent result; + auto v = variant.toMap(); + result.name = v.value("name").toString().toStdString(); + result.message.appendTextID(mapRegisterLocalizedString("map", map, TextIdentifier("town", town.instanceName, "event", result.name, "message"), v.value("message").toString().toStdString())); + result.players = playersFromVariant(v.value("players")); + result.humanAffected = v.value("humanAffected").toInt(); + result.computerAffected = v.value("computerAffected").toInt(); + result.firstOccurrence = v.value("firstOccurrence").toInt(); + result.nextOccurrence = v.value("nextOccurrence").toInt(); + result.resources = resourcesFromVariant(v.value("resources")); + result.buildings = buildingsFromVariant(v.value("buildings")); + result.creatures = creaturesFromVariant(v.value("creatures")); + return result; +} + +void TownEventsWidget::obtainData() +{ + for (const auto & event : town.events) + { + auto eventName = QString::fromStdString(event.name); + auto itemText = tr("Day %1 - %2").arg(event.firstOccurrence+1, 3).arg(eventName); + + auto * item = new QListWidgetItem(itemText); + item->setData(MapEditorRoles::TownEventRole, toVariant(event)); + ui->eventsList->addItem(item); + } +} + +void TownEventsWidget::commitChanges(MapController& controller) +{ + town.events.clear(); + for (int i = 0; i < ui->eventsList->count(); ++i) + { + const auto * item = ui->eventsList->item(i); + town.events.push_back(eventFromVariant(*controller.map(), town, item->data(MapEditorRoles::TownEventRole))); + } +} + +void TownEventsWidget::on_timedEventAdd_clicked() +{ + CCastleEvent event; + event.name = tr("New event").toStdString(); + auto* item = new QListWidgetItem(QString::fromStdString(event.name)); + item->setData(MapEditorRoles::TownEventRole, toVariant(event)); + ui->eventsList->addItem(item); + on_eventsList_itemActivated(item); +} + +void TownEventsWidget::on_timedEventRemove_clicked() +{ + delete ui->eventsList->takeItem(ui->eventsList->currentRow()); +} + +void TownEventsWidget::on_eventsList_itemActivated(QListWidgetItem* item) +{ + TownEventDialog dlg{ town, item, parentWidget() }; + dlg.exec(); +} + + +TownEventsDelegate::TownEventsDelegate(CGTownInstance & town, MapController & c) : QStyledItemDelegate(), town(town), controller(c) +{ +} + +QWidget* TownEventsDelegate::createEditor(QWidget * parent, const QStyleOptionViewItem & option, const QModelIndex & index) const +{ + return new TownEventsWidget(town, parent); +} + +void TownEventsDelegate::setEditorData(QWidget * editor, const QModelIndex & index) const +{ + if (auto * ed = qobject_cast(editor)) + { + ed->obtainData(); + } + else + { + QStyledItemDelegate::setEditorData(editor, index); + } +} + +void TownEventsDelegate::setModelData(QWidget * editor, QAbstractItemModel * model, const QModelIndex & index) const +{ + if (auto * ed = qobject_cast(editor)) + { + ed->commitChanges(controller); + } + else + { + QStyledItemDelegate::setModelData(editor, model, index); + } +} diff --git a/mapeditor/inspector/towneventswidget.h b/mapeditor/inspector/towneventswidget.h new file mode 100644 index 000000000..7bc85f6ca --- /dev/null +++ b/mapeditor/inspector/towneventswidget.h @@ -0,0 +1,58 @@ +/* + * towneventswidget.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 "../StdInc.h" +#include +#include "../lib/mapping/CMapDefines.h" +#include "../lib/mapObjects/CGTownInstance.h" +#include "../mapcontroller.h" + +namespace Ui { + class TownEventsWidget; +} + +class TownEventsWidget : public QDialog +{ + Q_OBJECT + +public: + explicit TownEventsWidget(CGTownInstance &, QWidget * parent = nullptr); + ~TownEventsWidget(); + + void obtainData(); + void commitChanges(MapController & controller); +private slots: + void on_timedEventAdd_clicked(); + void on_timedEventRemove_clicked(); + void on_eventsList_itemActivated(QListWidgetItem * item); + +private: + + Ui::TownEventsWidget * ui; + CGTownInstance & town; +}; + +class TownEventsDelegate : public QStyledItemDelegate +{ + Q_OBJECT +public: + using QStyledItemDelegate::QStyledItemDelegate; + + TownEventsDelegate(CGTownInstance &, MapController &); + + QWidget* createEditor(QWidget * parent, const QStyleOptionViewItem & option, const QModelIndex & index) const override; + void setEditorData(QWidget * editor, const QModelIndex & index) const override; + void setModelData(QWidget * editor, QAbstractItemModel * model, const QModelIndex & index) const override; + +private: + CGTownInstance & town; + MapController & controller; +}; diff --git a/mapeditor/inspector/towneventswidget.ui b/mapeditor/inspector/towneventswidget.ui new file mode 100644 index 000000000..cecc34149 --- /dev/null +++ b/mapeditor/inspector/towneventswidget.ui @@ -0,0 +1,93 @@ + + + TownEventsWidget + + + Qt::ApplicationModal + + + + 0 + 0 + 691 + 462 + + + + + 0 + 0 + + + + + 400 + 400 + + + + Town events + + + + + + + + Timed events + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 90 + 0 + + + + Add + + + + + + + + 90 + 0 + + + + Remove + + + + + + + + + true + + + + + + + + diff --git a/mapeditor/inspector/townspellswidget.cpp b/mapeditor/inspector/townspellswidget.cpp new file mode 100644 index 000000000..abfd4def2 --- /dev/null +++ b/mapeditor/inspector/townspellswidget.cpp @@ -0,0 +1,166 @@ +/* + * townspellswidget.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 "townspellswidget.h" +#include "ui_townspellswidget.h" +#include "inspector.h" +#include "mapeditorroles.h" +#include "../../lib/constants/StringConstants.h" +#include "../../lib/spells/CSpellHandler.h" + +TownSpellsWidget::TownSpellsWidget(CGTownInstance & town, QWidget * parent) : + QDialog(parent), + ui(new Ui::TownSpellsWidget), + town(town) +{ + ui->setupUi(this); + + possibleSpellLists = { ui->possibleSpellList1, ui->possibleSpellList2, ui->possibleSpellList3, ui->possibleSpellList4, ui->possibleSpellList5 }; + requiredSpellLists = { ui->requiredSpellList1, ui->requiredSpellList2, ui->requiredSpellList3, ui->requiredSpellList4, ui->requiredSpellList5 }; + + std::array mageGuilds = {BuildingID::MAGES_GUILD_1, BuildingID::MAGES_GUILD_2, BuildingID::MAGES_GUILD_3, BuildingID::MAGES_GUILD_4, BuildingID::MAGES_GUILD_5}; + for (int i = 0; i < mageGuilds.size(); i++) + { + ui->tabWidget->setTabEnabled(i, vstd::contains(town.getTown()->buildings, mageGuilds[i])); + } +} + +TownSpellsWidget::~TownSpellsWidget() +{ + delete ui; +} + + +void TownSpellsWidget::obtainData() +{ + initSpellLists(); + if (vstd::contains(town.possibleSpells, SpellID::PRESET)) { + ui->customizeSpells->setChecked(true); + } + else + { + ui->customizeSpells->setChecked(false); + ui->tabWidget->setEnabled(false); + } +} + +void TownSpellsWidget::resetSpells() +{ + town.possibleSpells.clear(); + town.obligatorySpells.clear(); + for (auto spellID : VLC->spellh->getDefaultAllowed()) + town.possibleSpells.push_back(spellID); +} + +void TownSpellsWidget::initSpellLists() +{ + auto spells = VLC->spellh->getDefaultAllowed(); + for (int i = 0; i < GameConstants::SPELL_LEVELS; i++) + { + std::vector spellsByLevel; + auto getSpellsByLevel = [i](auto spellID) { + return spellID.toEntity(VLC)->getLevel() == i + 1; + }; + vstd::copy_if(spells, std::back_inserter(spellsByLevel), getSpellsByLevel); + possibleSpellLists[i]->clear(); + requiredSpellLists[i]->clear(); + for (auto spellID : spellsByLevel) + { + auto spell = spellID.toEntity(VLC); + auto * possibleItem = new QListWidgetItem(QString::fromStdString(spell->getNameTranslated())); + possibleItem->setData(MapEditorRoles::SpellIDRole, QVariant::fromValue(spell->getIndex())); + possibleItem->setFlags(possibleItem->flags() | Qt::ItemIsUserCheckable); + possibleItem->setCheckState(vstd::contains(town.possibleSpells, spell->getId()) ? Qt::Checked : Qt::Unchecked); + possibleSpellLists[i]->addItem(possibleItem); + + auto * requiredItem = new QListWidgetItem(QString::fromStdString(spell->getNameTranslated())); + requiredItem->setData(MapEditorRoles::SpellIDRole, QVariant::fromValue(spell->getIndex())); + requiredItem->setFlags(requiredItem->flags() | Qt::ItemIsUserCheckable); + requiredItem->setCheckState(vstd::contains(town.obligatorySpells, spell->getId()) ? Qt::Checked : Qt::Unchecked); + requiredSpellLists[i]->addItem(requiredItem); + } + } +} + +void TownSpellsWidget::commitChanges() +{ + if (!ui->tabWidget->isEnabled()) + { + resetSpells(); + return; + } + + auto updateTownSpellList = [](auto uiSpellLists, auto & townSpellList) { + for (const QListWidget * spellList : uiSpellLists) + { + for (int i = 0; i < spellList->count(); ++i) + { + const auto * item = spellList->item(i); + if (item->checkState() == Qt::Checked) + { + townSpellList.push_back(item->data(MapEditorRoles::SpellIDRole).toInt()); + } + } + } + }; + + town.possibleSpells.clear(); + town.obligatorySpells.clear(); + town.possibleSpells.push_back(SpellID::PRESET); + updateTownSpellList(possibleSpellLists, town.possibleSpells); + updateTownSpellList(requiredSpellLists, town.obligatorySpells); +} + +void TownSpellsWidget::on_customizeSpells_toggled(bool checked) +{ + if (checked) + { + town.possibleSpells.push_back(SpellID::PRESET); + } + else + { + resetSpells(); + } + ui->tabWidget->setEnabled(checked); + initSpellLists(); +} + +TownSpellsDelegate::TownSpellsDelegate(CGTownInstance & town) : QStyledItemDelegate(), town(town) +{ +} + +QWidget * TownSpellsDelegate::createEditor(QWidget * parent, const QStyleOptionViewItem & option, const QModelIndex & index) const +{ + return new TownSpellsWidget(town, parent); +} + +void TownSpellsDelegate::setEditorData(QWidget * editor, const QModelIndex & index) const +{ + if (auto * ed = qobject_cast(editor)) + { + ed->obtainData(); + } + else + { + QStyledItemDelegate::setEditorData(editor, index); + } +} + +void TownSpellsDelegate::setModelData(QWidget * editor, QAbstractItemModel * model, const QModelIndex & index) const +{ + if (auto * ed = qobject_cast(editor)) + { + ed->commitChanges(); + } + else + { + QStyledItemDelegate::setModelData(editor, model, index); + } +} diff --git a/mapeditor/inspector/townspellswidget.h b/mapeditor/inspector/townspellswidget.h new file mode 100644 index 000000000..1548c25f7 --- /dev/null +++ b/mapeditor/inspector/townspellswidget.h @@ -0,0 +1,60 @@ +/* + * townspellswidget.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 "../../lib/mapObjects/CGTownInstance.h" + +namespace Ui { + class TownSpellsWidget; +} + + +class TownSpellsWidget : public QDialog +{ + Q_OBJECT + +public: + explicit TownSpellsWidget(CGTownInstance &, QWidget * parent = nullptr); + ~TownSpellsWidget(); + + void obtainData(); + void commitChanges(); + +private slots: + void on_customizeSpells_toggled(bool checked); + +private: + Ui::TownSpellsWidget * ui; + + CGTownInstance & town; + + std::array possibleSpellLists; + std::array requiredSpellLists; + + void resetSpells(); + void initSpellLists(); +}; + +class TownSpellsDelegate : public QStyledItemDelegate +{ + Q_OBJECT +public: + using QStyledItemDelegate::QStyledItemDelegate; + + TownSpellsDelegate(CGTownInstance&); + + QWidget * createEditor(QWidget * parent, const QStyleOptionViewItem & option, const QModelIndex& index) const override; + void setEditorData(QWidget * editor, const QModelIndex & index) const override; + void setModelData(QWidget * editor, QAbstractItemModel * model, const QModelIndex & index) const override; + +private: + CGTownInstance& town; +}; diff --git a/mapeditor/inspector/townspellswidget.ui b/mapeditor/inspector/townspellswidget.ui new file mode 100644 index 000000000..40156178b --- /dev/null +++ b/mapeditor/inspector/townspellswidget.ui @@ -0,0 +1,304 @@ + + + TownSpellsWidget + + + Qt::NonModal + + + + 0 + 0 + 600 + 480 + + + + + 0 + 0 + + + + + 600 + 480 + + + + Spells + + + Qt::LeftToRight + + + true + + + + 10 + + + 5 + + + + + Customize spells + + + false + + + + + + + + 0 + 0 + + + + 0 + + + true + + + + + 0 + 0 + + + + Level 1 + + + + 12 + + + 12 + + + 12 + + + + + + + Spell that may appear in mage guild + + + + + + + Spell that must appear in mage guild + + + + + + + + + + + + + + + + + 0 + 0 + + + + Level 2 + + + + 12 + + + 12 + + + 12 + + + + + + + Spell that may appear in mage guild + + + + + + + Spell that must appear in mage guild + + + + + + + + + + + + + + + + + 0 + 0 + + + + Level 3 + + + + 12 + + + 12 + + + 12 + + + + + + + Spell that may appear in mage guild + + + + + + + Spell that must appear in mage guild + + + + + + + + + + + + + + + + + 0 + 0 + + + + Level 4 + + + + 12 + + + 12 + + + 12 + + + + + + + Spell that may appear in mage guild + + + + + + + Spell that must appear in mage guild + + + + + + + + + + + + + + + + + 0 + 0 + + + + Level 5 + + + + 12 + + + 12 + + + 12 + + + + + + + Spell that may appear in mage guild + + + + + + + Spell that must appear in mage guild + + + + + + + + + + + + + + + + + + + + diff --git a/mapeditor/jsonutils.cpp b/mapeditor/jsonutils.cpp deleted file mode 100644 index f7ce677bb..000000000 --- a/mapeditor/jsonutils.cpp +++ /dev/null @@ -1,131 +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::fromUtf8(entry.first.c_str()), 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()) - { - break; - case JsonNode::JsonType::DATA_NULL: - return QVariant(); - break; - case JsonNode::JsonType::DATA_BOOL: - return QVariant(node.Bool()); - break; - case JsonNode::JsonType::DATA_FLOAT: - case JsonNode::JsonType::DATA_INTEGER: - return QVariant(node.Float()); - break; - case JsonNode::JsonType::DATA_STRING: - return QVariant(QString::fromUtf8(node.String().c_str())); - break; - case JsonNode::JsonType::DATA_VECTOR: - return JsonToList(node.Vector()); - break; - case JsonNode::JsonType::DATA_STRUCT: - return JsonToMap(node.Struct()); - } - return QVariant(); -} - -QVariant JsonFromFile(QString filename) -{ - QFile file(filename); - file.open(QFile::ReadOnly); - auto data = file.readAll(); - - if(data.size() == 0) - { - logGlobal->error("Failed to open file %s", filename.toUtf8().data()); - return QVariant(); - } - else - { - JsonNode node(reinterpret_cast(data.data()), data.size()); - return toVariant(node); - } -} - -JsonNode toJson(QVariant object) -{ - JsonNode ret; - - if(object.canConvert()) - ret.Struct() = VariantToMap(object.toMap()); - else if(object.canConvert()) - ret.Vector() = VariantToList(object.toList()); - else 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.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/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 610a205ed..345f645dc 100644 --- a/mapeditor/mainwindow.cpp +++ b/mapeditor/mainwindow.cpp @@ -16,6 +16,8 @@ #include #include #include +#include +#include #include "../lib/VCMIDirs.h" #include "../lib/VCMI_Lib.h" @@ -133,17 +135,33 @@ void MainWindow::parseCommandLine(ExtractionOptions & extractionOptions) void MainWindow::loadTranslation() { #ifdef ENABLE_QT_TRANSLATIONS - const std::string translationFile = settings["general"]["language"].String() + ".qm"; - logGlobal->info("Loading translation '%s'", translationFile); + const std::string translationFile = settings["general"]["language"].String()+ ".qm"; + QString translationFileResourcePath = QString{":/translation/%1"}.arg(translationFile.c_str()); - if (!translator.load(QString{":/translation/%1"}.arg(translationFile.c_str()))) + logGlobal->info("Loading translation %s", translationFile); + + if(!QFile::exists(translationFileResourcePath)) { - logGlobal->error("Failed to load translation"); + logGlobal->debug("Translation file %s does not exist", translationFileResourcePath.toStdString()); + return; + } + + if (!translator.load(translationFileResourcePath)) + { + logGlobal->error("Failed to load translation file %s", translationFileResourcePath.toStdString()); + return; + } + + if(translationFile == "english.qm") + { + // translator doesn't need to be installed for English return; } if (!qApp->installTranslator(&translator)) - logGlobal->error("Failed to install translator"); + { + logGlobal->error("Failed to install translator for translation file %s", translationFileResourcePath.toStdString()); + } #endif } @@ -166,6 +184,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 @@ -205,6 +224,8 @@ MainWindow::MainWindow(QWidget* parent) : ui->toolFill->setIcon(QIcon{":/icons/tool-fill.png"}); ui->toolSelect->setIcon(QIcon{":/icons/tool-select.png"}); ui->actionOpen->setIcon(QIcon{":/icons/document-open.png"}); + ui->actionOpenRecent->setIcon(QIcon{":/icons/document-open-recent.png"}); + ui->menuOpenRecent->setIcon(QIcon{":/icons/document-open-recent.png"}); ui->actionSave->setIcon(QIcon{":/icons/document-save.png"}); ui->actionNew->setIcon(QIcon{":/icons/document-new.png"}); ui->actionLevel->setIcon(QIcon{":/icons/toggle-underground.png"}); @@ -248,6 +269,8 @@ MainWindow::MainWindow(QWidget* parent) : scenePreview = new QGraphicsScene(this); ui->objectPreview->setScene(scenePreview); + connect(ui->actionOpenRecentMore, &QAction::triggered, this, &MainWindow::on_actionOpenRecent_triggered); + //loading objects loadObjectsTree(); @@ -301,7 +324,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); } @@ -395,9 +418,21 @@ bool MainWindow::openMap(const QString & filenameSelect) filename = filenameSelect; initializeMap(controller.map()->version != EMapFormat::VCMI); + + updateRecentMenu(filenameSelect); + return true; } +void MainWindow::updateRecentMenu(const QString & filenameSelect) { + QSettings s(Ui::teamName, Ui::appName); + QStringList recentFiles = s.value(recentlyOpenedFilesSetting).toStringList(); + recentFiles.removeAll(filenameSelect); + recentFiles.prepend(filenameSelect); + constexpr int maxRecentFiles = 10; + s.setValue(recentlyOpenedFilesSetting, QStringList(recentFiles.mid(0, maxRecentFiles))); +} + void MainWindow::on_actionOpen_triggered() { if(!getAnswerAboutUnsavedChanges()) @@ -412,6 +447,91 @@ void MainWindow::on_actionOpen_triggered() openMap(filenameSelect); } +void MainWindow::on_actionOpenRecent_triggered() +{ + QSettings s(Ui::teamName, Ui::appName); + QStringList recentFiles = s.value(recentlyOpenedFilesSetting).toStringList(); + + class RecentFileDialog : public QDialog + { + + public: + RecentFileDialog(const QStringList& recentFiles, QWidget *parent) + : QDialog(parent), layout(new QVBoxLayout(this)), listWidget(new QListWidget(this)) + { + + setWindowTitle(tr("Recently Opened Files")); + setMinimumWidth(600); + + connect(listWidget, &QListWidget::itemActivated, this, [this](QListWidgetItem *item) + { + accept(); + }); + + for (const QString &file : recentFiles) + { + QListWidgetItem *item = new QListWidgetItem(file); + listWidget->addItem(item); + } + + // Select most recent items by default. + // This enables a "CTRL+R => Enter"-workflow instead of "CTRL+R => 'mouse click on first item'" + if(listWidget->count() > 0) + { + listWidget->item(0)->setSelected(true); + } + + layout->setSizeConstraint(QLayout::SetMaximumSize); + layout->addWidget(listWidget); + } + + QString getSelectedFilePath() const + { + return listWidget->currentItem()->text(); + } + + private: + QVBoxLayout * layout; + QListWidget * listWidget; + }; + + RecentFileDialog d(recentFiles, this); + if(d.exec() == QDialog::Accepted && getAnswerAboutUnsavedChanges()) + { + openMap(d.getSelectedFilePath()); + } +} + +void MainWindow::on_menuOpenRecent_aboutToShow() +{ + // Clear all actions except "More...", lest the list will grow with each + // showing of the list + for (QAction* action : ui->menuOpenRecent->actions()) { + if (action != ui->actionOpenRecentMore) { + ui->menuOpenRecent->removeAction(action); + } + } + + QSettings s(Ui::teamName, Ui::appName); + QStringList recentFiles = s.value(recentlyOpenedFilesSetting).toStringList(); + + // Dynamically populate menuOpenRecent with one action per file. + for (const QString & file : recentFiles) { + QAction *action = new QAction(file, this); + ui->menuOpenRecent->insertAction(ui->actionOpenRecentMore, action); + connect(action, &QAction::triggered, this, [this, file]() { + if(!getAnswerAboutUnsavedChanges()) + return; + openMap(file); + }); + } + + // Finally add a separator between recent entries and "More..." + if(recentFiles.size() > 0) { + ui->menuOpenRecent->insertSeparator(ui->actionOpenRecentMore); + } +} + void MainWindow::saveMap() { if(!controller.map()) @@ -436,6 +556,18 @@ void MainWindow::saveMap() Translations::cleanupRemovedItems(*controller.map()); + for(auto obj : controller.map()->objects) + { + if(obj->ID == Obj::HERO_PLACEHOLDER) + { + auto hero = dynamic_cast(obj.get()); + if(hero->heroType.has_value()) + { + controller.map()->reservedCampaignHeroes.insert(hero->heroType.value()); + } + } + } + CMapService mapService; try { @@ -505,7 +637,7 @@ void MainWindow::roadOrRiverButtonClicked(ui8 type, bool isRoad) controller.commitRoadOrRiverChange(mapLevel, type, isRoad); } -void MainWindow::addGroupIntoCatalog(const std::string & groupName, bool staticOnly) +void MainWindow::addGroupIntoCatalog(const QString & groupName, bool staticOnly) { auto knownObjects = VLC->objtypeh->knownObjects(); for(auto ID : knownObjects) @@ -517,13 +649,13 @@ void MainWindow::addGroupIntoCatalog(const std::string & groupName, bool staticO } } -void MainWindow::addGroupIntoCatalog(const std::string & groupName, bool useCustomName, bool staticOnly, int ID) +void MainWindow::addGroupIntoCatalog(const QString & groupName, bool useCustomName, bool staticOnly, int ID) { QStandardItem * itemGroup = nullptr; - auto itms = objectsModel.findItems(QString::fromStdString(groupName)); + auto itms = objectsModel.findItems(groupName); if(itms.empty()) { - itemGroup = new QStandardItem(QString::fromStdString(groupName)); + itemGroup = new QStandardItem(groupName); objectsModel.appendRow(itemGroup); } else @@ -612,7 +744,7 @@ void MainWindow::loadObjectsTree() { auto *b = new QPushButton(QString::fromStdString(terrain->getNameTranslated())); ui->terrainLayout->addWidget(b); - connect(b, &QPushButton::clicked, this, [this, terrain]{ terrainButtonClicked(terrain->getId()); }); + connect(b, &QPushButton::clicked, this, [this, terrainID=terrain->getId()]{ terrainButtonClicked(terrainID); }); //filter QString displayName = QString::fromStdString(terrain->getNameTranslated()); @@ -627,7 +759,7 @@ void MainWindow::loadObjectsTree() { auto *b = new QPushButton(QString::fromStdString(road->getNameTranslated())); ui->roadLayout->addWidget(b); - connect(b, &QPushButton::clicked, this, [this, road]{ roadOrRiverButtonClicked(road->getIndex(), true); }); + connect(b, &QPushButton::clicked, this, [this, roadID=road->getIndex()]{ roadOrRiverButtonClicked(roadID, true); }); } //add spacer to keep terrain button on the top ui->roadLayout->addItem(new QSpacerItem(20, 20, QSizePolicy::Minimum, QSizePolicy::Expanding)); @@ -636,7 +768,7 @@ void MainWindow::loadObjectsTree() { auto *b = new QPushButton(QString::fromStdString(river->getNameTranslated())); ui->riverLayout->addWidget(b); - connect(b, &QPushButton::clicked, this, [this, river]{ roadOrRiverButtonClicked(river->getIndex(), false); }); + connect(b, &QPushButton::clicked, this, [this, riverID=river->getIndex()]{ roadOrRiverButtonClicked(riverID, false); }); } //add spacer to keep terrain button on the top ui->riverLayout->addItem(new QSpacerItem(20, 20, QSizePolicy::Minimum, QSizePolicy::Expanding)); @@ -655,138 +787,158 @@ void MainWindow::loadObjectsTree() ui->treeView->setSelectionMode(QAbstractItemView::SingleSelection); connect(ui->treeView->selectionModel(), SIGNAL(currentChanged(const QModelIndex &, const QModelIndex &)), this, SLOT(treeViewSelected(const QModelIndex &, const QModelIndex &))); + //groups + enum GroupCat { TOWNS, OBJECTS, HEROES, ARTIFACTS, RESOURCES, BANKS, DWELLINGS, GROUNDS, TELEPORTS, MINES, TRIGGERS, MONSTERS, QUESTS, WOG_OBJECTS, OBSTACLES, OTHER }; + QMap groups = { + { TOWNS, tr("Towns") }, + { OBJECTS, tr("Objects") }, + { HEROES, tr("Heroes") }, + { ARTIFACTS, tr("Artifacts") }, + { RESOURCES, tr("Resources") }, + { BANKS, tr("Banks") }, + { DWELLINGS, tr("Dwellings") }, + { GROUNDS, tr("Grounds") }, + { TELEPORTS, tr("Teleports") }, + { MINES, tr("Mines") }, + { TRIGGERS, tr("Triggers") }, + { MONSTERS, tr("Monsters") }, + { QUESTS, tr("Quests") }, + { WOG_OBJECTS, tr("Wog Objects") }, + { OBSTACLES, tr("Obstacles") }, + { OTHER, tr("Other") }, + }; //adding objects - addGroupIntoCatalog("TOWNS", false, false, Obj::TOWN); - addGroupIntoCatalog("TOWNS", false, false, Obj::RANDOM_TOWN); - addGroupIntoCatalog("TOWNS", true, false, Obj::SHIPYARD); - addGroupIntoCatalog("TOWNS", true, false, Obj::GARRISON); - addGroupIntoCatalog("TOWNS", true, false, Obj::GARRISON2); - addGroupIntoCatalog("OBJECTS", true, false, Obj::ARENA); - addGroupIntoCatalog("OBJECTS", true, false, Obj::BUOY); - addGroupIntoCatalog("OBJECTS", true, false, Obj::CARTOGRAPHER); - addGroupIntoCatalog("OBJECTS", true, false, Obj::SWAN_POND); - addGroupIntoCatalog("OBJECTS", true, false, Obj::COVER_OF_DARKNESS); - addGroupIntoCatalog("OBJECTS", true, false, Obj::CORPSE); - addGroupIntoCatalog("OBJECTS", true, false, Obj::FAERIE_RING); - addGroupIntoCatalog("OBJECTS", true, false, Obj::FOUNTAIN_OF_FORTUNE); - addGroupIntoCatalog("OBJECTS", true, false, Obj::FOUNTAIN_OF_YOUTH); - addGroupIntoCatalog("OBJECTS", true, false, Obj::GARDEN_OF_REVELATION); - addGroupIntoCatalog("OBJECTS", true, false, Obj::HILL_FORT); - addGroupIntoCatalog("OBJECTS", true, false, Obj::IDOL_OF_FORTUNE); - addGroupIntoCatalog("OBJECTS", true, false, Obj::LIBRARY_OF_ENLIGHTENMENT); - addGroupIntoCatalog("OBJECTS", true, false, Obj::LIGHTHOUSE); - addGroupIntoCatalog("OBJECTS", true, false, Obj::SCHOOL_OF_MAGIC); - addGroupIntoCatalog("OBJECTS", true, false, Obj::MAGIC_SPRING); - addGroupIntoCatalog("OBJECTS", true, false, Obj::MAGIC_WELL); - addGroupIntoCatalog("OBJECTS", true, false, Obj::MERCENARY_CAMP); - addGroupIntoCatalog("OBJECTS", true, false, Obj::MERMAID); - addGroupIntoCatalog("OBJECTS", true, false, Obj::MYSTICAL_GARDEN); - addGroupIntoCatalog("OBJECTS", true, false, Obj::OASIS); - addGroupIntoCatalog("OBJECTS", true, false, Obj::LEAN_TO); - addGroupIntoCatalog("OBJECTS", true, false, Obj::OBELISK); - addGroupIntoCatalog("OBJECTS", true, false, Obj::REDWOOD_OBSERVATORY); - addGroupIntoCatalog("OBJECTS", true, false, Obj::PILLAR_OF_FIRE); - addGroupIntoCatalog("OBJECTS", true, false, Obj::STAR_AXIS); - addGroupIntoCatalog("OBJECTS", true, false, Obj::RALLY_FLAG); - addGroupIntoCatalog("OBJECTS", true, false, Obj::WATERING_HOLE); - addGroupIntoCatalog("OBJECTS", true, false, Obj::SCHOLAR); - addGroupIntoCatalog("OBJECTS", true, false, Obj::SHRINE_OF_MAGIC_INCANTATION); - addGroupIntoCatalog("OBJECTS", true, false, Obj::SHRINE_OF_MAGIC_GESTURE); - addGroupIntoCatalog("OBJECTS", true, false, Obj::SHRINE_OF_MAGIC_THOUGHT); - addGroupIntoCatalog("OBJECTS", true, false, Obj::SIRENS); - addGroupIntoCatalog("OBJECTS", true, false, Obj::STABLES); - addGroupIntoCatalog("OBJECTS", true, false, Obj::TAVERN); - addGroupIntoCatalog("OBJECTS", true, false, Obj::TEMPLE); - addGroupIntoCatalog("OBJECTS", true, false, Obj::DEN_OF_THIEVES); - addGroupIntoCatalog("OBJECTS", true, false, Obj::LEARNING_STONE); - addGroupIntoCatalog("OBJECTS", true, false, Obj::TREE_OF_KNOWLEDGE); - addGroupIntoCatalog("OBJECTS", true, false, Obj::WAGON); - addGroupIntoCatalog("OBJECTS", true, false, Obj::SCHOOL_OF_WAR); - addGroupIntoCatalog("OBJECTS", true, false, Obj::WAR_MACHINE_FACTORY); - addGroupIntoCatalog("OBJECTS", true, false, Obj::WARRIORS_TOMB); - addGroupIntoCatalog("OBJECTS", true, false, Obj::WITCH_HUT); - addGroupIntoCatalog("OBJECTS", true, false, Obj::SANCTUARY); - addGroupIntoCatalog("OBJECTS", true, false, Obj::MARLETTO_TOWER); - addGroupIntoCatalog("HEROES", true, false, Obj::PRISON); - addGroupIntoCatalog("HEROES", false, false, Obj::HERO); - addGroupIntoCatalog("HEROES", false, false, Obj::RANDOM_HERO); - addGroupIntoCatalog("HEROES", false, false, Obj::HERO_PLACEHOLDER); - addGroupIntoCatalog("HEROES", false, false, Obj::BOAT); - addGroupIntoCatalog("ARTIFACTS", true, false, Obj::ARTIFACT); - addGroupIntoCatalog("ARTIFACTS", false, false, Obj::RANDOM_ART); - addGroupIntoCatalog("ARTIFACTS", false, false, Obj::RANDOM_TREASURE_ART); - addGroupIntoCatalog("ARTIFACTS", false, false, Obj::RANDOM_MINOR_ART); - addGroupIntoCatalog("ARTIFACTS", false, false, Obj::RANDOM_MAJOR_ART); - addGroupIntoCatalog("ARTIFACTS", false, false, Obj::RANDOM_RELIC_ART); - addGroupIntoCatalog("ARTIFACTS", true, false, Obj::SPELL_SCROLL); - addGroupIntoCatalog("ARTIFACTS", true, false, Obj::PANDORAS_BOX); - addGroupIntoCatalog("RESOURCES", true, false, Obj::RANDOM_RESOURCE); - addGroupIntoCatalog("RESOURCES", false, false, Obj::RESOURCE); - addGroupIntoCatalog("RESOURCES", true, false, Obj::SEA_CHEST); - addGroupIntoCatalog("RESOURCES", true, false, Obj::TREASURE_CHEST); - addGroupIntoCatalog("RESOURCES", true, false, Obj::CAMPFIRE); - addGroupIntoCatalog("RESOURCES", true, false, Obj::SHIPWRECK_SURVIVOR); - addGroupIntoCatalog("RESOURCES", true, false, Obj::FLOTSAM); - addGroupIntoCatalog("BANKS", true, false, Obj::CREATURE_BANK); - addGroupIntoCatalog("BANKS", true, false, Obj::DRAGON_UTOPIA); - addGroupIntoCatalog("BANKS", true, false, Obj::CRYPT); - addGroupIntoCatalog("BANKS", true, false, Obj::DERELICT_SHIP); - addGroupIntoCatalog("BANKS", true, false, Obj::PYRAMID); - addGroupIntoCatalog("BANKS", true, false, Obj::SHIPWRECK); - addGroupIntoCatalog("DWELLINGS", true, false, Obj::CREATURE_GENERATOR1); - addGroupIntoCatalog("DWELLINGS", true, false, Obj::CREATURE_GENERATOR2); - addGroupIntoCatalog("DWELLINGS", true, false, Obj::CREATURE_GENERATOR3); - addGroupIntoCatalog("DWELLINGS", true, false, Obj::CREATURE_GENERATOR4); - addGroupIntoCatalog("DWELLINGS", true, false, Obj::REFUGEE_CAMP); - addGroupIntoCatalog("DWELLINGS", false, false, Obj::RANDOM_DWELLING); - addGroupIntoCatalog("DWELLINGS", false, false, Obj::RANDOM_DWELLING_LVL); - addGroupIntoCatalog("DWELLINGS", false, false, Obj::RANDOM_DWELLING_FACTION); - addGroupIntoCatalog("GROUNDS", true, false, Obj::CURSED_GROUND1); - addGroupIntoCatalog("GROUNDS", true, false, Obj::MAGIC_PLAINS1); - addGroupIntoCatalog("GROUNDS", true, false, Obj::CLOVER_FIELD); - addGroupIntoCatalog("GROUNDS", true, false, Obj::CURSED_GROUND2); - addGroupIntoCatalog("GROUNDS", true, false, Obj::EVIL_FOG); - addGroupIntoCatalog("GROUNDS", true, false, Obj::FAVORABLE_WINDS); - addGroupIntoCatalog("GROUNDS", true, false, Obj::FIERY_FIELDS); - addGroupIntoCatalog("GROUNDS", true, false, Obj::HOLY_GROUNDS); - addGroupIntoCatalog("GROUNDS", true, false, Obj::LUCID_POOLS); - addGroupIntoCatalog("GROUNDS", true, false, Obj::MAGIC_CLOUDS); - addGroupIntoCatalog("GROUNDS", true, false, Obj::MAGIC_PLAINS2); - addGroupIntoCatalog("GROUNDS", true, false, Obj::ROCKLANDS); - addGroupIntoCatalog("GROUNDS", true, false, Obj::HOLE); - addGroupIntoCatalog("TELEPORTS", true, false, Obj::MONOLITH_ONE_WAY_ENTRANCE); - addGroupIntoCatalog("TELEPORTS", true, false, Obj::MONOLITH_ONE_WAY_EXIT); - addGroupIntoCatalog("TELEPORTS", true, false, Obj::MONOLITH_TWO_WAY); - addGroupIntoCatalog("TELEPORTS", true, false, Obj::SUBTERRANEAN_GATE); - addGroupIntoCatalog("TELEPORTS", true, false, Obj::WHIRLPOOL); - addGroupIntoCatalog("MINES", true, false, Obj::MINE); - addGroupIntoCatalog("MINES", false, false, Obj::ABANDONED_MINE); - addGroupIntoCatalog("MINES", true, false, Obj::WINDMILL); - addGroupIntoCatalog("MINES", true, false, Obj::WATER_WHEEL); - addGroupIntoCatalog("TRIGGERS", true, false, Obj::EVENT); - addGroupIntoCatalog("TRIGGERS", true, false, Obj::GRAIL); - addGroupIntoCatalog("TRIGGERS", true, false, Obj::SIGN); - addGroupIntoCatalog("TRIGGERS", true, false, Obj::OCEAN_BOTTLE); - addGroupIntoCatalog("MONSTERS", false, false, Obj::MONSTER); - addGroupIntoCatalog("MONSTERS", true, false, Obj::RANDOM_MONSTER); - addGroupIntoCatalog("MONSTERS", true, false, Obj::RANDOM_MONSTER_L1); - addGroupIntoCatalog("MONSTERS", true, false, Obj::RANDOM_MONSTER_L2); - addGroupIntoCatalog("MONSTERS", true, false, Obj::RANDOM_MONSTER_L3); - addGroupIntoCatalog("MONSTERS", true, false, Obj::RANDOM_MONSTER_L4); - addGroupIntoCatalog("MONSTERS", true, false, Obj::RANDOM_MONSTER_L5); - addGroupIntoCatalog("MONSTERS", true, false, Obj::RANDOM_MONSTER_L6); - addGroupIntoCatalog("MONSTERS", true, false, Obj::RANDOM_MONSTER_L7); - addGroupIntoCatalog("QUESTS", true, false, Obj::SEER_HUT); - addGroupIntoCatalog("QUESTS", true, false, Obj::BORDER_GATE); - addGroupIntoCatalog("QUESTS", true, false, Obj::QUEST_GUARD); - addGroupIntoCatalog("QUESTS", true, false, Obj::HUT_OF_MAGI); - addGroupIntoCatalog("QUESTS", true, false, Obj::EYE_OF_MAGI); - addGroupIntoCatalog("QUESTS", true, false, Obj::BORDERGUARD); - addGroupIntoCatalog("QUESTS", true, false, Obj::KEYMASTER); - addGroupIntoCatalog("wog object", true, false, Obj::WOG_OBJECT); - addGroupIntoCatalog("OBSTACLES", true); - addGroupIntoCatalog("OTHER", false); + addGroupIntoCatalog(groups[TOWNS], false, false, Obj::TOWN); + addGroupIntoCatalog(groups[TOWNS], false, false, Obj::RANDOM_TOWN); + addGroupIntoCatalog(groups[TOWNS], true, false, Obj::SHIPYARD); + addGroupIntoCatalog(groups[TOWNS], true, false, Obj::GARRISON); + addGroupIntoCatalog(groups[TOWNS], true, false, Obj::GARRISON2); + addGroupIntoCatalog(groups[OBJECTS], true, false, Obj::ARENA); + addGroupIntoCatalog(groups[OBJECTS], true, false, Obj::BUOY); + addGroupIntoCatalog(groups[OBJECTS], true, false, Obj::CARTOGRAPHER); + addGroupIntoCatalog(groups[OBJECTS], true, false, Obj::SWAN_POND); + addGroupIntoCatalog(groups[OBJECTS], true, false, Obj::COVER_OF_DARKNESS); + addGroupIntoCatalog(groups[OBJECTS], true, false, Obj::CORPSE); + addGroupIntoCatalog(groups[OBJECTS], true, false, Obj::FAERIE_RING); + addGroupIntoCatalog(groups[OBJECTS], true, false, Obj::FOUNTAIN_OF_FORTUNE); + addGroupIntoCatalog(groups[OBJECTS], true, false, Obj::FOUNTAIN_OF_YOUTH); + addGroupIntoCatalog(groups[OBJECTS], true, false, Obj::GARDEN_OF_REVELATION); + addGroupIntoCatalog(groups[OBJECTS], true, false, Obj::HILL_FORT); + addGroupIntoCatalog(groups[OBJECTS], true, false, Obj::IDOL_OF_FORTUNE); + addGroupIntoCatalog(groups[OBJECTS], true, false, Obj::LIBRARY_OF_ENLIGHTENMENT); + addGroupIntoCatalog(groups[OBJECTS], true, false, Obj::LIGHTHOUSE); + addGroupIntoCatalog(groups[OBJECTS], true, false, Obj::SCHOOL_OF_MAGIC); + addGroupIntoCatalog(groups[OBJECTS], true, false, Obj::MAGIC_SPRING); + addGroupIntoCatalog(groups[OBJECTS], true, false, Obj::MAGIC_WELL); + addGroupIntoCatalog(groups[OBJECTS], true, false, Obj::MERCENARY_CAMP); + addGroupIntoCatalog(groups[OBJECTS], true, false, Obj::MERMAID); + addGroupIntoCatalog(groups[OBJECTS], true, false, Obj::MYSTICAL_GARDEN); + addGroupIntoCatalog(groups[OBJECTS], true, false, Obj::OASIS); + addGroupIntoCatalog(groups[OBJECTS], true, false, Obj::LEAN_TO); + addGroupIntoCatalog(groups[OBJECTS], true, false, Obj::OBELISK); + addGroupIntoCatalog(groups[OBJECTS], true, false, Obj::REDWOOD_OBSERVATORY); + addGroupIntoCatalog(groups[OBJECTS], true, false, Obj::PILLAR_OF_FIRE); + addGroupIntoCatalog(groups[OBJECTS], true, false, Obj::STAR_AXIS); + addGroupIntoCatalog(groups[OBJECTS], true, false, Obj::RALLY_FLAG); + addGroupIntoCatalog(groups[OBJECTS], true, false, Obj::WATERING_HOLE); + addGroupIntoCatalog(groups[OBJECTS], true, false, Obj::SCHOLAR); + addGroupIntoCatalog(groups[OBJECTS], true, false, Obj::SHRINE_OF_MAGIC_INCANTATION); + addGroupIntoCatalog(groups[OBJECTS], true, false, Obj::SHRINE_OF_MAGIC_GESTURE); + addGroupIntoCatalog(groups[OBJECTS], true, false, Obj::SHRINE_OF_MAGIC_THOUGHT); + addGroupIntoCatalog(groups[OBJECTS], true, false, Obj::SIRENS); + addGroupIntoCatalog(groups[OBJECTS], true, false, Obj::STABLES); + addGroupIntoCatalog(groups[OBJECTS], true, false, Obj::TAVERN); + addGroupIntoCatalog(groups[OBJECTS], true, false, Obj::TEMPLE); + addGroupIntoCatalog(groups[OBJECTS], true, false, Obj::DEN_OF_THIEVES); + addGroupIntoCatalog(groups[OBJECTS], true, false, Obj::LEARNING_STONE); + addGroupIntoCatalog(groups[OBJECTS], true, false, Obj::TREE_OF_KNOWLEDGE); + addGroupIntoCatalog(groups[OBJECTS], true, false, Obj::WAGON); + addGroupIntoCatalog(groups[OBJECTS], true, false, Obj::SCHOOL_OF_WAR); + addGroupIntoCatalog(groups[OBJECTS], true, false, Obj::WAR_MACHINE_FACTORY); + addGroupIntoCatalog(groups[OBJECTS], true, false, Obj::WARRIORS_TOMB); + addGroupIntoCatalog(groups[OBJECTS], true, false, Obj::WITCH_HUT); + addGroupIntoCatalog(groups[OBJECTS], true, false, Obj::SANCTUARY); + addGroupIntoCatalog(groups[OBJECTS], true, false, Obj::MARLETTO_TOWER); + addGroupIntoCatalog(groups[HEROES], true, false, Obj::PRISON); + addGroupIntoCatalog(groups[HEROES], false, false, Obj::HERO); + addGroupIntoCatalog(groups[HEROES], false, false, Obj::RANDOM_HERO); + addGroupIntoCatalog(groups[HEROES], false, false, Obj::HERO_PLACEHOLDER); + addGroupIntoCatalog(groups[HEROES], false, false, Obj::BOAT); + addGroupIntoCatalog(groups[ARTIFACTS], true, false, Obj::ARTIFACT); + addGroupIntoCatalog(groups[ARTIFACTS], false, false, Obj::RANDOM_ART); + addGroupIntoCatalog(groups[ARTIFACTS], false, false, Obj::RANDOM_TREASURE_ART); + addGroupIntoCatalog(groups[ARTIFACTS], false, false, Obj::RANDOM_MINOR_ART); + addGroupIntoCatalog(groups[ARTIFACTS], false, false, Obj::RANDOM_MAJOR_ART); + addGroupIntoCatalog(groups[ARTIFACTS], false, false, Obj::RANDOM_RELIC_ART); + addGroupIntoCatalog(groups[ARTIFACTS], true, false, Obj::SPELL_SCROLL); + addGroupIntoCatalog(groups[ARTIFACTS], true, false, Obj::PANDORAS_BOX); + addGroupIntoCatalog(groups[RESOURCES], true, false, Obj::RANDOM_RESOURCE); + addGroupIntoCatalog(groups[RESOURCES], false, false, Obj::RESOURCE); + addGroupIntoCatalog(groups[RESOURCES], true, false, Obj::SEA_CHEST); + addGroupIntoCatalog(groups[RESOURCES], true, false, Obj::TREASURE_CHEST); + addGroupIntoCatalog(groups[RESOURCES], true, false, Obj::CAMPFIRE); + addGroupIntoCatalog(groups[RESOURCES], true, false, Obj::SHIPWRECK_SURVIVOR); + addGroupIntoCatalog(groups[RESOURCES], true, false, Obj::FLOTSAM); + addGroupIntoCatalog(groups[BANKS], true, false, Obj::CREATURE_BANK); + addGroupIntoCatalog(groups[BANKS], true, false, Obj::DRAGON_UTOPIA); + addGroupIntoCatalog(groups[BANKS], true, false, Obj::CRYPT); + addGroupIntoCatalog(groups[BANKS], true, false, Obj::DERELICT_SHIP); + addGroupIntoCatalog(groups[BANKS], true, false, Obj::PYRAMID); + addGroupIntoCatalog(groups[BANKS], true, false, Obj::SHIPWRECK); + addGroupIntoCatalog(groups[DWELLINGS], true, false, Obj::CREATURE_GENERATOR1); + addGroupIntoCatalog(groups[DWELLINGS], true, false, Obj::CREATURE_GENERATOR2); + addGroupIntoCatalog(groups[DWELLINGS], true, false, Obj::CREATURE_GENERATOR3); + addGroupIntoCatalog(groups[DWELLINGS], true, false, Obj::CREATURE_GENERATOR4); + addGroupIntoCatalog(groups[DWELLINGS], true, false, Obj::REFUGEE_CAMP); + addGroupIntoCatalog(groups[DWELLINGS], false, false, Obj::RANDOM_DWELLING); + addGroupIntoCatalog(groups[DWELLINGS], false, false, Obj::RANDOM_DWELLING_LVL); + addGroupIntoCatalog(groups[DWELLINGS], false, false, Obj::RANDOM_DWELLING_FACTION); + addGroupIntoCatalog(groups[GROUNDS], true, false, Obj::CURSED_GROUND1); + addGroupIntoCatalog(groups[GROUNDS], true, false, Obj::MAGIC_PLAINS1); + addGroupIntoCatalog(groups[GROUNDS], true, false, Obj::CLOVER_FIELD); + addGroupIntoCatalog(groups[GROUNDS], true, false, Obj::CURSED_GROUND2); + addGroupIntoCatalog(groups[GROUNDS], true, false, Obj::EVIL_FOG); + addGroupIntoCatalog(groups[GROUNDS], true, false, Obj::FAVORABLE_WINDS); + addGroupIntoCatalog(groups[GROUNDS], true, false, Obj::FIERY_FIELDS); + addGroupIntoCatalog(groups[GROUNDS], true, false, Obj::HOLY_GROUNDS); + addGroupIntoCatalog(groups[GROUNDS], true, false, Obj::LUCID_POOLS); + addGroupIntoCatalog(groups[GROUNDS], true, false, Obj::MAGIC_CLOUDS); + addGroupIntoCatalog(groups[GROUNDS], true, false, Obj::MAGIC_PLAINS2); + addGroupIntoCatalog(groups[GROUNDS], true, false, Obj::ROCKLANDS); + addGroupIntoCatalog(groups[GROUNDS], true, false, Obj::HOLE); + addGroupIntoCatalog(groups[TELEPORTS], true, false, Obj::MONOLITH_ONE_WAY_ENTRANCE); + addGroupIntoCatalog(groups[TELEPORTS], true, false, Obj::MONOLITH_ONE_WAY_EXIT); + addGroupIntoCatalog(groups[TELEPORTS], true, false, Obj::MONOLITH_TWO_WAY); + addGroupIntoCatalog(groups[TELEPORTS], true, false, Obj::SUBTERRANEAN_GATE); + addGroupIntoCatalog(groups[TELEPORTS], true, false, Obj::WHIRLPOOL); + addGroupIntoCatalog(groups[MINES], true, false, Obj::MINE); + addGroupIntoCatalog(groups[MINES], false, false, Obj::ABANDONED_MINE); + addGroupIntoCatalog(groups[MINES], true, false, Obj::WINDMILL); + addGroupIntoCatalog(groups[MINES], true, false, Obj::WATER_WHEEL); + addGroupIntoCatalog(groups[TRIGGERS], true, false, Obj::EVENT); + addGroupIntoCatalog(groups[TRIGGERS], true, false, Obj::GRAIL); + addGroupIntoCatalog(groups[TRIGGERS], true, false, Obj::SIGN); + addGroupIntoCatalog(groups[TRIGGERS], true, false, Obj::OCEAN_BOTTLE); + addGroupIntoCatalog(groups[MONSTERS], false, false, Obj::MONSTER); + addGroupIntoCatalog(groups[MONSTERS], true, false, Obj::RANDOM_MONSTER); + addGroupIntoCatalog(groups[MONSTERS], true, false, Obj::RANDOM_MONSTER_L1); + addGroupIntoCatalog(groups[MONSTERS], true, false, Obj::RANDOM_MONSTER_L2); + addGroupIntoCatalog(groups[MONSTERS], true, false, Obj::RANDOM_MONSTER_L3); + addGroupIntoCatalog(groups[MONSTERS], true, false, Obj::RANDOM_MONSTER_L4); + addGroupIntoCatalog(groups[MONSTERS], true, false, Obj::RANDOM_MONSTER_L5); + addGroupIntoCatalog(groups[MONSTERS], true, false, Obj::RANDOM_MONSTER_L6); + addGroupIntoCatalog(groups[MONSTERS], true, false, Obj::RANDOM_MONSTER_L7); + addGroupIntoCatalog(groups[QUESTS], true, false, Obj::SEER_HUT); + addGroupIntoCatalog(groups[QUESTS], true, false, Obj::BORDER_GATE); + addGroupIntoCatalog(groups[QUESTS], true, false, Obj::QUEST_GUARD); + addGroupIntoCatalog(groups[QUESTS], true, false, Obj::HUT_OF_MAGI); + addGroupIntoCatalog(groups[QUESTS], true, false, Obj::EYE_OF_MAGI); + addGroupIntoCatalog(groups[QUESTS], true, false, Obj::BORDERGUARD); + addGroupIntoCatalog(groups[QUESTS], true, false, Obj::KEYMASTER); + addGroupIntoCatalog(groups[WOG_OBJECTS], true, false, Obj::WOG_OBJECT); + addGroupIntoCatalog(groups[OBSTACLES], true); + addGroupIntoCatalog(groups[OTHER], false); } catch(const std::exception &) { @@ -1094,7 +1246,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/mainwindow.h b/mapeditor/mainwindow.h index 766500429..ba4fa4db9 100644 --- a/mapeditor/mainwindow.h +++ b/mapeditor/mainwindow.h @@ -28,6 +28,7 @@ class MainWindow : public QMainWindow const QString mainWindowSizeSetting = "MainWindow/Size"; const QString mainWindowPositionSetting = "MainWindow/Position"; const QString lastDirectorySetting = "MainWindow/Directory"; + const QString recentlyOpenedFilesSetting = "MainWindow/RecentlyOpenedFiles"; #ifdef ENABLE_QT_TRANSLATIONS QTranslator translator; @@ -58,6 +59,10 @@ public: private slots: void on_actionOpen_triggered(); + + void on_actionOpenRecent_triggered(); + + void on_menuOpenRecent_aboutToShow(); void on_actionSave_as_triggered(); @@ -153,8 +158,8 @@ public slots: private: void preparePreview(const QModelIndex & index); - void addGroupIntoCatalog(const std::string & groupName, bool staticOnly); - void addGroupIntoCatalog(const std::string & groupName, bool useCustomName, bool staticOnly, int ID); + void addGroupIntoCatalog(const QString & groupName, bool staticOnly); + void addGroupIntoCatalog(const QString & groupName, bool useCustomName, bool staticOnly, int ID); QAction * getActionPlayer(const PlayerColor &); @@ -170,6 +175,8 @@ private: void parseCommandLine(ExtractionOptions & extractionOptions); + void updateRecentMenu(const QString & filenameSelect); + private: Ui::MainWindow * ui; ObjectBrowserProxyModel * objectBrowser = nullptr; diff --git a/mapeditor/mainwindow.ui b/mapeditor/mainwindow.ui index 03a2d32d8..5a3e6b94c 100644 --- a/mapeditor/mainwindow.ui +++ b/mapeditor/mainwindow.ui @@ -58,8 +58,15 @@ File + + + Open Recent + + + + @@ -133,6 +140,7 @@ + @@ -1019,6 +1027,19 @@ Ctrl+O + + + Open Recent + + + + + More... + + + Ctrl+R + + Save diff --git a/mapeditor/mapcontroller.cpp b/mapeditor/mapcontroller.cpp index a5fecd21a..e1f6abfac 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,11 @@ #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" #include "scenelayer.h" @@ -111,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) { @@ -126,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; } @@ -137,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 type = VLC->heroh->objects[nih->subID]; + 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 @@ -164,7 +144,7 @@ void MapController::repairMap(CMap * map) const { nih->removeSpellFromSpellbook(SpellID::SPELLBOOK_PRESET); if(!nih->getArt(ArtifactPosition::SPELLBOOK) && type->haveSpellBook) - nih->putArtifact(ArtifactPosition::SPELLBOOK, ArtifactUtils::createNewArtifactInstance(ArtifactID::SPELLBOOK)); + nih->putArtifact(ArtifactPosition::SPELLBOOK, ArtifactUtils::createArtifact(ArtifactID::SPELLBOOK)); } } @@ -173,10 +153,11 @@ void MapController::repairMap(CMap * map) const { if(tnh->getTown()) { - vstd::erase_if(tnh->builtBuildings, [tnh](BuildingID bid) + for(const auto & building : tnh->getBuildings()) { - return !tnh->getTown()->buildings.count(bid); - }); + if(!tnh->getTown()->buildings.count(building)) + tnh->removeBuilding(building); + } vstd::erase_if(tnh->forbiddenBuildings, [tnh](BuildingID bid) { return !tnh->getTown()->buildings.count(bid); @@ -189,7 +170,7 @@ void MapController::repairMap(CMap * map) const if(art->ID == Obj::SPELL_SCROLL && !art->storedArtifact) { std::vector out; - for(auto spell : VLC->spellh->objects) //spellh size appears to be greater (?) + for(auto const & spell : VLC->spellh->objects) //spellh size appears to be greater (?) { //if(map->isAllowedSpell(spell->id)) { @@ -243,7 +224,7 @@ void MapController::setMap(std::unique_ptr cmap) void MapController::initObstaclePainters(CMap * map) { - for (auto terrain : VLC->terrainTypeHandler->objects) + for (auto const & terrain : VLC->terrainTypeHandler->objects) { auto terrainId = terrain->getId(); _obstaclePainters[terrainId] = std::make_unique(map); @@ -380,9 +361,16 @@ void MapController::pasteFromClipboard(int level) if(_clipboardShiftIndex == int3::getDirs().size()) _clipboardShiftIndex = 0; + QStringList errors; for(auto & objUniquePtr : _clipboard) { auto * obj = CMemorySerializer::deepCopy(*objUniquePtr).release(); + QString errorMsg; + if (!canPlaceObject(level, obj, errorMsg)) + { + errors.push_back(std::move(errorMsg)); + continue; + } auto newPos = objUniquePtr->pos + shift; if(_map->isInTheMap(newPos)) obj->pos = newPos; @@ -393,6 +381,8 @@ void MapController::pasteFromClipboard(int level) _scenes[level]->selectionObjectsView.selectObject(obj); _mapHandler->invalidate(obj); } + if(!errors.isEmpty()) + QMessageBox::warning(main, QObject::tr("Can't place object"), errors.join('\n')); _scenes[level]->objectsView.draw(); _scenes[level]->passabilityView.update(); @@ -439,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); } @@ -562,15 +552,13 @@ 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 = QString("There can be only one grail object on the map"); + error = QObject::tr("There can only be one grail object on the map."); return false; //maplimit reached } if(defaultPlayer == PlayerColor::NEUTRAL && (newObj->ID == Obj::HERO || newObj->ID == Obj::RANDOM_HERO)) { - error = "Hero cannot be created as NEUTRAL"; + error = QObject::tr("Hero %1 cannot be created as NEUTRAL.").arg(QString::fromStdString(newObj->instanceName)); return false; } @@ -612,16 +600,59 @@ ModCompatibilityInfo MapController::modAssessmentAll() ModCompatibilityInfo MapController::modAssessmentMap(const CMap & map) { ModCompatibilityInfo result; + + auto extractEntityMod = [&result](const auto & entity) + { + auto modScope = entity->getModScope(); + if(modScope != "core") + result[modScope] = VLC->modh->getModInfo(modScope).getVerificationInfo(); + }; + for(auto obj : map.objects) { - if(obj->ID == Obj::HERO) - continue; //stub! - auto handler = obj->getObjectHandler(); - auto modName = QString::fromStdString(handler->getJsonKey()).split(":").at(0).toStdString(); - if(modName != "core") - result[modName] = VLC->modh->getModInfo(modName).getVerificationInfo(); + auto modScope = handler->getModScope(); + if(modScope != "core") + result[modScope] = VLC->modh->getModInfo(modScope).getVerificationInfo(); + + if(obj->ID == Obj::TOWN || obj->ID == Obj::RANDOM_TOWN) + { + auto town = dynamic_cast(obj.get()); + for(const auto & spellID : town->possibleSpells) + { + if(spellID == SpellID::PRESET) + continue; + extractEntityMod(spellID.toEntity(VLC)); + } + + for(const auto & spellID : town->obligatorySpells) + { + extractEntityMod(spellID.toEntity(VLC)); + } + } + + if(obj->ID == Obj::HERO || obj->ID == Obj::RANDOM_HERO) + { + auto hero = dynamic_cast(obj.get()); + for(const auto & spellID : hero->getSpellsInSpellbook()) + { + if(spellID == SpellID::PRESET || spellID == SpellID::SPELLBOOK_PRESET) + continue; + extractEntityMod(spellID.toEntity(VLC)); + } + + for(const auto & [_, slotInfo] : hero->artifactsWorn) + { + extractEntityMod(slotInfo.artifact->getTypeId().toEntity(VLC)); + } + + for(const auto & art : hero->artifactsInBackpack) + { + extractEntityMod(art.artifact->getTypeId().toEntity(VLC)); + } + } } + //TODO: terrains? return result; } 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/mapeditorroles.h b/mapeditor/mapeditorroles.h new file mode 100644 index 000000000..ed50a0be9 --- /dev/null +++ b/mapeditor/mapeditorroles.h @@ -0,0 +1,23 @@ +/* + * mapeditorroles.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 "StdInc.h" + +enum MapEditorRoles +{ + TownEventRole = Qt::UserRole + 1, + PlayerIDRole, + BuildingIDRole, + SpellIDRole, + ObjectInstanceIDRole, + ArtifactIDRole, + ArtifactSlotRole, +}; diff --git a/mapeditor/maphandler.cpp b/mapeditor/maphandler.cpp index f5db55ba0..a7f0b2023 100644 --- a/mapeditor/maphandler.cpp +++ b/mapeditor/maphandler.cpp @@ -19,8 +19,6 @@ #include "../lib/mapObjects/CGHeroInstance.h" #include "../lib/mapObjects/ObjectTemplate.h" #include "../lib/mapObjects/MiscObjects.h" -#include "../lib/CHeroHandler.h" -#include "../lib/CTownHandler.h" #include "../lib/GameConstants.h" const int tileSize = 32; @@ -95,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; @@ -112,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); @@ -125,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); @@ -141,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; @@ -380,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); @@ -443,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/maphandler.h b/mapeditor/maphandler.h index 579e5363b..3f8cb5a26 100644 --- a/mapeditor/maphandler.h +++ b/mapeditor/maphandler.h @@ -75,8 +75,8 @@ private: TFlippedAnimations riverAnimations;//[river type, rotation] TFlippedCache riverImages;//[river type, view type, rotation] - std::vector tileObjects; //informations about map tiles - std::map> tilesCache; //set of tiles beloging to object + std::vector tileObjects; //information about map tiles + std::map> tilesCache; //set of tiles belonging to object const CMap * map = nullptr; diff --git a/mapeditor/mapsettings/abstractsettings.cpp b/mapeditor/mapsettings/abstractsettings.cpp index dd8a22e58..a21d2f24b 100644 --- a/mapeditor/mapsettings/abstractsettings.cpp +++ b/mapeditor/mapsettings/abstractsettings.cpp @@ -13,8 +13,6 @@ #include "../mapcontroller.h" #include "../../lib/mapObjects/CGHeroInstance.h" #include "../../lib/mapObjects/CGCreature.h" -#include "../../lib/CTownHandler.h" -#include "../../lib/CHeroHandler.h" #include "../../lib/mapObjects/CGCreature.h" //parses date for lose condition (1m 1w 1d) @@ -116,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/abstractsettings.h b/mapeditor/mapsettings/abstractsettings.h index 53ab653e5..bf729be06 100644 --- a/mapeditor/mapsettings/abstractsettings.h +++ b/mapeditor/mapsettings/abstractsettings.h @@ -14,6 +14,8 @@ #include "../../lib/mapObjects/CGTownInstance.h" #include "../../lib/mapObjects/CGHeroInstance.h" +Q_DECLARE_METATYPE(int3) + //parses date for lose condition (1m 1w 1d) int expiredDate(const QString & date); QString expiredDate(int date); diff --git a/mapeditor/mapsettings/eventsettings.cpp b/mapeditor/mapsettings/eventsettings.cpp index a128aa6ef..a74cf5a21 100644 --- a/mapeditor/mapsettings/eventsettings.cpp +++ b/mapeditor/mapsettings/eventsettings.cpp @@ -16,6 +16,29 @@ #include "../../lib/constants/NumericConstants.h" #include "../../lib/constants/StringConstants.h" +QString toQString(const PlayerColor & player) +{ + return QString::fromStdString(player.toString()); +} + +QVariant toVariant(const std::set & players) +{ + QVariantList result; + for(auto const id : players) + result.push_back(toQString(id)); + return result; +} + +std::set playersFromVariant(const QVariant & v) +{ + std::set result; + + for(auto const & id : v.toList()) + result.insert(PlayerColor(PlayerColor::decode(id.toString().toStdString()))); + + return result; +} + QVariant toVariant(const TResources & resources) { QVariantMap result; @@ -30,7 +53,28 @@ TResources resourcesFromVariant(const QVariant & v) for(auto r : v.toMap().keys()) vJson[r.toStdString()].Integer() = v.toMap().value(r).toInt(); return TResources(vJson); +} +QVariant toVariant(std::vector objects) +{ + QVariantList result; + for(auto obj : objects) + { + result.push_back(QVariant::fromValue(obj.num)); + } + return result; +} + +std::vector deletedObjectsIdsFromVariant(const QVariant & v) +{ + std::vector result; + for(auto idAsVariant : v.toList()) + { + auto id = idAsVariant.value(); + result.push_back(ObjectInstanceID(id)); + } + + return result; } QVariant toVariant(const CMapEvent & event) @@ -38,12 +82,13 @@ QVariant toVariant(const CMapEvent & event) QVariantMap result; result["name"] = QString::fromStdString(event.name); result["message"] = QString::fromStdString(event.message.toString()); - result["players"] = QVariant::fromValue(event.players); + result["players"] = toVariant(event.players); result["humanAffected"] = QVariant::fromValue(event.humanAffected); result["computerAffected"] = QVariant::fromValue(event.computerAffected); - result["firstOccurence"] = QVariant::fromValue(event.firstOccurence); - result["nextOccurence"] = QVariant::fromValue(event.nextOccurence); + result["firstOccurrence"] = QVariant::fromValue(event.firstOccurrence); + result["nextOccurrence"] = QVariant::fromValue(event.nextOccurrence); result["resources"] = toVariant(event.resources); + result["deletedObjectsInstances"] = toVariant(event.deletedObjectsInstances); return QVariant(result); } @@ -53,12 +98,13 @@ CMapEvent eventFromVariant(CMapHeader & mapHeader, const QVariant & variant) auto v = variant.toMap(); result.name = v.value("name").toString().toStdString(); result.message.appendTextID(mapRegisterLocalizedString("map", mapHeader, TextIdentifier("header", "event", result.name, "message"), v.value("message").toString().toStdString())); - result.players = v.value("players").toInt(); + result.players = playersFromVariant(v.value("players")); result.humanAffected = v.value("humanAffected").toInt(); result.computerAffected = v.value("computerAffected").toInt(); - result.firstOccurence = v.value("firstOccurence").toInt(); - result.nextOccurence = v.value("nextOccurence").toInt(); + result.firstOccurrence = v.value("firstOccurrence").toInt(); + result.nextOccurrence = v.value("nextOccurrence").toInt(); result.resources = resourcesFromVariant(v.value("resources")); + result.deletedObjectsInstances = deletedObjectsIdsFromVariant(v.value("deletedObjectsInstances")); return result; } @@ -115,6 +161,6 @@ void EventSettings::on_timedEventRemove_clicked() void EventSettings::on_eventsList_itemActivated(QListWidgetItem *item) { - new TimedEvent(item, parentWidget()); + new TimedEvent(*controller, item, parentWidget()); } diff --git a/mapeditor/mapsettings/eventsettings.h b/mapeditor/mapsettings/eventsettings.h index ef29f0308..c7be81f06 100644 --- a/mapeditor/mapsettings/eventsettings.h +++ b/mapeditor/mapsettings/eventsettings.h @@ -15,6 +15,13 @@ namespace Ui { class EventSettings; } +QString toQString(const PlayerColor & player); +QVariant toVariant(const TResources & resources); +QVariant toVariant(const std::set & players); + +TResources resourcesFromVariant(const QVariant & v); +std::set playersFromVariant(const QVariant & v); + class EventSettings : public AbstractSettings { Q_OBJECT diff --git a/mapeditor/mapsettings/generalsettings.cpp b/mapeditor/mapsettings/generalsettings.cpp index 74f405232..aa7d28206 100644 --- a/mapeditor/mapsettings/generalsettings.cpp +++ b/mapeditor/mapsettings/generalsettings.cpp @@ -29,6 +29,10 @@ void GeneralSettings::initialize(MapController & c) AbstractSettings::initialize(c); ui->mapNameEdit->setText(QString::fromStdString(controller->map()->name.toString())); ui->mapDescriptionEdit->setPlainText(QString::fromStdString(controller->map()->description.toString())); + ui->authorEdit->setText(QString::fromStdString(controller->map()->author.toString())); + ui->authorContactEdit->setText(QString::fromStdString(controller->map()->authorContact.toString())); + ui->mapCreationDateTimeEdit->setDateTime(QDateTime::fromSecsSinceEpoch(controller->map()->creationDateTime)); + ui->mapVersionEdit->setText(QString::fromStdString(controller->map()->mapVersion.toString())); ui->heroLevelLimit->setValue(controller->map()->levelLimit); ui->heroLevelLimitCheck->setChecked(controller->map()->levelLimit); @@ -61,6 +65,10 @@ void GeneralSettings::update() { controller->map()->name = MetaString::createFromTextID(mapRegisterLocalizedString("map", *controller->map(), TextIdentifier("header", "name"), ui->mapNameEdit->text().toStdString())); controller->map()->description = MetaString::createFromTextID(mapRegisterLocalizedString("map", *controller->map(), TextIdentifier("header", "description"), ui->mapDescriptionEdit->toPlainText().toStdString())); + controller->map()->author = MetaString::createFromRawString(ui->authorEdit->text().toStdString()); + controller->map()->authorContact = MetaString::createFromRawString(ui->authorContactEdit->text().toStdString()); + controller->map()->creationDateTime = ui->mapCreationDateTimeEdit->dateTime().toSecsSinceEpoch(); + controller->map()->mapVersion = MetaString::createFromRawString(ui->mapVersionEdit->text().toStdString()); if(ui->heroLevelLimitCheck->isChecked()) controller->map()->levelLimit = ui->heroLevelLimit->value(); else diff --git a/mapeditor/mapsettings/generalsettings.ui b/mapeditor/mapsettings/generalsettings.ui index e9759ff4f..cfa87101f 100644 --- a/mapeditor/mapsettings/generalsettings.ui +++ b/mapeditor/mapsettings/generalsettings.ui @@ -46,6 +46,50 @@ + + + + Author + + + + + + + + + + Author contact (e.g. email) + + + + + + + + + + Map Creation Time + + + + + + + true + + + + + + + Map Version + + + + + + diff --git a/mapeditor/mapsettings/loseconditions.cpp b/mapeditor/mapsettings/loseconditions.cpp index 79ffea12e..9e6229f1d 100644 --- a/mapeditor/mapsettings/loseconditions.cpp +++ b/mapeditor/mapsettings/loseconditions.cpp @@ -11,7 +11,7 @@ #include "loseconditions.h" #include "ui_loseconditions.h" #include "../mapcontroller.h" -#include "../lib/CGeneralTextHandler.h" +#include "../lib/texts/CGeneralTextHandler.h" LoseConditions::LoseConditions(QWidget *parent) : AbstractSettings(parent), @@ -43,7 +43,7 @@ void LoseConditions::initialize(MapController & c) for(auto & s : conditionStringsLose) { - ui->loseComboBox->addItem(QString::fromStdString(s)); + ui->loseComboBox->addItem(tr(s.c_str())); } ui->standardLoseCheck->setChecked(false); diff --git a/mapeditor/mapsettings/mapsettings.cpp b/mapeditor/mapsettings/mapsettings.cpp index 978916e77..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) : @@ -30,7 +30,7 @@ MapSettings::MapSettings(MapController & ctrl, QWidget *parent) : show(); - for(auto objectPtr : VLC->skillh->objects) + for(auto const & objectPtr : VLC->skillh->objects) { auto * item = new QListWidgetItem(QString::fromStdString(objectPtr->getNameTranslated())); item->setData(Qt::UserRole, QVariant::fromValue(objectPtr->getIndex())); @@ -38,7 +38,7 @@ MapSettings::MapSettings(MapController & ctrl, QWidget *parent) : item->setCheckState(controller.map()->allowedAbilities.count(objectPtr->getId()) ? Qt::Checked : Qt::Unchecked); ui->listAbilities->addItem(item); } - for(auto objectPtr : VLC->spellh->objects) + for(auto const & objectPtr : VLC->spellh->objects) { auto * item = new QListWidgetItem(QString::fromStdString(objectPtr->getNameTranslated())); item->setData(Qt::UserRole, QVariant::fromValue(objectPtr->getIndex())); @@ -46,7 +46,7 @@ MapSettings::MapSettings(MapController & ctrl, QWidget *parent) : item->setCheckState(controller.map()->allowedSpells.count(objectPtr->getId()) ? Qt::Checked : Qt::Unchecked); ui->listSpells->addItem(item); } - for(auto objectPtr : VLC->arth->objects) + for(auto const & objectPtr : VLC->arth->objects) { auto * item = new QListWidgetItem(QString::fromStdString(objectPtr->getNameTranslated())); item->setData(Qt::UserRole, QVariant::fromValue(objectPtr->getIndex())); @@ -54,7 +54,7 @@ MapSettings::MapSettings(MapController & ctrl, QWidget *parent) : item->setCheckState(controller.map()->allowedArtifact.count(objectPtr->getId()) ? Qt::Checked : Qt::Unchecked); ui->listArts->addItem(item); } - for(auto objectPtr : VLC->heroh->objects) + for(auto const & objectPtr : VLC->heroh->objects) { auto * item = new QListWidgetItem(QString::fromStdString(objectPtr->getNameTranslated())); item->setData(Qt::UserRole, QVariant::fromValue(objectPtr->getIndex())); 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 a0aec1c17..39b983295 100644 --- a/mapeditor/mapsettings/timedevent.cpp +++ b/mapeditor/mapsettings/timedevent.cpp @@ -10,13 +10,16 @@ #include "StdInc.h" #include "timedevent.h" #include "ui_timedevent.h" +#include "eventsettings.h" +#include "../mapeditorroles.h" #include "../../lib/constants/EntityIdentifiers.h" #include "../../lib/constants/StringConstants.h" -TimedEvent::TimedEvent(QListWidgetItem * t, QWidget *parent) : +TimedEvent::TimedEvent(MapController & c, QListWidgetItem * t, QWidget *parent) : + controller(c), QDialog(parent), - target(t), - ui(new Ui::TimedEvent) + ui(new Ui::TimedEvent), + target(t) { ui->setupUi(this); @@ -27,12 +30,13 @@ TimedEvent::TimedEvent(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->eventFirstOccurance->setValue(params.value("firstOccurence").toInt()); - ui->eventRepeatAfter->setValue(params.value("nextOccurence").toInt()); + ui->eventFirstOccurrence->setValue(params.value("firstOccurrence").toInt() + 1); + ui->eventRepeatAfter->setValue(params.value("nextOccurrence").toInt()); + auto playerList = params.value("players").toList(); for(int i = 0; i < PlayerColor::PLAYER_LIMIT_I; ++i) { - bool isAffected = (1 << i) & params.value("players").toInt(); + bool isAffected = playerList.contains(toQString(PlayerColor(i))); auto * item = new QListWidgetItem(QString::fromStdString(GameConstants::PLAYER_COLOR_NAMES[i])); item->setData(Qt::UserRole, QVariant::fromValue(i)); item->setCheckState(isAffected ? Qt::Checked : Qt::Unchecked); @@ -49,7 +53,14 @@ TimedEvent::TimedEvent(QListWidgetItem * t, QWidget *parent) : nval->setFlags(nval->flags() | Qt::ItemIsEditable); ui->resources->setItem(i, 1, nval); } - + auto deletedObjectInstances = params.value("deletedObjectsInstances").toList(); + for(auto const & idAsVariant : deletedObjectInstances) + { + auto id = ObjectInstanceID(idAsVariant.toInt()); + auto obj = controller.map()->objects[id]; + if(obj) + insertObjectToDelete(obj); + } show(); } @@ -66,15 +77,15 @@ 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["firstOccurence"] = QVariant::fromValue(ui->eventFirstOccurance->value()); - descriptor["nextOccurence"] = QVariant::fromValue(ui->eventRepeatAfter->value()); + descriptor["firstOccurrence"] = QVariant::fromValue(ui->eventFirstOccurrence->value() - 1); + descriptor["nextOccurrence"] = QVariant::fromValue(ui->eventRepeatAfter->value()); - int players = 0; + QVariantList players; for(int i = 0; i < ui->playersAffected->count(); ++i) { auto * item = ui->playersAffected->item(i); if(item->checkState() == Qt::Checked) - players |= 1 << i; + players.push_back(toQString(PlayerColor(i))); } descriptor["players"] = QVariant::fromValue(players); @@ -87,10 +98,63 @@ void TimedEvent::on_TimedEvent_finished(int result) } descriptor["resources"] = res; + QVariantList deletedObjects; + for(int i = 0; i < ui->deletedObjects->count(); ++i) + { + auto const & item = ui->deletedObjects->item(i); + auto data = item->data(MapEditorRoles::ObjectInstanceIDRole); + auto id = ObjectInstanceID(data.value()); + deletedObjects.push_back(QVariant::fromValue(id.num)); + } + descriptor["deletedObjectsInstances"] = QVariant::fromValue(deletedObjects); + target->setData(Qt::UserRole, descriptor); target->setText(ui->eventNameText->text()); } +void TimedEvent::on_addObjectToDelete_clicked() +{ + for(int lvl : {0, 1}) + { + auto & l = controller.scene(lvl)->objectPickerView; + l.highlight(); + l.update(); + QObject::connect(&l, &ObjectPickerLayer::selectionMade, this, &TimedEvent::onObjectPicked); + } + hide(); + dynamic_cast(parent()->parent()->parent()->parent()->parent()->parent()->parent())->hide(); +} + +void TimedEvent::on_removeObjectToDelete_clicked() +{ + delete ui->deletedObjects->takeItem(ui->deletedObjects->currentRow()); +} + +void TimedEvent::onObjectPicked(const CGObjectInstance * obj) +{ + show(); + dynamic_cast(parent()->parent()->parent()->parent()->parent()->parent()->parent())->show(); + + for(int lvl : {0, 1}) + { + auto & l = controller.scene(lvl)->objectPickerView; + l.clear(); + l.update(); + QObject::disconnect(&l, &ObjectPickerLayer::selectionMade, this, &TimedEvent::onObjectPicked); + } + + if(!obj) + return; + insertObjectToDelete(obj); +} + +void TimedEvent::insertObjectToDelete(const CGObjectInstance * obj) +{ + QString objectLabel = QString("%1, x: %2, y: %3, z: %4").arg(QString::fromStdString(obj->getObjectName())).arg(obj->pos.x).arg(obj->pos.y).arg(obj->pos.z); + auto * item = new QListWidgetItem(objectLabel); + item->setData(MapEditorRoles::ObjectInstanceIDRole, QVariant::fromValue(obj->id.num)); + ui->deletedObjects->addItem(item); +} void TimedEvent::on_pushButton_clicked() { diff --git a/mapeditor/mapsettings/timedevent.h b/mapeditor/mapsettings/timedevent.h index 5aab63fc2..d15ccfd02 100644 --- a/mapeditor/mapsettings/timedevent.h +++ b/mapeditor/mapsettings/timedevent.h @@ -11,6 +11,8 @@ #include +#include "mapcontroller.h" + namespace Ui { class TimedEvent; } @@ -20,18 +22,23 @@ class TimedEvent : public QDialog Q_OBJECT public: - explicit TimedEvent(QListWidgetItem *, QWidget *parent = nullptr); + explicit TimedEvent(MapController & map, QListWidgetItem *, QWidget * parent = nullptr); ~TimedEvent(); private slots: void on_TimedEvent_finished(int result); + void on_addObjectToDelete_clicked(); + void on_removeObjectToDelete_clicked(); + void onObjectPicked(const CGObjectInstance * obj); + void insertObjectToDelete(const CGObjectInstance * obj); void on_pushButton_clicked(); - void on_resources_itemDoubleClicked(QTableWidgetItem *item); + void on_resources_itemDoubleClicked(QTableWidgetItem * item); private: - Ui::TimedEvent *ui; + MapController & controller; + Ui::TimedEvent * ui; QListWidgetItem * target; }; diff --git a/mapeditor/mapsettings/timedevent.ui b/mapeditor/mapsettings/timedevent.ui index 92ac597ab..59ae81f48 100644 --- a/mapeditor/mapsettings/timedevent.ui +++ b/mapeditor/mapsettings/timedevent.ui @@ -9,8 +9,8 @@ 0 0 - 620 - 371 + 730 + 422 @@ -67,12 +67,16 @@ - Day of first occurance + Day of first occurrence - + + + 1 + + @@ -197,6 +201,34 @@ + + + + + + + + Objects to delete + + + + + + + Add + + + + + + + Remove + + + + + + diff --git a/mapeditor/mapsettings/translations.cpp b/mapeditor/mapsettings/translations.cpp index dd3f6e234..8a3ba6490 100644 --- a/mapeditor/mapsettings/translations.cpp +++ b/mapeditor/mapsettings/translations.cpp @@ -11,8 +11,8 @@ #include "StdInc.h" #include "translations.h" #include "ui_translations.h" -#include "../../lib/Languages.h" -#include "../../lib/CGeneralTextHandler.h" +#include "../../lib/texts/Languages.h" +#include "../../lib/texts/CGeneralTextHandler.h" #include "../../lib/mapObjects/CGObjectInstance.h" #include "../../lib/VCMI_Lib.h" diff --git a/mapeditor/mapsettings/translations.ui b/mapeditor/mapsettings/translations.ui index 41b6b8578..62c47bb3b 100644 --- a/mapeditor/mapsettings/translations.ui +++ b/mapeditor/mapsettings/translations.ui @@ -45,7 +45,7 @@ - Suppported + Supported diff --git a/mapeditor/mapsettings/victoryconditions.cpp b/mapeditor/mapsettings/victoryconditions.cpp index 9f84bc4d4..d77e2c135 100644 --- a/mapeditor/mapsettings/victoryconditions.cpp +++ b/mapeditor/mapsettings/victoryconditions.cpp @@ -11,11 +11,13 @@ #include "victoryconditions.h" #include "ui_victoryconditions.h" #include "../mapcontroller.h" -#include "../lib/CGeneralTextHandler.h" -#include "../lib/constants/StringConstants.h" -#include "../../lib/mapObjects/CGCreature.h" -#include "../inspector/townbulidingswidget.h" //to convert BuildingID to string +#include "../../lib/constants/StringConstants.h" +#include "../../lib/entities/faction/CTownHandler.h" +#include "../../lib/mapObjects/CGCreature.h" +#include "../../lib/texts/CGeneralTextHandler.h" + +#include "../inspector/townbuildingswidget.h" //to convert BuildingID to string VictoryConditions::VictoryConditions(QWidget *parent) : AbstractSettings(parent), @@ -46,7 +48,7 @@ void VictoryConditions::initialize(MapController & c) for(auto & s : conditionStringsWin) { - ui->victoryComboBox->addItem(QString::fromStdString(s)); + ui->victoryComboBox->addItem(tr(s.c_str())); } ui->standardVictoryCheck->setChecked(false); ui->onlyForHumansCheck->setChecked(false); diff --git a/mapeditor/mapview.cpp b/mapeditor/mapview.cpp index 5e37b48ff..29aeb9aa4 100644 --- a/mapeditor/mapview.cpp +++ b/mapeditor/mapview.cpp @@ -16,6 +16,7 @@ #include "../lib/mapObjectConstructors/AObjectTypeHandler.h" #include "../lib/mapObjectConstructors/CObjectClassesHandler.h" #include "../lib/mapping/CMap.h" +#include "../lib/VCMI_Lib.h" MinimapView::MinimapView(QWidget * parent): @@ -363,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 226b3249a..bbf32ad56 100644 --- a/mapeditor/playerparams.cpp +++ b/mapeditor/playerparams.cpp @@ -12,9 +12,8 @@ #include "playerparams.h" #include "ui_playerparams.h" #include "mapsettings/abstractsettings.h" -#include "../lib/CTownHandler.h" #include "../lib/constants/StringConstants.h" - +#include "../lib/entities/faction/CTownHandler.h" #include "../lib/mapping/CMap.h" PlayerParams::PlayerParams(MapController & ctrl, int playerId, QWidget *parent) : @@ -25,7 +24,7 @@ PlayerParams::PlayerParams(MapController & ctrl, int playerId, QWidget *parent) ui->setupUi(this); //set colors and teams - ui->teamId->addItem("No team", QVariant(TeamID::NO_TEAM)); + ui->teamId->addItem(tr("No team"), QVariant(TeamID::NO_TEAM)); for(int i = 0, index = 0; i < PlayerColor::PLAYER_LIMIT_I; ++i) { if(i == playerId || !controller.map()->players[i].canAnyonePlay()) @@ -48,7 +47,7 @@ PlayerParams::PlayerParams(MapController & ctrl, int playerId, QWidget *parent) //load factions for(auto idx : VLC->townh->getAllowedFactions()) { - const CFaction * faction = VLC->townh->objects.at(idx); + const auto & faction = VLC->townh->objects.at(idx); auto * item = new QListWidgetItem(QString::fromStdString(faction->getNameTranslated())); item->setData(Qt::UserRole, QVariant::fromValue(idx.getNum())); item->setFlags(item->flags() | Qt::ItemIsUserCheckable); @@ -78,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/resourceExtractor/ResourceConverter.h b/mapeditor/resourceExtractor/ResourceConverter.h index faef5abdd..fc3daf221 100644 --- a/mapeditor/resourceExtractor/ResourceConverter.h +++ b/mapeditor/resourceExtractor/ResourceConverter.h @@ -14,7 +14,7 @@ struct ConversionOptions { bool splitDefs = false; // splits TwCrPort, CPRSMALL, FlagPort, ITPA, ITPt, Un32 and Un44 into individual PNG's bool convertPcxToPng = false; // converts single Images (found in Images folder) from .pcx to png. - bool deleteOriginals = false; // delete original files, for the ones splitted / converted. + bool deleteOriginals = false; // delete original files, for the ones split / converted. }; // Struct for holding all Resource Extractor / Converter options diff --git a/mapeditor/resources.qrc b/mapeditor/resources.qrc index fc1b3bb74..e6b28c64c 100644 --- a/mapeditor/resources.qrc +++ b/mapeditor/resources.qrc @@ -5,6 +5,7 @@ icons/brush-4.png icons/document-new.png icons/document-open.png + icons/document-open-recent.png icons/document-save.png icons/edit-clear.png icons/edit-copy.png @@ -42,4 +43,4 @@ icons/zoom_plus.png icons/zoom_zero.png - \ No newline at end of file + diff --git a/mapeditor/scenelayer.cpp b/mapeditor/scenelayer.cpp index 2bf5fd170..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); } @@ -639,7 +639,7 @@ void MinimapLayer::update() pixmap.reset(new QPixmap(map->width, map->height)); QPainter painter(pixmap.get()); - //coordinate transfomation + //coordinate transformation for(int j = 0; j < map->height; ++j) { for(int i = 0; i < map->width; ++i) diff --git a/mapeditor/scenelayer.h b/mapeditor/scenelayer.h index fb640de22..468f29697 100644 --- a/mapeditor/scenelayer.h +++ b/mapeditor/scenelayer.h @@ -83,7 +83,7 @@ public: const std::set & selection() const; signals: - void selectionMade(bool anythingSlected); + void selectionMade(bool anythingSelected); private: std::set area; @@ -196,7 +196,7 @@ public: SelectionMode selectionMode = SelectionMode::NOTHING; signals: - void selectionMade(bool anythingSlected); + void selectionMade(bool anythingSelected); private: std::set selectedObjects; diff --git a/mapeditor/translation/chinese.ts b/mapeditor/translation/chinese.ts index 8621ba8ab..6c4676775 100644 --- a/mapeditor/translation/chinese.ts +++ b/mapeditor/translation/chinese.ts @@ -19,6 +19,30 @@ 紧凑队形 + + ArtifactWidget + + + + Artifact + 宝物 + + + + Equip where: + 装备位置: + + + + Save + 保存 + + + + Cancel + 取消 + + EventSettings @@ -42,7 +66,7 @@ 移除 - + New event 新事件 @@ -65,12 +89,32 @@ 地图描述 - + + Author + 作者 + + + + Author contact (e.g. email) + 作者联系方式(例如电子邮件) + + + + Map Creation Time + 地图创建时间 + + + + Map Version + 地图版本 + + + Limit maximum heroes level 限制英雄最大等级 - + Difficulty 难度 @@ -83,6 +127,34 @@ 生成地图中 + + HeroArtifactsWidget + + + Artifacts + 宝物 + + + + Add + 添加 + + + + Remove + 移除 + + + + Slot + 装备槽 + + + + Artifact + 宝物 + + HeroSkillsWidget @@ -223,423 +295,519 @@ 文件 - + + + Open Recent + 打开最近 + + + Map 地图 - + Edit 编辑 - + View 视图 - + Player 玩家 - + Toolbar 工具栏 - + Minimap 小地图 - + Map Objects View 地图物体视图 - + Browser 浏览器 - + Inspector 检视器 - + Property 属性 - + Value - + Tools 工具 - + Painting 绘制 - + Terrains 地形 - + Roads 道路 - + Rivers 河流 - + Preview 预览 - + Open 打开 - + + More... + 更多 + + + Save 保存 - + New 新建 - + Save as... 另存为 - + Ctrl+Shift+S Ctrl+Shift+S - + U/G 地上/地下 - - + + View underground 查看地下 - + Pass 可通行 - + Cut 剪切 - + Copy 复制 - + Paste 粘贴 - + Fill 填充 - + Fills the selection with obstacles 填充障碍物到选定区域 - + Grid 网格 - + General 通用 - + Map title and description 地图标题与描述 - + Players settings 玩家设置 - - + + Undo 撤销 - + Redo 重做 - + Erase 擦除 - + Neutral 中立 - + Validate 有效性验证 - - - - + + + + Update appearance 更新外观 - + Recreate obstacles 重建障碍物 - + Player 1 玩家1 - + Player 2 玩家2 - + Player 3 玩家3 - + Player 4 玩家4 - + Player 5 玩家5 - + Player 6 玩家6 - + Player 7 玩家7 - + Player 8 玩家8 - + Export as... 导出为 - + Translations 翻译 - + Ctrl+T Ctrl+T - - + + h3m converter h3m转换器 - + Lock 锁定 - + Lock objects on map to avoid unnecessary changes 锁定地图上的物体防止误操作 - + Ctrl+L Ctrl+L - + Unlock 解锁 - + Unlock all objects on the map 解锁地图上的所有物体 - + Ctrl+Shift+L Ctrl+Shift+L - + Zoom in 放大 - + Ctrl+= Ctrl+= - + Zoom out 缩小 - + Ctrl+- Ctrl+- - + Zoom reset 重置缩放 - + Ctrl+Shift+= 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) - + + Recently Opened Files + 最近打开文件 + + + Save map 保存地图 - + VCMI maps (*.vmap) VCMI地图(*.vmap) - + Type 类型 - + + Towns + 城镇 + + + + Objects + 物体 + + + + Heroes + 英雄 + + + + Artifacts + 宝物 + + + + Resources + 资源 + + + + Banks + 宝屋 + + + + Dwellings + 巢穴 + + + + Grounds + 地面 + + + + Teleports + 传送门 + + + + Mines + 矿井 + + + + Triggers + 触发器 + + + + Monsters + 怪物 + + + + Quests + 任务 + + + + Wog Objects + Wog物体 + + + + Obstacles + 障碍物 + + + + Other + 其他 + + + 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 转换地图失败,操作终止 @@ -715,7 +883,7 @@ MapView - + Can't place object 无法放置物体 @@ -824,7 +992,12 @@ (默认) - + + No team + 无队伍 + + + Player ID: %1 玩家ID: %1 @@ -889,41 +1062,70 @@ 高级 - + 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 无法在中立阵营被创建。 + QuestWidget @@ -1048,12 +1250,12 @@ 玩家 - + None - + Day %1 第 %1 日 @@ -1321,18 +1523,18 @@ 玩家 - + None - + Day %1 %1 天 - - + + Reward %1 奖励 %1 @@ -1394,47 +1596,264 @@ - Day of first occurance + Day of first occurrence 首次发生天数 - + Repeat after (0 = no repeat) 重复周期 (0 = 不重复) - + Affected players 生效玩家 - + Resources 资源 - + type 类型 - + qty 数量 - + + Objects to delete + 待移除的物体 + + + + Add + 添加 + + + + Remove + 移除 + + + Ok 确定 - TownBulidingsWidget + TownBuildingsWidget - + Buildings 建筑 + + + Build all + 建造所有建筑 + + + + Demolish all + 拆除所有建筑 + + + + Enable all + 允许所有建筑 + + + + Disable all + 禁止所有建筑 + + + + Type + 类型 + + + + Enabled + 已允许 + + + + Built + 已建造 + + + + TownEventDialog + + + Town event + 城镇事件 + + + + General + 通用 + + + + Event name + 事件名 + + + + Type event message text + 输入事件信息文本 + + + + Day of first occurrence + 首次发生天数 + + + + Repeat after (0 = no repeat) + 重复周期 (0 = 不重复) + + + + Affected players + 生效玩家 + + + + affects human + 人类玩家生效 + + + + affects AI + AI玩家生效 + + + + Resources + 资源 + + + + Buildings + 建筑 + + + + Creatures + 生物 + + + + OK + 确定 + + + + Creature level %1 / Creature level %1 Upgrade + %1级生物 / 升级后的%1级生物 + + + + Day %1 - %2 + %1 - %2 日 + + + + TownEventsWidget + + + Town events + 城镇事件 + + + + Timed events + 计时事件 + + + + Add + 添加 + + + + Remove + 移除 + + + + Day %1 - %2 + %1 - %2 日 + + + + New event + 新事件 + + + + TownSpellsWidget + + + Spells + 魔法 + + + + Customize spells + 自定义魔法 + + + + Level 1 + 1级 + + + + + + + + Spell that may appear in mage guild + 允许出现在魔法行会的魔法 + + + + + + + + Spell that must appear in mage guild + 必须出现在魔法行会的魔法 + + + + Level 2 + 2级 + + + + Level 3 + 3级 + + + + Level 4 + 4级 + + + + Level 5 + 5级 + Translations @@ -1450,7 +1869,7 @@ - Suppported + Supported 已支持 @@ -1493,107 +1912,107 @@ 未加载地图 - + No factions allowed for player %1 玩家 %1 没有可使用的种族 - + No players allowed to play this map 该地图未设置任何玩家 - + Map is allowed for one player and cannot be started 该地图只有一个玩家,无法开始游戏 - + No human players allowed to play this map 该地图缺少人类玩家 - + Armored instance %1 is UNFLAGGABLE but must have NEUTRAL or player owner 部队实例 %1 没有旗帜,需要设置为中立或任一玩家所有 - + Object %1 is assigned to non-playable player %2 物体 %1 属于无法游戏的玩家 %2 - - Town %1 has undefined owner %2 - 城镇 %1 拥有者 %2 未定义 - - - - Prison %1 must be a NEUTRAL - 监狱 %1 需要为中立 - - - - Hero %1 must have an owner - 英雄 %1 需要有一个所有者 - - - - Hero %1 is prohibited by map settings - 英雄 %1 被地图设置禁止 - - - - Hero %1 has duplicate on map - 英雄 %1 在地图上有多个 - - - - Hero %1 has an empty type and must be removed - 英雄 %1 属于空类型,必须移除 - - - - Spell scroll %1 is prohibited by map settings - 魔法卷轴 %1 被地图设置禁止 - - - + Spell scroll %1 doesn't have instance assigned and must be removed - 魔法卷轴 %1 未和任一实例关联,需要被移除 + 魔法卷轴 %1 没有分配任何实例,需要被移除 - + Artifact %1 is prohibited by map settings 宝物 %1 被地图设置禁止 - + + Player %1 has no towns and heroes assigned + 玩家 %1 没有分配任何城镇或英雄 + + + + Prison %1 must be a NEUTRAL + 监狱 %1 需要为中立 + + + + Hero %1 must have an owner + 英雄 %1 需要有一个所有者 + + + + Hero %1 is prohibited by map settings + 英雄 %1 被地图设置禁止 + + + + Hero %1 has duplicate on map + 英雄 %1 在地图上有多个 + + + + Hero %1 has an empty type and must be removed + 英雄 %1 属于空类型,必须移除 + + + + Spell scroll %1 is prohibited by map settings + 魔法卷轴 %1 被地图设置禁止 + + + Player %1 doesn't have any starting town 玩家 %1 没有一座起始城镇 - + Map name is not specified 地图名字为空 - + Map description is not specified 地图描述为空 - + Map contains object from mod "%1", but doesn't require it 地图包含模组 %1 的物体,但没有声明依赖该模组 - + Exception occurs during validation: %1 在验证地图期间发生异常:%1 - + Unknown exception occurs during validation 在验证地图期间发生未知异常 @@ -1626,47 +2045,47 @@ 参数 - + No special victory 无特殊胜利条件 - + Capture artifact 获取宝物 - + Hire creatures 雇佣生物 - + Accumulate resources 获得资源 - + Construct building 建造神器 - + Capture town 占领城镇 - + Defeat hero 击败英雄 - + Transport artifact 运送宝物 - + Kill monster 击杀怪物 @@ -1698,18 +2117,6 @@ Width 宽度 - - S (36x36) - 小(36x36) - - - M (72x72) - 中(72x72) - - - L (108x108) - 大(108x108) - XL (144x144) @@ -1726,7 +2133,7 @@ 玩家 - + 0 0 @@ -1825,42 +2232,62 @@ 岛屿 - + + Roads + 道路 + + + + Dirt + 泥土 + + + + Gravel + 砂砾 + + + + Cobblestone + 鹅卵石 + + + Template 模版 - + Custom seed 自定义种子 - + Generate random map 生成随机地图 - + Ok 确定 - + Cancel 取消 - + No template 缺少模版 - - No template for parameters scecified. Random map cannot be generated. + + No template for parameters specified. Random map cannot be generated. 未指定任一模版作为参数,随机地图无法生成。 - + RMG failure 随机地图生成失败 @@ -1868,27 +2295,27 @@ main - + Filepath of the map to open. 要打开的地图所在的文件路径。 - + Extract original H3 archives into a separate folder. 将原始H3文件解压到特定目录。 - + From an extracted archive, it Splits TwCrPort, CPRSMALL, FlagPort, ITPA, ITPt, Un32 and Un44 into individual PNG's. 数据文件解压后,将TwCrPort, CPRSMALL, FlagPort, ITPA, ITPt, Un32 und Un44切分为独立的PNG文件。 - + From an extracted archive, Converts single Images (found in Images folder) from .pcx to png. 数据文件解压后,将每一张图片(Images目录)从pcx格式转化为png格式。 - + Delete original files, for the ones split / converted. 当切分/转换完成后,原始文件将被删除。 diff --git a/mapeditor/translation/czech.ts b/mapeditor/translation/czech.ts index dbe0e096f..515dae06a 100644 --- a/mapeditor/translation/czech.ts +++ b/mapeditor/translation/czech.ts @@ -19,6 +19,30 @@ Úzká formace + + ArtifactWidget + + + + Artifact + Artefakt + + + + Equip where: + Umístění: + + + + Save + Uložit + + + + Cancel + Zrušit + + EventSettings @@ -42,7 +66,7 @@ Odebrat - + New event Nová událost @@ -65,12 +89,32 @@ Popis mapy - + + Author + Autor + + + + Author contact (e.g. email) + Kontakt na autora (např. email) + + + + Map Creation Time + Čas vytvoření mapy + + + + Map Version + Verze mapy + + + Limit maximum heroes level Omezit max. úroveň hrdinů - + Difficulty Obtížnost @@ -83,6 +127,34 @@ Generování mapy + + HeroArtifactsWidget + + + Artifacts + Artefakty + + + + Add + Přidat + + + + Remove + Odebrat + + + + Slot + Slot + + + + Artifact + Artefakt + + HeroSkillsWidget @@ -223,423 +295,519 @@ Soubor - + + + Open Recent + Otevřít poslední + + + Map Mapa - + Edit Upravit - + View Zobrazit - + Player Hráč - + Toolbar Panel nástrojů - + Minimap Minimapa - + Map Objects View Zobrazení objektů mapy - + Browser Prohlížeč - + Inspector Inspektor - + Property Vlastnost - + Value Hodnota - + Tools Nástroje - + Painting Malování - + Terrains Krajiny - + Roads Cesty - + Rivers Řeky - + Preview Náhled - + Open Otevřít - + + More... + Více... + + + Save Uložit - + New Nový - + Save as... Uložit jako... - + Ctrl+Shift+S Ctrl+Shift+S - + U/G P/Z - - + + View underground Zobrazit podzemí - + Pass Průchodnost - + Cut Vyjmout - + Copy Kopírovat - + Paste Vložit - + Fill Vyplnit - + Fills the selection with obstacles Vyplní výběr překážkami - + Grid Mřížka - + General Všeobecné - + Map title and description Název a popis mapy - + Players settings Hráčské nastavení - - + + Undo Zpět - + Redo Znovu - + Erase Smazat - + Neutral Neutrální - + Validate - Posoudit + Ověřit - - - - + + + + Update appearance Aktualizovat vzhled - + Recreate obstacles Přetvořit překážky - + Player 1 Hráč 1 - + Player 2 Hráč 2 - + Player 3 Hráč 3 - + Player 4 Hráč 4 - + Player 5 Hráč 5 - + Player 6 Hráč 6 - + Player 7 Hráč 7 - + Player 8 Hráč 8 - + Export as... Exportovat jako... - + Translations Překlady - + Ctrl+T Ctrl+T - - + + h3m converter Převodník h3m - + Lock Zamknout - + Lock objects on map to avoid unnecessary changes Zamknout objekty na mapě pro zabránění nadbytečných změn - + Ctrl+L Ctrl+L - + Unlock Odemknout - + Unlock all objects on the map Odemknout objekty na mapě - + Ctrl+Shift+L Ctrl+Shift+L - + Zoom in Přiblížit - + Ctrl+= Ctrl+= - + Zoom out Oddálit - + Ctrl+- Ctrl+- - + Zoom reset Zrušit přiblížení - + Ctrl+Shift+= 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) - + + Recently Opened Files + Naposledny otevřené soubory + + + Save map Uložit mapu - + VCMI maps (*.vmap) Mapy VCMI (*.vmap) - + Type Druh - + + Towns + Města + + + + Objects + Objekty + + + + Heroes + Hrdinové + + + + Artifacts + Artefakty + + + + Resources + Suroviny + + + + Banks + Zásobárny + + + + Dwellings + Obydlí + + + + Grounds + Země + + + + Teleports + Teleporty + + + + Mines + Doly + + + + Triggers + Spouštěče + + + + Monsters + Jednotky + + + + Quests + Úkoly + + + + Wog Objects + WoG objekty + + + + Obstacles + Překážky + + + + Other + Ostatní + + + 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 @@ -715,7 +883,7 @@ MapView - + Can't place object Nelze umístit objekt @@ -768,12 +936,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 @@ -816,7 +984,7 @@ Generate hero at main - + Vytvořit hrdinu v hlavním městě @@ -824,7 +992,12 @@ (výchozí) - + + No team + Bez týmu + + + Player ID: %1 ID hráče: %1 @@ -857,7 +1030,7 @@ Portrait - + Portrét @@ -889,41 +1062,72 @@ 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 + + + + 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Í. + QuestWidget @@ -1048,12 +1252,12 @@ Hráči - + None Žádný - + Day %1 Den %1 @@ -1094,7 +1298,7 @@ On select text - + Text při výběru @@ -1175,7 +1379,7 @@ Overflow - + Přetečení @@ -1321,18 +1525,18 @@ Hráči - + None Žádný - + Day %1 Den %1 - - + + Reward %1 Odměna %1 @@ -1394,47 +1598,264 @@ - Day of first occurance + Day of first occurrence 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í - + + Objects to delete + Objekty k odstranění + + + + Add + Přidat + + + + Remove + Odebrat + + + Ok Dobře - TownBulidingsWidget + TownBuildingsWidget - + Buildings Budovy + + + Build all + Postavit vše + + + + Demolish all + Zbořit vše + + + + Enable all + Povolit vše + + + + Disable all + Zakázat vše + + + + Type + Typ + + + + Enabled + Povoleno + + + + Built + Postaveno + + + + TownEventDialog + + + Town event + Událost ve městě + + + + General + Hlavní + + + + Event name + Název události + + + + Type event message text + Zadejte text události + + + + Day of first occurrence + Den prvního výskytu + + + + Repeat after (0 = no repeat) + Opakovat po (0 = bez opakováí) + + + + Affected players + Ovlivnění hráči + + + + affects human + ovlivňuje lidi + + + + affects AI + ovlivňuje AI + + + + Resources + Zdroje + + + + Buildings + Budovy + + + + Creatures + Jednotky + + + + OK + OK + + + + Creature level %1 / Creature level %1 Upgrade + Úroveň jednotky %1 / Úroveň jednotky%1 vylepšení + + + + Day %1 - %2 + Den %1 - %2 + + + + TownEventsWidget + + + Town events + Události ve městě + + + + Timed events + Načasované události + + + + Add + Přidat + + + + Remove + Odebrat + + + + Day %1 - %2 + Den %1 - %2 + + + + New event + Nová událost + + + + TownSpellsWidget + + + Spells + Kouzla + + + + Customize spells + Přizpůsobit kouzla + + + + Level 1 + 1. stupeň + + + + + + + + Spell that may appear in mage guild + Kouzlo, které se může objevit ve věži kouzel + + + + + + + + Spell that must appear in mage guild + Kouzlo, které se musí objevit ve věži kouzel + + + + Level 2 + 2. stupeň + + + + Level 3 + 3. stupeň + + + + Level 4 + 4. stupeň + + + + Level 5 + 5. stupeň + Translations @@ -1450,7 +1871,7 @@ - Suppported + Supported Podporovaný @@ -1485,7 +1906,7 @@ Map validation results - Výsledky posudku mapy + Výsledky ověření mapy @@ -1493,107 +1914,107 @@ Mapa není načtena - + No factions allowed for player %1 - - - - - No players allowed to play this map - Žádní hráči nejsou dovoleni hrát tuto mapu + Pro hráče %1 nejsou povoleny žádné frakce - 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 + No players allowed to play this map + Tato mapa neumožňuje hru žádnému hráči + Map is allowed for one player and cannot be started + 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 - + Object %1 is assigned to non-playable player %2 Objekt %1 je přiřazen nehratelnému hráči %2 - - Town %1 has undefined owner %2 - Město %1 nemá definovaného vlastníka %2 - - - - Prison %1 must be a NEUTRAL - Vězení %1 musí být NEUTRÁLNÍ - - - - Hero %1 must have an owner - Hrdina %1 musí mít vlastníka - - - - Hero %1 is prohibited by map settings - Hrdina %1 je zakázaný nastavením mapy - - - - Hero %1 has duplicate on map - Hrdina %1 má na mapě dvojníka - - - - Hero %1 has an empty type and must be removed - - - - - Spell scroll %1 is prohibited by map settings - Kouzlo %1 je zakázáno nastavením mapy - - - + 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 + + + + Prison %1 must be a NEUTRAL + Vězení %1 musí být NEUTRÁLNÍ + + + + Hero %1 must have an owner + Hrdina %1 musí mít vlastníka + + + + Hero %1 is prohibited by map settings + Hrdina %1 je zakázaný nastavením mapy + + + + Hero %1 has duplicate on map + Hrdina %1 má na mapě dvojníka + + + + Hero %1 has an empty type and must be removed + Hrdina %1 nemá přiřazený typ a musí být odstraněn + + + + Spell scroll %1 is prohibited by map settings + Kouzlo %1 je zakázáno nastavením mapy + + + Player %1 doesn't have any starting town Hráč %1 nemá žádné počáteční město - + Map name is not specified Mapa nemá název - + Map description is not specified Mapa nemá popis - + Map contains object from mod "%1", but doesn't require it Mapa obsahuje objekt z modifikace "%1". ale nevyžaduje ji - + Exception occurs during validation: %1 Při posudku nastala výjimka: %1 - + Unknown exception occurs during validation Nasta neznámá výjimka při posudku @@ -1626,47 +2047,47 @@ Parametry - + No special victory Bez speciálního vítězství - + Capture artifact Získat artefakt - + Hire creatures Najmout bojovníky - + Accumulate resources Nashromáždit zdroje - + Construct building Postavit budovu - + Capture town Získat město - + Defeat hero Porazit hrdinu - + Transport artifact Přesunout artefakt - + Kill monster Zabít příšeru @@ -1686,7 +2107,7 @@ Two level map - Dvě úrovně + Dvouvrstvá mapa @@ -1698,18 +2119,6 @@ Width Šířka - - S (36x36) - S (36x36) - - - M (72x72) - M (72x72) - - - L (108x108) - L (108x108) - XL (144x144) @@ -1726,7 +2135,7 @@ Hráči - + 0 0 @@ -1791,7 +2200,7 @@ Monster strength - Síla příšer + Síla jednotek @@ -1812,7 +2221,7 @@ Water content - Obsah vody + Vodní obsah @@ -1825,42 +2234,62 @@ Ostrovy - + + Roads + Cesty + + + + Dirt + Hlína + + + + Gravel + Štěrk + + + + Cobblestone + Dlažba + + + Template Šablona - + Custom seed Vlastní semínko - + Generate random map Vygenerovat náhodnou mapu - + Ok Dobře - + Cancel Zrušit - + No template Bez šablony - - No template for parameters scecified. Random map cannot be generated. + + No template for parameters specified. Random map cannot be generated. Žádná šablona pro vybrané parametry. Náhodná mapa nemůže být vygenerována. - + RMG failure Chyba RMG @@ -1868,29 +2297,29 @@ main - + Filepath of the map to open. - Cesta k souboru mapy pro otevření. + Cesta k souboru mapy, kterou chcete otevřít. - + Extract original H3 archives into a separate folder. Rozbalit originální archivy H3 do zvláštní složky. - + From an extracted archive, it Splits TwCrPort, CPRSMALL, FlagPort, ITPA, ITPt, Un32 and Un44 into individual PNG's. Z rozbaleného archivu rozdělí TwCrPort, CPRSMALL, FlagPort, ITPA, ITPt, Un32 a Un44 do jednotlivých PNG. - + From an extracted archive, Converts single Images (found in Images folder) from .pcx to png. Z rozbaleného archivu převede jednoduché obrázky (nalezené ve složce Images) z .pcx do png. - + 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 ce36c9f07..34c19c69d 100644 --- a/mapeditor/translation/english.ts +++ b/mapeditor/translation/english.ts @@ -19,6 +19,30 @@ + + ArtifactWidget + + + + Artifact + + + + + Equip where: + + + + + Save + + + + + Cancel + + + EventSettings @@ -42,7 +66,7 @@ - + New event @@ -65,12 +89,32 @@ - + + Author + + + + + Author contact (e.g. email) + + + + + Map Creation Time + + + + + Map Version + + + + Limit maximum heroes level - + Difficulty @@ -83,6 +127,34 @@ + + HeroArtifactsWidget + + + Artifacts + + + + + Add + + + + + Remove + + + + + Slot + + + + + Artifact + + + HeroSkillsWidget @@ -223,423 +295,519 @@ - + + + Open Recent + + + + Map - + Edit - + View - + Player - + Toolbar - + Minimap - + Map Objects View - + Browser - + Inspector - + Property - + Value - + Tools - + Painting - + Terrains - + Roads - + Rivers - + Preview - + Open - + + More... + + + + Save - + New - + Save as... - + Ctrl+Shift+S - + U/G - - + + View underground - + Pass - + Cut - + Copy - + Paste - + Fill - + Fills the selection with obstacles - + Grid - + General - + Map title and description - + Players settings - - + + Undo - + Redo - + Erase - + Neutral - + Validate - - - - + + + + Update appearance - + Recreate obstacles - + Player 1 - + Player 2 - + Player 3 - + Player 4 - + Player 5 - + Player 6 - + Player 7 - + Player 8 - + Export as... - + Translations - + Ctrl+T - - + + h3m converter - + Lock - + Lock objects on map to avoid unnecessary changes - + Ctrl+L - + Unlock - + Unlock all objects on the map - + Ctrl+Shift+L - + Zoom in - + Ctrl+= - + Zoom out - + Ctrl+- - + Zoom reset - + Ctrl+Shift+= - + Confirmation - + Unsaved changes will be lost, are you sure? - + Open map - + All supported maps (*.vmap *.h3m);;VCMI maps(*.vmap);;HoMM3 maps(*.h3m) - + + Recently Opened Files + + + + Save map - + VCMI maps (*.vmap) - + Type - + + Towns + + + + + Objects + + + + + Heroes + + + + + Artifacts + + + + + Resources + + + + + Banks + + + + + Dwellings + + + + + Grounds + + + + + Teleports + + + + + Mines + + + + + Triggers + + + + + Monsters + + + + + Quests + + + + + Wog Objects + + + + + Obstacles + + + + + Other + + + + 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 @@ -715,7 +883,7 @@ MapView - + Can't place object @@ -824,7 +992,12 @@ - + + No team + + + + Player ID: %1 @@ -889,41 +1062,71 @@ - + 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. + + QuestWidget @@ -1048,12 +1251,12 @@ - + None - + Day %1 @@ -1321,18 +1524,18 @@ - + None - + Day %1 - - + + Reward %1 @@ -1394,47 +1597,264 @@ - Day of first occurance + Day of first occurrence - + Repeat after (0 = no repeat) - + Affected players - + Resources - + type - + qty - + + Objects to delete + + + + + Add + + + + + Remove + + + + Ok - TownBulidingsWidget + TownBuildingsWidget - + Buildings + + + Build all + + + + + Demolish all + + + + + Enable all + + + + + Disable all + + + + + Type + + + + + Enabled + + + + + Built + + + + + TownEventDialog + + + Town event + + + + + General + + + + + Event name + + + + + Type event message text + + + + + Day of first occurrence + + + + + Repeat after (0 = no repeat) + + + + + Affected players + + + + + affects human + + + + + affects AI + + + + + Resources + + + + + Buildings + + + + + Creatures + + + + + OK + + + + + Creature level %1 / Creature level %1 Upgrade + + + + + Day %1 - %2 + + + + + TownEventsWidget + + + Town events + + + + + Timed events + + + + + Add + + + + + Remove + + + + + Day %1 - %2 + + + + + New event + + + + + TownSpellsWidget + + + Spells + + + + + Customize spells + + + + + Level 1 + + + + + + + + + Spell that may appear in mage guild + + + + + + + + + Spell that must appear in mage guild + + + + + Level 2 + + + + + Level 3 + + + + + Level 4 + + + + + Level 5 + + Translations @@ -1450,7 +1870,7 @@ - Suppported + Supported @@ -1493,107 +1913,107 @@ - + No factions allowed for player %1 - + No players allowed to play this map - + Map is allowed for one player and cannot be started - + No human players allowed to play this map - + Armored instance %1 is UNFLAGGABLE but must have NEUTRAL or player owner - + Object %1 is assigned to non-playable player %2 - - Town %1 has undefined owner %2 - - - - - Prison %1 must be a NEUTRAL - - - - - Hero %1 must have an owner - - - - - Hero %1 is prohibited by map settings - - - - - Hero %1 has duplicate on map - - - - - Hero %1 has an empty type and must be removed - - - - - Spell scroll %1 is prohibited by map settings - - - - + Spell scroll %1 doesn't have instance assigned and must be removed - + Artifact %1 is prohibited by map settings - - Player %1 doesn't have any starting town + + Player %1 has no towns and heroes assigned - - Map name is not specified + + Prison %1 must be a NEUTRAL + + + + + Hero %1 must have an owner + + + + + Hero %1 is prohibited by map settings + + + + + Hero %1 has duplicate on map + + + + + Hero %1 has an empty type and must be removed + + + + + Spell scroll %1 is prohibited by map settings + Player %1 doesn't have any starting town + + + + + Map name is not specified + + + + Map description is not specified - + Map contains object from mod "%1", but doesn't require it - + Exception occurs during validation: %1 - + Unknown exception occurs during validation @@ -1626,47 +2046,47 @@ - + No special victory - + Capture artifact - + Hire creatures - + Accumulate resources - + Construct building - + Capture town - + Defeat hero - + Transport artifact - + Kill monster @@ -1714,7 +2134,7 @@ - + 0 @@ -1813,42 +2233,62 @@ - + + Roads + + + + + Dirt + + + + + Gravel + + + + + Cobblestone + + + + Template - + Custom seed - + Generate random map - + Ok - + Cancel - + No template - - No template for parameters scecified. Random map cannot be generated. + + No template for parameters specified. Random map cannot be generated. - + RMG failure @@ -1856,27 +2296,27 @@ main - + Filepath of the map to open. - + Extract original H3 archives into a separate folder. - + From an extracted archive, it Splits TwCrPort, CPRSMALL, FlagPort, ITPA, ITPt, Un32 and Un44 into individual PNG's. - + From an extracted archive, Converts single Images (found in Images folder) from .pcx to png. - + Delete original files, for the ones split / converted. diff --git a/mapeditor/translation/french.ts b/mapeditor/translation/french.ts index ff2cda9f2..60f5aae48 100644 --- a/mapeditor/translation/french.ts +++ b/mapeditor/translation/french.ts @@ -19,6 +19,30 @@ Formation serrée + + ArtifactWidget + + + + Artifact + + + + + Equip where: + + + + + Save + Enregistrer + + + + Cancel + Annuler + + EventSettings @@ -42,7 +66,7 @@ Supprimer - + New event Nouvel évènement @@ -65,12 +89,32 @@ Description de la carte - + + Author + Auteur + + + + Author contact (e.g. email) + Contact de l'auteur (e.g. email) + + + + Map Creation Time + Temps de Création de la carte + + + + Map Version + Version de la carte + + + Limit maximum heroes level Limiter le niveau maximum des héros - + Difficulty Difficulté @@ -83,6 +127,34 @@ Générer une carte + + HeroArtifactsWidget + + + Artifacts + + + + + Add + Ajouter + + + + Remove + Supprimer + + + + Slot + + + + + Artifact + + + HeroSkillsWidget @@ -129,37 +201,37 @@ Spells - Sorts + Sorts Customize spells - + Personnaliser les sorts Level 1 - + Niveau 1 Level 2 - + Niveau 2 Level 3 - + Niveau 3 Level 4 - + Niveau 4 Level 5 - + Niveau 5 @@ -223,423 +295,519 @@ Fichier - + + + Open Recent + + + + Map Carte - + Edit Édition - + View Affichage - + Player Joueur - + Toolbar Barre d'outils - + Minimap Mini-carte - + Map Objects View Vue des objets cartographiques - + Browser Navigateur - + Inspector Inspecteur - + Property Propriété - + Value Valeur - + Tools Outils - + Painting Remplissage - + Terrains Terrains - + Roads Routes - + Rivers Rivières - + Preview Aperçu - + Open Ouvrir - + + More... + + + + Save Enregistrer - + New Nouveau - + Save as... Enregistrer sous... - + Ctrl+Shift+S Ctrl+Maj+S - + U/G Sous-sol/Surface - - + + View underground Voir le sous-sol - + Pass Passage - + Cut Couper - + Copy Copier - + Paste Coller - + Fill Remplir - + Fills the selection with obstacles Remplir la sélection d'obstacles - + Grid Grille - + General Général - + Map title and description Titre et description de la carte - + Players settings Paramètres des joueurs - - + + Undo Annuler - + Redo Rétablir - + Erase Effacer - + Neutral Neutre - + Validate Valider - - - - + + + + Update appearance Mettre à jour l'apparence - + Recreate obstacles Recréer des obstacles - + Player 1 Joueur 1 - + Player 2 Joueur 2 - + Player 3 Joueur 3 - + Player 4 Joueur 4 - + Player 5 Joueur 5 - + Player 6 Joueur 6 - + Player 7 Joueur 7 - + Player 8 Joueur 8 - + Export as... Exporter sous... - + Translations Traductions - + Ctrl+T Ctrl+T - - + + h3m converter convertisseur h3m - + Lock Vérouiller - + Lock objects on map to avoid unnecessary changes Vérouiller les objets sur la carte pour éviter des changements non nécessaires - + Ctrl+L Ctrl+L - + Unlock Déverouiller - + Unlock all objects on the map Dévérouiller tous les objets de la carte - + Ctrl+Shift+L Ctrl+Maj+L - + Zoom in Zoom avant - + Ctrl+= Ctrl+= - + Zoom out Zoom arrière - + Ctrl+- - + Zoom reset Remise à zéro du zoom - + Ctrl+Shift+= Ctrl+Maj+= - + Confirmation Confirmation - + Unsaved changes will be lost, are you sure? - Des modifications non sauvegardées vont être perdues. Êtes-vous sûr ? + Les modifications non sauvegardées seront perdues. Êtes-vous sûr ? - + Open map - Ouvrir la carte + 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) - + + Recently Opened Files + + + + Save map Enregistrer la carte - + VCMI maps (*.vmap) Cartes VCMI (*.vmap) - + Type Type - + + Towns + + + + + Objects + + + + + Heroes + Héros + + + + Artifacts + + + + + Resources + + + + + Banks + + + + + Dwellings + + + + + Grounds + + + + + Teleports + + + + + Mines + + + + + Triggers + + + + + Monsters + + + + + Quests + + + + + Wog Objects + + + + + Obstacles + + + + + Other + + + + 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 @@ -715,7 +883,7 @@ MapView - + Can't place object Impossible de placer l'objet @@ -763,7 +931,7 @@ Map objects mods - Object de carte des mods + objet de carte des mods @@ -773,7 +941,7 @@ Full content mods - Mods de conteniu complet + Mods de conteniu complete @@ -824,7 +992,12 @@ (par défaut) - + + No team + + + + Player ID: %1 Identifiant du joueur : %1 @@ -889,41 +1062,71 @@ 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. + QuestWidget @@ -1000,7 +1203,7 @@ Resources - Ressources + Resources @@ -1048,12 +1251,12 @@ Joueurs - + None Aucune - + Day %1 Jour %1 @@ -1134,7 +1337,7 @@ Event info - Informations d'évènements + Information d'évènements @@ -1180,18 +1383,18 @@ Movement - Mouvement + Movement Remove object - Supprimer les objects + Supprimer les objets Primary skills - Compétances principales + Compétances principles @@ -1221,7 +1424,7 @@ Resources - Ressources + Resources @@ -1321,18 +1524,18 @@ Joueurs - + None Aucune - + Day %1 Jour %1 - - + + Reward %1 Récompense %1 @@ -1394,47 +1597,264 @@ - Day of first occurance - Jour de la première occurence + Day of first occurrence + Jour de la première occurrence - + Repeat after (0 = no repeat) Récurrence (0 = pas de récurrence) - + Affected players Joueurs affectés - + Resources - Ressources + Resources - + type type - + qty quantité - + + Objects to delete + + + + + Add + Ajouter + + + + Remove + Supprimer + + + Ok OK - TownBulidingsWidget + TownBuildingsWidget - + Buildings Bâtiments + + + Build all + Construire tout + + + + Demolish all + Détruire tout + + + + Enable all + Autoriser tout + + + + Disable all + Interdire tout + + + + Type + Type + + + + Enabled + Autorisé·e + + + + Built + Construit·e + + + + TownEventDialog + + + Town event + Évènement de ville + + + + General + Général + + + + Event name + Nom de l'évènement + + + + Type event message text + Taper le message d'évènement + + + + Day of first occurrence + Jour de la première occurrence + + + + Repeat after (0 = no repeat) + Réoéter après (0 = pas de répétition) + + + + Affected players + Joueurs affectés + + + + affects human + afttecte les joueurs + + + + affects AI + affecte l'ordinateur + + + + Resources + Ressources + + + + Buildings + Bâtiments + + + + Creatures + Créatures + + + + OK + OK + + + + Creature level %1 / Creature level %1 Upgrade + Créature niveau %1 / Créature niveau %1 Augmenté + + + + Day %1 - %2 + Jour %1 - %2 + + + + TownEventsWidget + + + Town events + Évènements de ville + + + + Timed events + Évènements temporels + + + + Add + Ajouter + + + + Remove + Supprimer + + + + Day %1 - %2 + Jour %1 - %2 + + + + New event + Nouvel évènement + + + + TownSpellsWidget + + + Spells + Sorts + + + + Customize spells + Personnaliser les sorts + + + + Level 1 + Niveau 1 + + + + + + + + Spell that may appear in mage guild + Sort qui peut apparaitre dans la Guilde des Mages + + + + + + + + Spell that must appear in mage guild + + + + + Level 2 + Niveau 2 + + + + Level 3 + Niveau 3 + + + + Level 4 + Niveau 4 + + + + Level 5 + Niveau 5 + Translations @@ -1450,7 +1870,7 @@ - Suppported + Supported Supporté @@ -1493,107 +1913,107 @@ Aucune carte n'est chargée - + No factions allowed for player %1 Pas de factions autorisées pour le joueur %1 - + No players allowed to play this map Aucun joueur autorisé à jouer sur cette carte - + Map is allowed for one player and cannot be started La carte est autorisée pour un joueur et ne peut pas être démarrée - + No human players allowed to play this map Aucun joueur humain n'est autorisé à jouer sur cette carte - + Armored instance %1 is UNFLAGGABLE but must have NEUTRAL or player owner L'instance blindée %1 est IMMARQUABLE mais doit avoir un propriétaire NEUTRE ou joueur - + Object %1 is assigned to non-playable player %2 L'objet %1 est attribué au joueur non jouable %2 - - Town %1 has undefined owner %2 - La ville %1 a le propriétaire indéfini %2 + + Spell scroll %1 doesn't have instance assigned and must be removed + Le défilement de sort %1 n'a pas d'instance assignée et doit être enlevé - + + Artifact %1 is prohibited by map settings + L'artéfact %1 est interdit par la configuration de la carte + + + + Player %1 has no towns and heroes assigned + Le joueur %1 n'a pas de ville ni de héro assigné + + + Prison %1 must be a NEUTRAL La prison %1 doit être NEUTRE - + Hero %1 must have an owner Le héros %1 doit avoir un propriétaire - + Hero %1 is prohibited by map settings Le héros %1 est interdit par les paramètres de la carte - + Hero %1 has duplicate on map Le héros %1 a un doublon sur la carte - + Hero %1 has an empty type and must be removed Le héros %1 a un type vide et doit être supprimé - + Spell scroll %1 is prohibited by map settings Le défilement des sorts %1 est interdit par les paramètres de la carte - - Spell scroll %1 doesn't have instance assigned and must be removed - Le parchemin de sort %1 n'a pas d'instance assignée et doit être supprimé - - - - Artifact %1 is prohibited by map settings - L'artefact %1 est interdit par les paramètres de la carte - - - + Player %1 doesn't have any starting town Le joueur %1 n'a pas de ville de départ - + Map name is not specified Le nom de la carte n'est pas spécifié - + Map description is not specified La description de la carte n'est pas spécifiée - + Map contains object from mod "%1", but doesn't require it La carte contient des objets du mod "%1", mais il n'est pas requis - + Exception occurs during validation: %1 Une exception se produit lors de la validation : %1 - + Unknown exception occurs during validation Une exception inconnue se produit lors de la validation @@ -1626,49 +2046,49 @@ Paramètres - + No special victory Pas de victoire spéciale - + Capture artifact Récupérer l'artefact - + Hire creatures Engagez des créatures - + Accumulate resources - Accumuler des ressources + Accumuler des resources - + Construct building Construire un bâtiment - + Capture town Conquérir une ville - + Defeat hero Battre un héros - + Transport artifact Transporter un artefact - + Kill monster - Tuer un monstre + Tuer un monster @@ -1698,18 +2118,6 @@ Width Largeur - - S (36x36) - Petite (36x36) - - - M (72x72) - Moyenne (72x72) - - - L (108x108) - Grande (108x108) - XL (144x144) @@ -1726,7 +2134,7 @@ Joueurs - + 0 0 @@ -1738,32 +2146,32 @@ S (36x36) - + S (36x36) M (72x72) - + M (72x72) L (108x108) - + L (108x108) H (180x180) - + H (180x180) XH (216x216) - + XH (216x216) G (252x252) - + G (252x252) @@ -1802,7 +2210,7 @@ Normal - Normale + Normal @@ -1825,42 +2233,62 @@ Îles - + + Roads + Routes + + + + Dirt + + + + + Gravel + + + + + Cobblestone + + + + Template Modèle - + Custom seed Graine personnalisée - + Generate random map Générer une carte aléatoire - + Ok OK - + Cancel Annuler - + No template Pas de modèle - - No template for parameters scecified. Random map cannot be generated. + + No template for parameters specified. Random map cannot be generated. Pas de modèles pour les paramètres spécifiés. La carte aléatoire ne peut pas être générée. - + RMG failure Echec de RMG @@ -1868,29 +2296,29 @@ main - + Filepath of the map to open. Chemin du fichier de la carte à ouvrir. - + Extract original H3 archives into a separate folder. Extraire les archives H3 d'origine dans un dossier séparé. - + From an extracted archive, it Splits TwCrPort, CPRSMALL, FlagPort, ITPA, ITPt, Un32 and Un44 into individual PNG's. - À partir d'une archive extraite, il divise TwCrPort, CPRSMALL, FlagPort, ITPA, ITPt, Un32 et Un44 en fichiers PNG individuels. + À partir d'une archive extraite, il divise TwCrPort, CPRSMALL, FlagPort, ITPA, ITPt, Un32 et Un44 en fichiers PNG individuals. - + From an extracted archive, Converts single Images (found in Images folder) from .pcx to png. À partir d'une archive extraite, convertit des images uniques (trouvées dans le dossier Images) de .pcx en png. - + Delete original files, for the ones split / converted. - Supprimer les fichiers d'origine, pour ceux fractionnés/convertis. + Supprimer les fichiers d'origine, pour ceux fractionnés/converts. diff --git a/mapeditor/translation/german.ts b/mapeditor/translation/german.ts index 51ccff95a..509daef6f 100644 --- a/mapeditor/translation/german.ts +++ b/mapeditor/translation/german.ts @@ -19,6 +19,30 @@ Enge Formation + + ArtifactWidget + + + + Artifact + Artefakt + + + + Equip where: + Wo ausrüsten: + + + + Save + Speichern + + + + Cancel + Abbrechen + + EventSettings @@ -42,7 +66,7 @@ Entfernen - + New event Neues Ereignis @@ -65,12 +89,32 @@ Kartenbeschreibung - + + Author + Autor + + + + Author contact (e.g. email) + Autor-Kontakt (z.B. E-Mail) + + + + Map Creation Time + Kartenerstellungszeitpunkt + + + + Map Version + Kartenversion + + + Limit maximum heroes level Maximales Level des Helden begrenzen - + Difficulty Schwierigkeit @@ -83,6 +127,34 @@ Karte generieren + + HeroArtifactsWidget + + + Artifacts + Artefakte + + + + Add + Hinzufügen + + + + Remove + Entfernen + + + + Slot + Slot + + + + Artifact + Artefakt + + HeroSkillsWidget @@ -223,423 +295,519 @@ Datei - + + + Open Recent + Zuletzt geöffnet + + + Map Karte - + Edit Bearbeiten - + View Ansicht - + Player Spieler - + Toolbar Werkzeugleiste - + Minimap Minikarte - + Map Objects View Kartenobjekte-Ansicht - + Browser Browser - + Inspector Inspektor - + Property Eigenschaft - + Value Wert - + Tools Werkzeuge - + Painting Malen - + Terrains Terrains - + Roads Straßen - + Rivers Flüsse - + Preview Vorschau - + Open Öffnen - + + More... + Mehr... + + + Save Speichern - + New Neu - + Save as... Speichern unter... - + Ctrl+Shift+S Strg+Shift+S - + U/G U/G - - + + View underground Ansicht Untergrund - + Pass Passierbar - + Cut Ausschneiden - + Copy Kopieren - + Paste Einfügen - + Fill Füllen - + Fills the selection with obstacles Füllt die Auswahl mit Hindernissen - + Grid Raster - + General Allgemein - + Map title and description Titel und Beschreibung der Karte - + Players settings Spieler-Einstellungen - - + + Undo Rückgängig - + Redo Wiederholen - + Erase Löschen - + Neutral Neutral - + Validate Validieren - - - - + + + + Update appearance Aussehen aktualisieren - + Recreate obstacles Hindernisse neu erschaffen - + Player 1 Spieler 1 - + Player 2 Spieler 2 - + Player 3 Spieler 3 - + Player 4 Spieler 4 - + Player 5 Spieler 5 - + Player 6 Spieler 6 - + Player 7 Spieler 7 - + Player 8 Spieler 8 - + Export as... Exportieren als... - + Translations Übersetzungen - + Ctrl+T Strg+T - - + + h3m converter h3m-Konverter - + Lock Sperren - + Lock objects on map to avoid unnecessary changes Objekte auf der Karte sperren, um unnötige Änderungen zu vermeiden - + Ctrl+L Strg+L - + Unlock Entsperren - + Unlock all objects on the map Entsperre alle Objekte auf der Karte - + Ctrl+Shift+L Strg+Umschalt+L - + Zoom in Heranzoomen - + Ctrl+= Strg+= - + Zoom out Herauszoomen - + Ctrl+- Strg+- - + Zoom reset Zoom zurücksetzen - + Ctrl+Shift+= 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) - + + Recently Opened Files + Kürzlich geöffnete Dateien + + + Save map Karte speichern - + VCMI maps (*.vmap) VCMI-Karten (*.vmap) - + Type Typ - + + Towns + Städte + + + + Objects + Objekte + + + + Heroes + Helden + + + + Artifacts + Artefakte + + + + Resources + Ressourcen + + + + Banks + Bänke + + + + Dwellings + Unterkünfte + + + + Grounds + Gelände + + + + Teleports + Teleporter + + + + Mines + Minen + + + + Triggers + Trigger + + + + Monsters + Monster + + + + Quests + Aufgaben + + + + Wog Objects + Wog Objekte + + + + Obstacles + Hindernisse + + + + Other + Anderes + + + 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 @@ -715,7 +883,7 @@ MapView - + Can't place object Objekt kann nicht platziert werden @@ -824,7 +992,12 @@ (Standard) - + + No team + Kein Team + + + Player ID: %1 Spieler-ID: %1 @@ -889,41 +1062,71 @@ 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 + + + + 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. + QuestWidget @@ -1048,12 +1251,12 @@ Spieler - + None Keine - + Day %1 Tag %1 @@ -1321,18 +1524,18 @@ Spieler - + None Keine - + Day %1 Tag %1 - - + + Reward %1 Belohnung %1 @@ -1394,47 +1597,264 @@ - Day of first occurance + Day of first occurrence Tag des ersten Auftretens - + Repeat after (0 = no repeat) Wiederholung nach (0 = keine Wiederholung) - + Affected players Betroffene Spieler - + Resources Ressourcen - + type Typ - + qty anz. - + + Objects to delete + Objekte zum löschen + + + + Add + Hinzufügen + + + + Remove + Entfernen + + + Ok Ok - TownBulidingsWidget + TownBuildingsWidget - + Buildings Gebäude + + + Build all + Alle bauen + + + + Demolish all + Alle zerstören + + + + Enable all + Alle aktivieren + + + + Disable all + Alle deaktivieren + + + + Type + Typ + + + + Enabled + Aktiviert + + + + Built + Gebaut + + + + TownEventDialog + + + Town event + Stadt Ereignis + + + + General + Allgemein + + + + Event name + Ereignis-Name + + + + Type event message text + Ereignistext eingeben + + + + Day of first occurrence + Tag des ersten Auftretens + + + + Repeat after (0 = no repeat) + Wiederholung nach (0 = keine Wiederholung) + + + + Affected players + Betroffene Spieler + + + + affects human + beeinflusst Menschen + + + + affects AI + beeinflusst KI + + + + Resources + Ressourcen + + + + Buildings + Gebäude + + + + Creatures + Kreaturen + + + + OK + OK + + + + Creature level %1 / Creature level %1 Upgrade + Kreaturlevel %1 / Kreaturlevel %1 Aufgerüstet + + + + Day %1 - %2 + Tag %1 - %2 + + + + TownEventsWidget + + + Town events + Stadt Ereignisse + + + + Timed events + Zeitlich begrenzte Ereignisse + + + + Add + Hinzufügen + + + + Remove + Entfernen + + + + Day %1 - %2 + Tag %1 - %2 + + + + New event + Neues Ereignis + + + + TownSpellsWidget + + + Spells + Zaubersprüche + + + + Customize spells + Zaubersprüche anpassen + + + + Level 1 + Level 1 + + + + + + + + Spell that may appear in mage guild + Zauberspruch, der in der Magiergilde erscheinen kann + + + + + + + + Spell that must appear in mage guild + Zauberspruch, der in der Magiergilde erscheinen muss + + + + Level 2 + Level 2 + + + + Level 3 + Level 3 + + + + Level 4 + Level 4 + + + + Level 5 + Level 5 + Translations @@ -1450,7 +1870,7 @@ - Suppported + Supported Unterstützt @@ -1493,107 +1913,107 @@ Karte ist nicht geladen - + No factions allowed for player %1 Keine Fraktionen für Spieler %1 erlaubt - + No players allowed to play this map Keine Spieler dürfen diese Karte spielen - + Map is allowed for one player and cannot be started Karte ist für einen Spieler erlaubt und kann nicht gestartet werden - + No human players allowed to play this map Keine menschlichen Spieler dürfen diese Karte spielen - + Armored instance %1 is UNFLAGGABLE but must have NEUTRAL or player owner Gepanzerte Instanz %1 ist UNFLAGGABLE, muss aber NEUTRAL oder Spielerbesitzer haben - + Object %1 is assigned to non-playable player %2 Objekt %1 ist dem nicht spielbaren Spieler %2 zugewiesen - - Town %1 has undefined owner %2 - Stadt %1 hat undefinierten Besitzer %2 - - - - Prison %1 must be a NEUTRAL - Gefängnis %1 muss NEUTRAL sein - - - - Hero %1 must have an owner - Held %1 muss einen Besitzer haben - - - - Hero %1 is prohibited by map settings - Held %1 ist durch Karteneinstellungen verboten - - - - Hero %1 has duplicate on map - Held %1 hat Duplikat auf Karte - - - - Hero %1 has an empty type and must be removed - Held %1 hat einen leeren Typ und muss entfernt werden - - - - Spell scroll %1 is prohibited by map settings - Zauberschriftrolle %1 ist durch Karteneinstellungen verboten - - - + 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 + + + + Prison %1 must be a NEUTRAL + Gefängnis %1 muss NEUTRAL sein + + + + Hero %1 must have an owner + Held %1 muss einen Besitzer haben + + + + Hero %1 is prohibited by map settings + Held %1 ist durch Karteneinstellungen verboten + + + + Hero %1 has duplicate on map + Held %1 hat Duplikat auf Karte + + + + Hero %1 has an empty type and must be removed + Held %1 hat einen leeren Typ und muss entfernt werden + + + + Spell scroll %1 is prohibited by map settings + Zauberschriftrolle %1 ist durch Karteneinstellungen verboten + + + Player %1 doesn't have any starting town Spieler %1 hat keine Startstadt - + Map name is not specified Kartenname ist nicht angegeben - + Map description is not specified Kartenbeschreibung ist nicht angegeben - + Map contains object from mod "%1", but doesn't require it Karte enthält Objekt aus Mod "%1", benötigt es aber nicht - + Exception occurs during validation: %1 Bei der Validierung ist eine Ausnahme aufgetreten: %1 - + Unknown exception occurs during validation Unbekannte Ausnahme trat während der Validierung auf @@ -1626,47 +2046,47 @@ Parameter - + No special victory Kein besonderer Sieg - + Capture artifact Artefakt sammeln - + Hire creatures Kreaturen anheuern - + Accumulate resources Ressourcen ansammeln - + Construct building Gebäude errichten - + Capture town Stadt einnehmen - + Defeat hero Held besiegen - + Transport artifact Artefakt transportieren - + Kill monster Monster töten @@ -1698,18 +2118,6 @@ Width Breite - - S (36x36) - S (36x36) - - - M (72x72) - M (72x72) - - - L (108x108) - L (108x108) - XL (144x144) @@ -1726,7 +2134,7 @@ Spieler - + 0 0 @@ -1825,42 +2233,62 @@ Inseln - + + Roads + Straßen + + + + Dirt + Erde + + + + Gravel + Kies + + + + Cobblestone + Kopfsteinpflaster + + + Template Vorlage - + Custom seed Benutzerdefiniertes Seed - + Generate random map Zufällige Karte generieren - + Ok Ok - + Cancel Abbrechen - + No template Kein Template - - No template for parameters scecified. Random map cannot be generated. + + No template for parameters specified. Random map cannot be generated. Es wurde kein Template für Parameter erstellt. Zufällige Karte kann nicht generiert werden. - + RMG failure RMG-Fehler @@ -1868,27 +2296,27 @@ main - + Filepath of the map to open. Dateipfad der zu öffnenden Karte. - + Extract original H3 archives into a separate folder. Extrahieren Sie die Original-H3-Archive in einen separaten Ordner. - + From an extracted archive, it Splits TwCrPort, CPRSMALL, FlagPort, ITPA, ITPt, Un32 and Un44 into individual PNG's. Aus einem extrahierten Archiv zerlegt es TwCrPort, CPRSMALL, FlagPort, ITPA, ITPt, Un32 und Un44 in einzelne PNGs. - + From an extracted archive, Converts single Images (found in Images folder) from .pcx to png. Aus einem extrahierten Archiv werden einzelne Bilder (aus dem Ordner "Images") von .pcx in png konvertiert. - + Delete original files, for the ones split / converted. Löschen Sie die Originaldateien für die gesplitteten/konvertierten Dateien. diff --git a/mapeditor/translation/polish.ts b/mapeditor/translation/polish.ts index aa44292eb..4d79b5914 100644 --- a/mapeditor/translation/polish.ts +++ b/mapeditor/translation/polish.ts @@ -19,6 +19,30 @@ Zwarta formacja + + ArtifactWidget + + + + Artifact + Artefakt + + + + Equip where: + Umiejscowienie: + + + + Save + Zapisz + + + + Cancel + Anuluj + + EventSettings @@ -42,7 +66,7 @@ Usuń - + New event Nowe zdarzenie @@ -65,12 +89,32 @@ Opis mapy - + + Author + Autor + + + + Author contact (e.g. email) + Autor - kontakt (np. email) + + + + Map Creation Time + Data utworzenia mapy + + + + Map Version + Wersja mapy + + + Limit maximum heroes level Ogranicz maksymalny poziom bohaterów - + Difficulty Poziom trudności @@ -83,6 +127,34 @@ Trwa generowanie mapy + + HeroArtifactsWidget + + + Artifacts + Artefakty + + + + Add + Dodaj + + + + Remove + Usuń + + + + Slot + Slot + + + + Artifact + Artefakt + + HeroSkillsWidget @@ -223,423 +295,519 @@ Plik - + + + Open Recent + Otwórz ostatnie + + + Map Mapa - + Edit Edycja - + View Widok - + Player Gracz - + Toolbar Przybornik - + Minimap Minimapa - + Map Objects View Widok obiektów - + Browser Przeglądarka - + Inspector Inspektor - + Property Właściwość - + Value Wartość - + Tools Narzędzia - + Painting Malowanie - + Terrains Tereny - + Roads Drogi - + Rivers Rzeki - + Preview Podgląd - + Open Otwórz - + + More... + Więcej... + + + Save Zapisz - + New Nowy - + Save as... Zapisz jako... - + Ctrl+Shift+S Ctrl+Shift+S - + U/G Podziemia - - + + View underground Pokaż podziemia - + Pass Przejścia - + Cut Wytnij - + Copy Kopiuj - + Paste Wklej - + Fill Wypełnij - + Fills the selection with obstacles Wypełnia zaznaczony obszar przeszkodami - + Grid Siatka - + General Ogólne - + Map title and description Nazwa i opis mapy - + Players settings Ustawienia graczy - - + + Undo Cofnij - + Redo Przywróć - + Erase Wymaż - + Neutral Neutralny - + Validate Sprawdź - - - - + + + + Update appearance Aktualizuj wygląd - + Recreate obstacles Powtórnie stwórz przeszkody - + Player 1 Gracz 1 - + Player 2 Gracz 2 - + Player 3 Gracz 3 - + Player 4 Gracz 4 - + Player 5 Gracz 5 - + Player 6 Gracz 6 - + Player 7 Gracz 7 - + Player 8 Gracz 8 - + Export as... Eksportuj jako... - + Translations Tłumaczenia - + Ctrl+T Ctrl+T - - + + h3m converter konwerter h3m - + Lock Zablokuj - + Lock objects on map to avoid unnecessary changes Zablokuj obiekty na mapie by uniknąć przypadkowych zmian - + Ctrl+L Ctrl+L - + Unlock Odblokuj - + Unlock all objects on the map Odblokuj wszystkie obiekty na mapie - + Ctrl+Shift+L Ctrl+Shift+L - + Zoom in - Powiększ + Zbliż widok - + Ctrl+= Ctrl+= - + Zoom out - Pomniejsz + Oddal widok - + Ctrl+- Ctrl+- - + Zoom reset - Domyślne powiększenie + Domyślne oddalenie - + Ctrl+Shift+= 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) - + + Recently Opened Files + Ostatnio otwierane pliki + + + Save map Zapisz mapę - + VCMI maps (*.vmap) Mapy VCMI (*.vmap) - + Type Typ - + + Towns + Miasta + + + + Objects + + + + + Heroes + Bohaterowie + + + + Artifacts + Artefakty + + + + Resources + Zasoby + + + + Banks + Banki + + + + Dwellings + Siedliska + + + + Grounds + Tereny + + + + Teleports + Teleporty + + + + Mines + Kopalnie + + + + Triggers + Wyzwalacze + + + + Monsters + Potwory + + + + Quests + Zadania + + + + Wog Objects + Obiekty WOG + + + + Obstacles + Przeszkody + + + + Other + Inne + + + 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 + Wybierz folder do 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 @@ -715,7 +883,7 @@ MapView - + Can't place object Nie można umieścić obiektu @@ -824,7 +992,12 @@ (domyślny) - + + No team + Bez sojuszu + + + Player ID: %1 ID gracza: %1 @@ -889,40 +1062,71 @@ Ekspert - + Compliant Przyjazny - + Friendly Przychylny - + Aggressive Agresywny - + Hostile Wrogi - + Savage Nienawistny - - + + + No patrol + Brak patrolu + + + + + %n tile(s) + + + + + + + + + neutral neutralny - + UNFLAGGABLE - NIEOFLAGOWYWALNY + NIEFLAGOWALNY + + + + Can't place object + Nie można umieścić obiektu + + + + There can only be one grail object on the map. + Na mapie może znajdować się tylko jeden Graal. + + + + Hero %1 cannot be created as NEUTRAL. + Bohater %1 nie może zostać utworzony jako NEUTRALNY @@ -1048,12 +1252,12 @@ Gracze - + None Brak - + Day %1 Dzień %1 @@ -1321,18 +1525,18 @@ Gracze - + None Brak - + Day %1 Dzień %1 - - + + Reward %1 Nagroda %1 @@ -1394,47 +1598,264 @@ - Day of first occurance + Day of first occurrence Dzień pierwszego wystąpienia - + Repeat after (0 = no repeat) Powtórz po... (0 = nigdy) - + Affected players Dotyczy graczy - + Resources Zasoby - + type typ - + qty ilość - + + Objects to delete + Obiekty do usunięcia + + + + Add + Dodaj + + + + Remove + Usuń + + + Ok Ok - TownBulidingsWidget + TownBuildingsWidget - + Buildings Budynki + + + Build all + Zbuduj wsyzstkie + + + + Demolish all + Zburz wszystkie + + + + Enable all + Włącz wszystkie + + + + Disable all + Wyłącz wszystkie + + + + Type + Typ + + + + Enabled + Włączony + + + + Built + Zbudowany + + + + TownEventDialog + + + Town event + Zdarzenie miasta + + + + General + Ogólne + + + + Event name + Nazwa zdarzenia + + + + Type event message text + Wpisz treść komunikatu zdarzenia + + + + Day of first occurrence + Dzień pierwszego wystąpienia + + + + Repeat after (0 = no repeat) + Powtórz po... (0 = nigdy) + + + + Affected players + Dotyczy graczy + + + + affects human + dotyczy graczy ludzkich + + + + affects AI + dotyczy graczy AI + + + + Resources + Zasoby + + + + Buildings + Budynki + + + + Creatures + Stworzenia + + + + OK + OK + + + + Creature level %1 / Creature level %1 Upgrade + Stworzenie poziomu %1 / Ulepszone stworzenie poziomu %1 + + + + Day %1 - %2 + Dzień %1 - %2 + + + + TownEventsWidget + + + Town events + Zdarzenia miasta + + + + Timed events + Zdarzenia czasowe + + + + Add + Dodaj + + + + Remove + Usuń + + + + Day %1 - %2 + Dzień %1 - %2 + + + + New event + Nowe zdarzenie + + + + TownSpellsWidget + + + Spells + Zaklęcia + + + + Customize spells + Własne zaklęcia + + + + Level 1 + Poziom 1 + + + + + + + + Spell that may appear in mage guild + Zaklecia, które mogą pojawić się w gildii magów + + + + + + + + Spell that must appear in mage guild + Zaklecia, które muszą pojawić się w gildii magów + + + + Level 2 + Poziom 2 + + + + Level 3 + Poziom 3 + + + + Level 4 + Poziom 4 + + + + Level 5 + Poziom 5 + Translations @@ -1450,7 +1871,7 @@ - Suppported + Supported Wspierany @@ -1493,109 +1914,109 @@ Mapa nie została wczytana - + No factions allowed for player %1 Brak dozwolonych frakcji dla gracza %1 - + No players allowed to play this map Żaden gracz nie jest dozwolony do rozegrania tej mapy - + Map is allowed for one player and cannot be started Mapa jest dozwolona dla jednego gracza i nie może być rozpoczęta - + No human players allowed to play this map Żaden gracz ludzki nie został dozwolony by rozegrać tą mapę - + Armored instance %1 is UNFLAGGABLE but must have NEUTRAL or player owner - Obiekt z armią %1 jest nie do oflagowania, lecz musi mieć właściciela neutralnego lub gracza + Obiekt z armią %1 jest nie do oflagowania, lecz musi mieć być oznaczony jako neutralny lub przynależeć do gracza - + Object %1 is assigned to non-playable player %2 Obiekt %1 został przypisany do niegrywalnego gracza %2 - - Town %1 has undefined owner %2 - Miasto %1 ma niezdefiniowanego właściciela %2 + + Spell scroll %1 doesn't have instance assigned and must be removed + Zaklęcie ze zwoju %1 nie posiada przypisanego obiektu i musi zostać usunięte - + + Artifact %1 is prohibited by map settings + Artefakt %1 jest zabroniony w ustawieniach mapy + + + + Player %1 has no towns and heroes assigned + Gracz %1 nie posiada żadnych miast i bohaterów + + + Prison %1 must be a NEUTRAL Więzienie %1 musi być neutralne - + Hero %1 must have an owner Bohater %1 musi mieć właściciela - + Hero %1 is prohibited by map settings Bohater %1 jest zabroniony przez ustawienia mapy - + Hero %1 has duplicate on map Bohater %1 posiada duplikat na mapie - + Hero %1 has an empty type and must be removed Bohater %1 jest pustego typu i musi zostać usunięty - + Spell scroll %1 is prohibited by map settings Zwój z zaklęciem %1 jest zabroniony przez ustawienia mapy - - Spell scroll %1 doesn't have instance assigned and must be removed - Zwój z zaklęciem %1 nie ma przypisanej instancji i musi zostać usunięty - - - - Artifact %1 is prohibited by map settings - Artefakt %1 jest zabroniony przez ustawienia mapy - - - + Player %1 doesn't have any starting town Gracz %1 nie ma żadnego startowego miasta - + Map name is not specified Nazwa mapy nie została ustawiona - + Map description is not specified Opis mapy nie został ustawiony - + Map contains object from mod "%1", but doesn't require it Mapa zawiera obiekt z modyfikacji %1 ale nie wymaga tej modyfikacji - + Exception occurs during validation: %1 Wystąpił wyjątek podczas walidacji: %1 - + Unknown exception occurs during validation - Wystąpił nieznane wyjątek podczas walidacji + Wystąpił nieznany wyjątek podczas walidacji @@ -1626,47 +2047,47 @@ Parametry - + No special victory Bez specjalnych warunków zwycięstwa - + Capture artifact Zdobądź artefakt - + Hire creatures Zdobądź stworzenia - + Accumulate resources Zbierz zasoby - + Construct building Zbuduj budynek - + Capture town Zdobądź miasto - + Defeat hero Pokonaj bohatera - + Transport artifact Przenieś artefakt - + Kill monster Zabij potwora @@ -1698,18 +2119,6 @@ Width Szerokość - - S (36x36) - S (36x36) - - - M (72x72) - M (72x72) - - - L (108x108) - L (108x108) - XL (144x144) @@ -1726,7 +2135,7 @@ Gracze - + 0 0 @@ -1825,42 +2234,62 @@ Wyspy - + + Roads + Drogi + + + + Dirt + Ziemna + + + + Gravel + Żwirowa + + + + Cobblestone + Brukowana + + + Template Szablon - + Custom seed Własny seed - + Generate random map Generuj mapę losową - + Ok Ok - + Cancel Anuluj - + No template Brak szablonu - - No template for parameters scecified. Random map cannot be generated. + + No template for parameters specified. Random map cannot be generated. Brak szablonu dla wybranych parametrów. Mapa losowa nie może zostać wygenerowana. - + RMG failure Niepowodzenie generatora map losowych @@ -1868,27 +2297,27 @@ main - + Filepath of the map to open. Lokalizacja pliku mapy do otworzenia. - + Extract original H3 archives into a separate folder. Wyodrębnij oryginalne archiwa H3 do osobnego folderu. - + From an extracted archive, it Splits TwCrPort, CPRSMALL, FlagPort, ITPA, ITPt, Un32 and Un44 into individual PNG's. Z wyodrębnionego archiwum, rozdzielenie TwCrPort, CPRSMALL, FlagPort, ITPA, ITPt, Un32 i Un44 do poszczególnych plików PNG. - + From an extracted archive, Converts single Images (found in Images folder) from .pcx to png. Z wyodrębnionego archiwum, konwersja pojedynczych obrazków (znalezionych w folderze Images) z .pcx do .png. - + Delete original files, for the ones split / converted. Usuń oryginalne pliki, dla już rozdzielonych / skonwertowanych. diff --git a/mapeditor/translation/portuguese.ts b/mapeditor/translation/portuguese.ts index d44bc8074..0b049373f 100644 --- a/mapeditor/translation/portuguese.ts +++ b/mapeditor/translation/portuguese.ts @@ -19,6 +19,30 @@ Formação compacta + + ArtifactWidget + + + + Artifact + + + + + Equip where: + + + + + Save + Salvar + + + + Cancel + Cancelar + + EventSettings @@ -42,7 +66,7 @@ Remover - + New event Novo evento @@ -65,12 +89,32 @@ Descrição do mapa - - Limit maximum heroes level - Limite máximo do nível dos heróis + + Author + Autor - + + Author contact (e.g. email) + Contato do autor (ex: e-mail) + + + + Map Creation Time + Data de Criação do Mapa + + + + Map Version + Versão do Mapa + + + + Limit maximum heroes level + Limitar nível máximo dos heróis + + + Difficulty Dificuldade @@ -83,6 +127,34 @@ Gerando mapa + + HeroArtifactsWidget + + + Artifacts + Artefatos + + + + Add + Adicionar + + + + Remove + Remover + + + + Slot + + + + + Artifact + + + HeroSkillsWidget @@ -187,7 +259,7 @@ No special loss - Sem perda especial + Sem derrota especial @@ -223,423 +295,519 @@ Arquivo - + + + Open Recent + + + + Map Mapa - + Edit Editar - + View Visualizar - + Player Jogador - + Toolbar Barra de Ferramentas - + Minimap Minimapa - + Map Objects View Visualização de Objetos do Mapa - + Browser Navegador - + Inspector Inspetor - + Property Propriedade - + Value Valor - + Tools Ferramentas - + Painting Pintura - + Terrains Terrenos - + Roads Estradas - + Rivers Rios - + Preview Visualização - + Open Abrir - + + More... + + + + Save Salvar - + New Novo - + Save as... Salvar como... - + Ctrl+Shift+S Ctrl+Shift+S - + U/G Subterrâneo/Superfície - - + + View underground Visualizar subterrâneo - + Pass Passar - + Cut - Cortar + Recortar - + Copy Copiar - + Paste Colar - + Fill Preencher - + Fills the selection with obstacles Preenche a seleção com obstáculos - + Grid Grade - + General Geral - + Map title and description Título e descrição do mapa - + Players settings Configurações dos jogadores - - + + Undo Desfazer - + Redo Refazer - + Erase Apagar - + Neutral Neutro - + Validate Validar - - - - + + + + Update appearance Atualizar aparência - + Recreate obstacles Recriar obstáculos - + Player 1 Jogador 1 - + Player 2 Jogador 2 - + Player 3 Jogador 3 - + Player 4 Jogador 4 - + Player 5 Jogador 5 - + Player 6 Jogador 6 - + Player 7 Jogador 7 - + Player 8 Jogador 8 - + Export as... Exportar como... - + Translations Traduções - + Ctrl+T Ctrl+T - - + + h3m converter Conversor h3m - + 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 - + Ctrl+L Ctrl+L - + Unlock Desbloquear - + Unlock all objects on the map Desbloquear todos os objetos no mapa - + Ctrl+Shift+L Ctrl+Shift+L - + Zoom in Aumentar o zoom - + Ctrl+= Ctrl+= - + Zoom out Reduzir o zoom - + Ctrl+- Ctrl+- - + Zoom reset - Redefinir do zoom + Redefinir zoom - + Ctrl+Shift+= 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) - + + Recently Opened Files + + + + Save map Salvar mapa - + VCMI maps (*.vmap) Mapas do VCMI (*.vmap) - + Type Tipo - + + Towns + + + + + Objects + + + + + Heroes + Heróis + + + + Artifacts + Artefatos + + + + Resources + Recursos + + + + Banks + + + + + Dwellings + + + + + Grounds + + + + + Teleports + + + + + Mines + + + + + Triggers + + + + + Monsters + + + + + Quests + + + + + Wog Objects + + + + + Obstacles + + + + + Other + + + + 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 @@ -715,7 +883,7 @@ MapView - + Can't place object Não é possível colocar objeto @@ -811,7 +979,7 @@ Random faction - Fação aleatória + Facção aleatória @@ -824,7 +992,12 @@ (padrão) - + + No team + + + + Player ID: %1 ID do Jogador: %1 @@ -889,41 +1062,71 @@ 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. + QuestWidget @@ -975,7 +1178,7 @@ Primary skills - Habilidades principais + Habilidades primárias @@ -1048,12 +1251,12 @@ Jogadores - + None Nenhum - + Day %1 Dia %1 @@ -1191,7 +1394,7 @@ Primary skills - Habilidades principais + Habilidades primárias @@ -1321,18 +1524,18 @@ Jogadores - + None Nenhum - + Day %1 Dia %1 - - + + Reward %1 Recompensa %1 @@ -1347,7 +1550,7 @@ Tavern rumors - Boatos da taverna + Rumores da taverna @@ -1394,47 +1597,264 @@ - Day of first occurance + Day of first occurrence 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 - + + Objects to delete + + + + + Add + Adicionar + + + + Remove + Remover + + + Ok Ok - TownBulidingsWidget + TownBuildingsWidget - + Buildings Estruturas + + + Build all + Construir tudo + + + + Demolish all + Demolir tudo + + + + Enable all + Ativar tudo + + + + Disable all + Desativar tudo + + + + Type + Tipo + + + + Enabled + Ativado + + + + Built + Construído + + + + TownEventDialog + + + Town event + Evento da cidade + + + + General + Geral + + + + Event name + Nome do evento + + + + Type event message text + Introduza o texto da mensagem do evento + + + + Day of first occurrence + Dia da primeira ocorrência + + + + Repeat after (0 = no repeat) + Repetir após (0 = não repetir) + + + + Affected players + Jogadores afetados + + + + affects human + afeta humano + + + + affects AI + afeta IA + + + + Resources + Recursos + + + + Buildings + Estruturas + + + + Creatures + Criaturas + + + + OK + OK + + + + Creature level %1 / Creature level %1 Upgrade + Nível da criatura %1 / Nível da criatura %1 - Atualização + + + + Day %1 - %2 + Dia %1 - %2 + + + + TownEventsWidget + + + Town events + Eventos da cidade + + + + Timed events + Eventos Temporizados + + + + Add + Adicionar + + + + Remove + Remover + + + + Day %1 - %2 + Dia %1 - %2 + + + + New event + Novo Evento + + + + TownSpellsWidget + + + Spells + Feitiços + + + + Customize spells + Personalizar feitiços + + + + Level 1 + Nível 1 + + + + + + + + Spell that may appear in mage guild + Feitiço que pode aparecer na guilda dos magos + + + + + + + + Spell that must appear in mage guild + Feitiço que deve aparecer na guilda dos magos + + + + Level 2 + Nível 2 + + + + Level 3 + Nível 3 + + + + Level 4 + Nível 4 + + + + Level 5 + Nível 5 + Translations @@ -1450,7 +1870,7 @@ - Suppported + Supported Suportado @@ -1493,107 +1913,107 @@ O mapa não está carregado - + No factions allowed for player %1 Nenhuma facção permitida para o jogador %1 - + No players allowed to play this map Nenhum jogador permitido para jogar este mapa - + Map is allowed for one player and cannot be started O mapa é permitido para um jogador e não pode ser iniciado - + No human players allowed to play this map Nenhum jogador humano permitido para jogar este mapa - + Armored instance %1 is UNFLAGGABLE but must have NEUTRAL or player owner - A instância protegida %1 não tem bandeira mas precisa ser definida como neutra ou pertencente a um dos jogadores + A instância protegida %1 não tem bandeira mas precisa set definida como neutra ou pertencente a um dos jogadores - + Object %1 is assigned to non-playable player %2 O objeto %1 é atribuído a um jogador não jogável %2 - - Town %1 has undefined owner %2 - A cidade %1 possui proprietário indefinido %2 - - - - Prison %1 must be a NEUTRAL - A prisão %1 deve ser NEUTRA - - - - Hero %1 must have an owner - O herói %1 deve ter um proprietário - - - - Hero %1 is prohibited by map settings - O herói %1 é proibido pelas configurações do mapa - - - - Hero %1 has duplicate on map - O herói %1 possui duplicata no mapa - - - - Hero %1 has an empty type and must be removed - O herói %1 possui um tipo vazio e deve ser removido - - - - Spell scroll %1 is prohibited by map settings - O pergaminho mágico %1 é proibido pelas configurações do mapa - - - + Spell scroll %1 doesn't have instance assigned and must be removed - O pergaminho mágico %1 não tem instância atribuída e deve ser removido + O pergaminho de feitiço %1 não tem uma instância atribuída e deve ser removido - + Artifact %1 is prohibited by map settings O artefato %1 é proibido pelas configurações do mapa - + + Player %1 has no towns and heroes assigned + O jogador %1 não tem cidades e heróis atribuídos + + + + Prison %1 must be a NEUTRAL + A prisão %1 deve set NEUTRA + + + + Hero %1 must have an owner + O herói %1 deve ter um proprietário + + + + Hero %1 is prohibited by map settings + O herói %1 é proibido pelas configurações do mapa + + + + Hero %1 has duplicate on map + O herói %1 possui duplicata no mapa + + + + Hero %1 has an empty type and must be removed + O herói %1 possui um tipo vazio e deve set removido + + + + Spell scroll %1 is prohibited by map settings + O pergaminho mágico %1 é proibido pelas configurações do mapa + + + Player %1 doesn't have any starting town O jogador %1 não possui nenhuma cidade inicial - + Map name is not specified O nome do mapa não está especificado - + Map description is not specified A descrição do mapa não está especificada - + Map contains object from mod "%1", but doesn't require it O mapa contém objeto do mod "%1", mas não o requer - + Exception occurs during validation: %1 Ocorreu uma exceção durante a validação: %1 - + Unknown exception occurs during validation Ocorreu uma exceção desconhecida durante a validação @@ -1626,47 +2046,47 @@ Parâmetros - + No special victory Nenhuma vitória especial - + Capture artifact Capturar artefato - + Hire creatures Contratar criaturas - + Accumulate resources Acumular recursos - + Construct building Construir edifício - + Capture town Capturar cidade - + Defeat hero Derrotar herói - + Transport artifact - Transportar artefato + Transporter artefato - + Kill monster Matar monstro @@ -1698,18 +2118,6 @@ Width Largura - - S (36x36) - P (36x36) - - - M (72x72) - M (72x72) - - - L (108x108) - G (108x108) - XL (144x144) @@ -1726,7 +2134,7 @@ Jogadores - + 0 0 @@ -1825,42 +2233,62 @@ Ilhas - + + Roads + Estradas + + + + Dirt + Terra + + + + Gravel + Cascalho + + + + Cobblestone + Paralelepípedo + + + Template Modelo - + Custom seed Semente personalizada - + Generate random map Gerar mapa aleatório - + Ok Ok - + Cancel Cancelar - + No template Sem modelo - - No template for parameters scecified. Random map cannot be generated. - Sem modelo para os parâmetros especificados. O mapa aleatório não pode ser gerado. + + No template for parameters specified. Random map cannot be generated. + Sem modelo para os parâmetros especificados. O mapa aleatório não pode set gerado. - + RMG failure Falha do GMA @@ -1868,27 +2296,27 @@ main - + Filepath of the map to open. Caminho do arquivo do mapa para abrir. - + Extract original H3 archives into a separate folder. Extrair arquivos originais do H3 em uma pasta separada. - + From an extracted archive, it Splits TwCrPort, CPRSMALL, FlagPort, ITPA, ITPt, Un32 and Un44 into individual PNG's. - De um arquivo extraído, divide TwCrPort, CPRSMALL, FlagPort, ITPA, ITPt, Un32 e Un44 em PNGs individuais. + De um arquivo extraído, divide TwCrPort, CPRSMALL, FlagPort, ITPA, ITPt, Un32 e Un44 em ONGs individuals. - + From an extracted archive, Converts single Images (found in Images folder) from .pcx to png. De um arquivo extraído, converte imagens únicas (encontradas na pasta Imagens) de .pcx para png. - + Delete original files, for the ones split / converted. Excluir arquivos originais, para os divididos / convertidos. diff --git a/mapeditor/translation/russian.ts b/mapeditor/translation/russian.ts index a04e399be..1f12ef8ea 100644 --- a/mapeditor/translation/russian.ts +++ b/mapeditor/translation/russian.ts @@ -19,6 +19,30 @@ Суженная формация + + ArtifactWidget + + + + Artifact + + + + + Equip where: + + + + + Save + Сохранить + + + + Cancel + Отмена + + EventSettings @@ -42,7 +66,7 @@ - + New event @@ -65,12 +89,32 @@ Описание карты - + + Author + + + + + Author contact (e.g. email) + + + + + Map Creation Time + + + + + Map Version + + + + Limit maximum heroes level - + Difficulty Сложность @@ -83,6 +127,34 @@ Создание карты + + HeroArtifactsWidget + + + Artifacts + Артефакты + + + + Add + + + + + Remove + + + + + Slot + + + + + Artifact + + + HeroSkillsWidget @@ -223,423 +295,519 @@ Файл - + + + Open Recent + + + + Map Карта - + Edit Правка - + View Вид - + Player Игрок - + Toolbar Панель инструментов - + Minimap Мини-карта - + Map Objects View Объекты карты - + Browser Навигатор - + Inspector Инспектор - + Property Свойство - + Value Значение - + Tools - + Painting - + Terrains Земли - + Roads Дороги - + Rivers Реки - + Preview - + Open Открыть - + + More... + + + + Save Сохранить - + New Создать - + Save as... Сохранить как - + Ctrl+Shift+S Ctrl+Shift+S - + U/G П/Н - - + + View underground Вид на подземелье - + Pass Проходимость - + Cut Вырезать - + Copy Копировать - + Paste Вставить - + Fill Заливка - + Fills the selection with obstacles Заливает выбранное препятствиями - + Grid Сетка - + General Общее - + Map title and description Название и описание карты - + Players settings Настройки игроков - - + + Undo Отменить - + Redo Повторить - + Erase Удалить - + Neutral Нейтральный - + Validate Проверить - - - - + + + + Update appearance Обновить вид - + Recreate obstacles Обновить препятствия - + Player 1 Игрок 1 - + Player 2 Игрок 2 - + Player 3 Игрок 3 - + Player 4 Игрок 4 - + Player 5 Игрок 5 - + Player 6 Игрок 6 - + Player 7 Игрок 7 - + Player 8 Игрок 8 - + Export as... - + Translations - + Ctrl+T - - + + h3m converter - + Lock - + Lock objects on map to avoid unnecessary changes - + Ctrl+L - + Unlock - + Unlock all objects on the map - + Ctrl+Shift+L - + Zoom in - + Ctrl+= - + Zoom out - + Ctrl+- - + Zoom reset - + 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);;Карты Героев III (*.h3m) - + + Recently Opened Files + + + + Save map Сохранить карту - + VCMI maps (*.vmap) Карты VCMI (*.vmap) - + Type Тип - + + Towns + + + + + Objects + + + + + Heroes + Герои + + + + Artifacts + Артефакты + + + + Resources + + + + + Banks + + + + + Dwellings + + + + + Grounds + + + + + Teleports + + + + + Mines + + + + + Triggers + + + + + Monsters + + + + + Quests + + + + + Wog Objects + + + + + Obstacles + + + + + Other + + + + 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 @@ -715,7 +883,7 @@ MapView - + Can't place object @@ -824,7 +992,12 @@ (по умолчанию) - + + No team + + + + Player ID: %1 Игрок: %1 @@ -889,41 +1062,72 @@ - + 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. + + QuestWidget @@ -1048,12 +1252,12 @@ - + None Нет - + Day %1 @@ -1321,18 +1525,18 @@ - + None Нет - + Day %1 - - + + Reward %1 @@ -1394,47 +1598,264 @@ - Day of first occurance + Day of first occurrence - + Repeat after (0 = no repeat) - + Affected players - + Resources - + type - + qty - + + Objects to delete + + + + + Add + + + + + Remove + + + + Ok ОК - TownBulidingsWidget + TownBuildingsWidget - + Buildings Постройки + + + Build all + + + + + Demolish all + + + + + Enable all + + + + + Disable all + + + + + Type + Тип + + + + Enabled + + + + + Built + + + + + TownEventDialog + + + Town event + + + + + General + Общее + + + + Event name + + + + + Type event message text + + + + + Day of first occurrence + + + + + Repeat after (0 = no repeat) + + + + + Affected players + + + + + affects human + + + + + affects AI + + + + + Resources + + + + + Buildings + Постройки + + + + Creatures + + + + + OK + + + + + Creature level %1 / Creature level %1 Upgrade + + + + + Day %1 - %2 + + + + + TownEventsWidget + + + Town events + + + + + Timed events + + + + + Add + + + + + Remove + + + + + Day %1 - %2 + + + + + New event + + + + + TownSpellsWidget + + + Spells + Заклинания + + + + Customize spells + + + + + Level 1 + + + + + + + + + Spell that may appear in mage guild + + + + + + + + + Spell that must appear in mage guild + + + + + Level 2 + + + + + Level 3 + + + + + Level 4 + + + + + Level 5 + + Translations @@ -1450,7 +1871,7 @@ - Suppported + Supported @@ -1493,107 +1914,107 @@ - + No factions allowed for player %1 - + No players allowed to play this map - + Map is allowed for one player and cannot be started - + No human players allowed to play this map - + Armored instance %1 is UNFLAGGABLE but must have NEUTRAL or player owner - + Object %1 is assigned to non-playable player %2 - - Town %1 has undefined owner %2 - У города %1 неопределенный владелец %2 - - - - Prison %1 must be a NEUTRAL - - - - - Hero %1 must have an owner - - - - - Hero %1 is prohibited by map settings - - - - - Hero %1 has duplicate on map - - - - - Hero %1 has an empty type and must be removed - - - - - Spell scroll %1 is prohibited by map settings - - - - + Spell scroll %1 doesn't have instance assigned and must be removed - + Artifact %1 is prohibited by map settings - - Player %1 doesn't have any starting town + + Player %1 has no towns and heroes assigned - - Map name is not specified + + Prison %1 must be a NEUTRAL + + + + + Hero %1 must have an owner + + + + + Hero %1 is prohibited by map settings + + + + + Hero %1 has duplicate on map + + + + + Hero %1 has an empty type and must be removed + + + + + Spell scroll %1 is prohibited by map settings + Player %1 doesn't have any starting town + + + + + Map name is not specified + + + + Map description is not specified - + Map contains object from mod "%1", but doesn't require it - + Exception occurs during validation: %1 - + Unknown exception occurs during validation @@ -1626,47 +2047,47 @@ Параметры - + No special victory Нет специальной победы - + Capture artifact Взять артефакт - + Hire creatures Нанять существ - + Accumulate resources Собрать ресурсы - + Construct building Построить - + Capture town Захватить город - + Defeat hero Победить героя - + Transport artifact Переместить артефакт - + Kill monster @@ -1698,18 +2119,6 @@ Width Ширина - - S (36x36) - Мал. (36x36) - - - M (72x72) - Ср. (72x72) - - - L (108x108) - Бол. (108x108) - XL (144x144) @@ -1726,7 +2135,7 @@ Игроки - + 0 0 @@ -1825,42 +2234,62 @@ Острова - + + Roads + Дороги + + + + Dirt + + + + + Gravel + + + + + Cobblestone + + + + Template Шаблон - + Custom seed Пользовательское зерно - + Generate random map Сгенерировать случайную карту - + Ok ОК - + Cancel Отмена - + No template - - No template for parameters scecified. Random map cannot be generated. + + No template for parameters specified. Random map cannot be generated. - + RMG failure @@ -1868,27 +2297,27 @@ main - + Filepath of the map to open. Путь к файлу карты для открытия. - + Extract original H3 archives into a separate folder. Распаковать архивы оригинальных Героев III в отдельную папку. - + From an extracted archive, it Splits TwCrPort, CPRSMALL, FlagPort, ITPA, ITPt, Un32 and Un44 into individual PNG's. Разделение в распакованном архиве TwCrPort, CPRSMALL, FlagPort, ITPA, ITPt, Un32 и Un44 на отдельные PNG. - + From an extracted archive, Converts single Images (found in Images folder) from .pcx to png. Преобразование в расспакованном архиве изображений .pcx в .png. - + Delete original files, for the ones split / converted. Удалить оригиналы для преобразованных файлов. diff --git a/mapeditor/translation/spanish.ts b/mapeditor/translation/spanish.ts index 7ce8196a5..78e43a61b 100644 --- a/mapeditor/translation/spanish.ts +++ b/mapeditor/translation/spanish.ts @@ -19,6 +19,30 @@ Formación ajustada + + ArtifactWidget + + + + Artifact + + + + + Equip where: + + + + + Save + Guardar + + + + Cancel + Cancelar + + EventSettings @@ -42,7 +66,7 @@ - + New event @@ -65,12 +89,32 @@ Descripción del mapa - + + Author + + + + + Author contact (e.g. email) + + + + + Map Creation Time + + + + + Map Version + + + + Limit maximum heroes level - + Difficulty Dificultad @@ -83,6 +127,34 @@ Generando mapa + + HeroArtifactsWidget + + + Artifacts + Artefactos + + + + Add + + + + + Remove + + + + + Slot + + + + + Artifact + + + HeroSkillsWidget @@ -223,423 +295,519 @@ Archivo - + + + Open Recent + + + + Map Mapa - + Edit Editar - + View Ver - + Player Jugador - + Toolbar Barra de herramientas - + Minimap Miniatura del mapa - + Map Objects View Vista de Objetos del Mapa - + Browser Navegador - + Inspector Inspector - + Property Propiedad - + Value Valor - + Tools - + Painting - + Terrains Terrenos - + Roads Caminos - + Rivers Ríos - + Preview - + Open Abrir - + + More... + + + + Save Guardar - + New Nuevo - + Save as... Guardar como... - + Ctrl+Shift+S Ctrl+Shift+S - + U/G Subterráneo/Superficie - - + + View underground Ver subterráneo - + Pass Pasar - + Cut Cortar - + Copy Copiar - + Paste Pegar - + Fill Rellenar - + Fills the selection with obstacles Rellena la selección con obstáculos - + Grid Rejilla - + General General - + Map title and description Título y descripción del mapa - + Players settings Configuración de jugadores - - + + Undo Deshacer - + Redo Rehacer - + Erase Borrar - + Neutral Neutral - + Validate Validar - - - - + + + + Update appearance Actualizar apariencia - + Recreate obstacles Recrear obstáculos - + Player 1 Jugador 1 - + Player 2 Jugador 2 - + Player 3 Jugador 3 - + Player 4 Jugador 4 - + Player 5 Jugador 5 - + Player 6 Jugador 6 - + Player 7 Jugador 7 - + Player 8 Jugador 8 - + Export as... Exportar como... - + Translations - + Ctrl+T - - + + h3m converter - + Lock - + Lock objects on map to avoid unnecessary changes - + Ctrl+L - + Unlock - + Unlock all objects on the map - + Ctrl+Shift+L - + Zoom in - + Ctrl+= - + Zoom out - + Ctrl+- - + Zoom reset - + Ctrl+Shift+= - + 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) - + + Recently Opened Files + + + + Save map Guardar mapa - + VCMI maps (*.vmap) Mapas VCMI (*.vmap) - + Type Tipo - + + Towns + + + + + Objects + + + + + Heroes + Héroes + + + + Artifacts + Artefactos + + + + Resources + + + + + Banks + + + + + Dwellings + + + + + Grounds + + + + + Teleports + + + + + Mines + + + + + Triggers + + + + + Monsters + + + + + Quests + + + + + Wog Objects + + + + + Obstacles + + + + + Other + + + + 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 @@ -715,7 +883,7 @@ MapView - + Can't place object @@ -824,7 +992,12 @@ (predeterminado) - + + No team + + + + Player ID: %1 ID de jugador: %1 @@ -889,41 +1062,71 @@ - + 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. + + QuestWidget @@ -1048,12 +1251,12 @@ Jugadores - + None Ninguno - + Day %1 @@ -1321,18 +1524,18 @@ Jugadores - + None Ninguno - + Day %1 - - + + Reward %1 @@ -1394,47 +1597,264 @@ - Day of first occurance + Day of first occurrence - + Repeat after (0 = no repeat) - + Affected players - + Resources - + type - + qty - + + Objects to delete + + + + + Add + + + + + Remove + + + + Ok Aceptar - TownBulidingsWidget + TownBuildingsWidget - + Buildings Edificios + + + Build all + + + + + Demolish all + + + + + Enable all + + + + + Disable all + + + + + Type + Tipo + + + + Enabled + + + + + Built + + + + + TownEventDialog + + + Town event + + + + + General + General + + + + Event name + + + + + Type event message text + + + + + Day of first occurrence + + + + + Repeat after (0 = no repeat) + + + + + Affected players + + + + + affects human + + + + + affects AI + + + + + Resources + + + + + Buildings + Edificios + + + + Creatures + + + + + OK + + + + + Creature level %1 / Creature level %1 Upgrade + + + + + Day %1 - %2 + + + + + TownEventsWidget + + + Town events + + + + + Timed events + + + + + Add + + + + + Remove + + + + + Day %1 - %2 + + + + + New event + + + + + TownSpellsWidget + + + Spells + Hechizos + + + + Customize spells + + + + + Level 1 + + + + + + + + + Spell that may appear in mage guild + + + + + + + + + Spell that must appear in mage guild + + + + + Level 2 + + + + + Level 3 + + + + + Level 4 + + + + + Level 5 + + Translations @@ -1450,7 +1870,7 @@ - Suppported + Supported @@ -1493,107 +1913,107 @@ No se ha cargado ningún mapa - + No factions allowed for player %1 - + No players allowed to play this map No hay jugadores autorizados a jugar en este mapa - + Map is allowed for one player and cannot be started El mapa está autorizado para un jugador y no se puede iniciar - + No human players allowed to play this map Ningún jugador humano puede jugar en este mapa - + Armored instance %1 is UNFLAGGABLE but must have NEUTRAL or player owner La instancia protegida %1 NOSEPUEDEMARCAR, pero debe tener un propietario NEUTRAL o jugador - + Object %1 is assigned to non-playable player %2 El artículo %1 está asignado al jugador no jugable %2 - - Town %1 has undefined owner %2 - La ciudad %1 no tiene un propietario definido %2 + + Spell scroll %1 doesn't have instance assigned and must be removed + - + + Artifact %1 is prohibited by map settings + + + + + Player %1 has no towns and heroes assigned + + + + Prison %1 must be a NEUTRAL %1 prisión debe ser NEUTRA - + Hero %1 must have an owner El héroe %1 debe tener un propietario - + Hero %1 is prohibited by map settings El héroe %1 está prohibido por la configuración del mapa - + Hero %1 has duplicate on map El héroe %1 tiene un duplicado en el mapa - + Hero %1 has an empty type and must be removed El héroe %1 tiene un tipo vacío y debe eliminarse - + Spell scroll %1 is prohibited by map settings %1 desplazamiento de hechizos está prohibido por la configuración del mapa - - Spell scroll %1 doesn't have instance assigned and must be removed - Pergamino ortográfico %1 no tiene una instancia asignada y debe eliminarse - - - - Artifact %1 is prohibited by map settings - El artefacto %1 está prohibido por la configuración del mapa - - - + Player %1 doesn't have any starting town El jugador %1 no tiene ciudad inicial - + Map name is not specified No se especifica el nombre del mapa - + Map description is not specified No se especifica la descripción del mapa - + Map contains object from mod "%1", but doesn't require it - + Exception occurs during validation: %1 Se produce una excepción durante la validación: %1 - + Unknown exception occurs during validation Se produce una excepción desconocida durante la validación @@ -1626,47 +2046,47 @@ Parámetros - + No special victory Sin victoria especial - + Capture artifact Capturar artefacto - + Hire creatures Contratar criaturas - + Accumulate resources Acumular recursos - + Construct building Construir edificio - + Capture town Capturar ciudad - + Defeat hero Vencer héroe - + Transport artifact Transportar artefacto - + Kill monster @@ -1698,18 +2118,6 @@ Width Ancho - - S (36x36) - S (36x36) - - - M (72x72) - M (72x72) - - - L (108x108) - L (108x108) - XL (144x144) @@ -1726,7 +2134,7 @@ Jugadores - + 0 0 @@ -1825,42 +2233,62 @@ Islas - + + Roads + Caminos + + + + Dirt + + + + + Gravel + + + + + Cobblestone + + + + Template Plantilla - + Custom seed Semilla personalizada - + Generate random map Generar un mapa aleatorio - + Ok Aceptar - + Cancel Cancelar - + No template - - No template for parameters scecified. Random map cannot be generated. + + No template for parameters specified. Random map cannot be generated. - + RMG failure @@ -1868,27 +2296,27 @@ main - + Filepath of the map to open. Ruta del archivo del mapa a abrir. - + Extract original H3 archives into a separate folder. Extraer archivos originales de H3 en una carpeta separada. - + From an extracted archive, it Splits TwCrPort, CPRSMALL, FlagPort, ITPA, ITPt, Un32 and Un44 into individual PNG's. Desde un archivo extraído, separa TwCrPort, CPRSMALL, FlagPort, ITPA, ITPt, Un32 y Un44 en imágenes PNG individuales. - + From an extracted archive, Converts single Images (found in Images folder) from .pcx to png. Desde un archivo extraído, convierte imágenes individuales (encontradas en la carpeta Imágenes) de .pcx a png. - + Delete original files, for the ones split / converted. Eliminar archivos originales, por los que se han separado / convertido. diff --git a/mapeditor/translation/ukrainian.ts b/mapeditor/translation/ukrainian.ts index 2b54ff4cc..bde55275e 100644 --- a/mapeditor/translation/ukrainian.ts +++ b/mapeditor/translation/ukrainian.ts @@ -19,32 +19,56 @@ Щільна формація + + ArtifactWidget + + + + Artifact + Артефакт + + + + Equip where: + Де поставити: + + + + Save + Зберегти + + + + Cancel + Скасувати + + EventSettings Form - + Form Timed events - + Заплановані події Add - + Додати Remove - + Видалити - + New event - + Нова подія @@ -52,27 +76,47 @@ Form - + Form Map name - Назва мапи + Назва мапи Map description - Опис мапи + Опис мапи - + + Author + Автор + + + + Author contact (e.g. email) + Контакти автора (наприклад, електронна пошта) + + + + Map Creation Time + Час створення мапи + + + + Map Version + Версія мапи + + + Limit maximum heroes level - Обмежити максимальний рівень героїв + Обмежити максимальний рівень героїв - + Difficulty - Складність + Складність @@ -83,12 +127,40 @@ Побудова мапи + + HeroArtifactsWidget + + + Artifacts + Артефакти + + + + Add + Додати + + + + Remove + Видалити + + + + Slot + Слот + + + + Artifact + Артефакт + + HeroSkillsWidget Hero skills - + Вміння героя @@ -101,27 +173,27 @@ Add - + Додати Remove - + Видалити Skill - + Вміння Level - + Рівень Customize skills - + Користувацькі навички @@ -129,37 +201,37 @@ Spells - Закляття + Закляття Customize spells - + Користувацькі заклинання Level 1 - + 1-й рівень Level 2 - + 2-й рівень Level 3 - + 3-й рівень Level 4 - + 4-й рівень Level 5 - + 5-й рівень @@ -167,7 +239,7 @@ Form - + Form @@ -223,423 +295,519 @@ Файл - + + + Open Recent + Відкрити останні + + + Map Мапа - + Edit Редагування - + View Вигляд - + Player Гравець - + Toolbar Панель інструментів - + Minimap Мінімапа - + Map Objects View Перегляд об'єктів мапи - + Browser Навігатор - + Inspector Інспектор - + Property Властивість - + Value Значення - + Tools - + Інструменти - + Painting - + Малювання - + Terrains Землі - + Roads Шляхи - + Rivers Річки - + Preview - + Попередній перегляд - + Open Відкрити - + + More... + Ще... + + + Save Зберегти - + New Створити - + Save as... Зберегти як... - + Ctrl+Shift+S Ctrl+Shift+S - + U/G П/З - - + + View underground Дивитись підземелля - + Pass Прохідність - + Cut Вирізати - + Copy Скопіювати - + Paste Вставити - + Fill Заповнити - + Fills the selection with obstacles Заповнити перешкодами - + Grid Сітка - + General Загальний - + Map title and description Назва та опис мапи - + Players settings Налаштування гравців - - + + Undo Відмінити - + Redo Повторити - + Erase Стерти - + Neutral Нейтральний - + Validate Перевірити - - - - + + + + Update appearance Оновити вигляд - + Recreate obstacles Оновити перешкоди - + Player 1 Гравець 1 - + Player 2 Гравець 2 - + Player 3 Гравець 3 - + Player 4 Гравець 4 - + Player 5 Гравець 5 - + Player 6 Гравець 6 - + Player 7 Гравець 7 - + Player 8 Гравець 8 - + Export as... Експортувати як... - + Translations - + Переклади - + Ctrl+T - - + + h3m converter - + Lock - + Lock objects on map to avoid unnecessary changes - + Ctrl+L - + Unlock - + Unlock all objects on the map - + Ctrl+Shift+L - + Zoom in - + Ctrl+= - + Zoom out - + Ctrl+- - + Zoom reset - + 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);;Мапи HoMM3 (*.h3m) - + + Recently Opened Files + + + + Save map Зберегти мапу - + VCMI maps (*.vmap) Мапи VCMI - + Type Тип - + + Towns + + + + + Objects + + + + + Heroes + Герої + + + + Artifacts + Артефакти + + + + Resources + + + + + Banks + + + + + Dwellings + + + + + Grounds + + + + + Teleports + + + + + Mines + + + + + Triggers + + + + + Monsters + + + + + Quests + + + + + Wog Objects + + + + + Obstacles + + + + + Other + + + + 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 @@ -679,12 +847,12 @@ Timed - + Заплановані Rumors - + Чутки @@ -715,9 +883,9 @@ MapView - + Can't place object - + Неможливо розмістити об'єкт @@ -733,7 +901,7 @@ Form - + Form @@ -806,7 +974,7 @@ ... - + @@ -824,7 +992,12 @@ (за замовчуванням) - + + No team + + + + Player ID: %1 Гравець %1 @@ -889,41 +1062,72 @@ - + 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. + + QuestWidget @@ -1005,55 +1209,55 @@ Artifacts - Артефакти + Артефакти Spells - Закляття + Закляття Skills - + Вміння Creatures - + Істоти Add - + Додати Remove - + Видалити Heroes - Герої + Герої Hero classes - + Класи героїв Players - + Гравці - + None - Відсутня + Будь який - + Day %1 @@ -1321,18 +1525,18 @@ - + None Відсутня - + Day %1 - - + + Reward %1 @@ -1394,47 +1598,264 @@ - Day of first occurance + Day of first occurrence - + Repeat after (0 = no repeat) - + Affected players - + Resources - + type - + qty - + + Objects to delete + + + + + Add + + + + + Remove + + + + Ok Підтвердити - TownBulidingsWidget + TownBuildingsWidget - + Buildings Будівлі + + + Build all + + + + + Demolish all + + + + + Enable all + + + + + Disable all + + + + + Type + Тип + + + + Enabled + + + + + Built + + + + + TownEventDialog + + + Town event + + + + + General + Загальний + + + + Event name + + + + + Type event message text + + + + + Day of first occurrence + + + + + Repeat after (0 = no repeat) + + + + + Affected players + + + + + affects human + + + + + affects AI + + + + + Resources + + + + + Buildings + Будівлі + + + + Creatures + + + + + OK + + + + + Creature level %1 / Creature level %1 Upgrade + + + + + Day %1 - %2 + + + + + TownEventsWidget + + + Town events + + + + + Timed events + + + + + Add + + + + + Remove + + + + + Day %1 - %2 + + + + + New event + + + + + TownSpellsWidget + + + Spells + Закляття + + + + Customize spells + + + + + Level 1 + + + + + + + + + Spell that may appear in mage guild + + + + + + + + + Spell that must appear in mage guild + + + + + Level 2 + + + + + Level 3 + + + + + Level 4 + + + + + Level 5 + + Translations @@ -1450,7 +1871,7 @@ - Suppported + Supported @@ -1493,107 +1914,107 @@ - + No factions allowed for player %1 - + No players allowed to play this map - + Map is allowed for one player and cannot be started - + No human players allowed to play this map - + Armored instance %1 is UNFLAGGABLE but must have NEUTRAL or player owner - + Object %1 is assigned to non-playable player %2 - - Town %1 has undefined owner %2 - Місто %1 має невизначеного володаря %2 - - - - Prison %1 must be a NEUTRAL - - - - - Hero %1 must have an owner - - - - - Hero %1 is prohibited by map settings - - - - - Hero %1 has duplicate on map - - - - - Hero %1 has an empty type and must be removed - - - - - Spell scroll %1 is prohibited by map settings - - - - + Spell scroll %1 doesn't have instance assigned and must be removed - + Artifact %1 is prohibited by map settings - - Player %1 doesn't have any starting town + + Player %1 has no towns and heroes assigned - - Map name is not specified + + Prison %1 must be a NEUTRAL + + + + + Hero %1 must have an owner + + + + + Hero %1 is prohibited by map settings + + + + + Hero %1 has duplicate on map + + + + + Hero %1 has an empty type and must be removed + + + + + Spell scroll %1 is prohibited by map settings + Player %1 doesn't have any starting town + + + + + Map name is not specified + + + + Map description is not specified - + Map contains object from mod "%1", but doesn't require it - + Exception occurs during validation: %1 - + Unknown exception occurs during validation @@ -1626,47 +2047,47 @@ Параметри - + No special victory Немає особливої перемоги - + Capture artifact Отримати артефакт - + Hire creatures Найняти істот - + Accumulate resources Накопичити ресурси - + Construct building Побудувати будівлю - + Capture town Захопити місто - + Defeat hero Перемогти героя - + Transport artifact Доставити артефакт - + Kill monster @@ -1698,18 +2119,6 @@ Width Ширина - - S (36x36) - М (36x36) - - - M (72x72) - С (72x72) - - - L (108x108) - В (108x108) - XL (144x144) @@ -1726,7 +2135,7 @@ Гравців - + 0 0 @@ -1825,42 +2234,62 @@ Острови - + + Roads + Шляхи + + + + Dirt + + + + + Gravel + + + + + Cobblestone + + + + Template Шаблон - + Custom seed Користувацьке зерно - + Generate random map Згенерувати випадкову карту - + Ok Підтвердити - + Cancel Скасувати - + No template - - No template for parameters scecified. Random map cannot be generated. + + No template for parameters specified. Random map cannot be generated. - + RMG failure @@ -1868,27 +2297,27 @@ main - + Filepath of the map to open. - + Extract original H3 archives into a separate folder. - + From an extracted archive, it Splits TwCrPort, CPRSMALL, FlagPort, ITPA, ITPt, Un32 and Un44 into individual PNG's. - + From an extracted archive, Converts single Images (found in Images folder) from .pcx to png. - + Delete original files, for the ones split / converted. diff --git a/mapeditor/translation/vietnamese.ts b/mapeditor/translation/vietnamese.ts index 029004271..3bdb1dd75 100644 --- a/mapeditor/translation/vietnamese.ts +++ b/mapeditor/translation/vietnamese.ts @@ -19,6 +19,30 @@ Đội hình kín + + ArtifactWidget + + + + Artifact + + + + + Equip where: + + + + + Save + Lưu + + + + Cancel + Hủy + + EventSettings @@ -42,7 +66,7 @@ - + New event @@ -65,12 +89,32 @@ Mô tả bản đồ - + + Author + + + + + Author contact (e.g. email) + + + + + Map Creation Time + + + + + Map Version + + + + Limit maximum heroes level Giới hạn cấp tướng tối đa - + Difficulty Độ khó @@ -83,6 +127,34 @@ Tạo bản đồ + + HeroArtifactsWidget + + + Artifacts + Vật phẩm + + + + Add + + + + + Remove + + + + + Slot + + + + + Artifact + + + HeroSkillsWidget @@ -223,423 +295,519 @@ Tập tin - + Map Bản đồ - + Edit Hiệu chỉnh - + View Xem - + Player Người chơi - + Toolbar Thanh công cụ - + Minimap Bản đồ nhỏ - + Map Objects View Xem đối tượng bản đồ - + Browser Duyệt - + Inspector Giám định - + Property Đặc tính - + Value Giá trị - + Terrains Địa hình - + Roads Đường - + Rivers Sông - + Open Mở - + Save Lưu - + New Tạo mới - + Tools - + + + Open Recent + + + + Painting - + Preview - + + More... + + + + Save as... - + Ctrl+Shift+S Ctrl+Shift+S - + U/G U/G - - + + View underground Xem hang ngầm - + Pass Đi qua - + Cut Cắt - + Copy Sao chép - + Paste Dán - + Fill Làm đầy - + Fills the selection with obstacles Làm đầy vùng chọn với vật cản - + Grid Đường kẻ - + General Chung - + Map title and description Tên bản đồ và mô tả - + Players settings Cài đặt người chơi - - + + Undo Hoàn tác - + Redo Làm lại - + Erase Xóa - + Neutral Trung lập - + Validate Hiệu lực - - - - + + + + Update appearance Cập nhật hiện thị - + Recreate obstacles Tạo lại vật cản - + Player 1 Người chơi 1 - + Player 2 Người chơi 2 - + Player 3 Người chơi 3 - + Player 4 Người chơi 4 - + Player 5 Người chơi 5 - + Player 6 Người chơi 6 - + Player 7 Người chơi 7 - + Player 8 Người chơi 8 - + Export as... Xuất thành... - + Translations - + Ctrl+T - - + + h3m converter - + Lock - + Lock objects on map to avoid unnecessary changes - + Ctrl+L - + Unlock - + Unlock all objects on the map - + Ctrl+Shift+L - + Zoom in - + Ctrl+= - + Zoom out - + Ctrl+- - + Zoom reset - + Ctrl+Shift+= - + 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) - + + Recently Opened Files + + + + Save map Lưu bản đồ - + VCMI maps (*.vmap) Bản đồ VCMI (*.vmap) - + Type Loại - + + Towns + + + + + Objects + + + + + Heroes + Tướng + + + + Artifacts + Vật phẩm + + + + Resources + + + + + Banks + + + + + Dwellings + + + + + Grounds + + + + + Teleports + + + + + Mines + + + + + Triggers + + + + + Monsters + + + + + Quests + + + + + Wog Objects + + + + + Obstacles + + + + + Other + + + + 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 @@ -715,7 +883,7 @@ MapView - + Can't place object Không thể đặt vật thể @@ -824,7 +992,12 @@ (mặc định) - + + No team + + + + Player ID: %1 ID người chơi: %1 @@ -889,41 +1062,70 @@ - + 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. + + QuestWidget @@ -1048,12 +1250,12 @@ Người chơi - + None Không - + Day %1 @@ -1321,18 +1523,18 @@ Người chơi - + None Không - + Day %1 - - + + Reward %1 @@ -1394,47 +1596,264 @@ - Day of first occurance + Day of first occurrence - + Repeat after (0 = no repeat) - + Affected players - + Resources - + type - + qty - + + Objects to delete + + + + + Add + + + + + Remove + + + + Ok Đồng ý - TownBulidingsWidget + TownBuildingsWidget - + Buildings Công trình + + + Build all + + + + + Demolish all + + + + + Enable all + + + + + Disable all + + + + + Type + Loại + + + + Enabled + + + + + Built + + + + + TownEventDialog + + + Town event + + + + + General + Chung + + + + Event name + + + + + Type event message text + + + + + Day of first occurrence + + + + + Repeat after (0 = no repeat) + + + + + Affected players + + + + + affects human + + + + + affects AI + + + + + Resources + + + + + Buildings + Công trình + + + + Creatures + + + + + OK + + + + + Creature level %1 / Creature level %1 Upgrade + + + + + Day %1 - %2 + + + + + TownEventsWidget + + + Town events + + + + + Timed events + + + + + Add + + + + + Remove + + + + + Day %1 - %2 + + + + + New event + + + + + TownSpellsWidget + + + Spells + Phép + + + + Customize spells + + + + + Level 1 + + + + + + + + + Spell that may appear in mage guild + + + + + + + + + Spell that must appear in mage guild + + + + + Level 2 + + + + + Level 3 + + + + + Level 4 + + + + + Level 5 + + Translations @@ -1450,7 +1869,7 @@ - Suppported + Supported @@ -1493,107 +1912,107 @@ Bản đồ không thể tải - + No factions allowed for player %1 Không có tộc được phép cho người chơi %1 - + No players allowed to play this map Không có người chơi được phép chơi bản đồ này - + Map is allowed for one player and cannot be started Bản đồ cho phép 1 người chơi nhưng không thể bắt đầu - + No human players allowed to play this map Không có người nào được phép chơi bản đồ này - + Armored instance %1 is UNFLAGGABLE but must have NEUTRAL or player owner Thực thể %1 không gắn cờ nhưng phải có quái trung lập hoặc người chơi sở hữu - + Object %1 is assigned to non-playable player %2 Vật thể %1 được gán cho người không thể chơi %2 - - Town %1 has undefined owner %2 - Thành %1 có chủ nhân không xác định %2 + + Spell scroll %1 doesn't have instance assigned and must be removed + - + + Artifact %1 is prohibited by map settings + + + + + Player %1 has no towns and heroes assigned + + + + Prison %1 must be a NEUTRAL Nhà giam %1 phải trung lập - + Hero %1 must have an owner Tướng %1 phải có chủ - + Hero %1 is prohibited by map settings Tướng %1 bị cấm bởi bản đồ - + Hero %1 has duplicate on map Tướng %1 bị trùng trên bản đồ - + Hero %1 has an empty type and must be removed Tướng %1 có kiểu rỗng và phải được xóa - + Spell scroll %1 is prohibited by map settings Cuộn phép %1 bị cấm bởi bản đồ - - Spell scroll %1 doesn't have instance assigned and must be removed - Cuộn phép %1 không có đối tượng được gán và phải được xóa - - - - Artifact %1 is prohibited by map settings - Vật phẩm %1 bị cấm bởi bản đồ - - - + Player %1 doesn't have any starting town Người chơi %1 không có thành khởi đầu nào - + Map name is not specified Tên bản đồ không có - + Map description is not specified Mô tả bản đồ không có - + Map contains object from mod "%1", but doesn't require it Bản đồ chứa đối tượng từ bản mở rộng "%1", nhưng bản mở rộng đó không được yêu cầu - + Exception occurs during validation: %1 Ngoại lệ xuất hiện trong quá trình phê chuẩn: %1 - + Unknown exception occurs during validation Ngoại lệ chưa biết xuất hiện trong quá trình phê chuẩn: %1 @@ -1626,47 +2045,47 @@ Tham số - + No special victory Không có chiến thắng đặc biệt - + Capture artifact Đoạt vật phẩm - + Hire creatures Thuê quái - + Accumulate resources Cộng dồn tài nguyên - + Construct building Xây công trình - + Capture town Đoạt thành - + Defeat hero Đánh bại tướng - + Transport artifact Vận chuyển vật phẩm - + Kill monster @@ -1698,18 +2117,6 @@ Width Rộng - - S (36x36) - Nhỏ (36x36) - - - M (72x72) - Vừa (72x72) - - - L (108x108) - Lớn (108x108) - XL (144x144) @@ -1726,7 +2133,7 @@ Người chơi - + 0 0 @@ -1825,42 +2232,62 @@ Các đảo - + + Roads + Đường + + + + Dirt + + + + + Gravel + + + + + Cobblestone + + + + Template Mẫu - + Custom seed Tùy chỉnh ban đầu - + Generate random map Tạo bản đồ ngẫu nhiên - + Ok Đồng ý - + Cancel Hủy - + No template Không dùng mẫu - - No template for parameters scecified. Random map cannot be generated. + + No template for parameters specified. Random map cannot be generated. Không có mẫu cho tham số chỉ định. Bản đồ ngẫu nhiên không thể tạo - + RMG failure Tạo bản đồ ngẫu nhiên thất bại @@ -1868,27 +2295,27 @@ main - + Filepath of the map to open. Đường dẫn bản đồ - + Extract original H3 archives into a separate folder. Giải nén dữ liệu H3 gốc vào 1 thư mục riêng. - + From an extracted archive, it Splits TwCrPort, CPRSMALL, FlagPort, ITPA, ITPt, Un32 and Un44 into individual PNG's. Từ dữ liệu giải nén, chia TwCrPort, CPRSMALL, FlagPort, ITPA, ITPt, Un32 và Un44 thành những hình PNG riêng lẻ. - + From an extracted archive, Converts single Images (found in Images folder) from .pcx to png. Từ dữ liệu giải nén, chuyển đổi các hình đơn (được tìm thấy trong thư mục Images) từ .pcx sang .png. - + Delete original files, for the ones split / converted. Xóa các tập tin gốc đã được phân chia / chuyển đổi. diff --git a/mapeditor/validator.cpp b/mapeditor/validator.cpp index a70e0cd90..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), @@ -43,13 +43,13 @@ Validator::~Validator() delete ui; } -std::list Validator::validate(const CMap * map) +std::set Validator::validate(const CMap * map) { - std::list issues; + std::set issues; if(!map) { - issues.emplace_back(tr("Map is not loaded"), true); + issues.insert({ tr("Map is not loaded"), true }); return issues; } @@ -58,27 +58,29 @@ std::list Validator::validate(const CMap * map) //check player settings int hplayers = 0; int cplayers = 0; - std::map amountOfCastles; + std::map amountOfTowns; + std::map amountOfHeroes; + for(int i = 0; i < map->players.size(); ++i) { auto & p = map->players[i]; - if(p.canAnyonePlay()) - amountOfCastles[i] = 0; + if (p.canAnyonePlay()) + amountOfTowns[PlayerColor(i)] = 0; if(p.canComputerPlay) ++cplayers; if(p.canHumanPlay) ++hplayers; if(p.allowedFactions.empty()) - issues.emplace_back(QString(tr("No factions allowed for player %1")).arg(i), true); + issues.insert({ tr("No factions allowed for player %1").arg(i), true }); } if(hplayers + cplayers == 0) - issues.emplace_back(tr("No players allowed to play this map"), true); + issues.insert({ tr("No players allowed to play this map"), true }); if(hplayers + cplayers == 1) - issues.emplace_back(tr("Map is allowed for one player and cannot be started"), true); + issues.insert({ tr("Map is allowed for one player and cannot be started"), true }); if(!hplayers) - issues.emplace_back(tr("No human players allowed to play this map"), true); + issues.insert({ tr("No human players allowed to play this map"), true }); - std::set allHeroesOnMap; //used to find hero duplicated + std::set allHeroesOnMap; //used to find hero duplicated //checking all objects in the map for(auto o : map->objects) @@ -86,105 +88,111 @@ std::list Validator::validate(const CMap * map) //owners for objects if(o->getOwner() == PlayerColor::UNFLAGGABLE) { - if(dynamic_cast(o.get()) || - dynamic_cast(o.get()) || - dynamic_cast(o.get()) || - dynamic_cast(o.get()) || - dynamic_cast(o.get())) + if(dynamic_cast(o.get()) || + dynamic_cast(o.get()) || + dynamic_cast(o.get()) || + dynamic_cast(o.get()) || + dynamic_cast(o.get())) { - issues.emplace_back(QString(tr("Armored instance %1 is UNFLAGGABLE but must have NEUTRAL or player owner")).arg(o->instanceName.c_str()), true); + issues.insert({ tr("Armored instance %1 is UNFLAGGABLE but must have NEUTRAL or player owner").arg(o->instanceName.c_str()), true }); } } if(o->getOwner() != PlayerColor::NEUTRAL && o->getOwner().getNum() < map->players.size()) { if(!map->players[o->getOwner().getNum()].canAnyonePlay()) - issues.emplace_back(QString(tr("Object %1 is assigned to non-playable player %2")).arg(o->instanceName.c_str(), o->getOwner().toString().c_str()), true); + issues.insert({ tr("Object %1 is assigned to non-playable player %2").arg(o->instanceName.c_str(), o->getOwner().toString().c_str()), true }); } - //checking towns - if(auto * ins = dynamic_cast(o.get())) + //count towns + if(auto * ins = dynamic_cast(o.get())) { - bool has = amountOfCastles.count(ins->getOwner().getNum()); - if(!has && ins->getOwner() != PlayerColor::NEUTRAL) - issues.emplace_back(tr("Town %1 has undefined owner %2").arg(ins->instanceName.c_str(), ins->getOwner().toString().c_str()), true); - if(has) - ++amountOfCastles[ins->getOwner().getNum()]; + ++amountOfTowns[ins->getOwner()]; } - //checking heroes and prisons - if(auto * ins = dynamic_cast(o.get())) + //checking and counting heroes and prisons + if(auto * ins = dynamic_cast(o.get())) { if(ins->ID == Obj::PRISON) { if(ins->getOwner() != PlayerColor::NEUTRAL) - issues.emplace_back(QString(tr("Prison %1 must be a NEUTRAL")).arg(ins->instanceName.c_str()), true); + issues.insert({ tr("Prison %1 must be a NEUTRAL").arg(ins->instanceName.c_str()), true }); } else { - bool has = amountOfCastles.count(ins->getOwner().getNum()); - if(!has) - issues.emplace_back(QString(tr("Hero %1 must have an owner")).arg(ins->instanceName.c_str()), true); + if(ins->getOwner() == PlayerColor::NEUTRAL) + issues.insert({ tr("Hero %1 must have an owner").arg(ins->instanceName.c_str()), true }); + + ++amountOfHeroes[ins->getOwner()]; } - if(ins->type) + if(ins->getHeroTypeID().hasValue()) { - if(map->allowedHeroes.count(ins->getHeroType()) == 0) - issues.emplace_back(QString(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.emplace_back(QString(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.emplace_back(QString(tr("Hero %1 has an empty type and must be removed")).arg(ins->instanceName.c_str()), true); + issues.insert({ tr("Hero %1 has an empty type and must be removed").arg(ins->instanceName.c_str()), true }); } //checking for arts - if(auto * ins = dynamic_cast(o.get())) + if(auto * ins = dynamic_cast(o.get())) { if(ins->ID == Obj::SPELL_SCROLL) { - if(ins->storedArtifact) + if (ins->storedArtifact) { - if(map->allowedSpells.count(ins->storedArtifact->getScrollSpellID()) == 0) - issues.emplace_back(QString(tr("Spell scroll %1 is prohibited by map settings")).arg(ins->storedArtifact->getScrollSpellID().toEntity(VLC->spells())->getNameTranslated().c_str()), false); + if (map->allowedSpells.count(ins->storedArtifact->getScrollSpellID()) == 0) + issues.insert({ tr("Spell scroll %1 is prohibited by map settings").arg(ins->storedArtifact->getScrollSpellID().toEntity(VLC->spells())->getNameTranslated().c_str()), false }); } else - issues.emplace_back(QString(tr("Spell scroll %1 doesn't have instance assigned and must be removed")).arg(ins->instanceName.c_str()), true); + issues.insert({ tr("Spell scroll %1 doesn't have instance assigned and must be removed").arg(ins->instanceName.c_str()), true }); } else { if(ins->ID == Obj::ARTIFACT && map->allowedArtifact.count(ins->getArtifact()) == 0) { - issues.emplace_back(QString(tr("Artifact %1 is prohibited by map settings")).arg(ins->getObjectName().c_str()), false); + issues.insert({ tr("Artifact %1 is prohibited by map settings").arg(ins->getObjectName().c_str()), false }); } } } } //verification of starting towns - for(auto & mp : amountOfCastles) - if(mp.second == 0) - issues.emplace_back(QString(tr("Player %1 doesn't have any starting town")).arg(mp.first), false); + for (const auto & [player, counter] : amountOfTowns) + { + if (counter == 0) + { + // FIXME: heroesNames are empty even though heroes are on the map + // if(map->players[playerTCounter.first].heroesNames.empty()) + if(amountOfHeroes.count(player) == 0) + issues.insert({ tr("Player %1 has no towns and heroes assigned").arg(player + 1), true }); + else + issues.insert({ tr("Player %1 doesn't have any starting town").arg(player + 1), false }); + } + } //verification of map name and description if(map->name.empty()) - issues.emplace_back(tr("Map name is not specified"), false); + issues.insert({ tr("Map name is not specified"), false }); if(map->description.empty()) - issues.emplace_back(tr("Map description is not specified"), false); + issues.insert({ tr("Map description is not specified"), false }); //verificationfor mods for(auto & mod : MapController::modAssessmentMap(*map)) { if(!map->mods.count(mod.first)) { - issues.emplace_back(QString(tr("Map contains object from mod \"%1\", but doesn't require it")).arg(QString::fromStdString(VLC->modh->getModInfo(mod.first).getVerificationInfo().name)), true); + issues.insert({ tr("Map contains object from mod \"%1\", but doesn't require it").arg(QString::fromStdString(VLC->modh->getModInfo(mod.first).getVerificationInfo().name)), true }); } } } catch(const std::exception & e) { - issues.emplace_back(QString(tr("Exception occurs during validation: %1")).arg(e.what()), true); + issues.insert({ tr("Exception occurs during validation: %1").arg(e.what()), true }); } catch(...) { - issues.emplace_back(tr("Unknown exception occurs during validation"), true); + issues.insert({ tr("Unknown exception occurs during validation"), true }); } return issues; diff --git a/mapeditor/validator.h b/mapeditor/validator.h index ee455ba08..edcfddbc3 100644 --- a/mapeditor/validator.h +++ b/mapeditor/validator.h @@ -11,6 +11,7 @@ #pragma once #include +#include VCMI_LIB_NAMESPACE_BEGIN class CMap; @@ -30,13 +31,18 @@ public: bool critical; Issue(const QString & m, bool c): message(m), critical(c) {} + + bool operator <(const Issue & other) const + { + return message < other.message; + } }; public: explicit Validator(const CMap * map, QWidget *parent = nullptr); ~Validator(); - static std::list validate(const CMap * map); + static std::set validate(const CMap * map); private: Ui::Validator *ui; 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 e1cad1b90..cf67dc8f4 100644 --- a/mapeditor/windownewmap.cpp +++ b/mapeditor/windownewmap.cpp @@ -16,11 +16,12 @@ #include "../lib/VCMI_Lib.h" #include "../lib/mapping/CMapEditManager.h" #include "../lib/mapping/MapFormat.h" -#include "../lib/CGeneralTextHandler.h" +#include "../lib/texts/CGeneralTextHandler.h" +#include "../lib/CRandomGenerator.h" #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" @@ -149,6 +150,10 @@ bool WindowNewMap::loadUserSettings() ui->monsterOpt4->setChecked(true); break; } + ui->roadDirt->setChecked(mapGenOptions.isRoadEnabled(Road::DIRT_ROAD)); + ui->roadGravel->setChecked(mapGenOptions.isRoadEnabled(Road::GRAVEL_ROAD)); + ui->roadCobblestone->setChecked(mapGenOptions.isRoadEnabled(Road::COBBLESTONE_ROAD)); + ret = true; } @@ -201,6 +206,7 @@ std::unique_ptr generateEmptyMap(CMapGenOptions & options) { auto map = std::make_unique(nullptr); map->version = EMapFormat::VCMI; + map->creationDateTime = std::time(nullptr); map->width = options.getWidth(); map->height = options.getHeight(); map->twoLevel = options.getHasTwoLevels(); @@ -234,6 +240,10 @@ void WindowNewMap::on_okButton_clicked() mapGenOptions.setWaterContent(water); mapGenOptions.setMonsterStrength(monster); + + mapGenOptions.setRoadEnabled(Road::DIRT_ROAD, ui->roadDirt->isChecked()); + mapGenOptions.setRoadEnabled(Road::GRAVEL_ROAD, ui->roadGravel->isChecked()); + mapGenOptions.setRoadEnabled(Road::COBBLESTONE_ROAD, ui->roadCobblestone->isChecked()); saveUserSettings(); @@ -243,7 +253,7 @@ void WindowNewMap::on_okButton_clicked() //verify map template if(mapGenOptions.getPossibleTemplates().empty()) { - QMessageBox::warning(this, tr("No template"), tr("No template for parameters scecified. Random map cannot be generated.")); + QMessageBox::warning(this, tr("No template"), tr("No template for parameters specified. Random map cannot be generated.")); return; } diff --git a/mapeditor/windownewmap.ui b/mapeditor/windownewmap.ui index 3b97e1a44..e4121de3c 100644 --- a/mapeditor/windownewmap.ui +++ b/mapeditor/windownewmap.ui @@ -7,7 +7,7 @@ 0 0 444 - 445 + 506 @@ -52,7 +52,7 @@ 0 20 281 - 68 + 73 @@ -72,7 +72,7 @@ - Qt::ImhDigitsOnly + Qt::InputMethodHint::ImhDigitsOnly 36 @@ -98,7 +98,7 @@ - Qt::ImhDigitsOnly + Qt::InputMethodHint::ImhDigitsOnly 36 @@ -132,10 +132,10 @@ - Qt::Horizontal + Qt::Orientation::Horizontal - QSizePolicy::Fixed + QSizePolicy::Policy::Fixed @@ -207,7 +207,7 @@ 10 140 431 - 301 + 361 @@ -237,7 +237,7 @@ 10 20 391 - 68 + 72 @@ -546,7 +546,7 @@ - Qt::Horizontal + Qt::Orientation::Horizontal @@ -675,7 +675,104 @@ - Qt::Horizontal + Qt::Orientation::Horizontal + + + + 40 + 20 + + + + + + + + + + + 10 + 230 + 411 + 51 + + + + + 0 + 0 + + + + + 480 + 96 + + + + Roads + + + + + 0 + 20 + 411 + 26 + + + + + + + Dirt + + + + + + + Qt::Orientation::Horizontal + + + + 40 + 20 + + + + + + + + Gravel + + + + + + + Qt::Orientation::Horizontal + + + + 40 + 20 + + + + + + + + Cobblestone + + + + + + + Qt::Orientation::Horizontal @@ -692,9 +789,9 @@ 10 - 230 + 280 411 - 32 + 34 @@ -732,37 +829,37 @@ - - - false - + - 280 - 270 - 131 - 21 + 80 + 320 + 283 + 33 - - Qt::ImhDigitsOnly - - - 0 - - - - - - 110 - 270 - 161 - 20 - - - - Custom seed - + + + + + Custom seed + + + + + + + false + + + Qt::InputMethodHint::ImhDigitsOnly + + + 0 + + + + diff --git a/scripting/erm/ERM.cbp b/scripting/erm/ERM.cbp deleted file mode 100644 index bf75db9ed..000000000 --- a/scripting/erm/ERM.cbp +++ /dev/null @@ -1,117 +0,0 @@ - - - - - - diff --git a/scripting/erm/ERM.vcxproj b/scripting/erm/ERM.vcxproj deleted file mode 100644 index f2854e8b6..000000000 --- a/scripting/erm/ERM.vcxproj +++ /dev/null @@ -1,148 +0,0 @@ - - - - - Debug - Win32 - - - Debug - x64 - - - RD - Win32 - - - RD - x64 - - - - {8F202F43-106D-4F63-AD9D-B1D43E803E8C} - ERM - 10.0 - - - - DynamicLibrary - true - MultiByte - v142 - - - DynamicLibrary - true - MultiByte - v142 - - - DynamicLibrary - false - true - MultiByte - v142 - - - DynamicLibrary - false - true - MultiByte - v142 - - - - - - - - - - - - - - - - - - - - - - - - - - - ..\..\.. - - - $(VCMI_Out)\Scripting\ - - - ..\..\..\Scripting - - - $(VCMI_Out)\Scripting\ - - - - Use - StdInc.h - /Zm218 %(AdditionalOptions) - - - VCMI_lib.lib;%(AdditionalDependencies) - ..\..\..\libs;..\.. - - - - - Use - /Zm218 %(AdditionalOptions) - StdInc.h - true - - - VCMI_lib.lib;%(AdditionalDependencies) - ..\..\..\libs;..\.. - - - - - Use - /Zm218 %(AdditionalOptions) - StdInc.h - - - - - Use - /Zm218 %(AdditionalOptions) - StdInc.h - - - - - - - - - - - - - - - Create - StdInc.h - Create - Create - Create - - - - - - \ No newline at end of file diff --git a/scripting/erm/ERMInterpreter.cpp b/scripting/erm/ERMInterpreter.cpp index 0d60743e7..664b86d3d 100644 --- a/scripting/erm/ERMInterpreter.cpp +++ b/scripting/erm/ERMInterpreter.cpp @@ -1452,37 +1452,37 @@ bool ERMInterpreter::isATrigger( const ERM::TLine & line ) return false; } -ERM::EVOtions ERMInterpreter::getExpType(const ERM::TVOption & opt) +ERM::EVOptions ERMInterpreter::getExpType(const ERM::TVOption & opt) { struct Visitor { - ERM::EVOtions operator()(const boost::recursive_wrapper&) const + ERM::EVOptions operator()(const boost::recursive_wrapper&) const { - return ERM::EVOtions::VEXP; + return ERM::EVOptions::VEXP; } - ERM::EVOtions operator()(const ERM::TSymbol&) const + ERM::EVOptions operator()(const ERM::TSymbol&) const { - return ERM::EVOtions::SYMBOL; + return ERM::EVOptions::SYMBOL; } - ERM::EVOtions operator()(char) const + ERM::EVOptions operator()(char) const { - return ERM::EVOtions::CHAR; + return ERM::EVOptions::CHAR; } - ERM::EVOtions operator()(double) const + ERM::EVOptions operator()(double) const { - return ERM::EVOtions::DOUBLE; + return ERM::EVOptions::DOUBLE; } - ERM::EVOtions operator()(int) const + ERM::EVOptions operator()(int) const { - return ERM::EVOtions::INT; + return ERM::EVOptions::INT; } - ERM::EVOtions operator()(const ERM::Tcommand&) const + ERM::EVOptions operator()(const ERM::Tcommand&) const { - return ERM::EVOtions::TCMD; + return ERM::EVOptions::TCMD; } - ERM::EVOtions operator()(const ERM::TStringConstant&) const + ERM::EVOptions operator()(const ERM::TStringConstant&) const { - return ERM::EVOtions::STRINGC; + return ERM::EVOptions::STRINGC; } }; const Visitor v; diff --git a/scripting/erm/ERMInterpreter.h b/scripting/erm/ERMInterpreter.h index 748b632e9..c61f9bc51 100644 --- a/scripting/erm/ERMInterpreter.h +++ b/scripting/erm/ERMInterpreter.h @@ -305,7 +305,7 @@ class ERMInterpreter static bool isCMDATrigger(const ERM::Tcommand & cmd); static bool isATrigger(const ERM::TLine & line); - static ERM::EVOtions getExpType(const ERM::TVOption & opt); + static ERM::EVOptions getExpType(const ERM::TVOption & opt); ERM::TLine & retrieveLine(const VERMInterpreter::LinePointer & linePtr); static ERM::TTriggerBase & retrieveTrigger(ERM::TLine & line); public: diff --git a/scripting/erm/ERMParser.h b/scripting/erm/ERMParser.h index f4d8090cd..b6b38b3ec 100644 --- a/scripting/erm/ERMParser.h +++ b/scripting/erm/ERMParser.h @@ -241,7 +241,7 @@ namespace ERM //for #'symbol expression - enum EVOtions{VEXP, SYMBOL, CHAR, DOUBLE, INT, TCMD, STRINGC}; + enum EVOptions{VEXP, SYMBOL, CHAR, DOUBLE, INT, TCMD, STRINGC}; struct TVExp; typedef std::variant, TSymbol, char, double, int, Tcommand, TStringConstant > TVOption; //options in v-expression //v-expression diff --git a/scripting/lua/Lua.cbp b/scripting/lua/Lua.cbp deleted file mode 100644 index 8c1a2bdf1..000000000 --- a/scripting/lua/Lua.cbp +++ /dev/null @@ -1,171 +0,0 @@ - - - - - - diff --git a/scripting/lua/LuaScriptingContext.cpp b/scripting/lua/LuaScriptingContext.cpp index 47aacb458..5c351f623 100644 --- a/scripting/lua/LuaScriptingContext.cpp +++ b/scripting/lua/LuaScriptingContext.cpp @@ -14,6 +14,7 @@ #include #include #include +#include "vcmi/Services.h" #include "LuaStack.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/scripting/lua/api/events/BattleEvents.cpp b/scripting/lua/api/events/BattleEvents.cpp index 6196f3c90..895b9cdd7 100644 --- a/scripting/lua/api/events/BattleEvents.cpp +++ b/scripting/lua/api/events/BattleEvents.cpp @@ -42,8 +42,8 @@ const std::vector ApplyDamageProxy::REGISTER_CU true }, { - "getInitalDamage", - LuaMethodWrapper::invoke, + "getInitialDamage", + LuaMethodWrapper::invoke, false }, { diff --git a/scripts/lib/erm/MF.lua b/scripts/lib/erm/MF.lua index fd0506316..6055ea49e 100644 --- a/scripts/lib/erm/MF.lua +++ b/scripts/lib/erm/MF.lua @@ -9,7 +9,7 @@ function MF:new(ERM) end function MF:D(x) - return self.ERM.activeEvent:getInitalDamage() + return self.ERM.activeEvent:getInitialDamage() end function MF:E(x, ...) diff --git a/scripts/lib/erm/UN.lua b/scripts/lib/erm/UN.lua index 1aa3f943d..7ec10257c 100644 --- a/scripts/lib/erm/UN.lua +++ b/scripts/lib/erm/UN.lua @@ -247,7 +247,7 @@ function UN:T(x, town, level, dwellingSlot, creature) error ("UN:T is not implemented") end --- count objects, get coordiantes +-- count objects, get coordinates function UN:U(x, ...) error ("UN:U is not implemented") end diff --git a/server/CGameHandler.cpp b/server/CGameHandler.cpp index 8d19d6685..e73ef1e88 100644 --- a/server/CGameHandler.cpp +++ b/server/CGameHandler.cpp @@ -16,25 +16,26 @@ #include "ServerSpellCastEnvironment.h" #include "battles/BattleProcessor.h" #include "processors/HeroPoolProcessor.h" +#include "processors/NewTurnProcessor.h" #include "processors/PlayerMessageProcessor.h" #include "processors/TurnOrderProcessor.h" #include "queries/QueriesProcessor.h" #include "queries/MapQueries.h" +#include "queries/VisitQueries.h" #include "../lib/ArtifactUtils.h" #include "../lib/CArtHandler.h" -#include "../lib/CBuildingHandler.h" +#include "../lib/CConfigHandler.h" #include "../lib/CCreatureHandler.h" #include "../lib/CCreatureSet.h" -#include "../lib/CGeneralTextHandler.h" -#include "../lib/CHeroHandler.h" +#include "../lib/texts/CGeneralTextHandler.h" #include "../lib/CPlayerState.h" +#include "../lib/CRandomGenerator.h" #include "../lib/CSoundBase.h" #include "../lib/CThreadHelper.h" -#include "../lib/CTownHandler.h" #include "../lib/GameConstants.h" #include "../lib/UnlockGuard.h" -#include "../lib/GameSettings.h" +#include "../lib/IGameSettings.h" #include "../lib/ScriptHandler.h" #include "../lib/StartInfo.h" #include "../lib/TerrainHandler.h" @@ -43,23 +44,35 @@ #include "../lib/int3.h" #include "../lib/battle/BattleInfo.h" + +#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" + #include "../lib/gameState/CGameState.h" #include "../lib/mapping/CMap.h" #include "../lib/mapping/CMapService.h" + +#include "../lib/mapObjects/CGCreature.h" #include "../lib/mapObjects/CGMarket.h" +#include "../lib/mapObjects/TownBuildingInstance.h" #include "../lib/mapObjects/CGTownInstance.h" #include "../lib/mapObjects/MiscObjects.h" +#include "../lib/mapObjectConstructors/AObjectTypeHandler.h" +#include "../lib/mapObjectConstructors/CObjectClassesHandler.h" + #include "../lib/modding/ModIncompatibility.h" + #include "../lib/networkPacks/StackLocation.h" + #include "../lib/pathfinder/CPathfinder.h" #include "../lib/pathfinder/PathfinderOptions.h" #include "../lib/pathfinder/TurnInfo.h" -#include "../lib/registerTypes/RegisterTypesServerPacks.h" - #include "../lib/rmg/CMapGenOptions.h" #include "../lib/serializer/CSaveFile.h" @@ -68,7 +81,8 @@ #include "../lib/spells/CSpellHandler.h" -#include "vstd/CLoggerBase.h" +#include +#include #include #include #include @@ -78,53 +92,6 @@ #define COMPLAIN_RET(txt) {complain(txt); return false;} #define COMPLAIN_RETF(txt, FORMAT) {complain(boost::str(boost::format(txt) % FORMAT)); return false;} -template class CApplyOnGH; - -class CBaseForGHApply -{ -public: - virtual bool applyOnGH(CGameHandler * gh, CGameState * gs, CPack * pack) const =0; - virtual ~CBaseForGHApply(){} - template static CBaseForGHApply *getApplier(const U * t=nullptr) - { - return new CApplyOnGH(); - } -}; - -template class CApplyOnGH : public CBaseForGHApply -{ -public: - bool applyOnGH(CGameHandler * gh, CGameState * gs, CPack * pack) const override - { - T *ptr = static_cast(pack); - try - { - ApplyGhNetPackVisitor applier(*gh); - - ptr->visit(applier); - - return applier.getResult(); - } - catch(ExceptionNotAllowedAction & e) - { - (void)e; - return false; - } - } -}; - -template <> -class CApplyOnGH : public CBaseForGHApply -{ -public: - bool applyOnGH(CGameHandler * gh, CGameState * gs, CPack * pack) const override - { - logGlobal->error("Cannot apply on GH plain CPack!"); - assert(0); - return false; - } -}; - static inline double distance(int3 a, int3 b) { return std::sqrt((double)(a.x-b.x)*(a.x-b.x) + (a.y-b.y)*(a.y-b.y)); @@ -189,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) @@ -212,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 } } @@ -268,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); } @@ -339,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 @@ -352,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); } } @@ -390,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; @@ -398,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) @@ -408,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(); } @@ -422,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) @@ -437,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); @@ -464,50 +432,71 @@ 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 succesfullyApplied) + auto sendPackageResponse = [&](bool successfullyApplied) { PackageApplied applied; - applied.player = pack->player; - applied.result = succesfullyApplied; - applied.packType = CTypeList::getInstance().getTypeID(pack); - applied.requestID = pack->requestID; - pack->c->sendPack(&applied); + applied.player = pack.player; + applied.result = successfullyApplied; + applied.packType = CTypeList::getInstance().getTypeID(&pack); + applied.requestID = pack.requestID; + pack.c->sendPack(applied); }; - CBaseForGHApply * apply = applier->getApplier(CTypeList::getInstance().getTypeID(pack)); //and appropriate applier object - if(isBlockedByQueries(pack, pack->player)) + if(isBlockedByQueries(&pack, pack.player)) { sendPackageResponse(false); } - else if(apply) - { - const bool result = apply->applyOnGH(this, this->gs, pack); - if(result) - 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()); - - sendPackageResponse(true); - } else { - logGlobal->error("Message cannot be applied, cannot find applier (unregistered type)!"); - sendPackageResponse(false); - } + bool result; + try + { + ApplyGhNetPackVisitor applier(*this); + pack.visit(applier); + result = applier.getResult(); + } + catch(ExceptionNotAllowedAction &) + { + result = false; + } - vstd::clear_pointer(pack); + if(result) + 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()); + + sendPackageResponse(true); + } } CGameHandler::CGameHandler(CVCMIServer * lobby) @@ -517,14 +506,14 @@ CGameHandler::CGameHandler(CVCMIServer * lobby) , turnOrder(std::make_unique(this)) , queries(std::make_unique()) , playerMessages(std::make_unique(this)) + , randomNumberGenerator(std::make_unique()) , complainNoCreatures("No creatures to split") , complainNotEnoughCreatures("Cannot split that stack, not enough creatures!") , complainInvalidSlot("Invalid slot accessed!") , turnTimerHandler(std::make_unique(*this)) + , newTurnProcessor(std::make_unique(this)) { QID = 1; - applier = std::make_shared>(); - registerTypesServerPacks(*applier); spellEnv = new ServerSpellCastEnvironment(this); } @@ -546,10 +535,11 @@ void CGameHandler::reinitScripting() void CGameHandler::init(StartInfo *si, Load::ProgressAccumulator & progressTracking) { - if (si->seedToBeUsed == 0) - { - si->seedToBeUsed = CRandomGenerator::getDefault().nextInt(); - } + int requestedSeed = settings["server"]["seed"].Integer(); + if (requestedSeed != 0) + randomNumberGenerator->setSeed(requestedSeed); + logGlobal->info("Using random seed: %d", randomNumberGenerator->nextInt()); + CMapService mapService; gs = new CGameState(); gs->preInit(VLC, this); @@ -557,26 +547,18 @@ void CGameHandler::init(StartInfo *si, Load::ProgressAccumulator & progressTrack gs->init(&mapService, si, progressTracking); logGlobal->info("Gamestate initialized!"); - // reset seed, so that clients can't predict any following random values - getRandomGenerator().resetSeed(); - for (auto & elem : gs->players) turnOrder->addPlayer(elem.first); for (auto & elem : gs->map->allHeroes) { if(elem) - heroPool->getHeroSkillsRandomGenerator(elem->getHeroType()); // init RMG seed + heroPool->getHeroSkillsRandomGenerator(elem->getHeroTypeID()); // init RMG seed } reinitScripting(); } -static bool evntCmp(const CMapEvent &a, const CMapEvent &b) -{ - return a.earlierThan(b); -} - void CGameHandler::setPortalDwelling(const CGTownInstance * town, bool forced=false, bool clear = false) {// bool forced = true - if creature should be replaced, if false - only if no creature was set @@ -587,35 +569,35 @@ void CGameHandler::setPortalDwelling(const CGTownInstance * town, bool forced=fa return; } - if (forced || town->creatures.at(GameConstants::CREATURES_PER_TOWN).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[GameConstants::CREATURES_PER_TOWN].second.clear();//remove old one + ssi.creatures[town->getTown()->creatures.size()].second.clear();//remove old one - const std::vector > &dwellings = p->dwellings; - if (dwellings.empty())//no dwellings - just remove + std::set availableCreatures; + for (const auto & dwelling : p->getOwnedObjects()) { - sendAndApply(&ssi); - return; + const auto & dwellingCreatures = dwelling->asOwnable()->providedCreatures(); + availableCreatures.insert(dwellingCreatures.begin(), dwellingCreatures.end()); } - auto dwelling = *RandomGeneratorUtil::nextItem(dwellings, getRandomGenerator()); + if (availableCreatures.empty()) + return; - // for multi-creature dwellings like Golem Factory - auto creatureId = RandomGeneratorUtil::nextItem(dwelling->creatures, getRandomGenerator())->second[0]; + CreatureID creatureId = *RandomGeneratorUtil::nextItem(availableCreatures, getRandomGenerator()); if (clear) { - ssi.creatures[GameConstants::CREATURES_PER_TOWN].first = std::max(1, (VLC->creh->objects.at(creatureId)->getGrowth())/2); + ssi.creatures[town->getTown()->creatures.size()].first = std::max(1, (creatureId.toEntity(VLC)->getGrowth())/2); } else { - ssi.creatures[GameConstants::CREATURES_PER_TOWN].first = VLC->creh->objects.at(creatureId)->getGrowth(); + ssi.creatures[town->getTown()->creatures.size()].first = creatureId.toEntity(VLC)->getGrowth(); } - ssi.creatures[GameConstants::CREATURES_PER_TOWN].second.push_back(creatureId); - sendAndApply(&ssi); + ssi.creatures[town->getTown()->creatures.size()].second.push_back(creatureId); + sendAndApply(ssi); } } @@ -623,54 +605,34 @@ void CGameHandler::onPlayerTurnStarted(PlayerColor which) { events::PlayerGotTurn::defaultExecute(serverEventBus.get(), which); turnTimerHandler->onPlayerGetTurn(which); + newTurnProcessor->onPlayerTurnStarted(which); } void CGameHandler::onPlayerTurnEnded(PlayerColor which) { - const auto * playerState = gs->getPlayerState(which); - assert(playerState->status == EPlayerStatus::INGAME); + newTurnProcessor->onPlayerTurnEnded(which); +} - if (playerState->towns.empty()) +void CGameHandler::addStatistics(StatisticDataSet &stat) const +{ + for (const auto & elem : gs->players) { - DaysWithoutTown pack; - pack.player = which; - pack.daysWithoutCastle = playerState->daysWithoutCastle.value_or(0) + 1; - sendAndApply(&pack); + if (elem.first == PlayerColor::NEUTRAL || !elem.first.isValidPlayer()) + continue; + + auto data = StatisticDataSet::createEntry(&elem.second, gs); + + stat.add(data); } - else - { - if (playerState->daysWithoutCastle.has_value()) - { - DaysWithoutTown pack; - pack.player = which; - pack.daysWithoutCastle = std::nullopt; - sendAndApply(&pack); - } - } - - // check for 7 days without castle - checkVictoryLossConditionsForPlayer(which); - - bool newWeek = getDate(Date::DAY_OF_WEEK) == 7; // end of 7th day - - if (newWeek) //new heroes in tavern - heroPool->onNewWeek(which); } void CGameHandler::onNewTurn() { logGlobal->trace("Turn %d", gs->day+1); - NewTurn n; - n.specialWeek = NewTurn::NO_ACTION; - n.creatureid = CreatureID::NONE; - n.day = gs->day + 1; bool firstTurn = !getDate(Date::DAY); - bool newWeek = getDate(Date::DAY_OF_WEEK) == 7; //day numbers are confusing, as day was not yet switched bool newMonth = getDate(Date::DAY_OF_MONTH) == 28; - std::map hadGold;//starting gold - for buildings like dwarven treasury - if (firstTurn) { for (auto obj : gs->map->objects) @@ -680,241 +642,39 @@ void CGameHandler::onNewTurn() giveExperience(getHero(obj->id), 0); } } - } - for (auto & player : gs->players) - { - if (player.second.status != EPlayerStatus::INGAME) - continue; - - if (player.second.heroes.empty() && player.second.towns.empty()) - throw std::runtime_error("Invalid player in player state! Player " + std::to_string(player.first.getNum()) + ", map name: " + gs->map->name.toString() + ", map description: " + gs->map->description.toString()); - } - - if (newWeek && !firstTurn) - { - n.specialWeek = NewTurn::NORMAL; - bool deityOfFireBuilt = false; - for (const CGTownInstance *t : gs->map->towns) - { - if (t->hasBuilt(BuildingID::GRAIL, ETownType::INFERNO)) - { - deityOfFireBuilt = true; - break; - } - } - - if (deityOfFireBuilt) - { - n.specialWeek = NewTurn::DEITYOFFIRE; - n.creatureid = CreatureID::IMP; - } - else if(VLC->settings()->getBoolean(EGameSettings::CREATURES_ALLOW_RANDOM_SPECIAL_WEEKS)) - { - int monthType = getRandomGenerator().nextInt(99); - if (newMonth) //new month - { - if (monthType < 40) //double growth - { - n.specialWeek = NewTurn::DOUBLE_GROWTH; - if (VLC->settings()->getBoolean(EGameSettings::CREATURES_ALLOW_ALL_FOR_DOUBLE_MONTH)) - { - n.creatureid = VLC->creh->pickRandomMonster(getRandomGenerator()); - } - else if (VLC->creh->doubledCreatures.size()) - { - n.creatureid = *RandomGeneratorUtil::nextItem(VLC->creh->doubledCreatures, getRandomGenerator()); - } - else - { - complain("Cannot find creature that can be spawned!"); - n.specialWeek = NewTurn::NORMAL; - } - } - else if (monthType < 50) - n.specialWeek = NewTurn::PLAGUE; - } - else //it's a week, but not full month - { - if (monthType < 25) - { - n.specialWeek = NewTurn::BONUS_GROWTH; //+5 - std::pair newMonster(54, CreatureID()); - do - { - newMonster.second = VLC->creh->pickRandomMonster(getRandomGenerator()); - } while (VLC->creh->objects[newMonster.second] && - (*VLC->townh)[VLC->creatures()->getById(newMonster.second)->getFaction()]->town == nullptr); // find first non neutral creature - n.creatureid = newMonster.second; - } - } - } - } - - for (auto & elem : gs->players) - { - if (elem.first == PlayerColor::NEUTRAL) - continue; - - assert(elem.first.isValidPlayer());//illegal player number! - - std::pair playerGold(elem.first, elem.second.resources[EGameResID::GOLD]); - hadGold.insert(playerGold); - - if (firstTurn) + for (auto & elem : gs->players) heroPool->onNewWeek(elem.first); - n.res[elem.first] = elem.second.resources; - - if(!firstTurn) - { - for (GameResID k = GameResID::WOOD; k < GameResID::COUNT; k++) - { - n.res[elem.first][k] += elem.second.valOfBonuses(BonusType::RESOURCES_CONSTANT_BOOST, BonusSubtypeID(k)); - n.res[elem.first][k] += elem.second.valOfBonuses(BonusType::RESOURCES_TOWN_MULTIPLYING_BOOST, BonusSubtypeID(k)) * elem.second.towns.size(); - } - - if(newWeek) //weekly crystal generation if 1 or more crystal dragons in any hero army or town garrison - { - bool hasCrystalGenCreature = false; - for(CGHeroInstance * hero : elem.second.heroes) - { - for(auto stack : hero->stacks) - { - if(stack.second->hasBonusOfType(BonusType::SPECIAL_CRYSTAL_GENERATION)) - { - hasCrystalGenCreature = true; - break; - } - } - } - if(!hasCrystalGenCreature) //not found in armies, check towns - { - for(CGTownInstance * town : elem.second.towns) - { - for(auto stack : town->stacks) - { - if(stack.second->hasBonusOfType(BonusType::SPECIAL_CRYSTAL_GENERATION)) - { - hasCrystalGenCreature = true; - break; - } - } - } - } - if(hasCrystalGenCreature) - n.res[elem.first][EGameResID::CRYSTAL] += 3; - } - } - - for (CGHeroInstance *h : (elem).second.heroes) - { - if (h->visitedTown) - giveSpells(h->visitedTown, h); - - NewTurn::Hero hth; - hth.id = h->id; - auto ti = std::make_unique(h, 1); - // TODO: this code executed when bonuses of previous day not yet updated (this happen in NewTurn::applyGs). See issue 2356 - hth.move = h->movementPointsLimitCached(gs->map->getTile(h->visitablePos()).terType->isLand(), ti.get()); - hth.mana = h->getManaNewTurn(); - - n.heroes.insert(hth); - - if (!firstTurn) //not first day - { - for (GameResID k = GameResID::WOOD; k < GameResID::COUNT; k++) - { - n.res[elem.first][k] += h->valOfBonuses(BonusType::GENERATE_RESOURCE, BonusSubtypeID(k)); - } - } - } } + else + { + addStatistics(gameState()->statistic); // write at end of turn + } + for (CGTownInstance *t : gs->map->towns) { PlayerColor player = t->tempOwner; - handleTownEvents(t, n); - if (newWeek) //first day of week - { - if (t->hasBuilt(BuildingSubID::PORTAL_OF_SUMMONING)) - setPortalDwelling(t, true, (n.specialWeek == NewTurn::PLAGUE ? true : false)); //set creatures for Portal of Summoning - if (!firstTurn) - if (t->hasBuilt(BuildingSubID::TREASURY) && player.isValidPlayer()) - n.res[player][EGameResID::GOLD] += hadGold.at(player)/10; //give 10% of starting gold - - if (!vstd::contains(n.cres, t->id)) - { - n.cres[t->id].tid = t->id; - n.cres[t->id].creatures = t->creatures; - } - auto & sac = n.cres.at(t->id); - - for (int k=0; k < GameConstants::CREATURES_PER_TOWN; k++) //creature growths - { - if (!t->creatures.at(k).second.empty()) // there are creatures at this level - { - ui32 &availableCount = sac.creatures.at(k).first; - const CCreature *cre = t->creatures.at(k).second.back().toCreature(); - - if (n.specialWeek == NewTurn::PLAGUE) - availableCount = t->creatures.at(k).first / 2; //halve their number, no growth - else - { - if (firstTurn) //first day of game: use only basic growths - availableCount = cre->getGrowth(); - else - availableCount += t->creatureGrowth(k); - - //Deity of fire week - upgrade both imps and upgrades - if (n.specialWeek == NewTurn::DEITYOFFIRE && vstd::contains(t->creatures.at(k).second, n.creatureid)) - availableCount += 15; - - if (cre->getId() == n.creatureid) //bonus week, effect applies only to identical creatures - { - if (n.specialWeek == NewTurn::DOUBLE_GROWTH) - availableCount *= 2; - else if (n.specialWeek == NewTurn::BONUS_GROWTH) - availableCount += 5; - } - } - } - } - } - if (!firstTurn && player.isValidPlayer())//not the first day and town not neutral - { - n.res[player] = n.res[player] + t->dailyIncome(); - } 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 after veils apply - if (player != PlayerColor::NEUTRAL) //do not reveal fow for neutral player - { - FoWChange fw; - fw.mode = ETileVisibility::REVEALED; - fw.player = player; - // find all hidden tiles - const auto & fow = getPlayerTeam(player)->fogOfWarMap; - - auto shape = fow->shape(); - for(size_t z = 0; z < shape[0]; z++) - for(size_t x = 0; x < shape[1]; x++) - for(size_t y = 0; y < shape[2]; y++) - if (!(*fow)[z][x][y]) - fw.tiles.insert(int3(x, y, z)); - - sendAndApply (&fw); - } + // do it every new day before veils + if (player.isValidPlayer()) + changeFogOfWar(t->getSightCenter(), t->getSightRadius(), player, ETileVisibility::REVEALED); } + } + + for (CGTownInstance *t : gs->map->towns) + { if (t->hasBonusOfType (BonusType::DARKNESS)) { for (auto & player : gs->players) { if (getPlayerStatus(player.first) == EPlayerStatus::INGAME && getPlayerRelations(player.first, t->tempOwner) == PlayerRelations::ENEMIES) - changeFogOfWar(t->getSightCenter(), t->getFirstBonus(Selector::type()(BonusType::DARKNESS))->val, player.first, ETileVisibility::HIDDEN); + changeFogOfWar(t->getSightCenter(), t->valOfBonuses(BonusType::DARKNESS), player.first, ETileVisibility::HIDDEN); } } } @@ -924,70 +684,14 @@ void CGameHandler::onNewTurn() SetAvailableArtifacts saa; saa.id = ObjectInstanceID::NONE; pickAllowedArtsSet(saa.arts, getRandomGenerator()); - sendAndApply(&saa); + sendAndApply(saa); } - sendAndApply(&n); - if (newWeek) - { - //spawn wandering monsters - if (newMonth && (n.specialWeek == NewTurn::DOUBLE_GROWTH || n.specialWeek == NewTurn::DEITYOFFIRE)) - { - spawnWanderingMonsters(n.creatureid); - } - - //new week info popup - if (!firstTurn) - { - InfoWindow iw; - switch (n.specialWeek) - { - case NewTurn::DOUBLE_GROWTH: - iw.text.appendLocalString(EMetaText::ARRAY_TXT, 131); - iw.text.replaceNameSingular(n.creatureid); - iw.text.replaceNameSingular(n.creatureid); - break; - case NewTurn::PLAGUE: - iw.text.appendLocalString(EMetaText::ARRAY_TXT, 132); - break; - case NewTurn::BONUS_GROWTH: - iw.text.appendLocalString(EMetaText::ARRAY_TXT, 134); - iw.text.replaceNameSingular(n.creatureid); - iw.text.replaceNameSingular(n.creatureid); - break; - case NewTurn::DEITYOFFIRE: - iw.text.appendLocalString(EMetaText::ARRAY_TXT, 135); - iw.text.replaceNameSingular(CreatureID::IMP); //%s imp - iw.text.replaceNameSingular(CreatureID::IMP); //%s imp - iw.text.replacePositiveNumber(15);//%+d 15 - iw.text.replaceNameSingular(CreatureID::FAMILIAR); //%s familiar - iw.text.replacePositiveNumber(15);//%+d 15 - break; - default: - if (newMonth) - { - iw.text.appendLocalString(EMetaText::ARRAY_TXT, (130)); - iw.text.replaceLocalString(EMetaText::ARRAY_TXT, getRandomGenerator().nextInt(32, 41)); - } - else - { - iw.text.appendLocalString(EMetaText::ARRAY_TXT, (133)); - iw.text.replaceLocalString(EMetaText::ARRAY_TXT, getRandomGenerator().nextInt(43, 57)); - } - } - for (auto & elem : gs->players) - { - iw.player = elem.first; - sendAndApply(&iw); - } - } - } + newTurnProcessor->onNewTurn(); if (!firstTurn) checkVictoryLossConditionsForAll(); // check for map turn limit - logGlobal->trace("Info about turn %d has been sent!", n.day); - handleTimeEvents(); //call objects for (auto & elem : gs->map->objects) { @@ -1067,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) @@ -1081,9 +785,9 @@ bool CGameHandler::removeObject(const CGObjectInstance * obj, const PlayerColor RemoveObject ro; ro.objectID = obj->id; ro.initiator = initiator; - sendAndApply(&ro); + sendAndApply(ro); - checkVictoryLossConditionsForAll(); //eg if monster escaped (removing objs after battle is done dircetly by endBattle, not this function) + checkVictoryLossConditionsForAll(); //e.g. if monster escaped (removing objs after battle is done directly by endBattle, not this function) return true; } @@ -1100,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)) @@ -1126,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; @@ -1139,22 +842,22 @@ bool CGameHandler::moveHero(ObjectInstanceID hid, int3 dst, EMovementMode moveme tmh.movePoints = h->movementPointsRemaining(); //check if destination tile is available - auto pathfinderHelper = std::make_unique(gs, h, PathfinderOptions()); + auto pathfinderHelper = std::make_unique(gs, h, PathfinderOptions(this)); auto ti = pathfinderHelper->getTurnInfo(); const bool canFly = pathfinderHelper->hasBonusOfType(BonusType::FLYING_MOVEMENT) || (h->boat && h->boat->layer == EPathfindingLayer::AIR); const bool canWalkOnSea = pathfinderHelper->hasBonusOfType(BonusType::WATER_WALKING) || (h->boat && h->boat->layer == EPathfindingLayer::WATER); const 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; }; @@ -1172,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!"); @@ -1191,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 @@ -1199,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); } @@ -1221,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 @@ -1282,19 +985,18 @@ bool CGameHandler::moveHero(ObjectInstanceID hid, int3 dst, EMovementMode moveme if (blockingVisit()) // e.g. hero on the other side of teleporter return true; - EGuardLook guardsCheck = (VLC->settings()->getBoolean(EGameSettings::DIMENSION_DOOR_TRIGGERS_GUARDS) && movementMode == EMovementMode::DIMENSION_DOOR) + EGuardLook guardsCheck = (getSettings().getBoolean(EGameSettings::DIMENSION_DOOR_TRIGGERS_GUARDS) && movementMode == EMovementMode::DIMENSION_DOOR) ? CHECK_FOR_GUARDS : IGNORE_GUARDS; doMove(TryMoveHero::TELEPORTATION, guardsCheck, DONT_VISIT_DEST, LEAVING_TILE); // visit town for town portal \ castle gates - // do not use generic visitObjectOnTile to avoid double-teleporting - // if this moveHero call was triggered by teleporter + // do not visit any other objects, e.g. monoliths to avoid double-teleporting if (objectToVisit) { if (CGTownInstance * town = dynamic_cast(objectToVisit)) - town->onHeroVisit(h); + objectVisited(town, h); } return true; @@ -1314,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; @@ -1328,6 +1030,7 @@ bool CGameHandler::moveHero(ObjectInstanceID hid, int3 dst, EMovementMode moveme turnTimerHandler->setEndTurnAllowed(h->getOwner(), !movingOntoWater && !movingOntoObstacle); doMove(TryMoveHero::SUCCESS, lookForGuards, visitDest, LEAVING_TILE); + gs->statistic.accumulatedValues[asker].movementPointsUsed += tmh.movePoints; return true; } } @@ -1344,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)) @@ -1371,43 +1074,34 @@ void CGameHandler::setOwner(const CGObjectInstance * obj, const PlayerColor owne const CGTownInstance * town = dynamic_cast(obj); if (town) //town captured { + gs->statistic.accumulatedValues[owner].lastCapturedTownDay = gs->getDate(Date::DAY); + if (owner.isValidPlayer()) //new owner is real player { if (town->hasBuilt(BuildingSubID::PORTAL_OF_SUMMONING)) setPortalDwelling(town, true, false); } - - if (oldOwner.isValidPlayer()) //old owner is real player - { - if (getPlayerState(oldOwner)->towns.empty() && getPlayerState(oldOwner)->status != EPlayerStatus::LOSER) //previous player lost last town - { - InfoWindow iw; - iw.player = oldOwner; - iw.text.appendLocalString(EMetaText::GENERAL_TXT, 6); //%s, you have lost your last town. If you do not conquer another town in the next week, you will be eliminated. - iw.text.replaceName(oldOwner); - sendAndApply(&iw); - } - } } - const PlayerState * p = getPlayerState(owner); - - if ((obj->ID == Obj::CREATURE_GENERATOR1 || obj->ID == Obj::CREATURE_GENERATOR4) && p && p->dwellings.size()==1)//first dwelling captured + if ((obj->ID == Obj::CREATURE_GENERATOR1 || obj->ID == Obj::CREATURE_GENERATOR4)) { - for (const CGTownInstance * t : getPlayerState(owner)->towns) + if (owner.isValidPlayer()) { - if (t->hasBuilt(BuildingSubID::PORTAL_OF_SUMMONING)) - setPortalDwelling(t);//set initial creatures for all portals of summoning + for (const CGTownInstance * t : getPlayerState(owner)->getTowns()) + { + if (t->hasBuilt(BuildingSubID::PORTAL_OF_SUMMONING)) + setPortalDwelling(t);//set initial creatures for all portals of summoning + } } } } -void CGameHandler::showBlockingDialog(BlockingDialog *iw) +void CGameHandler::showBlockingDialog(const IObjectInterface * caller, BlockingDialog *iw) { - auto dialogQuery = std::make_shared(this, *iw); + auto dialogQuery = std::make_shared(this, caller, *iw); queries->addQuery(dialogQuery); iw->queryID = dialogQuery->queryID; - sendToAllClients(iw); + sendToAllClients(*iw); } void CGameHandler::showTeleportDialog(TeleportDialog *iw) @@ -1415,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 @@ -1433,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) @@ -1445,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); @@ -1466,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); @@ -1487,13 +1181,15 @@ void CGameHandler::takeCreatures(ObjectInstanceID objid, const std::vectorid; - vc.tid = obj->id; - vc.flags |= 1; - sendAndApply(&vc); + if (obj->visitingHero != hero && obj->garrisonHero != hero) + { + HeroVisitCastle vc; + vc.hid = hero->id; + vc.tid = obj->id; + vc.flags |= 1; + sendAndApply(vc); + } visitCastleObjects(obj, hero); - giveSpells (obj, hero); if (obj->visitingHero && obj->garrisonHero) useScholarSkill(obj->visitingHero->id, obj->garrisonHero->id); @@ -1502,8 +1198,28 @@ void CGameHandler::heroVisitCastle(const CGTownInstance * obj, const CGHeroInsta void CGameHandler::visitCastleObjects(const CGTownInstance * t, const CGHeroInstance * h) { - for (auto building : t->bonusingBuildings) - building->onHeroVisit(h); + std::vector visitors; + visitors.push_back(h); + visitCastleObjects(t, visitors); +} + +void CGameHandler::visitCastleObjects(const CGTownInstance * t, std::vector visitors) +{ + std::vector buildingsToVisit; + for (auto const & hero : visitors) + giveSpells (t, hero); + + for (auto & building : t->rewardableBuildings) + { + if (!t->getTown()->buildings.at(building.first)->manualHeroVisit && t->hasBuilt(building.first)) + buildingsToVisit.push_back(building.first); + } + + if (!buildingsToVisit.empty()) + { + auto visitQuery = std::make_shared(this, t, visitors, buildingsToVisit); + queries->addQuery(visitQuery); + } } void CGameHandler::stopHeroVisitCastle(const CGTownInstance * obj, const CGHeroInstance * hero) @@ -1511,14 +1227,20 @@ 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) +void CGameHandler::removeArtifact(const ArtifactLocation & al) { - EraseArtifact ea; - ea.al = al; - sendAndApply(&ea); + removeArtifact(al.artHolder, {al.slot}); +} + +void CGameHandler::removeArtifact(const ObjectInstanceID & srcId, const std::vector & slotsPack) +{ + BulkEraseArtifacts ea; + ea.artHolder = srcId; + ea.posPack.insert(ea.posPack.end(), slotsPack.begin(), slotsPack.end()); + sendAndApply(ea); } void CGameHandler::changeSpells(const CGHeroInstance * hero, bool give, const std::set &spells) @@ -1527,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) @@ -1546,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) @@ -1555,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) @@ -1564,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); @@ -1577,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) @@ -1647,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()) @@ -1675,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); } } @@ -1694,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) @@ -1741,7 +1473,7 @@ bool CGameHandler::isPlayerOwns(CPackForServer * pack, ObjectInstanceID id) void CGameHandler::throwNotAllowedAction(CPackForServer * pack) { if(pack->c) - playerMessages->sendSystemMessage(pack->c, "You are not allowed to perform this action!"); + playerMessages->sendSystemMessage(pack->c, MetaString::createFromTextID("vcmi.server.errors.notAllowed")); logNetwork->error("Player is not allowed to perform this action!"); throw ExceptionNotAllowedAction(); @@ -1749,12 +1481,13 @@ void CGameHandler::throwNotAllowedAction(CPackForServer * pack) void CGameHandler::wrongPlayerMessage(CPackForServer * pack, PlayerColor expectedplayer) { - std::ostringstream oss; - oss << "You were identified as player " << pack->player << " while expecting " << expectedplayer; - logNetwork->error(oss.str()); + auto str = MetaString::createFromTextID("vcmi.server.errors.wrongIdentified"); + str.appendName(pack->player); + str.appendName(expectedplayer); + logNetwork->error(str.toString()); if(pack->c) - playerMessages->sendSystemMessage(pack->c, oss.str()); + playerMessages->sendSystemMessage(pack->c, str); } void CGameHandler::throwIfWrongOwner(CPackForServer * pack, ObjectInstanceID id) @@ -1837,16 +1570,18 @@ bool CGameHandler::load(const std::string & filename) catch(const ModIncompatibility & e) { logGlobal->error("Failed to load game: %s", e.what()); - std::string errorMsg; + MetaString errorMsg; if(!e.whatMissing().empty()) { - errorMsg += VLC->generaltexth->translate("vcmi.server.errors.modsToEnable") + '\n'; - errorMsg += e.whatMissing(); + errorMsg.appendTextID("vcmi.server.errors.modsToEnable"); + errorMsg.appendRawString("\n"); + errorMsg.appendRawString(e.whatMissing()); } if(!e.whatExcessive().empty()) { - errorMsg += VLC->generaltexth->translate("vcmi.server.errors.modsToDisable") + '\n'; - errorMsg += e.whatExcessive(); + errorMsg.appendTextID("vcmi.server.errors.modsToDisable"); + errorMsg.appendRawString("\n"); + errorMsg.appendRawString(e.whatExcessive()); } lobby->announceMessage(errorMsg); return false; @@ -1857,14 +1592,17 @@ bool CGameHandler::load(const std::string & filename) MetaString errorMsg; errorMsg.appendTextID("vcmi.server.errors.unknownEntity"); errorMsg.replaceRawString(e.identifierName); - lobby->announceMessage(errorMsg.toString());//FIXME: should be localized on client side + lobby->announceMessage(errorMsg); return false; } catch(const std::exception & e) { logGlobal->error("Failed to load game: %s", e.what()); - lobby->announceMessage(std::string("Failed to load game: ") + e.what()); + auto str = MetaString::createFromTextID("vcmi.broadcast.failedLoadGame"); + str.appendRawString(": "); + str.appendRawString(e.what()); + lobby->announceMessage(str); return false; } gs->preInit(VLC, this); @@ -1912,7 +1650,7 @@ bool CGameHandler::bulkSplitStack(SlotID slotSrc, ObjectInstanceID srcOwner, si3 if(actualAmount <= howMany) break; } - sendAndApply(&bulkRS); + sendAndApply(bulkRS); return true; } @@ -1954,7 +1692,7 @@ bool CGameHandler::bulkMergeStacks(SlotID slotSrc, ObjectInstanceID srcOwner) rs.count = creatureSet.getStackCount(slot); bulkRS.moves.push_back(rs); } - sendAndApply(&bulkRS); + sendAndApply(bulkRS); return true; } @@ -2041,7 +1779,7 @@ bool CGameHandler::bulkMoveArmy(ObjectInstanceID srcArmy, ObjectInstanceID destA rs.count = move.second.second; bulkRS.moves.push_back(rs); } - sendAndApply(&bulkRS); + sendAndApply(bulkRS); return true; } @@ -2123,18 +1861,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) { @@ -2142,6 +1876,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); @@ -2303,12 +2042,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; @@ -2345,18 +2084,18 @@ bool CGameHandler::buildStructure(ObjectInstanceID tid, BuildingID requestedID, { if(buildingID >= BuildingID::DWELL_FIRST) //dwelling { - int level = (buildingID - BuildingID::DWELL_FIRST) % GameConstants::CREATURES_PER_TOWN; - int upgradeNumber = (buildingID - BuildingID::DWELL_FIRST) / GameConstants::CREATURES_PER_TOWN; + 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 ecountered when building dwelling (bid=%s):" + complain(boost::str(boost::format("Error encountered when building dwelling (bid=%s):" "no creature found (upgrade number %d, level %d!") % buildingID % upgradeNumber % level)); 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; @@ -2364,9 +2103,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); } @@ -2377,9 +2116,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); @@ -2389,13 +2128,13 @@ bool CGameHandler::buildStructure(ObjectInstanceID tid, BuildingID requestedID, }; //Checks if all requirements will be met with expected building list "buildingsThatWillBe" - auto areRequirementsFullfilled = [&](const BuildingID & buildID) + auto areRequirementsFulfilled = [&](const BuildingID & buildID) { return buildingsThatWillBe.count(buildID); }; //Init the vectors - for(auto & build : t->town->buildings) + for(auto & build : t->getTown()->buildings) { if(t->hasBuilt(build.first)) { @@ -2411,7 +2150,7 @@ bool CGameHandler::buildStructure(ObjectInstanceID tid, BuildingID requestedID, //Prepare structure (list of building ids will be filled later) NewStructures ns; ns.tid = tid; - ns.builded = force ? t->builded : (t->builded+1); + ns.built = force ? t->built : (t->built+1); std::queue buildingsToAdd; buildingsToAdd.push(requestedBuilding); @@ -2429,7 +2168,7 @@ bool CGameHandler::buildStructure(ObjectInstanceID tid, BuildingID requestedID, { auto actualRequirements = t->genBuildingRequirements(autoBuilding->bid); - if(actualRequirements.test(areRequirementsFullfilled)) + if(actualRequirements.test(areRequirementsFulfilled)) buildingsToAdd.push(autoBuilding); } } @@ -2440,10 +2179,13 @@ bool CGameHandler::buildStructure(ObjectInstanceID tid, BuildingID requestedID, //Take cost if(!force) + { giveResources(t->tempOwner, -requestedBuilding->resources); + gs->statistic.accumulatedValues[t->tempOwner].spentResourcesForBuildings += requestedBuilding->resources; + } //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) @@ -2452,26 +2194,67 @@ bool CGameHandler::buildStructure(ObjectInstanceID tid, BuildingID requestedID, // now when everything is built - reveal tiles for lookout tower changeFogOfWar(t->getSightCenter(), t->getSightRadius(), t->getOwner(), ETileVisibility::REVEALED); - if(t->garrisonHero) //garrison hero first - consistent with original H3 Mana Vortex and Battle Scholar Academy levelup windows order - visitCastleObjects(t, t->garrisonHero); - if(t->visitingHero) - visitCastleObjects(t, t->visitingHero); + if (!force) + { + //garrison hero first - consistent with original H3 Mana Vortex and Battle Scholar Academy levelup windows order + std::vector visitors; + if (t->garrisonHero) + visitors.push_back(t->garrisonHero); + if (t->visitingHero) + visitors.push_back(t->visitingHero); + + if (!visitors.empty()) + visitCastleObjects(t, visitors); + } checkVictoryLossConditionsForPlayer(t->tempOwner); return true; } +bool CGameHandler::visitTownBuilding(ObjectInstanceID tid, BuildingID bid) +{ + const CGTownInstance * t = getTown(tid); + + if(!t->hasBuilt(bid)) + return false; + + auto subID = t->getTown()->buildings.at(bid)->subId; + + if(subID == BuildingSubID::EBuildingSubID::BANK) + { + TResources res; + res[EGameResID::GOLD] = 2500; + giveResources(t->getOwner(), res); + + setObjPropertyValue(t->id, ObjProperty::BONUS_VALUE_SECOND, 2500); + return true; + } + + if (t->rewardableBuildings.count(bid) && t->visitingHero && t->getTown()->buildings.at(bid)->manualHeroVisit) + { + std::vector buildingsToVisit; + std::vector visitors; + buildingsToVisit.push_back(bid); + visitors.push_back(t->visitingHero); + auto visitQuery = std::make_shared(this, t, visitors, buildingsToVisit); + queries->addQuery(visitQuery); + return true; + } + + return true; +} + bool CGameHandler::razeStructure (ObjectInstanceID tid, BuildingID bid) { ///incomplete, simply erases target building const CGTownInstance * t = getTown(tid); - if (!vstd::contains(t->builtBuildings, bid)) + if(!t->hasBuilt(bid)) return false; RazeStructures rs; 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 // { @@ -2479,11 +2262,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)); @@ -2542,13 +2379,15 @@ bool CGameHandler::recruitCreatures(ObjectInstanceID objid, ObjectInstanceID dst } //recruit - giveResources(army->tempOwner, -(c->getFullRecruitCost() * cram)); + TResources cost = (c->getFullRecruitCost() * cram); + giveResources(army->tempOwner, -cost); + gs->statistic.accumulatedValues[army->tempOwner].spentResourcesForArmy += cost; SetAvailableCreatures sac; sac.tid = objid; sac.creatures = dwelling->creatures; sac.creatures[level].first -= cram; - sendAndApply(&sac); + sendAndApply(sac); if (warMachine) { @@ -2559,7 +2398,7 @@ bool CGameHandler::recruitCreatures(ObjectInstanceID objid, ObjectInstanceID dst COMPLAIN_RET_FALSE_IF(artId == ArtifactID::CATAPULT, "Catapult cannot be recruited!"); COMPLAIN_RET_FALSE_IF(nullptr == art, "Invalid war machine artifact"); - return giveHeroNewArtifact(hero, art); + return giveHeroNewArtifact(hero, artId, ArtifactPosition::FIRST_AVAILABLE); } else { @@ -2595,6 +2434,7 @@ bool CGameHandler::upgradeCreature(ObjectInstanceID objid, SlotID pos, CreatureI //take resources giveResources(player, -totalCost); + gs->statistic.accumulatedValues[player].spentResourcesForArmy += totalCost; //upgrade creature changeStackType(StackLocation(obj, pos), upgID.toCreature()); @@ -2610,7 +2450,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; } @@ -2622,12 +2462,12 @@ 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 std::pair toMerge; - if (dst->mergableStacks(toMerge, i->first) && allowMerging) + if (dst->mergeableStacks(toMerge, i->first) && allowMerging) { moveStack(StackLocation(dst, toMerge.first), StackLocation(dst, toMerge.second)); //merge toMerge.first into toMerge.second assert(!dst->hasStackAtSlot(toMerge.first)); //we have now a new free slot @@ -2669,7 +2509,7 @@ bool CGameHandler::swapGarrisonOnSiege(ObjectInstanceID tid) intown.visiting = ObjectInstanceID(); intown.garrison = town->visitingHero->id; } - sendAndApply(&intown); + sendAndApply(intown); return true; } @@ -2691,12 +2531,12 @@ 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 { - int mapCap = VLC->settings()->getInteger(EGameSettings::HEROES_PER_PLAYER_ON_MAP_CAP); + int mapCap = getSettings().getInteger(EGameSettings::HEROES_PER_PLAYER_ON_MAP_CAP); //check if moving hero out of town will break wandering heroes limit if (getHeroCount(town->garrisonHero->tempOwner,false) >= mapCap) { @@ -2708,7 +2548,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 @@ -2717,7 +2557,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 @@ -2740,10 +2580,16 @@ bool CGameHandler::moveArtifact(const PlayerColor & player, const ArtifactLocati if(!isAllowedExchange(src.artHolder, dst.artHolder)) COMPLAIN_RET("That heroes cannot make any exchange!"); + COMPLAIN_RET_FALSE_IF(!ArtifactUtils::checkIfSlotValid(*srcArtSet, src.slot), "moveArtifact: wrong artifact source slot"); const auto srcArtifact = srcArtSet->getArt(src.slot); - const auto dstArtifact = dstArtSet->getArt(dst.slot); + auto dstSlot = dst.slot; + if(dstSlot == ArtifactPosition::FIRST_AVAILABLE) + dstSlot = ArtifactUtils::getArtAnyPosition(dstArtSet, srcArtifact->getTypeId()); + if(!ArtifactUtils::checkIfSlotValid(*dstArtSet, dstSlot)) + return true; + const auto dstArtifact = dstArtSet->getArt(dstSlot); const bool isDstSlotOccupied = dstArtSet->bearerType() == ArtBearer::ALTAR ? false : dstArtifact != nullptr; - const bool isDstSlotBackpack = dstArtSet->bearerType() == ArtBearer::HERO ? ArtifactUtils::isSlotBackpack(dst.slot) : false; + const bool isDstSlotBackpack = dstArtSet->bearerType() == ArtBearer::HERO ? ArtifactUtils::isSlotBackpack(dstSlot) : false; if(srcArtifact == nullptr) COMPLAIN_RET("No artifact to move!"); @@ -2752,23 +2598,23 @@ bool CGameHandler::moveArtifact(const PlayerColor & player, const ArtifactLocati // Check if src/dest slots are appropriate for the artifacts exchanged. // Moving to the backpack is always allowed. - if((!srcArtifact || !isDstSlotBackpack) && !srcArtifact->canBePutAt(dstArtSet, dst.slot, true)) + if((!srcArtifact || !isDstSlotBackpack) && !srcArtifact->canBePutAt(dstArtSet, dstSlot, true)) COMPLAIN_RET("Cannot move artifact!"); auto srcSlotInfo = srcArtSet->getSlot(src.slot); - auto dstSlotInfo = dstArtSet->getSlot(dst.slot); + auto dstSlotInfo = dstArtSet->getSlot(dstSlot); 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 || dst.slot == ArtifactPosition::MACH4) + if(src.slot == ArtifactPosition::MACH4 || dstSlot == ArtifactPosition::MACH4) COMPLAIN_RET("Cannot move catapult!"); if(isDstSlotBackpack && !ArtifactUtils::isBackpackFreeSlots(dstArtSet)) COMPLAIN_RET("Backpack is full!"); - auto dstSlot = std::min(dst.slot, ArtifactPosition(ArtifactPosition::BACKPACK_START + dstArtSet->artifactsInBackpack.size())); + dstSlot = std::min(dstSlot, ArtifactPosition(ArtifactPosition::BACKPACK_START + dstArtSet->artifactsInBackpack.size())); if(src.slot == dstSlot && src.artHolder == dst.artHolder) COMPLAIN_RET("Won't move artifact: Dest same as source!"); @@ -2783,17 +2629,16 @@ bool CGameHandler::moveArtifact(const PlayerColor & player, const ArtifactLocati // Previous artifact must be swapped COMPLAIN_RET_FALSE_IF(!dstArtifact->canBePutAt(srcArtSet, src.slot, true), "Cannot swap artifacts!"); ma.artsPack1.push_back(BulkMoveArtifacts::LinkedSlots(dstSlot, src.slot)); - ma.swap = true; } auto hero = getHero(dst.artHolder); - if(ArtifactUtils::checkSpellbookIsNeeded(hero, srcArtifact->artType->getId(), dstSlot)) - giveHeroNewArtifact(hero, ArtifactID(ArtifactID::SPELLBOOK).toArtifact(), ArtifactPosition::SPELLBOOK); + 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) - ma.askAssemble = true; - sendAndApply(&ma); + if(src.artHolder != dst.artHolder && !isDstSlotBackpack) + ma.artsPack0.back().askAssemble = true; + sendAndApply(ma); return true; } @@ -2829,7 +2674,7 @@ bool CGameHandler::bulkMoveArtifacts(const PlayerColor & player, ObjectInstanceI if(auto dstHero = getHero(dstId)) { if(ArtifactUtils::checkSpellbookIsNeeded(dstHero, artifact->getTypeId(), dstSlot)) - giveHeroNewArtifact(dstHero, ArtifactID(ArtifactID::SPELLBOOK).toArtifact(), ArtifactPosition::SPELLBOOK); + giveHeroNewArtifact(dstHero, ArtifactID::SPELLBOOK, ArtifactPosition::SPELLBOOK); } } }; @@ -2894,26 +2739,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) { - auto artSet = getArtSet(heroID); - COMPLAIN_RET_FALSE_IF(artSet == nullptr, "scrollBackpackArtifacts: wrong hero's ID"); + const auto artSet = getArtSet(heroID); + 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; } @@ -2929,7 +2839,7 @@ bool CGameHandler::saveArtifactsCostume(const PlayerColor & player, const Object costume.costumeSet.emplace(slot, slotInfo->getArt()->getTypeId()); } - sendAndApply(&costume); + sendAndApply(costume); return true; } @@ -2961,15 +2871,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->getSlotByInstance(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--; } } @@ -2982,9 +2892,9 @@ bool CGameHandler::switchArtifactsCostume(const PlayerColor & player, const Obje estimateBackpackSize++; } - const auto backpackCap = VLC->settings()->getInteger(EGameSettings::HEROES_BACKPACK_CAP); + const auto backpackCap = getSettings().getInteger(EGameSettings::HEROES_BACKPACK_CAP); if((backpackCap < 0 || estimateBackpackSize <= backpackCap) && !bma.artsPack0.empty()) - sendAndApply(&bma); + sendAndApply(bma); } return true; } @@ -3022,25 +2932,28 @@ bool CGameHandler::assembleArtifacts(ObjectInstanceID heroID, ArtifactPosition a } if(ArtifactUtils::checkSpellbookIsNeeded(hero, assembleTo, artifactSlot)) - giveHeroNewArtifact(hero, ArtifactID(ArtifactID::SPELLBOOK).toArtifact(), ArtifactPosition::SPELLBOOK); + giveHeroNewArtifact(hero, ArtifactID::SPELLBOOK, ArtifactPosition::SPELLBOOK); 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; @@ -3079,7 +2992,7 @@ bool CGameHandler::buyArtifact(ObjectInstanceID hid, ArtifactID aid) return false; giveResource(hero->getOwner(),EGameResID::GOLD,-GameConstants::SPELLBOOK_GOLD_COST); - giveHeroNewArtifact(hero, ArtifactID(ArtifactID::SPELLBOOK).toArtifact(), ArtifactPosition::SPELLBOOK); + giveHeroNewArtifact(hero, ArtifactID::SPELLBOOK, ArtifactPosition::SPELLBOOK); assert(hero->getArt(ArtifactPosition::SPELLBOOK)); giveSpells(town,hero); return true; @@ -3093,11 +3006,21 @@ bool CGameHandler::buyArtifact(ObjectInstanceID hid, ArtifactID aid) const int price = art->getPrice(); COMPLAIN_RET_FALSE_IF(getPlayerState(hero->getOwner())->resources[EGameResID::GOLD] < price, "Not enough gold!"); - if ((town->hasBuilt(BuildingID::BLACKSMITH) && town->town->warMachine == aid) - || (town->hasBuilt(BuildingSubID::BALLISTA_YARD) && aid == ArtifactID::BALLISTA)) + if(town->isWarMachineAvailable(aid)) { + bool hasFreeSlot = false; + for(auto slot : art->getPossibleSlots().at(ArtBearer::HERO)) + if (hero->getArt(slot) == nullptr) + hasFreeSlot = true; + + if (!hasFreeSlot) + { + auto slot = art->getPossibleSlots().at(ArtBearer::HERO).front(); + removeArtifact(ArtifactLocation(hero->id, slot)); + } + giveResource(hero->getOwner(),EGameResID::GOLD,-price); - return giveHeroNewArtifact(hero, art); + return giveHeroNewArtifact(hero, aid, ArtifactPosition::FIRST_AVAILABLE); } else COMPLAIN_RET("This machine is unavailable here!"); @@ -3136,11 +3059,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; } @@ -3149,8 +3072,8 @@ bool CGameHandler::buyArtifact(const IMarket *m, const CGHeroInstance *h, GameRe if (!found) COMPLAIN_RET("Cannot find selected artifact on the list"); - sendAndApply(&saa); - giveHeroNewArtifact(h, aid.toArtifact(), ArtifactPosition::FIRST_AVAILABLE); + sendAndApply(saa); + giveHeroNewArtifact(h, aid, ArtifactPosition::FIRST_AVAILABLE); return true; } @@ -3159,11 +3082,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); @@ -3215,6 +3138,9 @@ bool CGameHandler::tradeResources(const IMarket *market, ui32 amountToSell, Play giveResource(player, toSell, -b1 * amountToBoy); giveResource(player, toBuy, b2 * amountToBoy); + gs->statistic.accumulatedValues[player].tradeVolume[toSell] += -b1 * amountToBoy; + gs->statistic.accumulatedValues[player].tradeVolume[toBuy] += b2 * amountToBoy; + return true; } @@ -3235,7 +3161,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 @@ -3310,7 +3236,7 @@ bool CGameHandler::setFormation(ObjectInstanceID hid, EArmyFormation formation) ChangeFormation cf; cf.hid = hid; cf.formation = formation; - sendAndApply(&cf); + sendAndApply(cf); return true; } @@ -3341,154 +3267,14 @@ bool CGameHandler::queryReply(QueryID qid, std::optional answer, Player return true; } -void CGameHandler::handleTimeEvents() -{ - gs->map->events.sort(evntCmp); - while(gs->map->events.size() && gs->map->events.front().firstOccurence+1 == gs->day) - { - CMapEvent ev = gs->map->events.front(); - - for (int player = 0; player < PlayerColor::PLAYER_LIMIT_I; player++) - { - auto color = PlayerColor(player); - - const PlayerState * pinfo = getPlayerState(color, false); //do not output error if player does not exist - - if (pinfo //player exists - && (ev.players & 1<human) - || (ev.humanAffected && pinfo->human) - ) - ) - { - //give resources - giveResources(color, ev.resources); - - //prepare dialog - InfoWindow iw; - iw.player = color; - iw.text = ev.message; - - for (GameResID i : GameResID::ALL_RESOURCES()) - { - if (ev.resources[i]) //if resource is changed, we add it to the dialog - iw.components.emplace_back(ComponentType::RESOURCE, i, ev.resources[i]); - } - - sendAndApply(&iw); //show dialog - } - } //PLAYERS LOOP - - if (ev.nextOccurence) - { - gs->map->events.pop_front(); - - ev.firstOccurence += ev.nextOccurence; - auto it = gs->map->events.begin(); - while(it != gs->map->events.end() && it->earlierThanOrEqual(ev)) - it++; - gs->map->events.insert(it, ev); - } - else - { - gs->map->events.pop_front(); - } - } - - //TODO send only if changed - UpdateMapEvents ume; - ume.events = gs->map->events; - sendAndApply(&ume); -} - -void CGameHandler::handleTownEvents(CGTownInstance * town, NewTurn &n) -{ - town->events.sort(evntCmp); - while(town->events.size() && town->events.front().firstOccurence == gs->day) - { - PlayerColor player = town->tempOwner; - CCastleEvent ev = town->events.front(); - const PlayerState * pinfo = getPlayerState(player, false); - - if (pinfo //player exists - && (ev.players & 1<human) - || (ev.humanAffected && pinfo->human))) - { - // dialog - InfoWindow iw; - iw.player = player; - iw.text = ev.message; - - if (ev.resources.nonZero()) - { - TResources was = n.res[player]; - n.res[player] += ev.resources; - n.res[player].amax(0); - - for (GameResID i : GameResID::ALL_RESOURCES()) - if (ev.resources[i] && pinfo->resources[i] != n.res.at(player)[i]) //if resource had changed, we add it to the dialog - iw.components.emplace_back(ComponentType::RESOURCE, i, n.res.at(player)[i] - was[i]); - } - - for (auto & i : ev.buildings) - { - // Only perform action if: - // 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)) - { - buildStructure(town->id, i, true); - iw.components.emplace_back(ComponentType::BUILDING, BuildingTypeUniqueID(town->getFaction(), i)); - } - } - - if (!ev.creatures.empty() && !vstd::contains(n.cres, town->id)) - { - n.cres[town->id].tid = town->id; - n.cres[town->id].creatures = town->creatures; - } - auto & sac = n.cres[town->id]; - - for (si32 i=0;icreatures.at(i).second.empty() && ev.creatures.at(i) > 0)//there is dwelling - { - sac.creatures[i].first += ev.creatures.at(i); - iw.components.emplace_back(ComponentType::CREATURE, town->creatures.at(i).second.back(), ev.creatures.at(i)); - } - } - sendAndApply(&iw); //show dialog - } - - if (ev.nextOccurence) - { - town->events.pop_front(); - - ev.firstOccurence += ev.nextOccurence; - auto it = town->events.begin(); - while(it != town->events.end() && it->earlierThanOrEqual(ev)) - it++; - town->events.insert(it, ev); - } - else - { - town->events.pop_front(); - } - } - - //TODO send only if changed - UpdateCastleEvents uce; - uce.town = town->id; - uce.events = town->events; - sendAndApply(&uce); -} - bool CGameHandler::complain(const std::string &problem) { #ifndef ENABLE_GOLDMASTER - playerMessages->broadcastSystemMessage("Server encountered a problem: " + problem); + MetaString str; + str.appendTextID("vcmi.broadcast.serverProblem"); + str.appendRawString(": "); + str.appendRawString(problem); + playerMessages->broadcastSystemMessage(str); #endif logGlobal->error(problem); return true; @@ -3511,7 +3297,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) @@ -3527,7 +3313,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) @@ -3555,9 +3341,9 @@ bool CGameHandler::isAllowedExchange(ObjectInstanceID id1, ObjectInstanceID id2) return true; } - auto market = dynamic_cast(o1); + auto market = getMarket(id1); if(market == nullptr) - market = dynamic_cast(o2); + market = getMarket(id2); if(market) return market->allowsTrade(EMarketMode::ARTIFACT_EXP); @@ -3602,7 +3388,7 @@ void CGameHandler::objectVisited(const CGObjectInstance * obj, const CGHeroInsta throw std::runtime_error("Can not visit object that is being visited"); } - std::shared_ptr visitQuery; + std::shared_ptr visitQuery; auto startVisit = [&](ObjectVisitStarted & event) { @@ -3621,7 +3407,7 @@ void CGameHandler::objectVisited(const CGObjectInstance * obj, const CGHeroInsta visitedObject = visitedTown; } } - visitQuery = std::make_shared(this, visitedObject, h, visitedObject->visitablePos()); + visitQuery = std::make_shared(this, visitedObject, h); queries->addQuery(visitQuery); //TODO real visit pos HeroVisit hv; @@ -3629,7 +3415,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); }; @@ -3640,11 +3426,11 @@ void CGameHandler::objectVisited(const CGObjectInstance * obj, const CGHeroInsta queries->popIfTop(visitQuery); //visit ends here if no queries were created } -void CGameHandler::objectVisitEnded(const CObjectVisitQuery & query) +void CGameHandler::objectVisitEnded(const CGHeroInstance *h, PlayerColor player) { using events::ObjectVisitEnded; - logGlobal->debug("%s visit ends.\n", query.visitingHero->nodeName()); + logGlobal->debug("%s visit ends.\n", h->nodeName()); auto endVisit = [&](ObjectVisitEnded & event) { @@ -3652,12 +3438,12 @@ void CGameHandler::objectVisitEnded(const CObjectVisitQuery & query) hv.player = event.getPlayer(); hv.heroId = event.getHero(); hv.starting = false; - sendAndApply(&hv); + sendAndApply(hv); }; //TODO: ObjectVisitEnded should also have id of visited object, //but this requires object being deleted only by `removeAfterVisit()` but not `removeObject()` - ObjectVisitEnded::defaultExecute(serverEventBus.get(), endVisit, query.players.front(), query.visitingHero->id); + ObjectVisitEnded::defaultExecute(serverEventBus.get(), endVisit, player, h->id); } bool CGameHandler::buildBoat(ObjectInstanceID objid, PlayerColor playerID) @@ -3672,9 +3458,9 @@ bool CGameHandler::buildBoat(ObjectInstanceID objid, PlayerColor playerID) TResources boatCost; obj->getBoatCost(boatCost); - TResources aviable = getPlayerState(playerID)->resources; + TResources available = getPlayerState(playerID)->resources; - if (!aviable.canAfford(boatCost)) + if (!available.canAfford(boatCost)) { complain("Not enough resources to build a boat!"); return false; @@ -3688,7 +3474,7 @@ bool CGameHandler::buildBoat(ObjectInstanceID objid, PlayerColor playerID) } giveResources(playerID, -boatCost); - createObject(tile, playerID, Obj::BOAT, obj->getBoatType().getNum()); + createBoat(tile, obj->getBoatType(), playerID); return true; } @@ -3723,12 +3509,14 @@ void CGameHandler::checkVictoryLossConditionsForPlayer(PlayerColor player) { InfoWindow iw; getVictoryLossMessage(player, victoryLossCheckResult, iw); - sendAndApply(&iw); + sendAndApply(iw); PlayerEndsGame peg; peg.player = player; peg.victoryLossCheckResult = victoryLossCheckResult; - sendAndApply(&peg); + peg.statistic = StatisticDataSet(gameState()->statistic); + addStatistics(peg.statistic); // add last turn befor win / loss + sendAndApply(peg); turnOrder->onPlayerEndsGame(player); @@ -3747,8 +3535,8 @@ void CGameHandler::checkVictoryLossConditionsForPlayer(PlayerColor player) getVictoryLossMessage(player, peg.victoryLossCheckResult, iw); iw.player = i->first; - sendAndApply(&iw); - sendAndApply(&peg); + sendAndApply(iw); + sendAndApply(peg); } } @@ -3760,10 +3548,10 @@ void CGameHandler::checkVictoryLossConditionsForPlayer(PlayerColor player) else { //copy heroes vector to avoid iterator invalidation as removal change PlayerState - auto hlp = p->heroes; + auto hlp = p->getHeroes(); for (auto h : hlp) //eliminate heroes { - if (h.get()) + if (h) removeObject(h, player); } @@ -3792,7 +3580,7 @@ void CGameHandler::checkVictoryLossConditionsForPlayer(PlayerColor player) InfoWindow iw; getVictoryLossMessage(player, victoryLossCheckResult.invert(), iw); iw.player = pc; - sendAndApply(&iw); + sendAndApply(iw); } } checkVictoryLossConditions(playerColors); @@ -3813,13 +3601,13 @@ bool CGameHandler::dig(const CGHeroInstance *h) if (h->diggingStatus() != EDiggingStatus::CAN_DIG) //checks for terrain and movement COMPLAIN_RETF("Hero cannot dig (error code %d)!", static_cast(h->diggingStatus())); - createObject(h->visitablePos(), h->getOwner(), Obj::HOLE, 0 ); + createHole(h->visitablePos(), h->getOwner()); //take MPs SetMovePoints smp; smp.hid = h->id; smp.val = 0; - sendAndApply(&smp); + sendAndApply(smp); InfoWindow iw; iw.type = EInfoWindowMode::AUTO; @@ -3831,20 +3619,20 @@ bool CGameHandler::dig(const CGHeroInstance *h) iw.text.appendLocalString(EMetaText::GENERAL_TXT, 58); //"Congratulations! After spending many hours digging here, your hero has uncovered the " ... iw.text.appendName(grail); // ... " The Grail" iw.soundID = soundBase::ULTIMATEARTIFACT; - giveHeroNewArtifact(h, grail.toArtifact(), ArtifactPosition::FIRST_AVAILABLE); //give grail - sendAndApply(&iw); + giveHeroNewArtifact(h, grail, ArtifactPosition::FIRST_AVAILABLE); //give grail + 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; @@ -3888,7 +3676,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]); @@ -3904,33 +3692,35 @@ bool CGameHandler::sacrificeCreatures(const IMarket * market, const CGHeroInstan return true; } -bool CGameHandler::sacrificeArtifact(const IMarket * m, const CGHeroInstance * hero, const std::vector & arts) +bool CGameHandler::sacrificeArtifact(const IMarket * market, const CGHeroInstance * hero, const std::vector & arts) { if (!hero) COMPLAIN_RET("You need hero to sacrifice artifact!"); if(hero->getAlignment() == EAlignment::EVIL) COMPLAIN_RET("Evil hero can't sacrifice artifact!"); - assert(m); - auto altarObj = dynamic_cast(m); + assert(market); + const auto artSet = market->getArtifactsStorage(); int expSum = 0; - auto finish = [this, &hero, &expSum]() + std::vector artPack; + auto finish = [this, &hero, &expSum, &artPack, market]() { + removeArtifact(market->getObjInstanceID(), artPack); giveExperience(hero, hero->calculateXp(expSum)); }; for(const auto & artInstId : arts) { - if(auto art = altarObj->getArtByInstanceId(artInstId)) + if(auto art = artSet->getArtByInstanceId(artInstId)) { - if(art->artType->isTradable()) + if(art->getType()->isTradable()) { int dmp; int expToGive; - m->getOffer(art->getTypeId(), 0, dmp, expToGive, EMarketMode::ARTIFACT_EXP); + market->getOffer(art->getTypeId(), 0, dmp, expToGive, EMarketMode::ARTIFACT_EXP); expSum += expToGive; - removeArtifact(ArtifactLocation(altarObj->id, altarObj->getSlotByInstance(art))); + artPack.push_back(artSet->getArtPos(art)); } else { @@ -3962,7 +3752,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; } @@ -3981,7 +3771,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; } @@ -4006,7 +3796,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; } @@ -4039,7 +3829,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)); @@ -4088,7 +3878,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; } @@ -4122,14 +3912,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); @@ -4137,11 +3928,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 { @@ -4156,11 +3947,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 @@ -4169,13 +3959,21 @@ bool CGameHandler::putArtifact(const ArtifactLocation & al, const CArtifactInsta } } -bool CGameHandler::giveHeroNewArtifact(const CGHeroInstance * h, const CArtifact * artType, ArtifactPosition pos) +bool CGameHandler::giveHeroNewArtifact( + const CGHeroInstance * h, const CArtifact * artType, const SpellID & spellId, const ArtifactPosition & pos) { assert(artType); + NewArtifact na; + na.artHolder = h->id; + na.artId = artType->getId(); + na.spellId = spellId; + na.pos = pos; + if(pos == ArtifactPosition::FIRST_AVAILABLE) { - if(!artType->canBePutAt(h, ArtifactUtils::getArtAnyPosition(h, artType->getId()))) + na.pos = ArtifactUtils::getArtAnyPosition(h, artType->getId()); + if(!artType->canBePutAt(h, na.pos)) COMPLAIN_RET("Cannot put artifact in that slot!"); } else if(ArtifactUtils::isSlotBackpack(pos)) @@ -4187,18 +3985,18 @@ bool CGameHandler::giveHeroNewArtifact(const CGHeroInstance * h, const CArtifact { COMPLAIN_RET_FALSE_IF(!artType->canBePutAt(h, pos, false), "Cannot put artifact in that slot!"); } + sendAndApply(na); + return true; +} - auto * newArtInst = new CArtifactInstance(); - newArtInst->artType = artType; // *NOT* via settype -> all bonus-related stuff must be done by NewArtifact apply +bool CGameHandler::giveHeroNewArtifact(const CGHeroInstance * h, const ArtifactID & artId, const ArtifactPosition & pos) +{ + return giveHeroNewArtifact(h, artId.toArtifact(), SpellID::NONE, pos); +} - NewArtifact na; - na.art = newArtInst; - sendAndApply(&na); // -> updates newArtInst!!! - - if(putArtifact(ArtifactLocation(h->id, pos), newArtInst, false)) - return true; - else - return false; +bool CGameHandler::giveHeroNewScroll(const CGHeroInstance * h, const SpellID & spellId, const ArtifactPosition & pos) +{ + return giveHeroNewArtifact(h, ArtifactID(ArtifactID::SPELL_SCROLL).toArtifact(), spellId, pos); } void CGameHandler::spawnWanderingMonsters(CreatureID creatureID) @@ -4218,7 +4016,7 @@ void CGameHandler::spawnWanderingMonsters(CreatureID creatureID) { auto count = cre->getRandomAmount(std::rand); - createObject(*tile, PlayerColor::NEUTRAL, Obj::MONSTER, creatureID); + createWanderingMonster(*tile, creatureID); auto monsterId = getTopObj(*tile)->id; setObjPropertyValue(monsterId, ObjProperty::MONSTER_COUNT, count); @@ -4232,7 +4030,7 @@ void CGameHandler::synchronizeArtifactHandlerLists() { UpdateArtHandlerLists uahl; uahl.allocatedArtifacts = gs->allocatedArtifacts; - sendAndApply(&uahl); + sendAndApply(uahl); } bool CGameHandler::isValidObject(const CGObjectInstance *obj) const @@ -4240,9 +4038,12 @@ 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 (!strcmp(typeid(*pack).name(), typeid(PlayerMessage).name())) + if (dynamic_cast(pack) != nullptr) + return false; + + if (dynamic_cast(pack) != nullptr) return false; auto query = queries->topQuery(player); @@ -4264,7 +4065,7 @@ void CGameHandler::removeAfterVisit(const CGObjectInstance *object) //If the object is being visited, there must be a matching query for (const auto &query : queries->allQueries()) { - if (auto someVistQuery = std::dynamic_pointer_cast(query)) + if (auto someVistQuery = std::dynamic_pointer_cast(query)) { if (someVistQuery->visitedObject == object) { @@ -4285,19 +4086,6 @@ void CGameHandler::changeFogOfWar(int3 center, ui32 radius, PlayerColor player, if (mode == ETileVisibility::HIDDEN) { getTilesInRange(tiles, center, radius, ETileVisibility::REVEALED, player); - - std::unordered_set observedTiles; //do not hide tiles observed by heroes. May lead to disastrous AI problems - auto p = getPlayerState(player); - for (auto h : p->heroes) - { - getTilesInRange(observedTiles, h->getSightCenter(), h->getSightRadius(), ETileVisibility::REVEALED, h->tempOwner); - } - for (auto t : p->towns) - { - getTilesInRange(observedTiles, t->getSightCenter(), t->getSightRadius(), ETileVisibility::REVEALED, t->tempOwner); - } - for (auto tile : observedTiles) - vstd::erase_if_present (tiles, tile); } else { @@ -4306,7 +4094,7 @@ void CGameHandler::changeFogOfWar(int3 center, ui32 radius, PlayerColor player, changeFogOfWar(tiles, player, mode); } -void CGameHandler::changeFogOfWar(std::unordered_set &tiles, PlayerColor player, ETileVisibility mode) +void CGameHandler::changeFogOfWar(const std::unordered_set &tiles, PlayerColor player, ETileVisibility mode) { if (tiles.empty()) return; @@ -4315,7 +4103,24 @@ void CGameHandler::changeFogOfWar(std::unordered_set &tiles, PlayerColor p fow.tiles = tiles; fow.player = player; fow.mode = mode; - sendAndApply(&fow); + + if (mode == ETileVisibility::HIDDEN) + { + // do not hide tiles observed by owned objects. May lead to disastrous AI problems + // FIXME: this leads to a bug - shroud of darkness from Necropolis does can not override Skyship from Tower + std::unordered_set observedTiles; + auto p = getPlayerState(player); + for (auto obj : p->getOwnedObjects()) + getTilesInRange(observedTiles, obj->getSightCenter(), obj->getSightRadius(), ETileVisibility::REVEALED, obj->getOwner()); + + for (auto tile : observedTiles) + vstd::erase_if_present (fow.tiles, tile); + + if (fow.tiles.empty()) + return; + } + + sendAndApply(fow); } const CGHeroInstance * CGameHandler::getVisitingHero(const CGObjectInstance *obj) @@ -4324,7 +4129,7 @@ const CGHeroInstance * CGameHandler::getVisitingHero(const CGObjectInstance *obj for(const auto & query : queries->allQueries()) { - auto visit = std::dynamic_pointer_cast(query); + auto visit = std::dynamic_pointer_cast(query); if (visit && visit->visitedObject == obj) return visit->visitingHero; } @@ -4337,7 +4142,7 @@ const CGObjectInstance * CGameHandler::getVisitingObject(const CGHeroInstance *h for(const auto & query : queries->allQueries()) { - auto visit = std::dynamic_pointer_cast(query); + auto visit = std::dynamic_pointer_cast(query); if (visit && visit->visitingHero == hero) return visit->visitedObject; } @@ -4354,7 +4159,7 @@ bool CGameHandler::isVisitCoveredByAnotherQuery(const CGObjectInstance *obj, con // visitation query is covered by other query that must be answered first if (auto topQuery = queries->topQuery(hero->getOwner())) - if (auto visit = std::dynamic_pointer_cast(topQuery)) + if (auto visit = std::dynamic_pointer_cast(topQuery)) return !(visit->visitedObject == obj && visit->visitingHero == hero); return true; @@ -4366,7 +4171,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) @@ -4375,17 +4180,42 @@ 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) +{ + SetBankConfiguration srb; + srb.objectID = objid; + srb.configuration = configuration; + sendAndApply(srb); +} + +void CGameHandler::setRewardableObjectConfiguration(ObjectInstanceID objid, const Rewardable::Configuration & configuration) +{ + SetRewardableConfiguration srb; + srb.objectID = objid; + srb.configuration = configuration; + sendAndApply(srb); +} + +void CGameHandler::setRewardableObjectConfiguration(ObjectInstanceID townInstanceID, BuildingID buildingID, const Rewardable::Configuration & configuration) +{ + SetRewardableConfiguration srb; + srb.objectID = townInstanceID; + srb.buildingID = buildingID; + srb.configuration = configuration; + sendAndApply(srb); } void CGameHandler::showInfoDialog(InfoWindow * iw) { - sendAndApply(iw); + sendAndApply(*iw); } -CRandomGenerator & CGameHandler::getRandomGenerator() +vstd::RNG & CGameHandler::getRandomGenerator() { - return CRandomGenerator::getDefault(); + return *randomNumberGenerator; } #if SCRIPTING_ENABLED @@ -4400,27 +4230,83 @@ scripting::Pool * CGameHandler::getGlobalContextPool() const //} #endif -void CGameHandler::createObject(const int3 & visitablePosition, const PlayerColor & initiator, MapObjectID type, MapObjectSubID subtype) + +CGObjectInstance * CGameHandler::createNewObject(const int3 & visitablePosition, MapObjectID objectID, MapObjectSubID subID) { + TerrainId terrainType = ETerrainId::NONE; + + if (!gs->isInTheMap(visitablePosition)) + throw std::runtime_error("Attempt to create object outside map at " + visitablePosition.toString()); + + const TerrainTile & t = gs->map->getTile(visitablePosition); + terrainType = t.getTerrainID(); + + auto handler = VLC->objtypeh->getHandlerFor(objectID, subID); + + CGObjectInstance * o = handler->create(gs->callback, nullptr); + handler->configureObject(o, getRandomGenerator()); + assert(o->ID == objectID); + + assert(!handler->getTemplates(terrainType).empty()); + if (handler->getTemplates().empty()) + throw std::runtime_error("Attempt to create object (" + std::to_string(objectID) + ", " + std::to_string(subID.getNum()) + ") with no templates!"); + + if (!handler->getTemplates(terrainType).empty()) + o->appearance = handler->getTemplates(terrainType).front(); + else + o->appearance = handler->getTemplates().front(); + + if (o->isVisitable()) + o->setAnchorPos(visitablePosition + o->getVisitableOffset()); + else + o->setAnchorPos(visitablePosition); + + return o; +} + +void CGameHandler::createWanderingMonster(const int3 & visitablePosition, CreatureID creature) +{ + auto createdObject = createNewObject(visitablePosition, Obj::MONSTER, creature); + + auto * cre = dynamic_cast(createdObject); + assert(cre); + cre->notGrowingTeam = cre->neverFlees = false; + cre->character = 2; + cre->gainedArtifact = ArtifactID::NONE; + cre->identifier = -1; + cre->addToSlot(SlotID(0), new CStackInstance(creature, -1)); //add placeholder stack + + newObject(createdObject, PlayerColor::NEUTRAL); +} + +void CGameHandler::createBoat(const int3 & visitablePosition, BoatId type, PlayerColor initiator) +{ + auto createdObject = createNewObject(visitablePosition, Obj::BOAT, type); + newObject(createdObject, initiator); +} + +void CGameHandler::createHole(const int3 & visitablePosition, PlayerColor initiator) +{ + auto createdObject = createNewObject(visitablePosition, Obj::HOLE, 0); + newObject(createdObject, initiator); +} + +void CGameHandler::newObject(CGObjectInstance * object, PlayerColor initiator) +{ + object->initObj(gs->getRandomGenerator()); + NewObject no; - no.ID = type; - no.subID = subtype; + no.newObject = object; no.initiator = initiator; - no.targetPos = visitablePosition; - sendAndApply(&no); + sendAndApply(no); } -void CGameHandler::startBattlePrimary(const CArmedInstance *army1, const CArmedInstance *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool creatureBank, const CGTownInstance *town) +void CGameHandler::startBattle(const CArmedInstance *army1, const CArmedInstance *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, const BattleLayout & layout, const CGTownInstance *town) { - battles->startBattlePrimary(army1, army2, tile, hero1, hero2, creatureBank, town); + battles->startBattle(army1, army2, tile, hero1, hero2, layout, town); } -void CGameHandler::startBattleI(const CArmedInstance *army1, const CArmedInstance *army2, int3 tile, bool creatureBank ) +void CGameHandler::startBattle(const CArmedInstance *army1, const CArmedInstance *army2 ) { - battles->startBattleI(army1, army2, tile, creatureBank); -} - -void CGameHandler::startBattleI(const CArmedInstance *army1, const CArmedInstance *army2, bool creatureBank ) -{ - battles->startBattleI(army1, army2, creatureBank); + battles->startBattle(army1, army2); } diff --git a/server/CGameHandler.h b/server/CGameHandler.h index eebb21f68..9d896f758 100644 --- a/server/CGameHandler.h +++ b/server/CGameHandler.h @@ -14,6 +14,8 @@ #include "../lib/IGameCallback.h" #include "../lib/LoadProgress.h" #include "../lib/ScriptHandler.h" +#include "../lib/gameState/GameStatistics.h" +#include "../lib/networkPacks/PacksForServer.h" VCMI_LIB_NAMESPACE_BEGIN @@ -23,8 +25,8 @@ class SpellCastEnvironment; class CConnection; class CCommanderInstance; class EVictoryLossCheckResult; +class CRandomGenerator; -struct CPack; struct CPackForServer; struct NewTurn; struct CGarrisonOperationPack; @@ -38,8 +40,6 @@ namespace scripting } #endif -template class CApplier; - VCMI_LIB_NAMESPACE_END class HeroPoolProcessor; @@ -51,11 +51,11 @@ class TurnOrderProcessor; class TurnTimerHandler; class QueriesProcessor; class CObjectVisitQuery; +class NewTurnProcessor; class CGameHandler : public IGameCallback, public Environment { CVCMIServer * lobby; - std::shared_ptr> applier; public: std::unique_ptr heroPool; @@ -63,6 +63,8 @@ public: std::unique_ptr queries; std::unique_ptr turnOrder; std::unique_ptr turnTimerHandler; + std::unique_ptr newTurnProcessor; + std::unique_ptr randomNumberGenerator; //use enums as parameters, because doMove(sth, true, false, true) is not readable enum EGuardLook {CHECK_FOR_GUARDS, IGNORE_GUARDS}; @@ -86,10 +88,18 @@ 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); + // Helpers to create new object of specified type + + CGObjectInstance * createNewObject(const int3 & visitablePosition, MapObjectID objectID, MapObjectSubID subID); + void createWanderingMonster(const int3 & visitablePosition, CreatureID creature); + void createBoat(const int3 & visitablePosition, BoatId type, PlayerColor initiator) override; + void createHole(const int3 & visitablePosition, PlayerColor initiator); + void newObject(CGObjectInstance * object, PlayerColor initiator); + explicit CGameHandler(CVCMIServer * lobby); ~CGameHandler(); @@ -97,14 +107,14 @@ 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 createObject(const int3 & visitablePosition, const PlayerColor & initiator, MapObjectID type, MapObjectSubID subtype) override; void setOwner(const CGObjectInstance * obj, PlayerColor owner) override; void giveExperience(const CGHeroInstance * hero, TExpType val) override; void changePrimSkill(const CGHeroInstance * hero, PrimarySkill which, si64 val, bool abs=false) override; void changeSecSkill(const CGHeroInstance * hero, SecondarySkill which, int val, bool abs=false) override; - void showBlockingDialog(BlockingDialog *iw) override; + void showBlockingDialog(const IObjectInterface * caller, BlockingDialog *iw) override; void showTeleportDialog(TeleportDialog *iw) override; void showGarrisonDialog(ObjectInstanceID upobj, ObjectInstanceID hid, bool removableUnits) override; void showObjectWindow(const CGObjectInstance * object, EOpenWindowMode window, const CGHeroInstance * visitor, bool addQuery) override; @@ -124,12 +134,15 @@ public: void removeAfterVisit(const CGObjectInstance *object) override; - bool giveHeroNewArtifact(const CGHeroInstance * h, const CArtifact * artType, ArtifactPosition pos = ArtifactPosition::FIRST_AVAILABLE) override; - bool putArtifact(const ArtifactLocation & al, const CArtifactInstance * art, std::optional askAssemble) override; + 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 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); @@ -137,9 +150,8 @@ public: void heroVisitCastle(const CGTownInstance * obj, const CGHeroInstance * hero) override; void stopHeroVisitCastle(const CGTownInstance * obj, const CGHeroInstance * hero) override; - void startBattlePrimary(const CArmedInstance *army1, const CArmedInstance *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool creatureBank = false, const CGTownInstance *town = nullptr) override; //use hero=nullptr for no hero - void startBattleI(const CArmedInstance *army1, const CArmedInstance *army2, int3 tile, bool creatureBank = false) override; //if any of armies is hero, hero will be used - void startBattleI(const CArmedInstance *army1, const CArmedInstance *army2, bool creatureBank = false) override; //if any of armies is hero, hero will be used, visitable tile of second obj is place of battle + void startBattle(const CArmedInstance *army1, const CArmedInstance *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, const BattleLayout & layout, const CGTownInstance *town) override; //use hero=nullptr for no hero + void startBattle(const CArmedInstance *army1, const CArmedInstance *army2) override; //if any of armies is hero, hero will be used, visitable tile of second obj is place of battle bool moveHero(ObjectInstanceID hid, int3 dst, EMovementMode movementMode, bool transit = false, PlayerColor asker = PlayerColor::NEUTRAL) override; void giveHeroBonus(GiveBonus * bonus) override; void setMovePoints(SetMovePoints * smp) override; @@ -150,7 +162,7 @@ public: void heroExchange(ObjectInstanceID hero1, ObjectInstanceID hero2) override; void changeFogOfWar(int3 center, ui32 radius, PlayerColor player, ETileVisibility mode) override; - void changeFogOfWar(std::unordered_set &tiles, PlayerColor player,ETileVisibility mode) override; + void changeFogOfWar(const std::unordered_set &tiles, PlayerColor player,ETileVisibility mode) override; void castSpell(const spells::Caster * caster, SpellID spellID, const int3 &pos) override; @@ -160,6 +172,9 @@ public: bool isVisitCoveredByAnotherQuery(const CGObjectInstance *obj, const CGHeroInstance *hero) override; void setObjPropertyValue(ObjectInstanceID objid, ObjProperty prop, int32_t value) override; void setObjPropertyID(ObjectInstanceID objid, ObjProperty prop, ObjPropertyID identifier) override; + void setBankObjectConfiguration(ObjectInstanceID objid, const BankConfig & configuration) override; + void setRewardableObjectConfiguration(ObjectInstanceID objid, const Rewardable::Configuration & configuration) override; + void setRewardableObjectConfiguration(ObjectInstanceID townInstanceID, BuildingID buildingID, const Rewardable::Configuration & configuration) override; void showInfoDialog(InfoWindow * iw) override; ////////////////////////////////////////////////////////////////////////// @@ -168,6 +183,7 @@ public: void visitObjectOnTile(const TerrainTile &t, const CGHeroInstance * h); bool teleportHero(ObjectInstanceID hid, ObjectInstanceID dstid, ui8 source, PlayerColor asker = PlayerColor::NEUTRAL); void visitCastleObjects(const CGTownInstance * obj, const CGHeroInstance * hero) override; + void visitCastleObjects(const CGTownInstance * obj, std::vector visitors); void levelUpHero(const CGHeroInstance * hero, SecondarySkill skill);//handle client respond and send one more request if needed void levelUpHero(const CGHeroInstance * hero);//initial call - check if hero have remaining levelups & handle them void levelUpCommander (const CCommanderInstance * c, int skill); //secondary skill 1 to 6, special skill : skill - 100 @@ -178,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; @@ -201,7 +217,9 @@ public: bool upgradeCreature( ObjectInstanceID objid, SlotID pos, CreatureID upgID ); bool recruitCreatures(ObjectInstanceID objid, ObjectInstanceID dst, CreatureID crid, ui32 cram, si32 level, PlayerColor player); 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); @@ -214,26 +232,23 @@ public: void onPlayerTurnStarted(PlayerColor which); void onPlayerTurnEnded(PlayerColor which); void onNewTurn(); + void addStatistics(StatisticDataSet &stat) const; - void handleTimeEvents(); - void handleTownEvents(CGTownInstance *town, NewTurn &n); bool complain(const std::string &problem); //sends message to all clients, prints on the logs and return true void objectVisited( const CGObjectInstance * obj, const CGHeroInstance * h ); - void objectVisitEnded(const CObjectVisitQuery &query); + void objectVisitEnded(const CGHeroInstance *h, PlayerColor player); bool dig(const CGHeroInstance *h); void moveArmy(const CArmedInstance *src, const CArmedInstance *dst, bool allowMerging); template void serialize(Handler &h) { h & QID; - h & getRandomGenerator(); + h & *randomNumberGenerator; h & *battles; h & *heroPool; h & *playerMessages; h & *turnOrder; - - if (h.version >= Handler::Version::TURN_TIMERS_STATE) - h & *turnTimerHandler; + h & *turnTimerHandler; #if SCRIPTING_ENABLED JsonNode scriptsState; @@ -245,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 @@ -267,7 +282,7 @@ public: void start(bool resume); void tick(int millisecondsPassed); - bool sacrificeArtifact(const IMarket * m, const CGHeroInstance * hero, const std::vector & arts); + bool sacrificeArtifact(const IMarket * market, const CGHeroInstance * hero, const std::vector & arts); void spawnWanderingMonsters(CreatureID creatureID); // Check for victory and loss conditions @@ -275,7 +290,7 @@ public: void checkVictoryLossConditions(const std::set & playerColors); void checkVictoryLossConditionsForAll(); - CRandomGenerator & getRandomGenerator(); + vstd::RNG & getRandomGenerator() override; #if SCRIPTING_ENABLED scripting::Pool * getGlobalContextPool() const override; diff --git a/server/CMakeLists.txt b/server/CMakeLists.txt index 6f7093ea4..a65c3ee0f 100644 --- a/server/CMakeLists.txt +++ b/server/CMakeLists.txt @@ -9,9 +9,11 @@ set(vcmiservercommon_SRCS queries/BattleQueries.cpp queries/CQuery.cpp queries/MapQueries.cpp + queries/VisitQueries.cpp queries/QueriesProcessor.cpp processors/HeroPoolProcessor.cpp + processors/NewTurnProcessor.cpp processors/PlayerMessageProcessor.cpp processors/TurnOrderProcessor.cpp @@ -35,9 +37,11 @@ set(vcmiservercommon_HEADERS queries/BattleQueries.h queries/CQuery.h queries/MapQueries.h + queries/VisitQueries.h queries/QueriesProcessor.h processors/HeroPoolProcessor.h + processors/NewTurnProcessor.h processors/PlayerMessageProcessor.h processors/TurnOrderProcessor.h diff --git a/server/CVCMIServer.cpp b/server/CVCMIServer.cpp index 256567ff1..2b0a27730 100644 --- a/server/CVCMIServer.cpp +++ b/server/CVCMIServer.cpp @@ -15,12 +15,18 @@ #include "LobbyNetPackVisitors.h" #include "processors/PlayerMessageProcessor.h" -#include "../lib/CHeroHandler.h" #include "../lib/CPlayerState.h" -#include "../lib/MetaString.h" -#include "../lib/registerTypes/RegisterTypesLobbyPacks.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" +#include "../lib/mapping/CMapHeader.h" +#include "../lib/rmg/CMapGenOptions.h" #include "../lib/serializer/CMemorySerializer.h" #include "../lib/serializer/Connection.h" +#include "../lib/texts/CGeneralTextHandler.h" // UUID generation #include @@ -28,64 +34,6 @@ #include #include -template class CApplyOnServer; - -class CBaseForServerApply -{ -public: - virtual bool applyOnServerBefore(CVCMIServer * srv, CPack * pack) const =0; - virtual void applyOnServerAfter(CVCMIServer * srv, CPack * pack) const =0; - virtual ~CBaseForServerApply() {} - template static CBaseForServerApply * getApplier(const U * t = nullptr) - { - return new CApplyOnServer(); - } -}; - -template class CApplyOnServer : public CBaseForServerApply -{ -public: - bool applyOnServerBefore(CVCMIServer * srv, CPack * pack) const override - { - T * ptr = static_cast(pack); - ClientPermissionsCheckerNetPackVisitor checker(*srv); - ptr->visit(checker); - - if(checker.getResult()) - { - ApplyOnServerNetPackVisitor applier(*srv); - ptr->visit(applier); - return applier.getResult(); - } - else - return false; - } - - void applyOnServerAfter(CVCMIServer * srv, CPack * pack) const override - { - T * ptr = static_cast(pack); - ApplyOnServerAfterAnnounceNetPackVisitor applier(*srv); - ptr->visit(applier); - } -}; - -template <> -class CApplyOnServer : public CBaseForServerApply -{ -public: - bool applyOnServerBefore(CVCMIServer * srv, CPack * pack) const override - { - logGlobal->error("Cannot apply plain CPack!"); - assert(0); - return false; - } - void applyOnServerAfter(CVCMIServer * srv, CPack * pack) const override - { - logGlobal->error("Cannot apply plain CPack!"); - assert(0); - } -}; - class CVCMIServerPackVisitor : public VCMI_LIB_WRAP_NAMESPACE(ICPackVisitor) { private: @@ -102,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!"); } @@ -118,7 +66,7 @@ public: } }; -CVCMIServer::CVCMIServer(uint16_t port, bool connectToLobby, bool runByClient) +CVCMIServer::CVCMIServer(uint16_t port, bool runByClient) : currentClientId(1) , currentPlayerId(1) , port(port) @@ -126,26 +74,32 @@ CVCMIServer::CVCMIServer(uint16_t port, bool connectToLobby, bool runByClient) { uuid = boost::uuids::to_string(boost::uuids::random_generator()()); logNetwork->trace("CVCMIServer created! UUID: %s", uuid); - applier = std::make_shared>(); - registerTypesLobbyPacks(*applier); networkHandler = INetworkHandler::createHandler(); - - if(connectToLobby) - lobbyProcessor = std::make_unique(*this); - else - startAcceptingIncomingConnections(); } CVCMIServer::~CVCMIServer() = default; -void CVCMIServer::startAcceptingIncomingConnections() -{ - logNetwork->info("Port %d will be used", port); +uint16_t CVCMIServer::prepare(bool connectToLobby) { + if(connectToLobby) { + lobbyProcessor = std::make_unique(*this); + return 0; + } else { + return startAcceptingIncomingConnections(); + } +} +uint16_t CVCMIServer::startAcceptingIncomingConnections() +{ + port + ? logNetwork->info("Port %d will be used", port) + : logNetwork->info("Randomly assigned port will be used"); + + // config port may be 0 => srvport will contain the OS-assigned port value networkServer = networkHandler->createServerTCP(*this); - networkServer->start(port); - logNetwork->info("Listening for connections at port %d", port); + auto srvport = networkServer->start(port); + logNetwork->info("Listening for connections at port %d", srvport); + return srvport; } void CVCMIServer::onNewConnection(const std::shared_ptr & connection) @@ -245,7 +199,6 @@ void CVCMIServer::prepareToRestart() } * si = * gh->gs->initialOpts; - si->seedToBeUsed = si->seedPostInit = 0; setState(EServerState::LOBBY); if (si->campState) { @@ -279,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)); } @@ -292,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); @@ -301,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; @@ -345,36 +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) { - CBaseForServerApply * apply = applier->getApplier(CTypeList::getInstance().getTypeID(pack.get())); - if(apply->applyOnServerBefore(this, pack.get())) - announcePack(std::move(pack)); + ClientPermissionsCheckerNetPackVisitor checker(*this); + pack.visit(checker); + + if(checker.getResult()) + { + ApplyOnServerNetPackVisitor applier(*this); + pack.visit(applier); + if (applier.getResult()) + announcePack(pack); + } } -void CVCMIServer::announcePack(std::unique_ptr pack) +void CVCMIServer::announcePack(CPackForLobby & pack) { for(auto activeConnection : activeConnections) { @@ -382,18 +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); } - applier->getApplier(CTypeList::getInstance().getTypeID(pack.get()))->applyOnServerAfter(this, pack.get()); + ApplyOnServerAfterAnnounceNetPackVisitor applier(*this); + pack.visit(applier); } -void CVCMIServer::announceMessage(MetaString txt) +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) @@ -403,13 +353,13 @@ void CVCMIServer::announceMessage(const std::string & txt) announceMessage(str); } -void CVCMIServer::announceTxt(MetaString txt, const std::string & playerName) +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) @@ -474,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; // @@ -510,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) @@ -537,7 +499,7 @@ void CVCMIServer::reconnectPlayer(int connId) } if(!startAiPack.players.empty()) - gh->sendAndApply(&startAiPack); + gh->sendAndApply(startAiPack); } } @@ -617,8 +579,6 @@ void CVCMIServer::updateStartInfoOnMapChange(std::shared_ptr mapInfo, pset.heroNameTextId = pinfo.mainCustomHeroNameTextId; pset.heroPortrait = pinfo.mainCustomHeroPortrait; } - - pset.handicap = PlayerSettings::NO_HANDICAP; } if(mi->isRandomMap && mapGenOpts) @@ -669,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) @@ -690,7 +650,7 @@ void CVCMIServer::setPlayer(PlayerColor clickedColor) //identify clicked player int clickedNameID = 0; //number of player - zero means AI, assume it initially if(clicked.isControlledByHuman()) - clickedNameID = *(clicked.connectedPlayerIDs.begin()); //if not AI - set appropiate ID + clickedNameID = *(clicked.connectedPlayerIDs.begin()); //if not AI - set appropriate ID if(clickedNameID > 0 && playerToRestore.id == clickedNameID) //player to restore is about to being replaced -> put him back to the old place { @@ -752,12 +712,66 @@ void CVCMIServer::setPlayerName(PlayerColor color, std::string name) if(player.connectedPlayerIDs.empty()) return; - int nameID = *(player.connectedPlayerIDs.begin()); //if not AI - set appropiate ID + int nameID = *(player.connectedPlayerIDs.begin()); //if not AI - set appropriate ID playerNames[nameID].name = name; setPlayerConnectedId(player, nameID); } +void CVCMIServer::setPlayerHandicap(PlayerColor color, Handicap handicap) +{ + if(color == PlayerColor::CANNOT_DETERMINE) + return; + + si->playerInfos[color].handicap = handicap; + + int humanPlayer = 0; + for (const auto & pi : si->playerInfos) + if(pi.second.isControlledByHuman()) + humanPlayer++; + + if(humanPlayer < 2) // Singleplayer + return; + + MetaString str; + str.appendTextID("vcmi.lobby.handicap"); + str.appendRawString(" "); + str.appendName(color); + str.appendRawString(":"); + + if(handicap.startBonus.empty() && handicap.percentIncome == 100 && handicap.percentGrowth == 100) + { + str.appendRawString(" "); + str.appendTextID("core.genrltxt.523"); + announceTxt(str); + return; + } + + for(auto & res : EGameResID::ALL_RESOURCES()) + if(handicap.startBonus[res] != 0) + { + str.appendRawString(" "); + str.appendName(res); + str.appendRawString(":"); + str.appendRawString(std::to_string(handicap.startBonus[res])); + } + if(handicap.percentIncome != 100) + { + str.appendRawString(" "); + str.appendTextID("core.jktext.32"); + str.appendRawString(":"); + str.appendRawString(std::to_string(handicap.percentIncome) + "%"); + } + if(handicap.percentGrowth != 100) + { + str.appendRawString(" "); + str.appendTextID("core.genrltxt.194"); + str.appendRawString(":"); + str.appendRawString(std::to_string(handicap.percentGrowth) + "%"); + } + announceTxt(str); +} + void CVCMIServer::optionNextCastle(PlayerColor player, int dir) { PlayerSettings & s = si->playerInfos[player]; @@ -995,20 +1009,53 @@ ui8 CVCMIServer::getIdOfFirstUnallocatedPlayer() const void CVCMIServer::multiplayerWelcomeMessage() { int humanPlayer = 0; - for (auto & pi : si->playerInfos) + for (const auto & pi : si->playerInfos) if(pi.second.isControlledByHuman()) humanPlayer++; if(humanPlayer < 2) // Singleplayer return; - gh->playerMessages->broadcastSystemMessage("Use '!help' to list available commands"); + gh->playerMessages->broadcastSystemMessage(MetaString::createFromTextID("vcmi.broadcast.command")); + + for (const auto & pi : si->playerInfos) + if(!pi.second.handicap.startBonus.empty() || pi.second.handicap.percentIncome != 100 || pi.second.handicap.percentGrowth != 100) + { + MetaString str; + str.appendTextID("vcmi.lobby.handicap"); + str.appendRawString(" "); + str.appendName(pi.first); + str.appendRawString(":"); + for(auto & res : EGameResID::ALL_RESOURCES()) + if(pi.second.handicap.startBonus[res] != 0) + { + str.appendRawString(" "); + str.appendName(res); + str.appendRawString(":"); + str.appendRawString(std::to_string(pi.second.handicap.startBonus[res])); + } + if(pi.second.handicap.percentIncome != 100) + { + str.appendRawString(" "); + str.appendTextID("core.jktext.32"); + str.appendRawString(":"); + str.appendRawString(std::to_string(pi.second.handicap.percentIncome) + "%"); + } + if(pi.second.handicap.percentGrowth != 100) + { + str.appendRawString(" "); + str.appendTextID("core.genrltxt.194"); + str.appendRawString(":"); + str.appendRawString(std::to_string(pi.second.handicap.percentGrowth) + "%"); + } + gh->playerMessages->broadcastSystemMessage(str); + } std::vector optionIds; if(si->extraOptionsInfo.cheatsAllowed) - optionIds.push_back("vcmi.optionsTab.cheatAllowed.hover"); + optionIds.emplace_back("vcmi.optionsTab.cheatAllowed.hover"); if(si->extraOptionsInfo.unlimitedReplay) - optionIds.push_back("vcmi.optionsTab.unlimitedReplay.hover"); + optionIds.emplace_back("vcmi.optionsTab.unlimitedReplay.hover"); if(!optionIds.size()) // No settings to publish return; diff --git a/server/CVCMIServer.h b/server/CVCMIServer.h index 6fa8f882b..813352ae3 100644 --- a/server/CVCMIServer.h +++ b/server/CVCMIServer.h @@ -25,8 +25,6 @@ struct PlayerSettings; class PlayerColor; class MetaString; -template class CApplier; - VCMI_LIB_NAMESPACE_END class CGameHandler; @@ -52,7 +50,6 @@ class CVCMIServer : public LobbyInfo, public INetworkServerListener, public INet std::unique_ptr networkHandler; - std::shared_ptr> applier; EServerState state = EServerState::LOBBY; std::shared_ptr findConnection(const std::shared_ptr &); @@ -66,6 +63,8 @@ public: /// List of all active connections std::vector> activeConnections; + uint16_t prepare(bool connectToLobby); + // INetworkListener impl void onDisconnected(const std::shared_ptr & connection, const std::string & errorMessage) override; void onPacketReceived(const std::shared_ptr & connection, const std::vector & message) override; @@ -74,7 +73,7 @@ public: std::shared_ptr gh; - CVCMIServer(uint16_t port, bool connectToLobby, bool runByClient); + CVCMIServer(uint16_t port, bool runByClient); ~CVCMIServer(); void run(); @@ -83,14 +82,14 @@ public: bool prepareToStartGame(); void prepareToRestart(); void startGameImmediately(); - void startAcceptingIncomingConnections(); + uint16_t startAcceptingIncomingConnections(); void threadHandleClient(std::shared_ptr c); - void announcePack(std::unique_ptr pack); + void announcePack(CPackForLobby & pack); bool passHost(int toConnectionId); - void announceTxt(MetaString txt, const std::string & playerName = "system"); + void announceTxt(const MetaString & txt, const std::string & playerName = "system"); void announceTxt(const std::string & txt, const std::string & playerName = "system"); void setPlayerConnectedId(PlayerSettings & pset, ui8 player) const; @@ -100,10 +99,10 @@ public: void clientDisconnected(std::shared_ptr c); void reconnectPlayer(int connId); - void announceMessage(MetaString txt); + void announceMessage(const MetaString & txt); void announceMessage(const std::string & txt); - void handleReceivedPack(std::unique_ptr pack); + void handleReceivedPack(CPackForLobby & pack); void updateAndPropagateLobbyState(); @@ -115,6 +114,7 @@ public: // Work with LobbyInfo void setPlayer(PlayerColor clickedColor); void setPlayerName(PlayerColor player, std::string name); + void setPlayerHandicap(PlayerColor player, Handicap handicap); void optionNextHero(PlayerColor player, int dir); //dir == -1 or +1 void optionSetHero(PlayerColor player, HeroTypeID id); HeroTypeID nextAllowedHero(PlayerColor player, HeroTypeID id, int direction); diff --git a/server/GlobalLobbyProcessor.cpp b/server/GlobalLobbyProcessor.cpp index 7f9a49820..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) @@ -67,7 +68,7 @@ void GlobalLobbyProcessor::onPacketReceived(const std::shared_ptr"); if(json["type"].String() == "operationFailed") return receiveOperationFailed(json); @@ -97,7 +98,7 @@ void GlobalLobbyProcessor::receiveOperationFailed(const JsonNode & json) void GlobalLobbyProcessor::receiveServerLoginSuccess(const JsonNode & json) { // no-op, wait just for any new commands from lobby - logGlobal->info("Lobby: Succesfully connected to lobby server"); + logGlobal->info("Lobby: Successfully connected to lobby server"); owner.startAcceptingIncomingConnections(); } @@ -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 2897a18d4..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) @@ -89,10 +90,12 @@ public: void visitLobbyChangePlayerOption(LobbyChangePlayerOption & pack) override; void visitLobbySetPlayer(LobbySetPlayer & pack) override; void visitLobbySetPlayerName(LobbySetPlayerName & pack) override; + void visitLobbySetPlayerHandicap(LobbySetPlayerHandicap & pack) override; void visitLobbySetTurnTime(LobbySetTurnTime & pack) override; void visitLobbySetExtraOptions(LobbySetExtraOptions & pack) override; void visitLobbySetSimturns(LobbySetSimturns & pack) override; 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 2303bc3e7..1b45736fb 100644 --- a/server/NetPacksLobbyServer.cpp +++ b/server/NetPacksLobbyServer.cpp @@ -17,10 +17,12 @@ #include "../lib/CRandomGenerator.h" #include "../lib/campaign/CampaignState.h" +#include "../lib/entities/faction/CTownHandler.h" +#include "../lib/entities/faction/CFaction.h" #include "../lib/serializer/Connection.h" #include "../lib/mapping/CMapInfo.h" #include "../lib/mapping/CMapHeader.h" -#include "../lib/CTownHandler.h" +#include "../lib/filesystem/Filesystem.h" void ClientPermissionsCheckerNetPackVisitor::visitForLobby(CPackForLobby & pack) { @@ -32,7 +34,7 @@ void ClientPermissionsCheckerNetPackVisitor::visitForLobby(CPackForLobby & pack) void ApplyOnServerAfterAnnounceNetPackVisitor::visitForLobby(CPackForLobby & pack) { - // Propogate options after every CLobbyPackToServer + // Propagate options after every CLobbyPackToServer if(pack.isForServer()) { srv.updateAndPropagateLobbyState(); @@ -107,6 +109,7 @@ void ClientPermissionsCheckerNetPackVisitor::visitLobbyClientDisconnected(LobbyC void ApplyOnServerNetPackVisitor::visitLobbyClientDisconnected(LobbyClientDisconnected & pack) { + pack.c->getConnection()->close(); srv.clientDisconnected(pack.c); result = true; } @@ -126,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(); @@ -345,6 +348,12 @@ void ApplyOnServerNetPackVisitor::visitLobbySetPlayerName(LobbySetPlayerName & p result = true; } +void ApplyOnServerNetPackVisitor::visitLobbySetPlayerHandicap(LobbySetPlayerHandicap & pack) +{ + srv.setPlayerHandicap(pack.color, pack.handicap); + result = true; +} + void ApplyOnServerNetPackVisitor::visitLobbySetSimturns(LobbySetSimturns & pack) { srv.si->simturnsInfo = pack.simturnsInfo; @@ -375,7 +384,6 @@ void ApplyOnServerNetPackVisitor::visitLobbyForceSetPlayer(LobbyForceSetPlayer & result = true; } - void ClientPermissionsCheckerNetPackVisitor::visitLobbyPvPAction(LobbyPvPAction & pack) { result = true; @@ -425,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 576b3dffc..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" @@ -27,7 +28,6 @@ #include "../lib/battle/Unit.h" #include "../lib/spells/CSpellHandler.h" #include "../lib/spells/ISpellMechanics.h" -#include "../lib/serializer/Cast.h" void ApplyGhNetPackVisitor::visitSaveGame(SaveGame & pack) { @@ -139,6 +139,22 @@ 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); + gh.throwIfPlayerNotActive(&pack); + + result = gh.visitTownBuilding(pack.tid, pack.bid); +} + void ApplyGhNetPackVisitor::visitRecruitCreatures(RecruitCreatures & pack) { gh.throwIfWrongPlayer(&pack); @@ -176,7 +192,7 @@ void ApplyGhNetPackVisitor::visitExchangeArtifacts(ExchangeArtifacts & pack) void ApplyGhNetPackVisitor::visitBulkExchangeArtifacts(BulkExchangeArtifacts & pack) { - if(dynamic_cast(gh.getObj(pack.srcHero)) == nullptr) + if(gh.getMarket(pack.srcHero) == nullptr) gh.throwIfWrongOwner(&pack, pack.srcHero); if(pack.swap) gh.throwIfWrongOwner(&pack, pack.dstHero); @@ -190,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) @@ -242,7 +243,7 @@ void ApplyGhNetPackVisitor::visitTradeOnMarketplace(TradeOnMarketplace & pack) { const CGObjectInstance * object = gh.getObj(pack.marketId); const CGHeroInstance * hero = gh.getHero(pack.heroId); - const auto * market = dynamic_cast(object); + const auto * market = gh.getMarket(pack.marketId); gh.throwIfWrongPlayer(&pack); gh.throwIfPlayerNotActive(&pack); @@ -283,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) @@ -389,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 568c8322a..73feeec8f 100644 --- a/server/ServerNetPackVisitors.h +++ b/server/ServerNetPackVisitors.h @@ -41,6 +41,8 @@ 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; void visitGarrisonHeroSwap(GarrisonHeroSwap & pack) override; @@ -60,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 035617d6e..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); } @@ -94,10 +94,15 @@ bool ServerSpellCastEnvironment::moveHero(ObjectInstanceID hid, int3 dst, EMovem return gh->moveHero(hid, dst, mode, false); } +void ServerSpellCastEnvironment::createBoat(const int3 & visitablePosition, BoatId type, PlayerColor initiator) +{ + return gh->createBoat(visitablePosition, type, initiator); +} + void ServerSpellCastEnvironment::genericQuery(Query * request, PlayerColor color, std::function)> callback) { 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 307f858ea..7af8a0a67 100644 --- a/server/ServerSpellCastEnvironment.h +++ b/server/ServerSpellCastEnvironment.h @@ -24,19 +24,20 @@ 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; bool moveHero(ObjectInstanceID hid, int3 dst, EMovementMode mode) override; + void createBoat(const int3 & visitablePosition, BoatId type, PlayerColor initiator) override; void genericQuery(Query * request, PlayerColor color, std::function)> callback) override; private: CGameHandler * gh; diff --git a/server/TurnTimerHandler.cpp b/server/TurnTimerHandler.cpp index b8dd196ed..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; } @@ -256,7 +256,7 @@ void TurnTimerHandler::onBattleLoop(const BattleID & battleID, int waitTime) if (!si->turnTimerInfo.isBattleEnabled()) return; - ui8 side = 0; + BattleSide side = BattleSide::NONE; const CStack * stack = nullptr; bool isTactisPhase = gs->getBattle(battleID)->battleTacticDist() > 0; diff --git a/server/VCMI_server.cbp b/server/VCMI_server.cbp deleted file mode 100644 index 6f26f4fc3..000000000 --- a/server/VCMI_server.cbp +++ /dev/null @@ -1,131 +0,0 @@ - - - - - - diff --git a/server/VCMI_server.vcxproj b/server/VCMI_server.vcxproj deleted file mode 100644 index 6009d4c59..000000000 --- a/server/VCMI_server.vcxproj +++ /dev/null @@ -1,180 +0,0 @@ - - - - - Debug - Win32 - - - Debug - x64 - - - RD - Win32 - - - RD - x64 - - - - {8AF697C3-465E-4910-B31B-576A9ECDB309} - VCMI_server - 10.0 - - - - Application - MultiByte - true - v142 - - - Application - MultiByte - true - v140_xp - - - Application - Unicode - v140_xp - - - Application - Unicode - v140_xp - - - - - - - - - - - - - - - - - - - - - - - - - - - <_ProjectFileVersion>10.0.30128.1 - .. - $(VCMI_Out) - $(Configuration)\ - $(Configuration)\ - $(VCMI_Out) - $(VCMI_Out) - $(Configuration)\ - $(Configuration)\ - AllRules.ruleset - AllRules.ruleset - - - - - AllRules.ruleset - AllRules.ruleset - - - - - - - - /MP4 %(AdditionalOptions)/Zm200 - Disabled - %(AdditionalIncludeDirectories) - false - EnableFastChecks - MultiThreadedDebugDLL - Level3 - EditAndContinue - 4251;%(DisableSpecificWarnings) - Use - StdInc.h - - - VCMI_lib.lib;zlib.lib;%(AdditionalDependencies) - ..\..\libs; - - - - - /MP4 %(AdditionalOptions)/Zm200 - 4251;%(DisableSpecificWarnings) - Use - StdInc.h - - - VCMI_lib.lib;zlib.lib;%(AdditionalDependencies) - - - - - /Oy- %(AdditionalOptions) - 4251;%(DisableSpecificWarnings) - Use - StdInc.h - true - - - VCMI_lib.lib;zlib.lib;%(AdditionalDependencies) - $(VCMI_Out) - /d2:-notypeopt %(AdditionalOptions) - - - - - /Oy- %(AdditionalOptions)/Zm200 - 4251;%(DisableSpecificWarnings) - Use - StdInc.h - - - VCMI_lib.lib;zlib.lib;%(AdditionalDependencies) - - - - - - - - - - Create - StdInc.h - Create - Create - Create - - - - - - - - - - - - {b952ffc5-3039-4de1-9f08-90acda483d8f} - - - - - - \ No newline at end of file diff --git a/server/battles/BattleActionProcessor.cpp b/server/battles/BattleActionProcessor.cpp index 2867c5bed..767cfc065 100644 --- a/server/battles/BattleActionProcessor.cpp +++ b/server/battles/BattleActionProcessor.cpp @@ -14,13 +14,14 @@ #include "../CGameHandler.h" -#include "../../lib/CGeneralTextHandler.h" +#include "../../lib/texts/CGeneralTextHandler.h" #include "../../lib/CStack.h" -#include "../../lib/GameSettings.h" +#include "../../lib/IGameSettings.h" #include "../../lib/battle/CBattleInfoCallback.h" #include "../../lib/battle/CObstacleInstance.h" #include "../../lib/battle/IBattleState.h" #include "../../lib/battle/BattleAction.h" +#include "../../lib/entities/building/TownFortifications.h" #include "../../lib/gameState/CGameState.h" #include "../../lib/networkPacks/PacksForClientBattle.h" #include "../../lib/networkPacks/SetStackEffect.h" @@ -29,6 +30,8 @@ #include "../../lib/spells/ISpellMechanics.h" #include "../../lib/spells/Problem.h" +#include + BattleActionProcessor::BattleActionProcessor(BattleProcessor * owner, CGameHandler * newGameHandler) : owner(owner) , gameHandler(newGameHandler) @@ -63,7 +66,7 @@ bool BattleActionProcessor::doRetreatAction(const CBattleInfoCallback & battle, return false; } - owner->setBattleResult(battle, EBattleResult::ESCAPE, !ba.side); + owner->setBattleResult(battle, EBattleResult::ESCAPE, battle.otherSide(ba.side)); return true; } @@ -84,7 +87,7 @@ bool BattleActionProcessor::doSurrenderAction(const CBattleInfoCallback & battle } gameHandler->giveResource(player, EGameResID::GOLD, -cost); - owner->setBattleResult(battle, EBattleResult::SURRENDER, !ba.side); + owner->setBattleResult(battle, EBattleResult::SURRENDER, battle.otherSide(ba.side)); return true; } @@ -184,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(); @@ -196,7 +199,7 @@ bool BattleActionProcessor::doDefendAction(const CBattleInfoCallback & battle, c message.lines.push_back(text); - gameHandler->sendAndApply(&message); + gameHandler->sendAndApply(message); return true; } @@ -273,7 +276,7 @@ bool BattleActionProcessor::doAttackAction(const CBattleInfoCallback & battle, c for (int i = 0; i < totalAttacks; ++i) { //first strike - if(i == 0 && firstStrike && retaliation && !stack->hasBonusOfType(BonusType::BLOCKS_RETALIATION)) + if(i == 0 && firstStrike && retaliation && !stack->hasBonusOfType(BonusType::BLOCKS_RETALIATION) && !stack->hasBonusOfType(BonusType::INVINCIBLE)) { makeAttack(battle, destinationStack, stack, 0, stack->getPosition(), true, false, true); } @@ -300,6 +303,7 @@ bool BattleActionProcessor::doAttackAction(const CBattleInfoCallback & battle, c //we check retaliation twice, so if it unblocked during attack it will work only on next attack if(stack->alive() && !stack->hasBonusOfType(BonusType::BLOCKS_RETALIATION) + && !stack->hasBonusOfType(BonusType::INVINCIBLE) && (i == 0 && !firstStrike) && retaliation && destinationStack->ableToRetaliate()) { @@ -344,20 +348,27 @@ bool BattleActionProcessor::doShootAction(const CBattleInfoCallback & battle, co return false; } - if (!destinationStack) + const bool emptyTileAreaAttack = battle.battleCanTargetEmptyHex(stack); + + if (!destinationStack && !emptyTileAreaAttack) { gameHandler->complain("No target to shoot!"); return false; } - static const auto firstStrikeSelector = Selector::typeSubtype(BonusType::FIRST_STRIKE, BonusCustomSubtype::damageTypeAll).Or(Selector::typeSubtype(BonusType::FIRST_STRIKE, BonusCustomSubtype::damageTypeRanged)); - const bool firstStrike = destinationStack->hasBonus(firstStrikeSelector); + bool firstStrike = false; + if(!emptyTileAreaAttack) + { + static const auto firstStrikeSelector = Selector::typeSubtype(BonusType::FIRST_STRIKE, BonusCustomSubtype::damageTypeAll).Or(Selector::typeSubtype(BonusType::FIRST_STRIKE, BonusCustomSubtype::damageTypeRanged)); + firstStrike = destinationStack->hasBonus(firstStrikeSelector); + } if (!firstStrike) makeAttack(battle, stack, destinationStack, 0, destination, true, true, false); //ranged counterattack - if (destinationStack->hasBonusOfType(BonusType::RANGED_RETALIATION) + if (!emptyTileAreaAttack + && destinationStack->hasBonusOfType(BonusType::RANGED_RETALIATION) && !stack->hasBonusOfType(BonusType::BLOCKS_RANGED_RETALIATION) && destinationStack->ableToRetaliate() && battle.battleCanShoot(destinationStack, stack->getPosition()) @@ -378,11 +389,9 @@ bool BattleActionProcessor::doShootAction(const CBattleInfoCallback & battle, co for(int i = firstStrike ? 0:1; i < totalRangedAttacks; ++i) { - if( - stack->alive() - && destinationStack->alive() - && stack->shots.canUse() - ) + if(stack->alive() + && (emptyTileAreaAttack || destinationStack->alive()) + && stack->shots.canUse()) { makeAttack(battle, stack, destinationStack, 0, destination, false, true, false); } @@ -587,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); @@ -596,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) @@ -625,7 +634,7 @@ int BattleActionProcessor::moveStack(const CBattleInfoCallback & battle, int sta return 0; //initing necessary tables - auto accessibility = battle.getAccesibility(curStack); + auto accessibility = battle.getAccessibility(curStack); std::set passed; //Ignore obstacles on starting position passed.insert(curStack->getPosition()); @@ -649,7 +658,7 @@ int BattleActionProcessor::moveStack(const CBattleInfoCallback & battle, int sta bool canUseGate = false; auto dbState = battle.battleGetGateState(); - if(battle.battleGetSiegeLevel() > 0 && curStack->unitSide() == BattleSide::DEFENDER && + if(battle.battleGetFortifications().wallsHealth > 0 && curStack->unitSide() == BattleSide::DEFENDER && dbState != EGateState::DESTROYED && dbState != EGateState::BLOCKED) { @@ -707,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 @@ -719,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 @@ -847,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(); } @@ -872,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) @@ -888,7 +897,7 @@ int BattleActionProcessor::moveStack(const CBattleInfoCallback & battle, int sta } } //handle last hex separately for deviation - if (VLC->settings()->getBoolean(EGameSettings::COMBAT_ONE_HEX_TRIGGERS_OBSTACLES)) + if (gameHandler->getSettings().getBoolean(EGameSettings::COMBAT_ONE_HEX_TRIGGERS_OBSTACLES)) { if (dest == battle::Unit::occupiedHex(start, curStack->doubleWide(), curStack->unitSide()) || start == battle::Unit::occupiedHex(dest, curStack->doubleWide(), curStack->unitSide())) @@ -904,9 +913,13 @@ int BattleActionProcessor::moveStack(const CBattleInfoCallback & battle, int sta void BattleActionProcessor::makeAttack(const CBattleInfoCallback & battle, const CStack * attacker, const CStack * defender, int distance, BattleHex targetHex, bool first, bool ranged, bool counter) { - if(first && !counter) + 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; @@ -927,7 +940,7 @@ void BattleActionProcessor::makeAttack(const CBattleInfoCallback & battle, const if(attackerLuck > 0) { - auto diceSize = VLC->settings()->getVector(EGameSettings::COMBAT_GOOD_LUCK_DICE); + auto diceSize = gameHandler->getSettings().getVector(EGameSettings::COMBAT_GOOD_LUCK_DICE); size_t diceIndex = std::min(diceSize.size(), attackerLuck) - 1; // array index, so 0-indexed if(diceSize.size() > 0 && gameHandler->getRandomGenerator().nextInt(1, diceSize[diceIndex]) == 1) @@ -936,7 +949,7 @@ void BattleActionProcessor::makeAttack(const CBattleInfoCallback & battle, const if(attackerLuck < 0) { - auto diceSize = VLC->settings()->getVector(EGameSettings::COMBAT_BAD_LUCK_DICE); + auto diceSize = gameHandler->getSettings().getVector(EGameSettings::COMBAT_BAD_LUCK_DICE); size_t diceIndex = std::min(diceSize.size(), -attackerLuck) - 1; // array index, so 0-indexed if(diceSize.size() > 0 && gameHandler->getRandomGenerator().nextInt(1, diceSize[diceIndex]) == 1) @@ -956,18 +969,18 @@ void BattleActionProcessor::makeAttack(const CBattleInfoCallback & battle, const bat.flags |= BattleAttack::BALLISTA_DOUBLE_DMG; } - int64_t drainedLife = 0; + battle::HealInfo healInfo; // only primary target - if(defender->alive()) - drainedLife += applyBattleEffects(battle, bat, attackerState, fireShield, defender, distance, false); + if(defender && defender->alive()) + applyBattleEffects(battle, bat, attackerState, fireShield, defender, healInfo, distance, false); //multiple-hex normal attack std::set attackedCreatures = battle.getAttackedCreatures(attacker, targetHex, bat.shot()); //creatures other than primary target for(const CStack * stack : attackedCreatures) { if(stack != defender && stack->alive()) //do not hit same stack twice - drainedLife += applyBattleEffects(battle, bat, attackerState, fireShield, stack, distance, true); + applyBattleEffects(battle, bat, attackerState, fireShield, stack, healInfo, distance, true); } std::shared_ptr bonus = attacker->getFirstBonus(Selector::type()(BonusType::SPELL_LIKE_ATTACK)); @@ -995,7 +1008,7 @@ void BattleActionProcessor::makeAttack(const CBattleInfoCallback & battle, const { if(stack != defender && stack->alive()) //do not hit same stack twice { - drainedLife += applyBattleEffects(battle, bat, attackerState, fireShield, stack, distance, true); + applyBattleEffects(battle, bat, attackerState, fireShield, stack, healInfo, distance, true); } } @@ -1019,13 +1032,13 @@ void BattleActionProcessor::makeAttack(const CBattleInfoCallback & battle, const bat.attackerChanges.changedStacks.push_back(info); } - if (drainedLife > 0) + if (healInfo.healedHealthPoints > 0) bat.flags |= BattleAttack::LIFE_DRAIN; for (BattleStackAttacked & bsa : bat.bsa) bsa.battleID = battle.getBattle()->getBattleID(); - gameHandler->sendAndApply(&bat); + gameHandler->sendAndApply(bat); { const bool multipleTargets = bat.bsa.size() > 1; @@ -1039,26 +1052,17 @@ void BattleActionProcessor::makeAttack(const CBattleInfoCallback & battle, const totalKills += bsa.killedAmount; } - { - MetaString text; - attacker->addText(text, EMetaText::GENERAL_TXT, 376); - attacker->addNameReplacement(text); - text.replaceNumber(totalDamage); - blm.lines.push_back(text); - } + addGenericDamageLog(blm, attackerState, totalDamage); - addGenericKilledLog(blm, defender, totalKills, multipleTargets); + if(defender) + addGenericKilledLog(blm, defender, totalKills, multipleTargets); } // drain life effect (as well as log entry) must be applied after the attack - if(drainedLife > 0) + if(healInfo.healedHealthPoints > 0) { - MetaString text; - attackerState->addText(text, EMetaText::GENERAL_TXT, 361); - attackerState->addNameReplacement(text, false); - text.replaceNumber(drainedLife); - defender->addNameReplacement(text, true); - blm.lines.push_back(std::move(text)); + addGenericDrainedLifeLog(blm, attackerState, defender, healInfo.healedHealthPoints); + addGenericResurrectedLog(blm, attackerState, defender, healInfo.resurrectedCount); } if(!fireShield.empty()) @@ -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,9 +1119,10 @@ void BattleActionProcessor::makeAttack(const CBattleInfoCallback & battle, const } } - gameHandler->sendAndApply(&blm); + gameHandler->sendAndApply(blm); - handleAfterAttackCasting(battle, ranged, attacker, defender); + if(defender) + handleAfterAttackCasting(battle, ranged, attacker, defender); } void BattleActionProcessor::attackCasting(const CBattleInfoCallback & battle, bool ranged, BonusType attackMode, const battle::Unit * attacker, const CStack * defender) @@ -1273,10 +1278,9 @@ void BattleActionProcessor::handleDeathStare(const CBattleInfoCallback & battle, int singleCreatureKillChancePercent = attacker->valOfBonuses(BonusType::DEATH_STARE, subtype); double chanceToKill = singleCreatureKillChancePercent / 100.0; vstd::amin(chanceToKill, 1); //cap at 100% - std::binomial_distribution<> distribution(attacker->getCount(), chanceToKill); - int killedCreatures = distribution(gameHandler->getRandomGenerator().getStdGenerator()); + int killedCreatures = gameHandler->getRandomGenerator().nextBinomialInt(attacker->getCount(), chanceToKill); - int maxToKill = (attacker->getCount() * singleCreatureKillChancePercent + 99) / 100; + int maxToKill = vstd::divideAndCeil(attacker->getCount() * singleCreatureKillChancePercent, 100); vstd::amin(killedCreatures, maxToKill); killedCreatures += (attacker->level() * attacker->valOfBonuses(BonusType::DEATH_STARE, BonusCustomSubtype::deathStareCommander)) / defender->level(); @@ -1351,7 +1355,7 @@ void BattleActionProcessor::handleAfterAttackCasting(const CBattleInfoCallback & double chanceToTrigger = attacker->valOfBonuses(BonusType::TRANSMUTATION) / 100.0f; vstd::amin(chanceToTrigger, 1); //cap at 100% - if(gameHandler->getRandomGenerator().getDoubleRange(0, 1)() > chanceToTrigger) + if(gameHandler->getRandomGenerator().nextDouble(0, 1) > chanceToTrigger) return; int bonusAdditionalInfo = attacker->getBonus(Selector::type()(BonusType::TRANSMUTATION))->additionalInfo[0]; @@ -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)) @@ -1415,7 +1419,7 @@ void BattleActionProcessor::handleAfterAttackCasting(const CBattleInfoCallback & vstd::amin(chanceToTrigger, 1); //cap trigger chance at 100% - if(gameHandler->getRandomGenerator().getDoubleRange(0, 1)() > chanceToTrigger) + if(gameHandler->getRandomGenerator().nextDouble(0, 1) > chanceToTrigger) return; BattleStackAttacked bsa; @@ -1430,12 +1434,12 @@ 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); } } -int64_t BattleActionProcessor::applyBattleEffects(const CBattleInfoCallback & battle, BattleAttack & bat, std::shared_ptr attackerState, FireShieldInfo & fireShield, const CStack * def, int distance, bool secondary) +void BattleActionProcessor::applyBattleEffects(const CBattleInfoCallback & battle, BattleAttack & bat, std::shared_ptr attackerState, FireShieldInfo & fireShield, const CStack * def, battle::HealInfo & healInfo, int distance, bool secondary) const { BattleStackAttacked bsa; if(secondary) @@ -1456,14 +1460,11 @@ int64_t BattleActionProcessor::applyBattleEffects(const CBattleInfoCallback & ba CStack::prepareAttacked(bsa, gameHandler->getRandomGenerator(), bai.defender->acquireState()); //calculate casualties } - int64_t drainedLife = 0; - //life drain handling if(attackerState->hasBonusOfType(BonusType::LIFE_DRAIN) && def->isLiving()) { int64_t toHeal = bsa.damageAmount * attackerState->valOfBonuses(BonusType::LIFE_DRAIN) / 100; - attackerState->heal(toHeal, EHealLevel::RESURRECT, EHealPower::PERMANENT); - drainedLife += toHeal; + healInfo += attackerState->heal(toHeal, EHealLevel::RESURRECT, EHealPower::PERMANENT); } //soul steal handling @@ -1477,8 +1478,7 @@ int64_t BattleActionProcessor::applyBattleEffects(const CBattleInfoCallback & ba { int64_t toHeal = bsa.killedAmount * attackerState->valOfBonuses(BonusType::SOUL_STEAL, subtype) * attackerState->getMaxHealth(); bool permanent = subtype == BonusCustomSubtype::soulStealPermanent; - attackerState->heal(toHeal, EHealLevel::OVERHEAL, (permanent ? EHealPower::PERMANENT : EHealPower::ONE_BATTLE)); - drainedLife += toHeal; + healInfo += attackerState->heal(toHeal, EHealLevel::OVERHEAL, (permanent ? EHealPower::PERMANENT : EHealPower::ONE_BATTLE)); break; } } @@ -1499,8 +1499,6 @@ int64_t BattleActionProcessor::applyBattleEffects(const CBattleInfoCallback & ba auto fireShieldDamage = (std::min(def->getAvailableHealth(), bsa.damageAmount) * def->valOfBonuses(BonusType::FIRE_SHIELD)) / 100; fireShield.push_back(std::make_pair(def, fireShieldDamage)); } - - return drainedLife; } void BattleActionProcessor::sendGenericKilledLog(const CBattleInfoCallback & battle, const CStack * defender, int32_t killed, bool multiple) @@ -1510,11 +1508,11 @@ void BattleActionProcessor::sendGenericKilledLog(const CBattleInfoCallback & bat BattleLogMessage blm; blm.battleID = battle.getBattle()->getBattleID(); addGenericKilledLog(blm, defender, killed, multiple); - gameHandler->sendAndApply(&blm); + gameHandler->sendAndApply(blm); } } -void BattleActionProcessor::addGenericKilledLog(BattleLogMessage & blm, const CStack * defender, int32_t killed, bool multiple) +void BattleActionProcessor::addGenericKilledLog(BattleLogMessage & blm, const CStack * defender, int32_t killed, bool multiple) const { if(killed > 0) { @@ -1542,6 +1540,46 @@ void BattleActionProcessor::addGenericKilledLog(BattleLogMessage & blm, const CS } } +void BattleActionProcessor::addGenericDamageLog(BattleLogMessage& blm, const std::shared_ptr &attackerState, int64_t damageDealt) const +{ + MetaString text; + attackerState->addText(text, EMetaText::GENERAL_TXT, 376); + attackerState->addNameReplacement(text); + text.replaceNumber(damageDealt); + blm.lines.push_back(std::move(text)); +} + +void BattleActionProcessor::addGenericDrainedLifeLog(BattleLogMessage& blm, const std::shared_ptr& attackerState, const CStack* defender, int64_t drainedLife) const +{ + MetaString text; + attackerState->addText(text, EMetaText::GENERAL_TXT, 361); + attackerState->addNameReplacement(text); + text.replaceNumber(drainedLife); + defender->addNameReplacement(text); + blm.lines.push_back(std::move(text)); +} + +void BattleActionProcessor::addGenericResurrectedLog(BattleLogMessage& blm, const std::shared_ptr& attackerState, const CStack* defender, int64_t resurrected) const +{ + if (resurrected > 0) + { + auto text = blm.lines.back().toString(); + text.pop_back(); // erase '.' at the end of line with life drain info + MetaString ms = MetaString::createFromRawString(text); + if (resurrected == 1) + { + ms.appendLocalString(EMetaText::GENERAL_TXT, 363); // "\n and one rises from the dead." + } + else + { + ms.appendLocalString(EMetaText::GENERAL_TXT, 364); // "\n and %d rise from the dead." + ms.replaceNumber(resurrected); + } + blm.lines[blm.lines.size() - 1] = std::move(ms); + } + +} + bool BattleActionProcessor::makeAutomaticBattleAction(const CBattleInfoCallback & battle, const BattleAction & ba) { return makeBattleActionImpl(battle, ba); @@ -1549,7 +1587,7 @@ bool BattleActionProcessor::makeAutomaticBattleAction(const CBattleInfoCallback bool BattleActionProcessor::makePlayerBattleAction(const CBattleInfoCallback & battle, PlayerColor player, const BattleAction &ba) { - if (ba.side != 0 && ba.side != 1 && gameHandler->complain("Can not make action - invalid battle side!")) + if (ba.side != BattleSide::ATTACKER && ba.side != BattleSide::DEFENDER && gameHandler->complain("Can not make action - invalid battle side!")) return false; if(battle.battleGetTacticDist() != 0) diff --git a/server/battles/BattleActionProcessor.h b/server/battles/BattleActionProcessor.h index ec616f888..72744dc96 100644 --- a/server/battles/BattleActionProcessor.h +++ b/server/battles/BattleActionProcessor.h @@ -19,11 +19,12 @@ class CBattleInfoCallback; struct BattleHex; class CStack; class PlayerColor; -enum class BonusType; +enum class BonusType : uint8_t; namespace battle { class Unit; +struct HealInfo; class CUnitState; } @@ -53,10 +54,13 @@ class BattleActionProcessor : boost::noncopyable std::set getSpellsForAttackCasting(TConstBonusListPtr spells, const CStack *defender); // damage, drain life & fire shield; returns amount of drained life - int64_t applyBattleEffects(const CBattleInfoCallback & battle, BattleAttack & bat, std::shared_ptr attackerState, FireShieldInfo & fireShield, const CStack * def, int distance, bool secondary); + void applyBattleEffects(const CBattleInfoCallback & battle, BattleAttack & bat, std::shared_ptr attackerState, FireShieldInfo & fireShield, const CStack * def, battle::HealInfo & healInfo, int distance, bool secondary) const; void sendGenericKilledLog(const CBattleInfoCallback & battle, const CStack * defender, int32_t killed, bool multiple); - void addGenericKilledLog(BattleLogMessage & blm, const CStack * defender, int32_t killed, bool multiple); + void addGenericKilledLog(BattleLogMessage & blm, const CStack * defender, int32_t killed, bool multiple) const; + void addGenericDamageLog(BattleLogMessage& blm, const std::shared_ptr &attackerState, int64_t damageDealt) const; + void addGenericDrainedLifeLog(BattleLogMessage& blm, const std::shared_ptr &attackerState, const CStack* defender, int64_t drainedLife) const; + void addGenericResurrectedLog(BattleLogMessage& blm, const std::shared_ptr &attackerState, const CStack* defender, int64_t resurrected) const; bool canStackAct(const CBattleInfoCallback & battle, const CStack * stack); diff --git a/server/battles/BattleFlowProcessor.cpp b/server/battles/BattleFlowProcessor.cpp index 93474ef0c..e98053e40 100644 --- a/server/battles/BattleFlowProcessor.cpp +++ b/server/battles/BattleFlowProcessor.cpp @@ -16,23 +16,27 @@ #include "../TurnTimerHandler.h" #include "../../lib/CStack.h" -#include "../../lib/GameSettings.h" +#include "../../lib/IGameSettings.h" #include "../../lib/battle/CBattleInfoCallback.h" #include "../../lib/battle/IBattleState.h" +#include "../../lib/entities/building/TownFortifications.h" #include "../../lib/gameState/CGameState.h" #include "../../lib/mapObjects/CGTownInstance.h" #include "../../lib/networkPacks/PacksForClientBattle.h" #include "../../lib/spells/BonusCaster.h" +#include "../../lib/spells/CSpellHandler.h" #include "../../lib/spells/ISpellMechanics.h" #include "../../lib/spells/ObstacleCasterProxy.h" +#include + BattleFlowProcessor::BattleFlowProcessor(BattleProcessor * owner, CGameHandler * newGameHandler) : owner(owner) , gameHandler(newGameHandler) { } -void BattleFlowProcessor::summonGuardiansHelper(const CBattleInfoCallback & battle, std::vector & output, const BattleHex & targetPosition, ui8 side, bool targetIsTwoHex) //return hexes for summoning two hex monsters in output, target = unit to guard +void BattleFlowProcessor::summonGuardiansHelper(const CBattleInfoCallback & battle, std::vector & output, const BattleHex & targetPosition, BattleSide side, bool targetIsTwoHex) //return hexes for summoning two hex monsters in output, target = unit to guard { int x = targetPosition.getX(); int y = targetPosition.getY(); @@ -111,13 +115,18 @@ void BattleFlowProcessor::tryPlaceMoats(const CBattleInfoCallback & battle) { const auto * town = battle.battleGetDefendedTown(); + if (!town) + return; + + const auto & fortifications = town->fortificationsLevel(); + //Moat should be initialized here, because only here we can use spellcasting - if (town && town->fortLevel() >= CGTownInstance::CITADEL) + if (fortifications.hasMoat) { const auto * h = battle.battleGetFightingHero(BattleSide::DEFENDER); const auto * actualCaster = h ? static_cast(h) : nullptr; auto moatCaster = spells::SilentCaster(battle.sideToPlayer(BattleSide::DEFENDER), actualCaster); - auto cast = spells::BattleCast(&battle, &moatCaster, spells::Mode::PASSIVE, town->town->moatAbility.toSpell()); + auto cast = spells::BattleCast(&battle, &moatCaster, spells::Mode::PASSIVE, fortifications.moatSpell.toSpell()); auto target = spells::Target(); cast.cast(gameHandler->spellEnv, target); } @@ -126,7 +135,7 @@ void BattleFlowProcessor::tryPlaceMoats(const CBattleInfoCallback & battle) void BattleFlowProcessor::onBattleStarted(const CBattleInfoCallback & battle) { tryPlaceMoats(battle); - + gameHandler->turnTimerHandler->onBattleStart(battle.getBattle()->getBattleID()); if (battle.battleGetTacticDist() == 0) @@ -139,7 +148,7 @@ void BattleFlowProcessor::trySummonGuardians(const CBattleInfoCallback & battle, return; std::shared_ptr summonInfo = stack->getBonus(Selector::type()(BonusType::SUMMON_GUARDIANS)); - auto accessibility = battle.getAccesibility(); + auto accessibility = battle.getAccessibility(); CreatureID creatureData = summonInfo->subtype.as(); std::vector targetHexes; const bool targetIsBig = stack->unitType()->isDoubleWide(); //target = creature to guard @@ -170,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); } } @@ -178,12 +187,12 @@ 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) { - for (int i = 0; i < 2; ++i) + for(auto i : {BattleSide::ATTACKER, BattleSide::DEFENDER}) { auto h = battle.battleGetFightingHero(i); if (!h) @@ -232,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(); @@ -278,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()) @@ -318,14 +327,16 @@ 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); if (!tryMakeAutomaticAction(battle, next)) { - setActiveStack(battle, next); - break; + if(next->alive()) { + setActiveStack(battle, next); + break; + } } } } @@ -336,7 +347,7 @@ bool BattleFlowProcessor::tryMakeAutomaticAction(const CBattleInfoCallback & bat int nextStackMorale = next->moraleVal(); if(!next->hadMorale && !next->waited() && nextStackMorale < 0) { - auto diceSize = VLC->settings()->getVector(EGameSettings::COMBAT_BAD_MORALE_DICE); + auto diceSize = gameHandler->getSettings().getVector(EGameSettings::COMBAT_BAD_MORALE_DICE); size_t diceIndex = std::min(diceSize.size(), -nextStackMorale) - 1; // array index, so 0-indexed if(diceSize.size() > 0 && gameHandler->getRandomGenerator().nextInt(1, diceSize[diceIndex]) == 1) @@ -387,20 +398,47 @@ bool BattleFlowProcessor::tryMakeAutomaticAction(const CBattleInfoCallback & bat attack.side = next->unitSide(); attack.stackNumber = next->unitId(); - //TODO: select target by priority + // TODO: unify logic with AI? + // Find best target using logic similar to H3 AI + + const auto & isBetterTarget = [&battle](const battle::Unit * candidate, const battle::Unit * current) + { + bool candidateInsideWalls = battle.battleIsInsideWalls(candidate->getPosition()); + bool currentInsideWalls = battle.battleIsInsideWalls(current->getPosition()); + + if (candidateInsideWalls != currentInsideWalls) + return candidateInsideWalls > currentInsideWalls; + + // also check for war machines - shooters are more dangerous than war machines, ballista or catapult + bool candidateCanShoot = candidate->canShoot() && candidate->unitType()->warMachine == ArtifactID::NONE; + bool currentCanShoot = current->canShoot() && current->unitType()->warMachine == ArtifactID::NONE; + + if (candidateCanShoot != currentCanShoot) + return candidateCanShoot > currentCanShoot; + + int64_t candidateTargetValue = static_cast(candidate->unitType()->getAIValue() * candidate->getCount()); + int64_t currentTargetValue = static_cast(current->unitType()->getAIValue() * current->getCount()); + + return candidateTargetValue > currentTargetValue; + }; const battle::Unit * target = nullptr; for(auto & elem : battle.battleGetAllStacks(true)) { - if(elem->unitType()->getId() != CreatureID::CATAPULT - && elem->unitOwner() != next->unitOwner() - && elem->isValidTarget() - && battle.battleCanShoot(next, elem->getPosition())) - { - target = elem; - break; - } + if (elem->unitOwner() == next->unitOwner()) + continue; + + if (!elem->isValidTarget()) + continue; + + if (!battle.battleCanShoot(next, elem->getPosition())) + continue; + + if (target && !isBetterTarget(elem, target)) + continue; + + target = elem; } if(target == nullptr) @@ -488,7 +526,7 @@ bool BattleFlowProcessor::rollGoodMorale(const CBattleInfoCallback & battle, con && next->canMove() && nextStackMorale > 0) { - auto diceSize = VLC->settings()->getVector(EGameSettings::COMBAT_GOOD_MORALE_DICE); + auto diceSize = gameHandler->getSettings().getVector(EGameSettings::COMBAT_GOOD_MORALE_DICE); size_t diceIndex = std::min(diceSize.size(), nextStackMorale) - 1; // array index, so 0-indexed if(diceSize.size() > 0 && gameHandler->getRandomGenerator().nextInt(1, diceSize[diceIndex]) == 1) @@ -499,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; } } @@ -527,6 +565,20 @@ void BattleFlowProcessor::onActionMade(const CBattleInfoCallback & battle, const if(battle.battleGetTacticDist() != 0) return; + // creature will not skip the turn after casting a spell if spell uses canCastWithoutSkip + if(ba.actionType == EActionType::MONSTER_SPELL) + { + assert(activeStack != nullptr); + assert(actedStack != nullptr); + + // 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; + } + } + if (ba.isUnitAction()) { assert(activeStack != nullptr); @@ -569,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; @@ -612,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) @@ -654,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); } } @@ -667,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); } } } @@ -683,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); } } } @@ -703,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); } } } @@ -713,7 +765,7 @@ void BattleFlowProcessor::stackTurnTrigger(const CBattleInfoCallback & battle, c return b->subtype.as() == SpellID::NONE; }); - int side = *battle.playerToSide(st->unitOwner()); + BattleSide side = battle.playerToSide(st->unitOwner()); if(st->canCast() && battle.battleGetEnchanterCounter(side) == 0) { bool cast = false; @@ -728,8 +780,14 @@ void BattleFlowProcessor::stackTurnTrigger(const CBattleInfoCallback & battle, c }); spells::BattleCast parameters(&battle, st, spells::Mode::ENCHANTER, spell); parameters.setSpellLevel(bonus->val); - parameters.massive = true; - parameters.smart = true; + + auto &levelInfo = spell->getLevelInfo(bonus->val); + bool isDamageSpell = spell->isDamage() || spell->isOffensive(); + if (!isDamageSpell || levelInfo.smartTarget || !levelInfo.range.empty()) + { + parameters.massive = true; + parameters.smart = true; + } //todo: recheck effect level if(parameters.castIfPossible(gameHandler->spellEnv, spells::Target(1, spells::Destination()))) { @@ -742,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); } } } @@ -756,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/BattleFlowProcessor.h b/server/battles/BattleFlowProcessor.h index e781b0a75..70ef29249 100644 --- a/server/battles/BattleFlowProcessor.h +++ b/server/battles/BattleFlowProcessor.h @@ -9,6 +9,8 @@ */ #pragma once +#include "../lib/battle/BattleSide.h" + VCMI_LIB_NAMESPACE_BEGIN class CStack; struct BattleHex; @@ -35,7 +37,7 @@ class BattleFlowProcessor : boost::noncopyable bool rollGoodMorale(const CBattleInfoCallback & battle, const CStack * stack); bool tryMakeAutomaticAction(const CBattleInfoCallback & battle, const CStack * stack); - void summonGuardiansHelper(const CBattleInfoCallback & battle, std::vector & output, const BattleHex & targetPosition, ui8 side, bool targetIsTwoHex); + void summonGuardiansHelper(const CBattleInfoCallback & battle, std::vector & output, const BattleHex & targetPosition, BattleSide side, bool targetIsTwoHex); void trySummonGuardians(const CBattleInfoCallback & battle, const CStack * stack); void tryPlaceMoats(const CBattleInfoCallback & battle); void castOpeningSpells(const CBattleInfoCallback & battle); diff --git a/server/battles/BattleProcessor.cpp b/server/battles/BattleProcessor.cpp index 14f359397..7a799d5db 100644 --- a/server/battles/BattleProcessor.cpp +++ b/server/battles/BattleProcessor.cpp @@ -23,13 +23,17 @@ #include "../../lib/battle/CBattleInfoCallback.h" #include "../../lib/battle/CObstacleInstance.h" #include "../../lib/battle/BattleInfo.h" +#include "../../lib/battle/BattleLayout.h" +#include "../../lib/entities/building/TownFortifications.h" #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) @@ -48,67 +52,59 @@ 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::restartBattlePrimary(const BattleID & battleID, const CArmedInstance *army1, const CArmedInstance *army2, int3 tile, - const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool creatureBank, - const CGTownInstance *town) +void BattleProcessor::restartBattle(const BattleID & battleID, const CArmedInstance *army1, const CArmedInstance *army2, int3 tile, + const CGHeroInstance *hero1, const CGHeroInstance *hero2, const BattleLayout & layout, const CGTownInstance *town) { auto battle = gameHandler->gameState()->getBattle(battleID); - auto lastBattleQuery = std::dynamic_pointer_cast(gameHandler->queries->topQuery(battle->sides[0].color)); + auto lastBattleQuery = std::dynamic_pointer_cast(gameHandler->queries->topQuery(battle->getSide(BattleSide::ATTACKER).color)); if(!lastBattleQuery) - lastBattleQuery = std::dynamic_pointer_cast(gameHandler->queries->topQuery(battle->sides[1].color)); + lastBattleQuery = std::dynamic_pointer_cast(gameHandler->queries->topQuery(battle->getSide(BattleSide::DEFENDER).color)); assert(lastBattleQuery); //existing battle query for retying auto-combat if(lastBattleQuery) { - const CGHeroInstance*heroes[2]; - heroes[0] = hero1; - heroes[1] = hero2; + BattleSideArray heroes{hero1, hero2}; - for(int i : {0, 1}) + for(auto i : {BattleSide::ATTACKER, BattleSide::DEFENDER}) { if(heroes[i]) { SetMana restoreInitialMana; restoreInitialMana.val = lastBattleQuery->initialHeroMana[i]; restoreInitialMana.hid = heroes[i]->id; - gameHandler->sendAndApply(&restoreInitialMana); + gameHandler->sendAndApply(restoreInitialMana); } } lastBattleQuery->result = std::nullopt; - assert(lastBattleQuery->belligerents[0] == battle->sides[0].armyObject); - assert(lastBattleQuery->belligerents[1] == battle->sides[1].armyObject); + assert(lastBattleQuery->belligerents[BattleSide::ATTACKER] == battle->getSide(BattleSide::ATTACKER).armyObject); + assert(lastBattleQuery->belligerents[BattleSide::DEFENDER] == battle->getSide(BattleSide::DEFENDER).armyObject); } BattleCancelled bc; bc.battleID = battleID; - gameHandler->sendAndApply(&bc); + gameHandler->sendAndApply(bc); - startBattlePrimary(army1, army2, tile, hero1, hero2, creatureBank, town); + startBattle(army1, army2, tile, hero1, hero2, layout, town); } -void BattleProcessor::startBattlePrimary(const CArmedInstance *army1, const CArmedInstance *army2, int3 tile, - const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool creatureBank, - const CGTownInstance *town) +void BattleProcessor::startBattle(const CArmedInstance *army1, const CArmedInstance *army2, int3 tile, + const CGHeroInstance *hero1, const CGHeroInstance *hero2, const BattleLayout & layout, const CGTownInstance *town) { assert(gameHandler->gameState()->getBattle(army1->getOwner()) == nullptr); assert(gameHandler->gameState()->getBattle(army2->getOwner()) == nullptr); - const CArmedInstance *armies[2]; - armies[0] = army1; - armies[1] = army2; - const CGHeroInstance*heroes[2]; - heroes[0] = hero1; - heroes[1] = hero2; + BattleSideArray armies{army1, army2}; + BattleSideArrayheroes{hero1, hero2}; - auto battleID = setupBattle(tile, armies, heroes, creatureBank, town); //initializes stacks, places creatures on battlefield, blocks and informs player interfaces + auto battleID = setupBattle(tile, armies, heroes, layout, town); //initializes stacks, places creatures on battlefield, blocks and informs player interfaces const auto * battle = gameHandler->gameState()->getBattle(battleID); assert(battle); @@ -122,13 +118,13 @@ void BattleProcessor::startBattlePrimary(const CArmedInstance *army1, const CArm GiveBonus giveBonus(GiveBonus::ETarget::OBJECT); giveBonus.id = hero1->id; giveBonus.bonus = bonus; - gameHandler->sendAndApply(&giveBonus); + gameHandler->sendAndApply(giveBonus); } } - auto lastBattleQuery = std::dynamic_pointer_cast(gameHandler->queries->topQuery(battle->sides[0].color)); + auto lastBattleQuery = std::dynamic_pointer_cast(gameHandler->queries->topQuery(battle->getSide(BattleSide::ATTACKER).color)); if(!lastBattleQuery) - lastBattleQuery = std::dynamic_pointer_cast(gameHandler->queries->topQuery(battle->sides[1].color)); + lastBattleQuery = std::dynamic_pointer_cast(gameHandler->queries->topQuery(battle->getSide(BattleSide::DEFENDER).color)); if (lastBattleQuery) { @@ -139,7 +135,7 @@ void BattleProcessor::startBattlePrimary(const CArmedInstance *army1, const CArm auto newBattleQuery = std::make_shared(gameHandler, battle); // store initial mana to reset if battle has been restarted - for(int i : {0, 1}) + for(auto i : {BattleSide::ATTACKER, BattleSide::DEFENDER}) if(heroes[i]) newBattleQuery->initialHeroMana[i] = heroes[i]->mana; @@ -149,48 +145,52 @@ void BattleProcessor::startBattlePrimary(const CArmedInstance *army1, const CArm flowProcessor->onBattleStarted(*battle); } -void BattleProcessor::startBattleI(const CArmedInstance *army1, const CArmedInstance *army2, int3 tile, bool creatureBank) +void BattleProcessor::startBattle(const CArmedInstance *army1, const CArmedInstance *army2) { - startBattlePrimary(army1, army2, tile, - army1->ID == Obj::HERO ? static_cast(army1) : nullptr, - army2->ID == Obj::HERO ? static_cast(army2) : nullptr, - creatureBank); + startBattle(army1, army2, army2->visitablePos(), + army1->ID == Obj::HERO ? dynamic_cast(army1) : nullptr, + army2->ID == Obj::HERO ? dynamic_cast(army2) : nullptr, + BattleLayout::createDefaultLayout(gameHandler, army1, army2), + nullptr); } -void BattleProcessor::startBattleI(const CArmedInstance *army1, const CArmedInstance *army2, bool creatureBank) -{ - startBattleI(army1, army2, army2->visitablePos(), creatureBank); -} - -BattleID BattleProcessor::setupBattle(int3 tile, const CArmedInstance *armies[2], const CGHeroInstance *heroes[2], bool creatureBank, const CGTownInstance *town) +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[0] && heroes[0]->boat && heroes[1] && heroes[1]->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, creatureBank, town); + bs.info = BattleInfo::setupBattle(tile, terrain, battlefieldType, armies, heroes, layout, town); bs.battleID = gameHandler->gameState()->nextBattleID; - engageIntoBattle(bs.info->sides[0].color); - engageIntoBattle(bs.info->sides[1].color); + engageIntoBattle(bs.info->getSide(BattleSide::ATTACKER).color); + engageIntoBattle(bs.info->getSide(BattleSide::DEFENDER).color); - auto lastBattleQuery = std::dynamic_pointer_cast(gameHandler->queries->topQuery(bs.info->sides[0].color)); + auto lastBattleQuery = std::dynamic_pointer_cast(gameHandler->queries->topQuery(bs.info->getSide(BattleSide::ATTACKER).color)); if(!lastBattleQuery) - lastBattleQuery = std::dynamic_pointer_cast(gameHandler->queries->topQuery(bs.info->sides[1].color)); - bool isDefenderHuman = bs.info->sides[1].color.isValidPlayer() && gameHandler->getPlayerState(bs.info->sides[1].color)->isHuman(); - bool isAttackerHuman = gameHandler->getPlayerState(bs.info->sides[0].color)->isHuman(); + lastBattleQuery = std::dynamic_pointer_cast(gameHandler->queries->topQuery(bs.info->getSide(BattleSide::DEFENDER).color)); + bool isDefenderHuman = bs.info->getSide(BattleSide::DEFENDER).color.isValidPlayer() && gameHandler->getPlayerState(bs.info->getSide(BattleSide::DEFENDER).color)->isHuman(); + bool isAttackerHuman = gameHandler->getPlayerState(bs.info->getSide(BattleSide::ATTACKER).color)->isHuman(); bool onlyOnePlayerHuman = isDefenderHuman != isAttackerHuman; bs.info->replayAllowed = lastBattleQuery == nullptr && onlyOnePlayerHuman; - gameHandler->sendAndApply(&bs); + gameHandler->sendAndApply(bs); return bs.battleID; } @@ -198,7 +198,7 @@ BattleID BattleProcessor::setupBattle(int3 tile, const CArmedInstance *armies[2] bool BattleProcessor::checkBattleStateChanges(const CBattleInfoCallback & battle) { //check if drawbridge state need to be changes - if (battle.battleGetSiegeLevel() > 0) + if (battle.battleGetFortifications().wallsHealth > 0) updateGateState(battle); if (resultProcessor->battleIsEnding(battle)) @@ -268,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) @@ -284,7 +284,7 @@ bool BattleProcessor::makePlayerBattleAction(const BattleID & battleID, PlayerCo return result; } -void BattleProcessor::setBattleResult(const CBattleInfoCallback & battle, EBattleResult resultType, int victoriusSide) +void BattleProcessor::setBattleResult(const CBattleInfoCallback & battle, EBattleResult resultType, BattleSide victoriusSide) { resultProcessor->setBattleResult(battle, resultType, victoriusSide); resultProcessor->endBattle(battle); diff --git a/server/battles/BattleProcessor.h b/server/battles/BattleProcessor.h index e284def89..480627b21 100644 --- a/server/battles/BattleProcessor.h +++ b/server/battles/BattleProcessor.h @@ -10,6 +10,7 @@ #pragma once #include "../../lib/GameConstants.h" +#include "../../lib/battle/BattleSide.h" VCMI_LIB_NAMESPACE_BEGIN class CGHeroInstance; @@ -19,6 +20,7 @@ class BattleAction; class int3; class CBattleInfoCallback; struct BattleResult; +struct BattleLayout; class BattleID; VCMI_LIB_NAMESPACE_END @@ -44,24 +46,22 @@ class BattleProcessor : boost::noncopyable void engageIntoBattle(PlayerColor player); bool checkBattleStateChanges(const CBattleInfoCallback & battle); - BattleID setupBattle(int3 tile, const CArmedInstance *armies[2], const CGHeroInstance *heroes[2], bool creatureBank, const CGTownInstance *town); + BattleID setupBattle(int3 tile, BattleSideArray armies, BattleSideArray heroes, const BattleLayout & layout, const CGTownInstance *town); bool makeAutomaticBattleAction(const CBattleInfoCallback & battle, const BattleAction & ba); - void setBattleResult(const CBattleInfoCallback & battle, EBattleResult resultType, int victoriusSide); + void setBattleResult(const CBattleInfoCallback & battle, EBattleResult resultType, BattleSide victoriusSide); public: explicit BattleProcessor(CGameHandler * gameHandler); ~BattleProcessor(); /// Starts battle with specified parameters - void startBattlePrimary(const CArmedInstance *army1, const CArmedInstance *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool creatureBank = false, const CGTownInstance *town = nullptr); - /// Starts battle between two armies (which can also be heroes) at specified tile - void startBattleI(const CArmedInstance *army1, const CArmedInstance *army2, int3 tile, bool creatureBank = false); + void startBattle(const CArmedInstance *army1, const CArmedInstance *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, const BattleLayout & layout, const CGTownInstance *town); /// Starts battle between two armies (which can also be heroes) at position of 2nd object - void startBattleI(const CArmedInstance *army1, const CArmedInstance *army2, bool creatureBank = false); + void startBattle(const CArmedInstance *army1, const CArmedInstance *army2); /// Restart ongoing battle and end previous battle - void restartBattlePrimary(const BattleID & battleID, const CArmedInstance *army1, const CArmedInstance *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool creatureBank = false, const CGTownInstance *town = nullptr); + void restartBattle(const BattleID & battleID, const CArmedInstance *army1, const CArmedInstance *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, const BattleLayout & layout, const CGTownInstance *town); /// Processing of incoming battle action netpack bool makePlayerBattleAction(const BattleID & battleID, PlayerColor player, const BattleAction & ba); diff --git a/server/battles/BattleResultProcessor.cpp b/server/battles/BattleResultProcessor.cpp index 6244c78fc..411343e16 100644 --- a/server/battles/BattleResultProcessor.cpp +++ b/server/battles/BattleResultProcessor.cpp @@ -19,7 +19,7 @@ #include "../../lib/ArtifactUtils.h" #include "../../lib/CStack.h" #include "../../lib/CPlayerState.h" -#include "../../lib/GameSettings.h" +#include "../../lib/IGameSettings.h" #include "../../lib/battle/CBattleInfoCallback.h" #include "../../lib/battle/IBattleState.h" #include "../../lib/battle/SideInBattle.h" @@ -27,16 +27,17 @@ #include "../../lib/mapObjects/CGTownInstance.h" #include "../../lib/networkPacks/PacksForClientBattle.h" #include "../../lib/networkPacks/PacksForClient.h" -#include "../../lib/serializer/Cast.h" #include "../../lib/spells/CSpellHandler.h" +#include + BattleResultProcessor::BattleResultProcessor(BattleProcessor * owner, CGameHandler * newGameHandler) // : owner(owner) : gameHandler(newGameHandler) { } -CasualtiesAfterBattle::CasualtiesAfterBattle(const CBattleInfoCallback & battle, uint8_t sideInBattle): +CasualtiesAfterBattle::CasualtiesAfterBattle(const CBattleInfoCallback & battle, BattleSide sideInBattle): army(battle.battleGetArmyObject(sideInBattle)) { heroWithDeadCommander = ObjectInstanceID(); @@ -79,7 +80,7 @@ CasualtiesAfterBattle::CasualtiesAfterBattle(const CBattleInfoCallback & battle, else if(warMachine != ArtifactID::CATAPULT && st->getCount() <= 0) { logGlobal->debug("War machine has been destroyed"); - auto hero = dynamic_ptr_cast (army); + auto hero = dynamic_cast (army); if (hero) removedWarMachines.push_back (ArtifactLocation(hero->id, hero->getArtPos(warMachine, true))); else @@ -177,7 +178,7 @@ void CasualtiesAfterBattle::updateArmy(CGameHandler *gh) scp.heroid = heroWithDeadCommander; scp.which = SetCommanderProperty::ALIVE; scp.amount = 0; - gh->sendAndApply(&scp); + gh->sendAndApply(scp); } } @@ -203,25 +204,18 @@ FinishingBattleHelper::FinishingBattleHelper(const CBattleInfoCallback & info, c this->remainingBattleQueriesCount = remainingBattleQueriesCount; } -//FinishingBattleHelper::FinishingBattleHelper() -//{ -// winnerHero = loserHero = nullptr; -// winnerSide = 0; -// remainingBattleQueriesCount = 0; -//} - void BattleResultProcessor::endBattle(const CBattleInfoCallback & battle) { - auto const & giveExp = [](BattleResult &r) + auto const & giveExp = [&battle](BattleResult &r) { - if (r.winner > 1) + if (r.winner == BattleSide::NONE) { // draw return; } - r.exp[0] = 0; - r.exp[1] = 0; - for (auto i = r.casualties[!r.winner].begin(); i!=r.casualties[!r.winner].end(); i++) + r.exp[BattleSide::ATTACKER] = 0; + r.exp[BattleSide::DEFENDER] = 0; + for (auto i = r.casualties[battle.otherSide(r.winner)].begin(); i!=r.casualties[battle.otherSide(r.winner)].end(); i++) { r.exp[r.winner] += VLC->creh->objects.at(i->first)->valOfBonuses(BonusType::STACK_HEALTH) * i->second; } @@ -239,9 +233,9 @@ void BattleResultProcessor::endBattle(const CBattleInfoCallback & battle) if (battleResult->result == EBattleResult::NORMAL) // give 500 exp for defeating hero, unless he escaped { if(heroAttacker) - battleResult->exp[1] += 500; + battleResult->exp[BattleSide::DEFENDER] += 500; if(heroDefender) - battleResult->exp[0] += 500; + battleResult->exp[BattleSide::ATTACKER] += 500; } // Give 500 exp to winner if a town was conquered during the battle @@ -250,17 +244,17 @@ void BattleResultProcessor::endBattle(const CBattleInfoCallback & battle) battleResult->exp[BattleSide::ATTACKER] += 500; if(heroAttacker) - battleResult->exp[0] = heroAttacker->calculateXp(battleResult->exp[0]);//scholar skill + battleResult->exp[BattleSide::ATTACKER] = heroAttacker->calculateXp(battleResult->exp[BattleSide::ATTACKER]);//scholar skill if(heroDefender) - battleResult->exp[1] = heroDefender->calculateXp(battleResult->exp[1]); + battleResult->exp[BattleSide::DEFENDER] = heroDefender->calculateXp(battleResult->exp[BattleSide::DEFENDER]); - auto battleQuery = std::dynamic_pointer_cast(gameHandler->queries->topQuery(battle.sideToPlayer(0))); + auto battleQuery = std::dynamic_pointer_cast(gameHandler->queries->topQuery(battle.sideToPlayer(BattleSide::ATTACKER))); if(!battleQuery) - battleQuery = std::dynamic_pointer_cast(gameHandler->queries->topQuery(battle.sideToPlayer(1))); + battleQuery = std::dynamic_pointer_cast(gameHandler->queries->topQuery(battle.sideToPlayer(BattleSide::DEFENDER))); if (!battleQuery) { logGlobal->error("Cannot find battle query!"); - gameHandler->complain("Player " + boost::lexical_cast(battle.sideToPlayer(0)) + " has no battle query at the top!"); + gameHandler->complain("Player " + boost::lexical_cast(battle.sideToPlayer(BattleSide::ATTACKER)) + " has no battle query at the top!"); return; } @@ -297,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); @@ -305,9 +299,9 @@ void BattleResultProcessor::endBattle(const CBattleInfoCallback & battle) void BattleResultProcessor::endBattleConfirm(const CBattleInfoCallback & battle) { - auto battleQuery = std::dynamic_pointer_cast(gameHandler->queries->topQuery(battle.sideToPlayer(0))); + auto battleQuery = std::dynamic_pointer_cast(gameHandler->queries->topQuery(battle.sideToPlayer(BattleSide::ATTACKER))); if(!battleQuery) - battleQuery = std::dynamic_pointer_cast(gameHandler->queries->topQuery(battle.sideToPlayer(1))); + battleQuery = std::dynamic_pointer_cast(gameHandler->queries->topQuery(battle.sideToPlayer(BattleSide::DEFENDER))); if(!battleQuery) { logGlobal->trace("No battle query, battle end was confirmed by another player"); @@ -323,176 +317,9 @@ void BattleResultProcessor::endBattleConfirm(const CBattleInfoCallback & battle) CasualtiesAfterBattle cab1(battle, BattleSide::ATTACKER); CasualtiesAfterBattle cab2(battle, BattleSide::DEFENDER); - ChangeSpells cs; //for Eagle Eye - - if(!finishingBattle->isDraw() && finishingBattle->winnerHero) - { - if (int eagleEyeLevel = finishingBattle->winnerHero->valOfBonuses(BonusType::LEARN_BATTLE_SPELL_LEVEL_LIMIT)) - { - double eagleEyeChance = finishingBattle->winnerHero->valOfBonuses(BonusType::LEARN_BATTLE_SPELL_CHANCE); - for(auto & spellId : battle.getBattle()->getUsedSpells(battle.otherSide(battleResult->winner))) - { - auto spell = spellId.toEntity(VLC->spells()); - if(spell && spell->getLevel() <= eagleEyeLevel && !finishingBattle->winnerHero->spellbookContainsSpell(spell->getId()) && gameHandler->getRandomGenerator().nextInt(99) < eagleEyeChance) - cs.spells.insert(spell->getId()); - } - } - } - std::vector arts; //display them in window - - if(result == EBattleResult::NORMAL && !finishingBattle->isDraw() && finishingBattle->winnerHero) - { - auto sendMoveArtifact = [&](const CArtifactInstance *art, MoveArtifact *ma) - { - const auto slot = ArtifactUtils::getArtAnyPosition(finishingBattle->winnerHero, art->getTypeId()); - if(slot != ArtifactPosition::PRE_FIRST) - { - arts.push_back(art); - ma->dst = ArtifactLocation(finishingBattle->winnerHero->id, slot); - if(ArtifactUtils::isSlotBackpack(slot)) - ma->askAssemble = false; - gameHandler->sendAndApply(ma); - } - }; - - if (finishingBattle->loserHero) - { - //TODO: wrap it into a function, somehow (std::variant -_-) - auto artifactsWorn = finishingBattle->loserHero->artifactsWorn; - for (auto artSlot : artifactsWorn) - { - MoveArtifact ma; - ma.src = ArtifactLocation(finishingBattle->loserHero->id, artSlot.first); - const CArtifactInstance * art = finishingBattle->loserHero->getArt(artSlot.first); - if (art && !art->artType->isBig() && - art->artType->getId() != ArtifactID::SPELLBOOK) - // don't move war machines or locked arts (spellbook) - { - sendMoveArtifact(art, &ma); - } - } - for(int slotNumber = finishingBattle->loserHero->artifactsInBackpack.size() - 1; slotNumber >= 0; slotNumber--) - { - //we assume that no big artifacts can be found - MoveArtifact ma; - ma.src = ArtifactLocation(finishingBattle->loserHero->id, - ArtifactPosition(ArtifactPosition::BACKPACK_START + slotNumber)); //backpack automatically shifts arts to beginning - const CArtifactInstance * art = finishingBattle->loserHero->getArt(ArtifactPosition::BACKPACK_START + slotNumber); - if (art->artType->getId() != ArtifactID::GRAIL) //grail may not be won - { - sendMoveArtifact(art, &ma); - } - } - if (finishingBattle->loserHero->commander) //TODO: what if commanders belong to no hero? - { - artifactsWorn = finishingBattle->loserHero->commander->artifactsWorn; - for (auto artSlot : artifactsWorn) - { - MoveArtifact ma; - ma.src = ArtifactLocation(finishingBattle->loserHero->id, artSlot.first); - ma.src.creature = finishingBattle->loserHero->findStack(finishingBattle->loserHero->commander); - const auto art = finishingBattle->loserHero->commander->getArt(artSlot.first); - if (art && !art->artType->isBig()) - { - sendMoveArtifact(art, &ma); - } - } - } - } - - auto loser = battle.otherSide(battleResult->winner); - - for (auto armySlot : battle.battleGetArmyObject(loser)->stacks) - { - auto artifactsWorn = armySlot.second->artifactsWorn; - for(const auto & artSlot : artifactsWorn) - { - MoveArtifact ma; - ma.src = ArtifactLocation(finishingBattle->loserHero->id, artSlot.first); - ma.src.creature = finishingBattle->loserHero->findStack(finishingBattle->loserHero->commander); - const auto art = finishingBattle->loserHero->commander->getArt(artSlot.first); - if (art && !art->artType->isBig()) - { - sendMoveArtifact(art, &ma); - } - } - } - } - - if (arts.size()) //display loot - { - InfoWindow iw; - iw.player = finishingBattle->winnerHero->tempOwner; - - iw.text.appendLocalString (EMetaText::GENERAL_TXT, 30); //You have captured enemy artifact - - for (auto art : arts) //TODO; separate function to display loot for various ojects? - { - if (art->artType->getId() == ArtifactID::SPELL_SCROLL) - iw.components.emplace_back(ComponentType::SPELL_SCROLL, art->getScrollSpellID()); - else - iw.components.emplace_back(ComponentType::ARTIFACT, art->artType->getId()); - - if (iw.components.size() >= 14) - { - gameHandler->sendAndApply(&iw); - iw.components.clear(); - } - } - if (iw.components.size()) - { - gameHandler->sendAndApply(&iw); - } - } - //Eagle Eye secondary skill handling - if (!cs.spells.empty()) - { - cs.learn = 1; - cs.hid = finishingBattle->winnerHero->id; - - InfoWindow iw; - iw.player = finishingBattle->winnerHero->tempOwner; - iw.text.appendLocalString(EMetaText::GENERAL_TXT, 221); //Through eagle-eyed observation, %s is able to learn %s - iw.text.replaceRawString(finishingBattle->winnerHero->getNameTranslated()); - - std::ostringstream names; - for (int i = 0; i < cs.spells.size(); i++) - { - names << "%s"; - if (i < cs.spells.size() - 2) - names << ", "; - else if (i < cs.spells.size() - 1) - names << "%s"; - } - names << "."; - - iw.text.replaceRawString(names.str()); - - auto it = cs.spells.begin(); - for (int i = 0; i < cs.spells.size(); i++, it++) - { - iw.text.replaceName(*it); - if (i == cs.spells.size() - 2) //we just added pre-last name - iw.text.replaceLocalString(EMetaText::GENERAL_TXT, 141); // " and " - iw.components.emplace_back(ComponentType::SPELL, *it); - } - gameHandler->sendAndApply(&iw); - gameHandler->sendAndApply(&cs); - } cab1.updateArmy(gameHandler); cab2.updateArmy(gameHandler); //take casualties after battle is deleted - if(finishingBattle->loserHero) //remove beaten hero - { - RemoveObject ro(finishingBattle->loserHero->id, finishingBattle->victor); - gameHandler->sendAndApply(&ro); - } - if(finishingBattle->isDraw() && finishingBattle->winnerHero) //for draw case both heroes should be removed - { - RemoveObject ro(finishingBattle->winnerHero->id, finishingBattle->loser); - gameHandler->sendAndApply(&ro); - } - if(battleResult->winner == BattleSide::DEFENDER && finishingBattle->winnerHero && finishingBattle->winnerHero->visitedTown @@ -505,16 +332,200 @@ void BattleResultProcessor::endBattleConfirm(const CBattleInfoCallback & battle) if(!finishingBattle->isDraw() && battleResult->exp[finishingBattle->winnerSide] && finishingBattle->winnerHero) gameHandler->giveExperience(finishingBattle->winnerHero, battleResult->exp[finishingBattle->winnerSide]); + // Eagle Eye handling + if(!finishingBattle->isDraw() && finishingBattle->winnerHero) + { + ChangeSpells spells; + + if(auto eagleEyeLevel = finishingBattle->winnerHero->valOfBonuses(BonusType::LEARN_BATTLE_SPELL_LEVEL_LIMIT)) + { + auto eagleEyeChance = finishingBattle->winnerHero->valOfBonuses(BonusType::LEARN_BATTLE_SPELL_CHANCE); + for(auto & spellId : battle.getBattle()->getUsedSpells(battle.otherSide(battleResult->winner))) + { + auto spell = spellId.toEntity(VLC->spells()); + if(spell + && spell->getLevel() <= eagleEyeLevel + && !finishingBattle->winnerHero->spellbookContainsSpell(spell->getId()) + && gameHandler->getRandomGenerator().nextInt(99) < eagleEyeChance) + { + spells.spells.insert(spell->getId()); + } + } + } + + if(!spells.spells.empty()) + { + spells.learn = 1; + spells.hid = finishingBattle->winnerHero->id; + + InfoWindow iw; + iw.player = finishingBattle->winnerHero->tempOwner; + iw.text.appendLocalString(EMetaText::GENERAL_TXT, 221); //Through eagle-eyed observation, %s is able to learn %s + iw.text.replaceRawString(finishingBattle->winnerHero->getNameTranslated()); + + std::ostringstream names; + for(int i = 0; i < spells.spells.size(); i++) + { + names << "%s"; + if(i < spells.spells.size() - 2) + names << ", "; + else if(i < spells.spells.size() - 1) + names << "%s"; + } + names << "."; + + iw.text.replaceRawString(names.str()); + + auto it = spells.spells.begin(); + for(int i = 0; i < spells.spells.size(); i++, it++) + { + iw.text.replaceName(*it); + if(i == spells.spells.size() - 2) //we just added pre-last name + iw.text.replaceLocalString(EMetaText::GENERAL_TXT, 141); // " and " + iw.components.emplace_back(ComponentType::SPELL, *it); + } + gameHandler->sendAndApply(iw); + gameHandler->sendAndApply(spells); + } + } + // Artifacts handling + if(result == EBattleResult::NORMAL && !finishingBattle->isDraw() && finishingBattle->winnerHero) + { + std::vector arts; // display them in window + CArtifactFittingSet artFittingSet(*finishingBattle->winnerHero); + + const auto addArtifactToTransfer = [&artFittingSet, &arts](BulkMoveArtifacts & pack, const ArtifactPosition & srcSlot, const CArtifactInstance * art) + { + assert(art); + const auto dstSlot = ArtifactUtils::getArtAnyPosition(&artFittingSet, art->getTypeId()); + if(dstSlot != ArtifactPosition::PRE_FIRST) + { + pack.artsPack0.emplace_back(BulkMoveArtifacts::LinkedSlots(srcSlot, dstSlot)); + if(ArtifactUtils::isSlotEquipment(dstSlot)) + pack.artsPack0.back().askAssemble = true; + arts.emplace_back(art); + artFittingSet.putArtifact(dstSlot, const_cast(art)); + } + }; + const auto sendArtifacts = [this](BulkMoveArtifacts & bma) + { + if(!bma.artsPack0.empty()) + gameHandler->sendAndApply(bma); + }; + + BulkMoveArtifacts packHero(finishingBattle->winnerHero->getOwner(), ObjectInstanceID::NONE, finishingBattle->winnerHero->id, false); + if(finishingBattle->loserHero) + { + packHero.srcArtHolder = finishingBattle->loserHero->id; + for(const auto & slot : ArtifactUtils::commonWornSlots()) + { + if(const auto artSlot = finishingBattle->loserHero->artifactsWorn.find(slot); + artSlot != finishingBattle->loserHero->artifactsWorn.end() && ArtifactUtils::isArtRemovable(*artSlot)) + { + addArtifactToTransfer(packHero, artSlot->first, artSlot->second.getArt()); + } + } + for(const auto & artSlot : finishingBattle->loserHero->artifactsInBackpack) + { + if(const auto art = artSlot.getArt(); art->getTypeId() != ArtifactID::GRAIL) + addArtifactToTransfer(packHero, finishingBattle->loserHero->getArtPos(art), art); + } + + if(finishingBattle->loserHero->commander) + { + BulkMoveArtifacts packCommander(finishingBattle->winnerHero->getOwner(), finishingBattle->loserHero->id, finishingBattle->winnerHero->id, false); + packCommander.srcCreature = finishingBattle->loserHero->findStack(finishingBattle->loserHero->commander); + for(const auto & artSlot : finishingBattle->loserHero->commander->artifactsWorn) + addArtifactToTransfer(packCommander, artSlot.first, artSlot.second.getArt()); + sendArtifacts(packCommander); + } + auto armyObj = battle.battleGetArmyObject(battle.otherSide(battleResult->winner)); + for(const auto & armySlot : armyObj->stacks) + { + BulkMoveArtifacts packsArmy(finishingBattle->winnerHero->getOwner(), finishingBattle->loserHero->id, finishingBattle->winnerHero->id, false); + packsArmy.srcArtHolder = armyObj->id; + packsArmy.srcCreature = armySlot.first; + for(const auto & artSlot : armySlot.second->artifactsWorn) + addArtifactToTransfer(packsArmy, artSlot.first, armySlot.second->getArt(artSlot.first)); + sendArtifacts(packsArmy); + } + } + // Display loot + if(!arts.empty()) + { + InfoWindow iw; + iw.player = finishingBattle->winnerHero->tempOwner; + iw.text.appendLocalString(EMetaText::GENERAL_TXT, 30); //You have captured enemy artifact + + for(const auto art : arts) //TODO; separate function to display loot for various objects? + { + if(art->isScroll()) + iw.components.emplace_back(ComponentType::SPELL_SCROLL, art->getScrollSpellID()); + else + iw.components.emplace_back(ComponentType::ARTIFACT, art->getTypeId()); + + if(iw.components.size() >= GameConstants::INFO_WINDOW_ARTIFACTS_MAX_ITEMS) + { + gameHandler->sendAndApply(iw); + iw.components.clear(); + } + } + gameHandler->sendAndApply(iw); + } + if(!packHero.artsPack0.empty()) + sendArtifacts(packHero); + } + + // Remove beaten hero + if(finishingBattle->loserHero) + { + //add statistics + if(!finishingBattle->isDraw()) + { + ConstTransitivePtr strongestHero = nullptr; + for(auto & hero : gameHandler->gameState()->getPlayerState(finishingBattle->loser)->getHeroes()) + if(!strongestHero || hero->exp > strongestHero->exp) + strongestHero = hero; + if(strongestHero->id == finishingBattle->loserHero->id && strongestHero->level > 5) + gameHandler->gameState()->statistic.accumulatedValues[finishingBattle->victor].lastDefeatedStrongestHeroDay = gameHandler->gameState()->getDate(Date::DAY); + } + + RemoveObject ro(finishingBattle->loserHero->id, finishingBattle->victor); + 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); + } + + // add statistic + if(battle.sideToPlayer(BattleSide::ATTACKER) == PlayerColor::NEUTRAL || battle.sideToPlayer(BattleSide::DEFENDER) == PlayerColor::NEUTRAL) + { + gameHandler->gameState()->statistic.accumulatedValues[battle.sideToPlayer(BattleSide::ATTACKER)].numBattlesNeutral++; + gameHandler->gameState()->statistic.accumulatedValues[battle.sideToPlayer(BattleSide::DEFENDER)].numBattlesNeutral++; + if(!finishingBattle->isDraw()) + gameHandler->gameState()->statistic.accumulatedValues[battle.sideToPlayer(finishingBattle->winnerSide)].numWinBattlesNeutral++; + } + else + { + gameHandler->gameState()->statistic.accumulatedValues[battle.sideToPlayer(BattleSide::ATTACKER)].numBattlesPlayer++; + gameHandler->gameState()->statistic.accumulatedValues[battle.sideToPlayer(BattleSide::DEFENDER)].numBattlesPlayer++; + if(!finishingBattle->isDraw()) + gameHandler->gameState()->statistic.accumulatedValues[battle.sideToPlayer(finishingBattle->winnerSide)].numWinBattlesPlayer++; + } + BattleResultAccepted raccepted; raccepted.battleID = battle.getBattle()->getBattleID(); - raccepted.heroResult[0].army = const_cast(battle.battleGetArmyObject(BattleSide::ATTACKER)); - raccepted.heroResult[1].army = const_cast(battle.battleGetArmyObject(BattleSide::DEFENDER)); - raccepted.heroResult[0].hero = const_cast(battle.battleGetFightingHero(BattleSide::ATTACKER)); - raccepted.heroResult[1].hero = const_cast(battle.battleGetFightingHero(BattleSide::DEFENDER)); - raccepted.heroResult[0].exp = battleResult->exp[0]; - raccepted.heroResult[1].exp = battleResult->exp[1]; - raccepted.winnerSide = finishingBattle->winnerSide; - gameHandler->sendAndApply(&raccepted); + raccepted.heroResult[BattleSide::ATTACKER].army = const_cast(battle.battleGetArmyObject(BattleSide::ATTACKER)); + raccepted.heroResult[BattleSide::DEFENDER].army = const_cast(battle.battleGetArmyObject(BattleSide::DEFENDER)); + raccepted.heroResult[BattleSide::ATTACKER].hero = const_cast(battle.battleGetFightingHero(BattleSide::ATTACKER)); + raccepted.heroResult[BattleSide::DEFENDER].hero = const_cast(battle.battleGetFightingHero(BattleSide::DEFENDER)); + 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->queries->popIfTop(battleQuery); //--> continuation (battleAfterLevelUp) occurs after level-up gameHandler->queries are handled or on removing query @@ -545,37 +556,43 @@ 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}; gameHandler->checkVictoryLossConditions(playerColors); if (result.result == EBattleResult::SURRENDER) + { + gameHandler->gameState()->statistic.accumulatedValues[finishingBattle->loser].numHeroSurrendered++; gameHandler->heroPool->onHeroSurrendered(finishingBattle->loser, finishingBattle->loserHero); + } if (result.result == EBattleResult::ESCAPE) + { + gameHandler->gameState()->statistic.accumulatedValues[finishingBattle->loser].numHeroEscaped++; gameHandler->heroPool->onHeroEscaped(finishingBattle->loser, finishingBattle->loserHero); + } - if (result.winner != 2 && finishingBattle->winnerHero && finishingBattle->winnerHero->stacks.empty() + if (result.winner != BattleSide::NONE && finishingBattle->winnerHero && finishingBattle->winnerHero->stacks.empty() && (!finishingBattle->winnerHero->commander || !finishingBattle->winnerHero->commander->alive)) { RemoveObject ro(finishingBattle->winnerHero->id, finishingBattle->winnerHero->getOwner()); - gameHandler->sendAndApply(&ro); + gameHandler->sendAndApply(ro); - if (VLC->settings()->getBoolean(EGameSettings::HEROES_RETREAT_ON_WIN_WITHOUT_TROOPS)) + if (gameHandler->getSettings().getBoolean(EGameSettings::HEROES_RETREAT_ON_WIN_WITHOUT_TROOPS)) gameHandler->heroPool->onHeroEscaped(finishingBattle->victor, finishingBattle->winnerHero); } @@ -583,7 +600,7 @@ void BattleResultProcessor::battleAfterLevelUp(const BattleID & battleID, const battleResults.erase(battleID); } -void BattleResultProcessor::setBattleResult(const CBattleInfoCallback & battle, EBattleResult resultType, int victoriusSide) +void BattleResultProcessor::setBattleResult(const CBattleInfoCallback & battle, EBattleResult resultType, BattleSide victoriusSide) { assert(battleResults.count(battle.getBattle()->getBattleID()) == 0); diff --git a/server/battles/BattleResultProcessor.h b/server/battles/BattleResultProcessor.h index f82f18fba..2e5e73bd1 100644 --- a/server/battles/BattleResultProcessor.h +++ b/server/battles/BattleResultProcessor.h @@ -12,6 +12,7 @@ #include "../../lib/GameConstants.h" #include "../../lib/networkPacks/StackLocation.h" #include "../../lib/networkPacks/ArtifactLocation.h" +#include "../../lib/battle/BattleSide.h" VCMI_LIB_NAMESPACE_BEGIN struct SideInBattle; @@ -34,7 +35,7 @@ struct CasualtiesAfterBattle TSummoned summoned; ObjectInstanceID heroWithDeadCommander; //TODO: unify stack locations - CasualtiesAfterBattle(const CBattleInfoCallback & battle, uint8_t sideInBattle); + CasualtiesAfterBattle(const CBattleInfoCallback & battle, BattleSide sideInBattle); void updateArmy(CGameHandler * gh); }; @@ -42,11 +43,11 @@ struct FinishingBattleHelper { FinishingBattleHelper(const CBattleInfoCallback & battle, const BattleResult & result, int RemainingBattleQueriesCount); - inline bool isDraw() const {return winnerSide == 2;} + inline bool isDraw() const {return winnerSide == BattleSide::NONE;} const CGHeroInstance *winnerHero, *loserHero; PlayerColor victor, loser; - ui8 winnerSide; + BattleSide winnerSide; int remainingBattleQueriesCount; @@ -74,7 +75,7 @@ public: bool battleIsEnding(const CBattleInfoCallback & battle) const; - void setBattleResult(const CBattleInfoCallback & battle, EBattleResult resultType, int victoriusSide); + void setBattleResult(const CBattleInfoCallback & battle, EBattleResult resultType, BattleSide victoriusSide); void endBattle(const CBattleInfoCallback & battle); //ends battle void endBattleConfirm(const CBattleInfoCallback & battle); void battleAfterLevelUp(const BattleID & battleID, const BattleResult & result); diff --git a/server/processors/HeroPoolProcessor.cpp b/server/processors/HeroPoolProcessor.cpp index 32bedb747..976c74b6f 100644 --- a/server/processors/HeroPoolProcessor.cpp +++ b/server/processors/HeroPoolProcessor.cpp @@ -13,17 +13,19 @@ #include "TurnOrderProcessor.h" #include "../CGameHandler.h" -#include "../../lib/CHeroHandler.h" +#include "../../lib/CRandomGenerator.h" #include "../../lib/CPlayerState.h" -#include "../../lib/GameSettings.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" #include "../../lib/gameState/CGameState.h" #include "../../lib/gameState/TavernHeroesPool.h" #include "../../lib/gameState/TavernSlot.h" -#include "../../lib/GameSettings.h" +#include "../../lib/IGameSettings.h" HeroPoolProcessor::HeroPoolProcessor(CGameHandler * gameHandler) : gameHandler(gameHandler) @@ -46,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; @@ -69,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) @@ -81,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) @@ -97,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) @@ -111,7 +113,7 @@ void HeroPoolProcessor::selectNewHeroForSlot(const PlayerColor & color, TavernHe if (newHero) { - sah.hid = newHero->getHeroType(); + sah.hid = newHero->getHeroTypeID(); if (giveArmy) { @@ -122,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 @@ -130,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) @@ -157,15 +159,15 @@ bool HeroPoolProcessor::hireHero(const ObjectInstanceID & objectID, const HeroTy if (playerState->resources[EGameResID::GOLD] < GameConstants::HERO_GOLD_COST && gameHandler->complain("Not enough gold for buying hero!")) return false; - if (gameHandler->getHeroCount(player, false) >= VLC->settings()->getInteger(EGameSettings::HEROES_PER_PLAYER_ON_MAP_CAP) && gameHandler->complain("Cannot hire hero, too many wandering heroes already!")) + if (gameHandler->getHeroCount(player, false) >= gameHandler->getSettings().getInteger(EGameSettings::HEROES_PER_PLAYER_ON_MAP_CAP) && gameHandler->complain("Cannot hire hero, too many wandering heroes already!")) return false; - if (gameHandler->getHeroCount(player, true) >= VLC->settings()->getInteger(EGameSettings::HEROES_PER_PLAYER_TOTAL_CAP) && gameHandler->complain("Cannot hire hero, too many heroes garrizoned and wandering already!")) + if (gameHandler->getHeroCount(player, true) >= gameHandler->getSettings().getInteger(EGameSettings::HEROES_PER_PLAYER_TOTAL_CAP) && gameHandler->complain("Cannot hire hero, too many heroes garrizoned and wandering already!")) return false; if (nextHero != HeroTypeID::NONE) // player attempts to invite next hero { - if(!VLC->settings()->getBoolean(EGameSettings::HEROES_TAVERN_INVITE) && gameHandler->complain("Inviting heroes not allowed!")) + if(!gameHandler->getSettings().getBoolean(EGameSettings::HEROES_TAVERN_INVITE) && gameHandler->complain("Inviting heroes not allowed!")) return false; if(!heroesPool->unusedHeroesFromPool().count(nextHero) && gameHandler->complain("Cannot invite specified hero!")) @@ -207,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; } @@ -220,19 +222,18 @@ 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) { //Create a new boat for hero - gameHandler->createObject(targetPos, player, Obj::BOAT, recruitedHero->getBoatType().getNum()); - + gameHandler->createBoat(targetPos, recruitedHero->getBoatType(), player); hr.boatId = gameHandler->getTopObj(targetPos)->id; } // 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); @@ -242,10 +243,7 @@ bool HeroPoolProcessor::hireHero(const ObjectInstanceID & objectID, const HeroTy gameHandler->giveResource(player, EGameResID::GOLD, -GameConstants::HERO_GOLD_COST); if(town) - { - gameHandler->visitCastleObjects(town, recruitedHero); - gameHandler->giveSpells(town, recruitedHero); - } + gameHandler->objectVisited(town, recruitedHero); // If new hero has scouting he might reveal more terrain than we saw before gameHandler->changeFogOfWar(recruitedHero->getSightCenter(), recruitedHero->getSightRadius(), player, ETileVisibility::REVEALED); @@ -261,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; @@ -285,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); @@ -321,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) @@ -368,7 +366,7 @@ CGHeroInstance * HeroPoolProcessor::pickHeroFor(bool isNative, const PlayerColor return *RandomGeneratorUtil::nextItem(possibleHeroes, getRandomGenerator(player)); } -CRandomGenerator & HeroPoolProcessor::getHeroSkillsRandomGenerator(const HeroTypeID & hero) +vstd::RNG & HeroPoolProcessor::getHeroSkillsRandomGenerator(const HeroTypeID & hero) { if (heroSeed.count(hero) == 0) { @@ -379,7 +377,7 @@ CRandomGenerator & HeroPoolProcessor::getHeroSkillsRandomGenerator(const HeroTyp return *heroSeed.at(hero); } -CRandomGenerator & HeroPoolProcessor::getRandomGenerator(const PlayerColor & player) +vstd::RNG & HeroPoolProcessor::getRandomGenerator(const PlayerColor & player) { if (playerSeed.count(player) == 0) { diff --git a/server/processors/HeroPoolProcessor.h b/server/processors/HeroPoolProcessor.h index 8d0d8552d..0a2159d58 100644 --- a/server/processors/HeroPoolProcessor.h +++ b/server/processors/HeroPoolProcessor.h @@ -19,8 +19,13 @@ class PlayerColor; class CGHeroInstance; class HeroTypeID; class ObjectInstanceID; -class CRandomGenerator; class CHeroClass; +class CRandomGenerator; + +namespace vstd +{ +class RNG; +} VCMI_LIB_NAMESPACE_END @@ -46,7 +51,7 @@ class HeroPoolProcessor : boost::noncopyable CGHeroInstance * pickHeroFor(bool isNative, const PlayerColor & player); - CRandomGenerator & getRandomGenerator(const PlayerColor & player); + vstd::RNG & getRandomGenerator(const PlayerColor & player); TavernHeroSlot selectSlotForRole(const PlayerColor & player, TavernSlotRole roleID); @@ -58,7 +63,7 @@ public: void onNewWeek(const PlayerColor & color); - CRandomGenerator & getHeroSkillsRandomGenerator(const HeroTypeID & hero); + vstd::RNG & getHeroSkillsRandomGenerator(const HeroTypeID & hero); /// Incoming net pack handling bool hireHero(const ObjectInstanceID & objectID, const HeroTypeID & hid, const PlayerColor & player, const HeroTypeID & nextHero); diff --git a/server/processors/NewTurnProcessor.cpp b/server/processors/NewTurnProcessor.cpp new file mode 100644 index 000000000..abd130793 --- /dev/null +++ b/server/processors/NewTurnProcessor.cpp @@ -0,0 +1,718 @@ +/* + * NewTurnProcessor.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 "NewTurnProcessor.h" + +#include "HeroPoolProcessor.h" + +#include "../CGameHandler.h" + +#include "../../lib/CPlayerState.h" +#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" +#include "../../lib/gameState/SThievesGuildInfo.h" +#include "../../lib/mapObjects/CGHeroInstance.h" +#include "../../lib/mapObjects/CGTownInstance.h" +#include "../../lib/mapObjects/IOwnableObject.h" +#include "../../lib/mapping/CMap.h" +#include "../../lib/networkPacks/PacksForClient.h" +#include "../../lib/networkPacks/StackLocation.h" +#include "../../lib/pathfinder/TurnInfo.h" +#include "../../lib/texts/CGeneralTextHandler.h" + +#include + +NewTurnProcessor::NewTurnProcessor(CGameHandler * gameHandler) + :gameHandler(gameHandler) +{ +} + +void NewTurnProcessor::handleTimeEvents(PlayerColor color) +{ + for (auto const & event : gameHandler->gameState()->map->events) + { + if (!event.occursToday(gameHandler->gameState()->day)) + continue; + + if (!event.affectsPlayer(color, gameHandler->getPlayerState(color)->isHuman())) + continue; + + InfoWindow iw; + iw.player = color; + iw.text = event.message; + + //give resources + if (!event.resources.empty()) + { + gameHandler->giveResources(color, event.resources); + for (GameResID i : GameResID::ALL_RESOURCES()) + if (event.resources[i]) + iw.components.emplace_back(ComponentType::RESOURCE, i, event.resources[i]); + } + + //remove objects specified by event + for(const ObjectInstanceID objectIdToRemove : event.deletedObjectsInstances) + { + auto objectInstance = gameHandler->getObj(objectIdToRemove, false); + if(objectInstance != nullptr) + gameHandler->removeObject(objectInstance, PlayerColor::NEUTRAL); + } + gameHandler->sendAndApply(iw); //show dialog + } +} + +void NewTurnProcessor::handleTownEvents(const CGTownInstance * town) +{ + for (auto const & event : town->events) + { + if (!event.occursToday(gameHandler->gameState()->day)) + continue; + + PlayerColor player = town->getOwner(); + if (!event.affectsPlayer(player, gameHandler->getPlayerState(player)->isHuman())) + continue; + + // dialog + InfoWindow iw; + iw.player = player; + iw.text = event.message; + + if (event.resources.nonZero()) + { + gameHandler->giveResources(player, event.resources); + + for (GameResID i : GameResID::ALL_RESOURCES()) + if (event.resources[i]) + iw.components.emplace_back(ComponentType::RESOURCE, i, event.resources[i]); + } + + for (auto & i : event.buildings) + { + // Only perform action if: + // 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->getTown()->buildings.count(i) && !town->hasBuilt(i)) + { + gameHandler->buildStructure(town->id, i, true); + iw.components.emplace_back(ComponentType::BUILDING, BuildingTypeUniqueID(town->getFactionID(), i)); + } + } + + if (!event.creatures.empty()) + { + SetAvailableCreatures sac; + sac.tid = town->id; + sac.creatures = town->creatures; + + for (si32 i=0;icreatures.size() && !town->creatures.at(i).second.empty() && event.creatures.at(i) > 0)//there is dwelling + { + sac.creatures[i].first += event.creatures.at(i); + 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 + } +} + +void NewTurnProcessor::onPlayerTurnStarted(PlayerColor which) +{ + const auto * playerState = gameHandler->gameState()->getPlayerState(which); + + handleTimeEvents(which); + for (const auto * t : playerState->getTowns()) + handleTownEvents(t); + + for (const auto * t : playerState->getTowns()) + { + //garrison hero first - consistent with original H3 Mana Vortex and Battle Scholar Academy levelup windows order + if (t->garrisonHero != nullptr) + gameHandler->objectVisited(t, t->garrisonHero); + + if (t->visitingHero != nullptr) + gameHandler->objectVisited(t, t->visitingHero); + } +} + +void NewTurnProcessor::onPlayerTurnEnded(PlayerColor which) +{ + const auto * playerState = gameHandler->gameState()->getPlayerState(which); + assert(playerState->status == EPlayerStatus::INGAME); + + if (playerState->getTowns().empty()) + { + DaysWithoutTown pack; + pack.player = which; + pack.daysWithoutCastle = playerState->daysWithoutCastle.value_or(0) + 1; + gameHandler->sendAndApply(pack); + } + else + { + if (playerState->daysWithoutCastle.has_value()) + { + DaysWithoutTown pack; + pack.player = which; + pack.daysWithoutCastle = std::nullopt; + gameHandler->sendAndApply(pack); + } + } + + // check for 7 days without castle + gameHandler->checkVictoryLossConditionsForPlayer(which); + + bool newWeek = gameHandler->getDate(Date::DAY_OF_WEEK) == 7; // end of 7th day + + if (newWeek) //new heroes in tavern + gameHandler->heroPool->onNewWeek(which); +} + +ResourceSet NewTurnProcessor::generatePlayerIncome(PlayerColor playerID, bool newWeek) +{ + const auto & playerSettings = gameHandler->getPlayerSettings(playerID); + const PlayerState & state = gameHandler->gameState()->players.at(playerID); + ResourceSet income; + + for (const auto & town : state.getTowns()) + { + if (newWeek && town->hasBuilt(BuildingSubID::TREASURY)) + { + //give 10% of starting gold + income[EGameResID::GOLD] += state.resources[EGameResID::GOLD] / 10; + } + + //give resources if there's a Mystic Pond + if (newWeek && town->hasBuilt(BuildingSubID::MYSTIC_POND)) + { + static constexpr std::array rareResources = { + GameResID::MERCURY, + GameResID::SULFUR, + GameResID::CRYSTAL, + GameResID::GEMS + }; + + auto resID = *RandomGeneratorUtil::nextItem(rareResources, gameHandler->getRandomGenerator()); + int resVal = gameHandler->getRandomGenerator().nextInt(1, 4); + + income[resID] += resVal; + + gameHandler->setObjPropertyValue(town->id, ObjProperty::BONUS_VALUE_FIRST, resID); + gameHandler->setObjPropertyValue(town->id, ObjProperty::BONUS_VALUE_SECOND, resVal); + } + } + + for (GameResID k = GameResID::WOOD; k < GameResID::COUNT; k++) + { + income += state.valOfBonuses(BonusType::RESOURCES_CONSTANT_BOOST, BonusSubtypeID(k)); + income += state.valOfBonuses(BonusType::RESOURCES_TOWN_MULTIPLYING_BOOST, BonusSubtypeID(k)) * state.getTowns().size(); + } + + if(newWeek) //weekly crystal generation if 1 or more crystal dragons in any hero army or town garrison + { + bool hasCrystalGenCreature = false; + for (const auto & hero : state.getHeroes()) + for(auto stack : hero->stacks) + if(stack.second->hasBonusOfType(BonusType::SPECIAL_CRYSTAL_GENERATION)) + hasCrystalGenCreature = true; + + for(const auto & town : state.getTowns()) + for(auto stack : town->stacks) + if(stack.second->hasBonusOfType(BonusType::SPECIAL_CRYSTAL_GENERATION)) + hasCrystalGenCreature = true; + + if(hasCrystalGenCreature) + income[EGameResID::CRYSTAL] += 3; + } + + TResources incomeHandicapped = income; + incomeHandicapped.applyHandicap(playerSettings->handicap.percentIncome); + + 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; +} + +SetAvailableCreatures NewTurnProcessor::generateTownGrowth(const CGTownInstance * t, EWeekType weekType, CreatureID creatureWeek, bool firstDay) +{ + SetAvailableCreatures sac; + PlayerColor player = t->tempOwner; + + sac.tid = t->id; + sac.creatures = t->creatures; + + for (int k=0; k < t->getTown()->creatures.size(); k++) + { + if (t->creatures.at(k).second.empty()) + continue; + + uint32_t creaturesBefore = t->creatures.at(k).first; + uint32_t creatureGrowth = 0; + const CCreature *cre = t->creatures.at(k).second.back().toCreature(); + + if (firstDay) + { + creatureGrowth = cre->getGrowth(); + } + else + { + creatureGrowth = t->creatureGrowth(k); + + //Deity of fire week - upgrade both imps and upgrades + if (weekType == EWeekType::DEITYOFFIRE && vstd::contains(t->creatures.at(k).second, creatureWeek)) + creatureGrowth += 15; + + //bonus week, effect applies only to identical creatures + if (weekType == EWeekType::BONUS_GROWTH && cre->getId() == creatureWeek) + creatureGrowth += 5; + } + + // Neutral towns have halved creature growth + if (!player.isValidPlayer()) + creatureGrowth /= 2; + + uint32_t resultingCreatures = 0; + + if (weekType == EWeekType::PLAGUE) + resultingCreatures = creaturesBefore / 2; + else if (weekType == EWeekType::DOUBLE_GROWTH && vstd::contains(t->creatures.at(k).second, creatureWeek)) + resultingCreatures = (creaturesBefore + creatureGrowth) * 2; + else + resultingCreatures = creaturesBefore + creatureGrowth; + + sac.creatures.at(k).first = resultingCreatures; + } + + return sac; +} + +void NewTurnProcessor::updateNeutralTownGarrison(const CGTownInstance * t, int currentWeek) const +{ + assert(t); + assert(!t->getOwner().isValidPlayer()); + + constexpr int randomRollsCounts = 3; // H3 makes around 3 random rolls to make simple bell curve distribution + constexpr int upgradeChance = 5; // Chance for a unit to get an upgrade + constexpr int growthChanceFort = 80; // Chance for growth to occur in towns with fort built + constexpr int growthChanceVillage = 40; // Chance for growth to occur in towns without fort + + const auto & takeFromAvailable = [this, t](CreatureID creatureID) + { + int tierToSubstract = -1; + for (int i = 0; i < t->getTown()->creatures.size(); ++i) + if (vstd::contains(t->getTown()->creatures[i], creatureID)) + tierToSubstract = i; + + if (tierToSubstract == -1) + return; // impossible? + + int creaturesAvailable = t->creatures[tierToSubstract].first; + int creaturesRecruited = creatureID.toCreature()->getGrowth(); + int creaturesLeft = std::max(0, creaturesAvailable - creaturesRecruited); + + if (creaturesLeft != creaturesAvailable) + { + SetAvailableCreatures sac; + sac.tid = t->id; + sac.creatures = t->creatures; + sac.creatures[tierToSubstract].first = creaturesLeft; + gameHandler->sendAndApply(sac); + } + }; + + int growthChance = t->hasFort() ? growthChanceFort : growthChanceVillage; + int growthRoll = gameHandler->getRandomGenerator().nextInt(0, 99); + + if (growthRoll >= growthChance) + return; + + int tierRoll = 0; + for(int i = 0; i < randomRollsCounts; ++i) + tierRoll += gameHandler->getRandomGenerator().nextInt(0, currentWeek); + + // NOTE: determined by observing H3 games, might not match H3 100% + int tierToGrow = std::clamp(tierRoll / randomRollsCounts, 0, 6) + 1; + + bool upgradeUnit = gameHandler->getRandomGenerator().nextInt(0, 99) < upgradeChance; + + // Check if town garrison already has unit of specified tier + for(const auto & slot : t->Slots()) + { + const auto * creature = slot.second->getCreature(); + + if (creature->getFactionID() != t->getFactionID()) + continue; + + if (creature->getLevel() != tierToGrow) + continue; + + StackLocation stackLocation(t, slot.first); + gameHandler->changeStackCount(stackLocation, creature->getGrowth(), false); + takeFromAvailable(creature->getGrowth()); + + if (upgradeUnit && !creature->upgrades.empty()) + { + CreatureID upgraded = *RandomGeneratorUtil::nextItem(creature->upgrades, gameHandler->getRandomGenerator()); + gameHandler->changeStackType(stackLocation, upgraded.toCreature()); + } + else + gameHandler->changeStackType(stackLocation, creature); + return; + } + + // No existing creatures in garrison, but we have a free slot we can use + SlotID freeSlotID = t->getFreeSlot(); + if (freeSlotID.validSlot()) + { + for (auto const & tierVector : t->getTown()->creatures) + { + CreatureID baseCreature = tierVector.at(0); + + if (baseCreature.toEntity(VLC)->getLevel() != tierToGrow) + continue; + + StackLocation stackLocation(t, freeSlotID); + + if (upgradeUnit && !baseCreature.toCreature()->upgrades.empty()) + { + CreatureID upgraded = *RandomGeneratorUtil::nextItem(baseCreature.toCreature()->upgrades, gameHandler->getRandomGenerator()); + gameHandler->insertNewStack(stackLocation, upgraded.toCreature(), upgraded.toCreature()->getGrowth()); + takeFromAvailable(upgraded.toCreature()->getGrowth()); + } + else + { + gameHandler->insertNewStack(stackLocation, baseCreature.toCreature(), baseCreature.toCreature()->getGrowth()); + takeFromAvailable(baseCreature.toCreature()->getGrowth()); + } + + return; + } + } +} + +RumorState NewTurnProcessor::pickNewRumor() +{ + RumorState newRumor; + + static const std::vector rumorTypes = {RumorState::TYPE_MAP, RumorState::TYPE_SPECIAL, RumorState::TYPE_RAND, RumorState::TYPE_RAND}; + std::vector sRumorTypes = { + RumorState::RUMOR_OBELISKS, RumorState::RUMOR_ARTIFACTS, RumorState::RUMOR_ARMY, RumorState::RUMOR_INCOME}; + if(gameHandler->gameState()->map->grailPos.valid()) // Grail should always be on map, but I had related crash I didn't manage to reproduce + sRumorTypes.push_back(RumorState::RUMOR_GRAIL); + + int rumorId = -1; + int rumorExtra = -1; + auto & rand = gameHandler->getRandomGenerator(); + newRumor.type = *RandomGeneratorUtil::nextItem(rumorTypes, rand); + + do + { + switch(newRumor.type) + { + case RumorState::TYPE_SPECIAL: + { + SThievesGuildInfo tgi; + gameHandler->gameState()->obtainPlayersStats(tgi, 20); + rumorId = *RandomGeneratorUtil::nextItem(sRumorTypes, rand); + if(rumorId == RumorState::RUMOR_GRAIL) + { + rumorExtra = gameHandler->gameState()->getTile(gameHandler->gameState()->map->grailPos)->getTerrainID().getNum(); + break; + } + + std::vector players = {}; + switch(rumorId) + { + case RumorState::RUMOR_OBELISKS: + players = tgi.obelisks[0]; + break; + + case RumorState::RUMOR_ARTIFACTS: + players = tgi.artifacts[0]; + break; + + case RumorState::RUMOR_ARMY: + players = tgi.army[0]; + break; + + case RumorState::RUMOR_INCOME: + players = tgi.income[0]; + break; + } + rumorExtra = RandomGeneratorUtil::nextItem(players, rand)->getNum(); + + break; + } + case RumorState::TYPE_MAP: + // Makes sure that map rumors only used if there enough rumors too choose from + if(!gameHandler->gameState()->map->rumors.empty() && (gameHandler->gameState()->map->rumors.size() > 1 || !gameHandler->gameState()->currentRumor.last.count(RumorState::TYPE_MAP))) + { + rumorId = rand.nextInt(gameHandler->gameState()->map->rumors.size() - 1); + break; + } + else + newRumor.type = RumorState::TYPE_RAND; + [[fallthrough]]; + + case RumorState::TYPE_RAND: + auto vector = VLC->generaltexth->findStringsWithPrefix("core.randtvrn"); + rumorId = rand.nextInt((int)vector.size() - 1); + + break; + } + } + while(!newRumor.update(rumorId, rumorExtra)); + + return newRumor; +} + +std::tuple NewTurnProcessor::pickWeekType(bool newMonth) +{ + for (const CGTownInstance *t : gameHandler->gameState()->map->towns) + { + if (t->hasBuilt(BuildingID::GRAIL, ETownType::INFERNO)) + return { EWeekType::DEITYOFFIRE, CreatureID::IMP }; + } + + if(!gameHandler->getSettings().getBoolean(EGameSettings::CREATURES_ALLOW_RANDOM_SPECIAL_WEEKS)) + return { EWeekType::NORMAL, CreatureID::NONE}; + + int monthType = gameHandler->getRandomGenerator().nextInt(99); + if (newMonth) //new month + { + if (monthType < 40) //double growth + { + if (gameHandler->getSettings().getBoolean(EGameSettings::CREATURES_ALLOW_ALL_FOR_DOUBLE_MONTH)) + { + CreatureID creatureID = VLC->creh->pickRandomMonster(gameHandler->getRandomGenerator()); + return { EWeekType::DOUBLE_GROWTH, creatureID}; + } + else if (VLC->creh->doubledCreatures.size()) + { + CreatureID creatureID = *RandomGeneratorUtil::nextItem(VLC->creh->doubledCreatures, gameHandler->getRandomGenerator()); + return { EWeekType::DOUBLE_GROWTH, creatureID}; + } + else + { + gameHandler->complain("Cannot find creature that can be spawned!"); + return { EWeekType::NORMAL, CreatureID::NONE}; + } + } + + if (monthType < 50) + return { EWeekType::PLAGUE, CreatureID::NONE}; + + return { EWeekType::NORMAL, CreatureID::NONE}; + } + else //it's a week, but not full month + { + if (monthType < 25) + { + std::pair newMonster(54, CreatureID()); + do + { + newMonster.second = VLC->creh->pickRandomMonster(gameHandler->getRandomGenerator()); + } while (VLC->creh->objects[newMonster.second] && + (*VLC->townh)[VLC->creatures()->getById(newMonster.second)->getFactionID()]->town == nullptr); // find first non neutral creature + + return { EWeekType::BONUS_GROWTH, newMonster.second}; + } + return { EWeekType::NORMAL, CreatureID::NONE}; + } +} + +std::vector NewTurnProcessor::updateHeroesManaPoints() +{ + std::vector result; + + for (auto & elem : gameHandler->gameState()->players) + { + for (CGHeroInstance *h : elem.second.getHeroes()) + { + int32_t newMana = h->getManaNewTurn(); + + if (newMana != h->mana) + result.emplace_back(h->id, newMana, true); + } + } + return result; +} + +std::vector NewTurnProcessor::updateHeroesMovementPoints() +{ + std::vector result; + + for (auto & elem : gameHandler->gameState()->players) + { + for (CGHeroInstance *h : elem.second.getHeroes()) + { + 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()).isLand(), ti.get()); + + if (newMovementPoints != h->movementPointsRemaining()) + result.emplace_back(h->id, newMovementPoints, true); + } + } + return result; +} + +InfoWindow NewTurnProcessor::createInfoWindow(EWeekType weekType, CreatureID creatureWeek, bool newMonth) +{ + InfoWindow iw; + switch (weekType) + { + case EWeekType::DOUBLE_GROWTH: + iw.text.appendLocalString(EMetaText::ARRAY_TXT, 131); + iw.text.replaceNameSingular(creatureWeek); + iw.text.replaceNameSingular(creatureWeek); + break; + case EWeekType::PLAGUE: + iw.text.appendLocalString(EMetaText::ARRAY_TXT, 132); + break; + case EWeekType::BONUS_GROWTH: + iw.text.appendLocalString(EMetaText::ARRAY_TXT, 134); + iw.text.replaceNameSingular(creatureWeek); + iw.text.replaceNameSingular(creatureWeek); + break; + case EWeekType::DEITYOFFIRE: + iw.text.appendLocalString(EMetaText::ARRAY_TXT, 135); + iw.text.replaceNameSingular(CreatureID::IMP); //%s imp + iw.text.replaceNameSingular(CreatureID::IMP); //%s imp + iw.text.replacePositiveNumber(15);//%+d 15 + iw.text.replaceNameSingular(CreatureID::FAMILIAR); //%s familiar + iw.text.replacePositiveNumber(15);//%+d 15 + break; + default: + if (newMonth) + { + iw.text.appendLocalString(EMetaText::ARRAY_TXT, (130)); + iw.text.replaceLocalString(EMetaText::ARRAY_TXT, gameHandler->getRandomGenerator().nextInt(32, 41)); + } + else + { + iw.text.appendLocalString(EMetaText::ARRAY_TXT, (133)); + iw.text.replaceLocalString(EMetaText::ARRAY_TXT, gameHandler->getRandomGenerator().nextInt(43, 57)); + } + } + return iw; +} + +NewTurn NewTurnProcessor::generateNewTurnPack() +{ + NewTurn n; + n.specialWeek = EWeekType::FIRST_WEEK; + n.creatureid = CreatureID::NONE; + n.day = gameHandler->gameState()->day + 1; + + bool firstTurn = !gameHandler->getDate(Date::DAY); + 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; + + if (!firstTurn) + { + for (const auto & player : gameHandler->gameState()->players) + n.playerIncome[player.first] = generatePlayerIncome(player.first, newWeek); + } + + if (newWeek && !firstTurn) + { + auto [specialWeek, creatureID] = pickWeekType(newMonth); + n.specialWeek = specialWeek; + n.creatureid = creatureID; + } + + n.heroesMana = updateHeroesManaPoints(); + n.heroesMovement = updateHeroesMovementPoints(); + + if (newWeek) + { + for (CGTownInstance *t : gameHandler->gameState()->map->towns) + n.availableCreatures.push_back(generateTownGrowth(t, n.specialWeek, n.creatureid, firstTurn)); + } + + if (newWeek) + n.newRumor = pickNewRumor(); + + if (newWeek) + { + //new week info popup + if (n.specialWeek != EWeekType::FIRST_WEEK) + n.newWeekNotification = createInfoWindow(n.specialWeek, n.creatureid, newMonth); + } + + return n; +} + +void NewTurnProcessor::onNewTurn() +{ + NewTurn n = generateNewTurnPack(); + + bool firstTurn = !gameHandler->getDate(Date::DAY); + 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); + + if (newWeek) + { + for (CGTownInstance *t : gameHandler->gameState()->map->towns) + if (t->hasBuilt(BuildingSubID::PORTAL_OF_SUMMONING)) + gameHandler->setPortalDwelling(t, true, (n.specialWeek == EWeekType::PLAGUE ? true : false)); //set creatures for Portal of Summoning + } + + if (newWeek && !firstTurn) + { + for (CGTownInstance *t : gameHandler->gameState()->map->towns) + { + if (!t->getOwner().isValidPlayer()) + updateNeutralTownGarrison(t, 1 + gameHandler->getDate(Date::DAY) / 7); + } + } + + //spawn wandering monsters + if (newMonth && (n.specialWeek == EWeekType::DOUBLE_GROWTH || n.specialWeek == EWeekType::DEITYOFFIRE)) + { + gameHandler->spawnWanderingMonsters(n.creatureid); + } + + logGlobal->trace("Info about turn %d has been sent!", n.day); +} diff --git a/server/processors/NewTurnProcessor.h b/server/processors/NewTurnProcessor.h new file mode 100644 index 000000000..c203f5d35 --- /dev/null +++ b/server/processors/NewTurnProcessor.h @@ -0,0 +1,53 @@ +/* + * NewTurnProcessor.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 "../../lib/constants/EntityIdentifiers.h" +#include "../../lib/constants/Enumerations.h" +#include "../../lib/gameState/RumorState.h" + +VCMI_LIB_NAMESPACE_BEGIN +class CGTownInstance; +class ResourceSet; +struct SetAvailableCreatures; +struct SetMovePoints; +struct SetMana; +struct InfoWindow; +struct NewTurn; +VCMI_LIB_NAMESPACE_END + +class CGameHandler; + +class NewTurnProcessor : boost::noncopyable +{ + CGameHandler * gameHandler; + + std::vector updateHeroesManaPoints(); + std::vector updateHeroesMovementPoints(); + + ResourceSet generatePlayerIncome(PlayerColor playerID, bool newWeek); + SetAvailableCreatures generateTownGrowth(const CGTownInstance * town, EWeekType weekType, CreatureID creatureWeek, bool firstDay); + RumorState pickNewRumor(); + InfoWindow createInfoWindow(EWeekType weekType, CreatureID creatureWeek, bool newMonth); + std::tuple pickWeekType(bool newMonth); + + NewTurn generateNewTurnPack(); + void handleTimeEvents(PlayerColor player); + void handleTownEvents(const CGTownInstance *town); + + void updateNeutralTownGarrison(const CGTownInstance * t, int currentWeek) const; + +public: + NewTurnProcessor(CGameHandler * gameHandler); + + void onNewTurn(); + void onPlayerTurnStarted(PlayerColor color); + void onPlayerTurnEnded(PlayerColor color); +}; diff --git a/server/processors/PlayerMessageProcessor.cpp b/server/processors/PlayerMessageProcessor.cpp index 67a9b1d7c..89b034128 100644 --- a/server/processors/PlayerMessageProcessor.cpp +++ b/server/processors/PlayerMessageProcessor.cpp @@ -16,9 +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" @@ -28,6 +29,8 @@ #include "../../lib/networkPacks/PacksForClient.h" #include "../../lib/networkPacks/StackLocation.h" #include "../../lib/serializer/Connection.h" +#include "../../lib/spells/CSpellHandler.h" +#include "../lib/VCMIDirs.h" PlayerMessageProcessor::PlayerMessageProcessor(CGameHandler * gameHandler) :gameHandler(gameHandler) @@ -67,7 +70,7 @@ void PlayerMessageProcessor::commandExit(PlayerColor player, const std::vectorgameLobby()->setState(EServerState::SHUTDOWN); } @@ -97,7 +100,7 @@ void PlayerMessageProcessor::commandKick(PlayerColor player, const std::vectorsendAndApply(&pc); + gameHandler->sendAndApply(pc); gameHandler->checkVictoryLossConditionsForPlayer(playerToKick); } } @@ -112,7 +115,11 @@ void PlayerMessageProcessor::commandSave(PlayerColor player, const std::vectorsave("Saves/" + words[1]); - broadcastSystemMessage("game saved as " + words[1]); + MetaString str; + str.appendTextID("vcmi.broadcast.gameSavedAs"); + str.appendRawString(" "); + str.appendRawString(words[1]); + broadcastSystemMessage(str); } } @@ -123,54 +130,70 @@ void PlayerMessageProcessor::commandCheaters(PlayerColor player, const std::vect { if(player.second.cheated) { - broadcastSystemMessage("Player " + player.first.toString() + " is cheater!"); + auto str = MetaString::createFromTextID("vcmi.broadcast.playerCheater"); + str.replaceName(player.first); + broadcastSystemMessage(str); playersCheated++; } } if(!playersCheated) - broadcastSystemMessage("No cheaters registered!"); + broadcastSystemMessage(MetaString::createFromTextID("vcmi.broadcast.noCheater")); +} + +void PlayerMessageProcessor::commandStatistic(PlayerColor player, const std::vector & words) +{ + bool isHost = gameHandler->gameLobby()->isPlayerHost(player); + if(!isHost) + return; + + std::string path = gameHandler->gameState()->statistic.writeCsv(); + + auto str = MetaString::createFromTextID("vcmi.broadcast.statisticFile"); + str.replaceRawString(path); + broadcastSystemMessage(str); } void PlayerMessageProcessor::commandHelp(PlayerColor player, const std::vector & words) { - broadcastSystemMessage("Available commands to host:"); - broadcastSystemMessage("'!exit' - immediately ends current game"); - broadcastSystemMessage("'!kick ' - kick specified player from the game"); - broadcastSystemMessage("'!save ' - save game under specified filename"); - broadcastSystemMessage("Available commands to all players:"); - broadcastSystemMessage("'!help' - display this help"); - broadcastSystemMessage("'!cheaters' - list players that entered cheat command during game"); - broadcastSystemMessage("'!vote' - allows to change some game settings if all players vote for it"); + broadcastSystemMessage(MetaString::createFromTextID("vcmi.broadcast.help.commands")); + broadcastSystemMessage(MetaString::createFromTextID("vcmi.broadcast.help.exit")); + broadcastSystemMessage(MetaString::createFromTextID("vcmi.broadcast.help.kick")); + broadcastSystemMessage(MetaString::createFromTextID("vcmi.broadcast.help.save")); + broadcastSystemMessage(MetaString::createFromTextID("vcmi.broadcast.help.statistic")); + broadcastSystemMessage(MetaString::createFromTextID("vcmi.broadcast.help.commandsAll")); + broadcastSystemMessage(MetaString::createFromTextID("vcmi.broadcast.help.help")); + broadcastSystemMessage(MetaString::createFromTextID("vcmi.broadcast.help.cheaters")); + broadcastSystemMessage(MetaString::createFromTextID("vcmi.broadcast.help.vote")); } void PlayerMessageProcessor::commandVote(PlayerColor player, const std::vector & words) { if(words.size() < 2) { - broadcastSystemMessage("'!vote simturns allow X' - allow simultaneous turns for specified number of days, or until contact"); - broadcastSystemMessage("'!vote simturns force X' - force simultaneous turns for specified number of days, blocking player contacts"); - broadcastSystemMessage("'!vote simturns abort' - abort simultaneous turns once this turn ends"); - broadcastSystemMessage("'!vote timer prolong X' - prolong base timer for all players by specified number of seconds"); + broadcastSystemMessage(MetaString::createFromTextID("vcmi.broadcast.vote.allow")); + broadcastSystemMessage(MetaString::createFromTextID("vcmi.broadcast.vote.force")); + broadcastSystemMessage(MetaString::createFromTextID("vcmi.broadcast.vote.abort")); + broadcastSystemMessage(MetaString::createFromTextID("vcmi.broadcast.vote.timer")); return; } - if(words[1] == "yes" || words[1] == "no") + if(words[1] == "yes" || words[1] == "no" || words[1] == MetaString::createFromTextID("vcmi.broadcast.vote.yes").toString() || words[1] == MetaString::createFromTextID("vcmi.broadcast.vote.no").toString()) { if(currentVote == ECurrentChatVote::NONE) { - broadcastSystemMessage("No active voting!"); + broadcastSystemMessage(MetaString::createFromTextID("vcmi.broadcast.vote.noActive")); return; } - if(words[1] == "yes") + if(words[1] == "yes" || words[1] == MetaString::createFromTextID("vcmi.broadcast.vote.yes").toString()) { awaitingPlayers.erase(player); if(awaitingPlayers.empty()) finishVoting(); return; } - if(words[1] == "no") + if(words[1] == "no" || words[1] == MetaString::createFromTextID("vcmi.broadcast.vote.no").toString()) { abortVoting(); return; @@ -225,28 +248,36 @@ void PlayerMessageProcessor::commandVote(PlayerColor player, const std::vectorturnOrder->setMaxSimturnsDuration(currentVoteParameter); break; case ECurrentChatVote::SIMTURNS_FORCE: - broadcastSystemMessage("Voting successful. Simultaneous turns will run for " + std::to_string(currentVoteParameter) + " more days. Contacts are blocked"); + msg.appendTextID("vcmi.broadcast.vote.success.contactsBlocked"); + msg.replaceRawString(std::to_string(currentVoteParameter)); + broadcastSystemMessage(msg); gameHandler->turnOrder->setMinSimturnsDuration(currentVoteParameter); break; case ECurrentChatVote::SIMTURNS_ABORT: - broadcastSystemMessage("Voting successful. Simultaneous turns will end on next day"); + msg.appendTextID("vcmi.broadcast.vote.success.nextDay"); + broadcastSystemMessage(msg); gameHandler->turnOrder->setMinSimturnsDuration(0); gameHandler->turnOrder->setMaxSimturnsDuration(0); break; case ECurrentChatVote::TIMER_PROLONG: - broadcastSystemMessage("Voting successful. Timer for all players has been prolonger for " + std::to_string(currentVoteParameter) + " seconds"); + msg.appendTextID("vcmi.broadcast.vote.success.timer"); + msg.replaceRawString(std::to_string(currentVoteParameter)); + broadcastSystemMessage(msg); gameHandler->turnTimerHandler->prolongTimers(currentVoteParameter * 1000); break; } @@ -257,7 +288,7 @@ void PlayerMessageProcessor::finishVoting() void PlayerMessageProcessor::abortVoting() { - broadcastSystemMessage("Player voted against change. Voting aborted"); + broadcastSystemMessage(MetaString::createFromTextID("vcmi.broadcast.vote.aborted")); currentVote = ECurrentChatVote::NONE; } @@ -266,25 +297,33 @@ void PlayerMessageProcessor::startVoting(PlayerColor initiator, ECurrentChatVote currentVote = what; currentVoteParameter = parameter; + MetaString msg; switch(currentVote) { case ECurrentChatVote::SIMTURNS_ALLOW: - broadcastSystemMessage("Started voting to allow simultaneous turns for " + std::to_string(parameter) + " more days"); + msg.appendTextID("vcmi.broadcast.vote.start.untilContacts"); + msg.replaceRawString(std::to_string(parameter)); + broadcastSystemMessage(msg); break; case ECurrentChatVote::SIMTURNS_FORCE: - broadcastSystemMessage("Started voting to force simultaneous turns for " + std::to_string(parameter) + " more days"); + msg.appendTextID("vcmi.broadcast.vote.start.contactsBlocked"); + msg.replaceRawString(std::to_string(parameter)); + broadcastSystemMessage(msg); break; case ECurrentChatVote::SIMTURNS_ABORT: - broadcastSystemMessage("Started voting to end simultaneous turns starting from next day"); + msg.appendTextID("vcmi.broadcast.vote.start.nextDay"); + broadcastSystemMessage(msg); break; case ECurrentChatVote::TIMER_PROLONG: - broadcastSystemMessage("Started voting to prolong timer for all players by " + std::to_string(parameter) + " seconds"); + msg.appendTextID("vcmi.broadcast.vote.start.timer"); + msg.replaceRawString(std::to_string(parameter)); + broadcastSystemMessage(msg); break; default: return; } - broadcastSystemMessage("Type '!vote yes' to agree to this change or '!vote no' to vote against it"); + broadcastSystemMessage(MetaString::createFromTextID("vcmi.broadcast.vote.hint")); awaitingPlayers.clear(); for(PlayerColor player(0); player < PlayerColor::PLAYER_LIMIT; ++player) @@ -318,6 +357,8 @@ void PlayerMessageProcessor::handleCommand(PlayerColor player, const std::string commandSave(player, words); if(words[0] == "!cheaters") commandCheaters(player, words); + if(words[0] == "!statistic") + commandStatistic(player, words); } void PlayerMessageProcessor::cheatGiveSpells(PlayerColor player, const CGHeroInstance * hero) @@ -327,7 +368,7 @@ void PlayerMessageProcessor::cheatGiveSpells(PlayerColor player, const CGHeroIns ///Give hero spellbook if (!hero->hasSpellbook()) - gameHandler->giveHeroNewArtifact(hero, ArtifactID(ArtifactID::SPELLBOOK).toArtifact(), ArtifactPosition::SPELLBOOK); + gameHandler->giveHeroNewArtifact(hero, ArtifactID::SPELLBOOK, ArtifactPosition::SPELLBOOK); ///Give all spells with bonus (to allow banned spells) GiveBonus giveBonus(GiveBonus::ETarget::OBJECT); @@ -337,7 +378,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 @@ -345,7 +386,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) @@ -353,7 +394,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() @@ -405,11 +446,11 @@ void PlayerMessageProcessor::cheatGiveMachines(PlayerColor player, const CGHeroI return; if (!hero->getArt(ArtifactPosition::MACH1)) - gameHandler->giveHeroNewArtifact(hero, ArtifactID(ArtifactID::BALLISTA).toArtifact(), ArtifactPosition::MACH1); + gameHandler->giveHeroNewArtifact(hero, ArtifactID::BALLISTA, ArtifactPosition::MACH1); if (!hero->getArt(ArtifactPosition::MACH2)) - gameHandler->giveHeroNewArtifact(hero, ArtifactID(ArtifactID::AMMO_CART).toArtifact(), ArtifactPosition::MACH2); + gameHandler->giveHeroNewArtifact(hero, ArtifactID::AMMO_CART, ArtifactPosition::MACH2); if (!hero->getArt(ArtifactPosition::MACH3)) - gameHandler->giveHeroNewArtifact(hero, ArtifactID(ArtifactID::FIRST_AID_TENT).toArtifact(), ArtifactPosition::MACH3); + gameHandler->giveHeroNewArtifact(hero, ArtifactID::FIRST_AID_TENT, ArtifactPosition::MACH3); } void PlayerMessageProcessor::cheatGiveArtifacts(PlayerColor player, const CGHeroInstance * hero, std::vector words) @@ -423,7 +464,7 @@ void PlayerMessageProcessor::cheatGiveArtifacts(PlayerColor player, const CGHero { auto artID = VLC->identifiers()->getIdentifier(ModScope::scopeGame(), "artifact", word, false); if(artID && VLC->arth->objects[*artID]) - gameHandler->giveHeroNewArtifact(hero, ArtifactID(*artID).toArtifact(), ArtifactPosition::FIRST_AVAILABLE); + gameHandler->giveHeroNewArtifact(hero, ArtifactID(*artID), ArtifactPosition::FIRST_AVAILABLE); } } else @@ -431,11 +472,23 @@ void PlayerMessageProcessor::cheatGiveArtifacts(PlayerColor player, const CGHero for(int g = 7; g < VLC->arth->objects.size(); ++g) //including artifacts from mods { if(VLC->arth->objects[g]->canBePutAt(hero)) - gameHandler->giveHeroNewArtifact(hero, ArtifactID(g).toArtifact(), ArtifactPosition::FIRST_AVAILABLE); + gameHandler->giveHeroNewArtifact(hero, ArtifactID(g), ArtifactPosition::FIRST_AVAILABLE); } } } +void PlayerMessageProcessor::cheatGiveScrolls(PlayerColor player, const CGHeroInstance * hero) +{ + if(!hero) + return; + + for(const auto & spell : VLC->spellh->objects) + if(gameHandler->gameState()->isAllowed(spell->getId()) && !spell->isSpecial()) + { + gameHandler->giveHeroNewScroll(hero, spell->getId(), ArtifactPosition::FIRST_AVAILABLE); + } +} + void PlayerMessageProcessor::cheatLevelup(PlayerColor player, const CGHeroInstance * hero, std::vector words) { if (!hero) @@ -491,7 +544,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; @@ -536,7 +589,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) @@ -544,7 +597,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) @@ -560,12 +613,12 @@ void PlayerMessageProcessor::cheatMapReveal(PlayerColor player, bool reveal) for(int z = 0; z < mapSize.z; z++) for(int x = 0; x < mapSize.x; x++) for(int y = 0; y < mapSize.y; y++) - if(!(*fowMap)[z][x][y] || fc.mode == ETileVisibility::HIDDEN) + if(!fowMap[z][x][y] || fc.mode == ETileVisibility::HIDDEN) hlp_tab[lastUnc++] = int3(x, y, z); fc.tiles.insert(hlp_tab, hlp_tab + lastUnc); delete [] hlp_tab; - gameHandler->sendAndApply(&fc); + gameHandler->sendAndApply(fc); } void PlayerMessageProcessor::cheatPuzzleReveal(PlayerColor player) @@ -583,7 +636,7 @@ void PlayerMessageProcessor::cheatPuzzleReveal(PlayerColor player) PlayerCheated pc; pc.player = color; - gameHandler->sendAndApply(&pc); + gameHandler->sendAndApply(pc); } } } @@ -659,7 +712,8 @@ bool PlayerMessageProcessor::handleCheatCode(const std::string & cheat, PlayerCo "vcmiolorin", "vcmiexp", "vcmiluck", "nwcfollowthewhiterabbit", "vcmimorale", "nwcmorpheus", - "vcmigod", "nwctheone" + "vcmigod", "nwctheone", + "vcmiscrolls" }; if (!vstd::contains(townTargetedCheats, cheatName) && !vstd::contains(playerTargetedCheats, cheatName) && !vstd::contains(heroTargetedCheats, cheatName)) @@ -685,7 +739,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()); @@ -694,17 +748,17 @@ bool PlayerMessageProcessor::handleCheatCode(const std::string & cheat, PlayerCo executeCheatCode(cheatName, i.first, ObjectInstanceID::NONE, parameters); if (vstd::contains(townTargetedCheats, cheatName)) - for (const auto & t : i.second.towns) + for (const auto & t : i.second.getTowns()) executeCheatCode(cheatName, i.first, t->id, parameters); if (vstd::contains(heroTargetedCheats, cheatName)) - for (const auto & h : i.second.heroes) + for (const auto & h : i.second.getHeroes()) executeCheatCode(cheatName, i.first, h->id, parameters); } PlayerCheated pc; pc.player = player; - gameHandler->sendAndApply(&pc); + gameHandler->sendAndApply(pc); if (!playerTargetedCheat) executeCheatCode(cheatName, player, currObj, words); @@ -736,6 +790,7 @@ void PlayerMessageProcessor::executeCheatCode(const std::string & cheatName, Pla const auto & doCheatRevealPuzzle = [&]() { cheatPuzzleReveal(player); }; const auto & doCheatMaxLuck = [&]() { cheatMaxLuck(player, hero); }; const auto & doCheatMaxMorale = [&]() { cheatMaxMorale(player, hero); }; + const auto & doCheatGiveScrolls = [&]() { cheatGiveScrolls(player, hero); }; const auto & doCheatTheOne = [&]() { if(!hero) @@ -804,6 +859,7 @@ void PlayerMessageProcessor::executeCheatCode(const std::string & cheatName, Pla {"nwcmorpheus", doCheatMaxMorale }, {"vcmigod", doCheatTheOne }, {"nwctheone", doCheatTheOne }, + {"vcmiscrolls", doCheatGiveScrolls }, }; assert(callbacks.count(cheatName)); @@ -811,11 +867,11 @@ void PlayerMessageProcessor::executeCheatCode(const std::string & cheatName, Pla callbacks.at(cheatName)(); } -void PlayerMessageProcessor::sendSystemMessage(std::shared_ptr connection, MetaString message) +void PlayerMessageProcessor::sendSystemMessage(std::shared_ptr connection, const MetaString & message) { SystemMessage sm; sm.text = message; - connection->sendPack(&sm); + connection->sendPack(sm); } void PlayerMessageProcessor::sendSystemMessage(std::shared_ptr connection, const std::string & message) @@ -829,7 +885,7 @@ void PlayerMessageProcessor::broadcastSystemMessage(MetaString message) { SystemMessage sm; sm.text = message; - gameHandler->sendToAllClients(&sm); + gameHandler->sendToAllClients(sm); } void PlayerMessageProcessor::broadcastSystemMessage(const std::string & message) @@ -842,5 +898,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/PlayerMessageProcessor.h b/server/processors/PlayerMessageProcessor.h index bbb3e0b92..c068911ed 100644 --- a/server/processors/PlayerMessageProcessor.h +++ b/server/processors/PlayerMessageProcessor.h @@ -46,6 +46,7 @@ class PlayerMessageProcessor void cheatGiveArmy(PlayerColor player, const CGHeroInstance * hero, std::vector words); void cheatGiveMachines(PlayerColor player, const CGHeroInstance * hero); void cheatGiveArtifacts(PlayerColor player, const CGHeroInstance * hero, std::vector words); + void cheatGiveScrolls(PlayerColor player, const CGHeroInstance * hero); void cheatLevelup(PlayerColor player, const CGHeroInstance * hero, std::vector words); void cheatExperience(PlayerColor player, const CGHeroInstance * hero, std::vector words); void cheatMovement(PlayerColor player, const CGHeroInstance * hero, std::vector words); @@ -62,6 +63,7 @@ class PlayerMessageProcessor void commandKick(PlayerColor player, const std::vector & words); void commandSave(PlayerColor player, const std::vector & words); void commandCheaters(PlayerColor player, const std::vector & words); + void commandStatistic(PlayerColor player, const std::vector & words); void commandHelp(PlayerColor player, const std::vector & words); void commandVote(PlayerColor player, const std::vector & words); @@ -76,7 +78,7 @@ public: void playerMessage(PlayerColor player, const std::string & message, ObjectInstanceID currObj); /// Send message to specific client with "System" as sender - void sendSystemMessage(std::shared_ptr connection, MetaString message); + void sendSystemMessage(std::shared_ptr connection, const MetaString & message); void sendSystemMessage(std::shared_ptr connection, const std::string & message); /// Send message to all players with "System" as sender diff --git a/server/processors/TurnOrderProcessor.cpp b/server/processors/TurnOrderProcessor.cpp index f5c728469..899df2b04 100644 --- a/server/processors/TurnOrderProcessor.cpp +++ b/server/processors/TurnOrderProcessor.cpp @@ -72,7 +72,7 @@ void TurnOrderProcessor::updateAndNotifyContactStatus() { // Simturns between all players have ended - send single global notification if (!blockedContacts.empty()) - gameHandler->playerMessages->broadcastSystemMessage("Simultaneous turns have ended"); + gameHandler->playerMessages->broadcastSystemMessage(MetaString::createFromTextID("vcmi.broadcast.simturn.end")); } else { @@ -83,11 +83,11 @@ void TurnOrderProcessor::updateAndNotifyContactStatus() continue; MetaString message; - message.appendRawString("Simultaneous turns between players %s and %s have ended"); // FIXME: we should send MetaString itself and localize it on client side + message.appendTextID("vcmi.broadcast.simturn.endBetween"); message.replaceName(contact.a); message.replaceName(contact.b); - gameHandler->playerMessages->broadcastSystemMessage(message.toString()); + gameHandler->playerMessages->broadcastSystemMessage(message); } } @@ -121,7 +121,7 @@ bool TurnOrderProcessor::playersInContact(PlayerColor left, PlayerColor right) c } } - for(const auto & hero : leftInfo->heroes) + for(const auto & hero : leftInfo->getHeroes()) { CPathsInfo out(mapSize, hero); auto config = std::make_shared(out, gameHandler->gameState(), hero); @@ -137,7 +137,7 @@ bool TurnOrderProcessor::playersInContact(PlayerColor left, PlayerColor right) c leftReachability[z][x][y] = true; } - for(const auto & hero : rightInfo->heroes) + for(const auto & hero : rightInfo->getHeroes()) { CPathsInfo out(mapSize, hero); auto config = std::make_shared(out, gameHandler->gameState(), hero); @@ -179,7 +179,7 @@ bool TurnOrderProcessor::computeCanActSimultaneously(PlayerColor active, PlayerC if (activeInfo->human != waitingInfo->human) { - // only one AI and one human can play simultaneoulsy from single connection + // only one AI and one human can play simultaneously from single connection if (!gameHandler->getStartInfo()->simturnsInfo.allowHumanWithAI) return false; } @@ -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 485f1f9fa..52fcbd63b 100644 --- a/server/queries/BattleQueries.cpp +++ b/server/queries/BattleQueries.cpp @@ -17,43 +17,44 @@ #include "../../lib/battle/IBattleState.h" #include "../../lib/battle/SideInBattle.h" +#include "../../lib/battle/BattleLayout.h" #include "../../lib/CPlayerState.h" #include "../../lib/mapObjects/CGObjectInstance.h" #include "../../lib/mapObjects/CGTownInstance.h" #include "../../lib/networkPacks/PacksForServer.h" -#include "../../lib/serializer/Cast.h" -void CBattleQuery::notifyObjectAboutRemoval(const CObjectVisitQuery & objectVisit) const +void CBattleQuery::notifyObjectAboutRemoval(const CGObjectInstance * visitedObject, const CGHeroInstance * visitingHero) const { assert(result); if(result) - objectVisit.visitedObject->battleFinished(objectVisit.visitingHero, *result); + visitedObject->battleFinished(visitingHero, *result); } CBattleQuery::CBattleQuery(CGameHandler * owner, const IBattleInfo * bi): CQuery(owner), battleID(bi->getBattleID()) { - belligerents[0] = bi->getSideArmy(0); - belligerents[1] = bi->getSideArmy(1); + belligerents[BattleSide::ATTACKER] = bi->getSideArmy(BattleSide::ATTACKER); + belligerents[BattleSide::DEFENDER] = bi->getSideArmy(BattleSide::DEFENDER); - addPlayer(bi->getSidePlayer(0)); - addPlayer(bi->getSidePlayer(1)); + addPlayer(bi->getSidePlayer(BattleSide::ATTACKER)); + addPlayer(bi->getSidePlayer(BattleSide::DEFENDER)); } CBattleQuery::CBattleQuery(CGameHandler * owner): CQuery(owner) { - belligerents[0] = belligerents[1] = nullptr; + belligerents[BattleSide::ATTACKER] = nullptr; + belligerents[BattleSide::DEFENDER] = nullptr; } -bool CBattleQuery::blocksPack(const CPack * pack) const +bool CBattleQuery::blocksPack(const CPackForServer * pack) const { - if(dynamic_ptr_cast(pack) != nullptr) + if(dynamic_cast(pack) != nullptr) return false; - if(dynamic_ptr_cast(pack) != nullptr) + if(dynamic_cast(pack) != nullptr) return false; return true; @@ -81,8 +82,8 @@ CBattleDialogQuery::CBattleDialogQuery(CGameHandler * owner, const IBattleInfo * bi(bi), result(Br) { - addPlayer(bi->getSidePlayer(0)); - addPlayer(bi->getSidePlayer(1)); + addPlayer(bi->getSidePlayer(BattleSide::ATTACKER)); + addPlayer(bi->getSidePlayer(BattleSide::DEFENDER)); } void CBattleDialogQuery::onRemoval(PlayerColor color) @@ -95,14 +96,14 @@ void CBattleDialogQuery::onRemoval(PlayerColor color) assert(answer); if(*answer == 1) { - gh->battles->restartBattlePrimary( + gh->battles->restartBattle( bi->getBattleID(), - bi->getSideArmy(0), - bi->getSideArmy(1), + bi->getSideArmy(BattleSide::ATTACKER), + bi->getSideArmy(BattleSide::DEFENDER), bi->getLocation(), - bi->getSideHero(0), - bi->getSideHero(1), - bi->isCreatureBank(), + bi->getSideHero(BattleSide::ATTACKER), + bi->getSideHero(BattleSide::DEFENDER), + bi->getLayout(), bi->getDefendedTown() ); } diff --git a/server/queries/BattleQueries.h b/server/queries/BattleQueries.h index 07b6d7b56..3cb525a28 100644 --- a/server/queries/BattleQueries.h +++ b/server/queries/BattleQueries.h @@ -11,6 +11,7 @@ #include "CQuery.h" #include "../../lib/networkPacks/PacksForClientBattle.h" +#include "../../lib/battle/BattleSide.h" VCMI_LIB_NAMESPACE_BEGIN class IBattleInfo; @@ -20,16 +21,16 @@ VCMI_LIB_NAMESPACE_END class CBattleQuery : public CQuery { public: - std::array belligerents; - std::array initialHeroMana; + BattleSideArray belligerents; + BattleSideArray initialHeroMana; BattleID battleID; std::optional result; CBattleQuery(CGameHandler * owner); CBattleQuery(CGameHandler * owner, const IBattleInfo * Bi); //TODO - void notifyObjectAboutRemoval(const CObjectVisitQuery &objectVisit) const override; - bool blocksPack(const CPack *pack) const override; + void notifyObjectAboutRemoval(const CGObjectInstance * visitedObject, const CGHeroInstance * visitingHero) const override; + bool blocksPack(const CPackForServer *pack) const override; void onRemoval(PlayerColor color) override; void onExposure(QueryPtr topQuery) override; }; @@ -37,10 +38,10 @@ public: class CBattleDialogQuery : public CDialogQuery { bool resultProcessed = false; -public: - CBattleDialogQuery(CGameHandler * owner, const IBattleInfo * Bi, std::optional Br); - const IBattleInfo * bi; std::optional result; + +public: + CBattleDialogQuery(CGameHandler * owner, const IBattleInfo * Bi, std::optional Br); void onRemoval(PlayerColor color) override; }; diff --git a/server/queries/CQuery.cpp b/server/queries/CQuery.cpp index 0580b75ed..26a3b571c 100644 --- a/server/queries/CQuery.cpp +++ b/server/queries/CQuery.cpp @@ -14,27 +14,8 @@ #include "../CGameHandler.h" -#include "../../lib/serializer/Cast.h" #include "../../lib/networkPacks/PacksForServer.h" -template -std::string formatContainer(const Container & c, std::string delimeter = ", ", std::string opener = "(", std::string closer=")") -{ - std::string ret = opener; - auto itr = std::begin(c); - if(itr != std::end(c)) - { - ret += std::to_string(*itr); - while(++itr != std::end(c)) - { - ret += delimeter; - ret += std::to_string(*itr); - } - } - ret += closer; - return ret; -} - std::ostream & operator<<(std::ostream & out, const CQuery & query) { return out << query.toString(); @@ -100,12 +81,12 @@ void CQuery::onRemoval(PlayerColor color) } -bool CQuery::blocksPack(const CPack * pack) const +bool CQuery::blocksPack(const CPackForServer * pack) const { return false; } -void CQuery::notifyObjectAboutRemoval(const CObjectVisitQuery & objectVisit) const +void CQuery::notifyObjectAboutRemoval(const CGObjectInstance * visitedObject, const CGHeroInstance * visitingHero) const { } @@ -131,10 +112,10 @@ 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_ptr_cast(pack)) + if(auto reply = dynamic_cast(pack)) return !vstd::contains(players, reply->player); return true; @@ -151,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); } @@ -168,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 5773ecadd..5cdae43d4 100644 --- a/server/queries/CQuery.h +++ b/server/queries/CQuery.h @@ -13,7 +13,9 @@ VCMI_LIB_NAMESPACE_BEGIN -struct CPack; +struct CPackForServer; +class CGObjectInstance; +class CGHeroInstance; VCMI_LIB_NAMESPACE_END @@ -40,25 +42,36 @@ public: CQuery(CGameHandler * gh); - virtual bool blocksPack(const CPack *pack) const; //query can block attempting actions by player. Eg. he can't move hero during the battle. + /// query can block attempting actions by player. Eg. he can't move hero during the battle. + virtual bool blocksPack(const CPackForServer *pack) const; - virtual bool endsByPlayerAnswer() const; //query is removed after player gives answer (like dialogs) - virtual void onAdding(PlayerColor color); //called just before query is pushed on stack - virtual void onAdded(PlayerColor color); //called right after query is pushed on stack - virtual void onRemoval(PlayerColor color); //called after query is removed from stack - virtual void onExposure(QueryPtr topQuery);//called when query immediately above is removed and this is exposed (becomes top) - virtual std::string toString() const; + /// query is removed after player gives answer (like dialogs) + virtual bool endsByPlayerAnswer() const; - virtual void notifyObjectAboutRemoval(const CObjectVisitQuery &objectVisit) const; + /// called just before query is pushed on stack + virtual void onAdding(PlayerColor color); + + /// called right after query is pushed on stack + virtual void onAdded(PlayerColor color); + + /// called after query is removed from stack + virtual void onRemoval(PlayerColor color); + + /// called when query immediately above is removed and this is exposed (becomes top) + virtual void onExposure(QueryPtr topQuery); + + /// called when this query is being removed and must report its result to currently visited object + virtual void notifyObjectAboutRemoval(const CGObjectInstance * visitedObject, const CGHeroInstance * visitingHero) const; virtual void setReply(std::optional reply); + virtual std::string toString() const; virtual ~CQuery(); 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); @@ -69,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; @@ -80,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 72f9bfbce..a2ae04911 100644 --- a/server/queries/MapQueries.cpp +++ b/server/queries/MapQueries.cpp @@ -13,10 +13,9 @@ #include "QueriesProcessor.h" #include "../CGameHandler.h" #include "../TurnTimerHandler.h" -#include "../../lib/mapObjects/MiscObjects.h" #include "../../lib/mapObjects/CGHeroInstance.h" +#include "../../lib/mapObjects/MiscObjects.h" #include "../../lib/networkPacks/PacksForServer.h" -#include "../../lib/serializer/Cast.h" TimerPauseQuery::TimerPauseQuery(CGameHandler * owner, PlayerColor player): CQuery(owner) @@ -24,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); } @@ -44,41 +43,9 @@ bool TimerPauseQuery::endsByPlayerAnswer() const return true; } -CObjectVisitQuery::CObjectVisitQuery(CGameHandler * owner, const CGObjectInstance * Obj, const CGHeroInstance * Hero, int3 Tile): - CQuery(owner), visitedObject(Obj), visitingHero(Hero), tile(Tile), removeObjectAfterVisit(false) +void CGarrisonDialogQuery::notifyObjectAboutRemoval(const CGObjectInstance * visitedObject, const CGHeroInstance * visitingHero) const { - addPlayer(Hero->tempOwner); -} - -bool CObjectVisitQuery::blocksPack(const CPack *pack) const -{ - //During the visit itself ALL actions are blocked. - //(However, the visit may trigger a query above that'll pass some.) - return true; -} - -void CObjectVisitQuery::onRemoval(PlayerColor color) -{ - gh->objectVisitEnded(*this); - - //TODO or should it be destructor? - //Can object visit affect 2 players and what would be desired behavior? - if(removeObjectAfterVisit) - gh->removeObject(visitedObject, color); -} - -void CObjectVisitQuery::onExposure(QueryPtr topQuery) -{ - //Object may have been removed and deleted. - if(gh->isValidObject(visitedObject)) - topQuery->notifyObjectAboutRemoval(*this); - - owner->popIfTop(*this); -} - -void CGarrisonDialogQuery::notifyObjectAboutRemoval(const CObjectVisitQuery & objectVisit) const -{ - objectVisit.visitedObject->garrisonDialogClosed(objectVisit.visitingHero); + visitedObject->garrisonDialogClosed(visitingHero); } CGarrisonDialogQuery::CGarrisonDialogQuery(CGameHandler * owner, const CArmedInstance * up, const CArmedInstance * down): @@ -91,28 +58,28 @@ 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); ourIds.insert(this->exchangingArmies[1]->id); - if(auto stacks = dynamic_ptr_cast(pack)) + if(auto stacks = dynamic_cast(pack)) return !vstd::contains(ourIds, stacks->id1) || !vstd::contains(ourIds, stacks->id2); - if(auto stacks = dynamic_ptr_cast(pack)) + if(auto stacks = dynamic_cast(pack)) return !vstd::contains(ourIds, stacks->srcOwner); - if(auto stacks = dynamic_ptr_cast(pack)) + if(auto stacks = dynamic_cast(pack)) return !vstd::contains(ourIds, stacks->srcOwner); - if(auto stacks = dynamic_ptr_cast(pack)) + if(auto stacks = dynamic_cast(pack)) return !vstd::contains(ourIds, stacks->srcOwner); - if(auto stacks = dynamic_ptr_cast(pack)) + if(auto stacks = dynamic_cast(pack)) return !vstd::contains(ourIds, stacks->srcArmy) || !vstd::contains(ourIds, stacks->destArmy); - if(auto arts = dynamic_ptr_cast(pack)) + if(auto arts = dynamic_cast(pack)) { if(auto id1 = arts->src.artHolder) if(!vstd::contains(ourIds, id1)) @@ -123,41 +90,42 @@ bool CGarrisonDialogQuery::blocksPack(const CPack * pack) const return true; return false; } - if(auto dismiss = dynamic_ptr_cast(pack)) + if(auto dismiss = dynamic_cast(pack)) return !vstd::contains(ourIds, dismiss->id); - if(auto arts = dynamic_ptr_cast(pack)) + if(auto arts = dynamic_cast(pack)) return !vstd::contains(ourIds, arts->srcHero) || !vstd::contains(ourIds, arts->dstHero); - if(auto arts = dynamic_ptr_cast(pack)) + if(auto arts = dynamic_cast(pack)) return !vstd::contains(ourIds, arts->artHolder); - if(auto art = dynamic_ptr_cast(pack)) + if(auto art = dynamic_cast(pack)) { if(auto id = art->al.artHolder) return !vstd::contains(ourIds, id); } - if(auto dismiss = dynamic_ptr_cast(pack)) + if(auto dismiss = dynamic_cast(pack)) return !vstd::contains(ourIds, dismiss->heroID); - if(auto upgrade = dynamic_ptr_cast(pack)) + if(auto upgrade = dynamic_cast(pack)) return !vstd::contains(ourIds, upgrade->id); - if(auto formation = dynamic_ptr_cast(pack)) + if(auto formation = dynamic_cast(pack)) return !vstd::contains(ourIds, formation->hid); return CDialogQuery::blocksPack(pack); } -void CBlockingDialogQuery::notifyObjectAboutRemoval(const CObjectVisitQuery & objectVisit) const +void CBlockingDialogQuery::notifyObjectAboutRemoval(const CGObjectInstance * visitedObject, const CGHeroInstance * visitingHero) const { assert(answer); - objectVisit.visitedObject->blockingDialogAnswered(objectVisit.visitingHero, *answer); + caller->blockingDialogAnswered(visitingHero, *answer); } -CBlockingDialogQuery::CBlockingDialogQuery(CGameHandler * owner, const BlockingDialog & bd): - CDialogQuery(owner) +CBlockingDialogQuery::CBlockingDialogQuery(CGameHandler * owner, const IObjectInterface * caller, const BlockingDialog & bd): + CDialogQuery(owner), + caller(caller) { this->bd = bd; addPlayer(bd.player); @@ -175,60 +143,59 @@ 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) { - if(dynamic_ptr_cast(pack) != nullptr) + if(dynamic_cast(pack) != nullptr) return false; // If hero has no free slots, he might get some stacks merged automatically - if(dynamic_ptr_cast(pack) != nullptr) + if(dynamic_cast(pack) != nullptr) return false; } if (mode == EOpenWindowMode::TAVERN_WINDOW) { - if(dynamic_ptr_cast(pack) != nullptr) + if(dynamic_cast(pack) != nullptr) return false; } if (mode == EOpenWindowMode::UNIVERSITY_WINDOW) { - if(dynamic_ptr_cast(pack) != nullptr) + if(dynamic_cast(pack) != nullptr) return false; } if (mode == EOpenWindowMode::MARKET_WINDOW) { - if(dynamic_ptr_cast(pack) != nullptr) + if(dynamic_cast(pack) != nullptr) return false; - if(dynamic_ptr_cast(pack) != nullptr) + if(dynamic_cast(pack) != nullptr) return false; - if(dynamic_ptr_cast(pack) != nullptr) + if(dynamic_cast(pack) != nullptr) return false; - if(dynamic_ptr_cast(pack)) + if(dynamic_cast(pack)) return false; - if(dynamic_ptr_cast(pack)) + if(dynamic_cast(pack)) return false; - if(dynamic_ptr_cast(pack) != nullptr) + if(dynamic_cast(pack) != nullptr) return false; } return CDialogQuery::blocksPack(pack); } -void CTeleportDialogQuery::notifyObjectAboutRemoval(const CObjectVisitQuery & objectVisit) const +void CTeleportDialogQuery::notifyObjectAboutRemoval(const CGObjectInstance * visitedObject, const CGHeroInstance * visitingHero) const { - // do not change to dynamic_ptr_cast - SIGSEGV! - auto obj = dynamic_cast(objectVisit.visitedObject); + auto obj = dynamic_cast(visitedObject); if(obj) - obj->teleportDialogAnswered(objectVisit.visitingHero, *answer, td.exits); + obj->teleportDialogAnswered(visitingHero, *answer, td.exits); else logGlobal->error("Invalid instance in teleport query"); } @@ -254,9 +221,9 @@ void CHeroLevelUpDialogQuery::onRemoval(PlayerColor color) gh->levelUpHero(hero, hlu.skills[*answer]); } -void CHeroLevelUpDialogQuery::notifyObjectAboutRemoval(const CObjectVisitQuery & objectVisit) const +void CHeroLevelUpDialogQuery::notifyObjectAboutRemoval(const CGObjectInstance * visitedObject, const CGHeroInstance * visitingHero) const { - objectVisit.visitedObject->heroLevelUpDone(objectVisit.visitingHero); + visitedObject->heroLevelUpDone(visitingHero); } CCommanderLevelUpDialogQuery::CCommanderLevelUpDialogQuery(CGameHandler * owner, const CommanderLevelUp & Clu, const CGHeroInstance * Hero): @@ -273,9 +240,9 @@ void CCommanderLevelUpDialogQuery::onRemoval(PlayerColor color) gh->levelUpCommander(hero->commander, clu.skills[*answer]); } -void CCommanderLevelUpDialogQuery::notifyObjectAboutRemoval(const CObjectVisitQuery & objectVisit) const +void CCommanderLevelUpDialogQuery::notifyObjectAboutRemoval(const CGObjectInstance * visitedObject, const CGHeroInstance * visitingHero) const { - objectVisit.visitedObject->heroLevelUpDone(objectVisit.visitingHero); + visitedObject->heroLevelUpDone(visitingHero); } CHeroMovementQuery::CHeroMovementQuery(CGameHandler * owner, const TryMoveHero & Tmh, const CGHeroInstance * Hero, bool VisitDestAfterVictory): @@ -306,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) @@ -315,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 7a6cabf3c..f202e11b7 100644 --- a/server/queries/MapQueries.h +++ b/server/queries/MapQueries.h @@ -15,12 +15,9 @@ VCMI_LIB_NAMESPACE_BEGIN class CGHeroInstance; class CGObjectInstance; -class int3; +class IObjectInterface; VCMI_LIB_NAMESPACE_END - -class TurnTimerHandler; - //Created when player starts turn or when player puts game on [ause //Removed when player accepts a turn or continur play class TimerPauseQuery : public CQuery @@ -28,29 +25,12 @@ 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; }; -//Created when hero visits object. -//Removed when query above is resolved (or immediately after visit if no queries were created) -class CObjectVisitQuery : public CQuery -{ -public: - const CGObjectInstance *visitedObject; - const CGHeroInstance *visitingHero; - int3 tile; //may be different than hero pos -> eg. visit via teleport - bool removeObjectAfterVisit; - - CObjectVisitQuery(CGameHandler * owner, const CGObjectInstance *Obj, const CGHeroInstance *Hero, int3 Tile); - - bool blocksPack(const CPack *pack) const override; - void onRemoval(PlayerColor color) override; - void onExposure(QueryPtr topQuery) override; -}; - //Created when hero attempts move and something happens //(not necessarily position change, could be just an object interaction). class CHeroMovementQuery : public CQuery @@ -73,19 +53,20 @@ public: std::array exchangingArmies; CGarrisonDialogQuery(CGameHandler * owner, const CArmedInstance *up, const CArmedInstance *down); - void notifyObjectAboutRemoval(const CObjectVisitQuery &objectVisit) const override; - bool blocksPack(const CPack *pack) const override; + void notifyObjectAboutRemoval(const CGObjectInstance * visitedObject, const CGHeroInstance * visitingHero) const override; + bool blocksPack(const CPackForServer *pack) const override; }; //yes/no and component selection dialogs class CBlockingDialogQuery : public CDialogQuery { public: + const IObjectInterface * caller; BlockingDialog bd; //copy of pack... debug purposes - CBlockingDialogQuery(CGameHandler * owner, const BlockingDialog &bd); + CBlockingDialogQuery(CGameHandler * owner, const IObjectInterface * caller, const BlockingDialog &bd); - void notifyObjectAboutRemoval(const CObjectVisitQuery &objectVisit) const override; + void notifyObjectAboutRemoval(const CGObjectInstance * visitedObject, const CGHeroInstance * visitingHero) const override; }; class OpenWindowQuery : public CDialogQuery @@ -94,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; }; @@ -105,7 +86,7 @@ public: CTeleportDialogQuery(CGameHandler * owner, const TeleportDialog &td); - void notifyObjectAboutRemoval(const CObjectVisitQuery &objectVisit) const override; + void notifyObjectAboutRemoval(const CGObjectInstance * visitedObject, const CGHeroInstance * visitingHero) const override; }; class CHeroLevelUpDialogQuery : public CDialogQuery @@ -114,7 +95,7 @@ public: CHeroLevelUpDialogQuery(CGameHandler * owner, const HeroLevelUp &Hlu, const CGHeroInstance * Hero); void onRemoval(PlayerColor color) override; - void notifyObjectAboutRemoval(const CObjectVisitQuery &objectVisit) const override; + void notifyObjectAboutRemoval(const CGObjectInstance * visitedObject, const CGHeroInstance * visitingHero) const override; HeroLevelUp hlu; const CGHeroInstance * hero; @@ -126,7 +107,7 @@ public: CCommanderLevelUpDialogQuery(CGameHandler * owner, const CommanderLevelUp &Clu, const CGHeroInstance * Hero); void onRemoval(PlayerColor color) override; - void notifyObjectAboutRemoval(const CObjectVisitQuery &objectVisit) const override; + void notifyObjectAboutRemoval(const CGObjectInstance * visitedObject, const CGHeroInstance * visitingHero) const override; CommanderLevelUp clu; const CGHeroInstance * hero; diff --git a/server/queries/VisitQueries.cpp b/server/queries/VisitQueries.cpp new file mode 100644 index 000000000..f5f34fbb3 --- /dev/null +++ b/server/queries/VisitQueries.cpp @@ -0,0 +1,87 @@ +/* + * VisitQueries.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 "VisitQueries.h" + +#include "../../lib/mapObjects/CGHeroInstance.h" +#include "../../lib/mapObjects/CGTownInstance.h" +#include "../../lib/mapObjects/TownBuildingInstance.h" +#include "../CGameHandler.h" +#include "QueriesProcessor.h" + +VisitQuery::VisitQuery(CGameHandler * owner, const CGObjectInstance * Obj, const CGHeroInstance * Hero) + : CQuery(owner) + , visitedObject(Obj) + , visitingHero(Hero) +{ + addPlayer(Hero->tempOwner); +} + +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.) + return true; +} + +void MapObjectVisitQuery::onExposure(QueryPtr topQuery) +{ + //Object may have been removed and deleted. + if(gh->isValidObject(visitedObject)) + topQuery->notifyObjectAboutRemoval(visitedObject, visitingHero); + + owner->popIfTop(*this); +} + +MapObjectVisitQuery::MapObjectVisitQuery(CGameHandler * owner, const CGObjectInstance * Obj, const CGHeroInstance * Hero) + : VisitQuery(owner, Obj, Hero) + , removeObjectAfterVisit(false) +{ +} + +void MapObjectVisitQuery::onRemoval(PlayerColor color) +{ + gh->objectVisitEnded(visitingHero, players.front()); + + //Can object visit affect 2 players and what would be desired behavior? + if(removeObjectAfterVisit) + gh->removeObject(visitedObject, color); +} + +TownBuildingVisitQuery::TownBuildingVisitQuery(CGameHandler * owner, const CGTownInstance * Obj, std::vector heroes, std::vector buildingToVisit) + : VisitQuery(owner, Obj, heroes.front()) + , visitedTown(Obj) +{ + // generate in reverse order - first building-hero pair to handle must be in the end of vector + for (auto const * hero : boost::adaptors::reverse(heroes)) + for (auto const & building : boost::adaptors::reverse(buildingToVisit)) + visitedBuilding.push_back({ hero, building}); +} + +void TownBuildingVisitQuery::onExposure(QueryPtr topQuery) +{ + topQuery->notifyObjectAboutRemoval(visitedObject, visitingHero); + + onAdded(players.front()); +} + +void TownBuildingVisitQuery::onAdded(PlayerColor color) +{ + while (!visitedBuilding.empty() && owner->topQuery(color).get() == this) + { + visitingHero = visitedBuilding.back().hero; + const auto * building = visitedTown->rewardableBuildings.at(visitedBuilding.back().building); + building->onHeroVisit(visitingHero); + visitedBuilding.pop_back(); + } + + if (visitedBuilding.empty() && owner->topQuery(color).get() == this) + owner->popIfTop(*this); +} diff --git a/server/queries/VisitQueries.h b/server/queries/VisitQueries.h new file mode 100644 index 000000000..fe41df45f --- /dev/null +++ b/server/queries/VisitQueries.h @@ -0,0 +1,59 @@ +/* + * VisitQueries.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 "CQuery.h" + +VCMI_LIB_NAMESPACE_BEGIN +class CGTownInstance; +VCMI_LIB_NAMESPACE_END + +//Created when hero visits object. +//Removed when query above is resolved (or immediately after visit if no queries were created) +class VisitQuery : public CQuery +{ +protected: + VisitQuery(CGameHandler * owner, const CGObjectInstance * Obj, const CGHeroInstance * Hero); + +public: + const CGObjectInstance * visitedObject; + const CGHeroInstance * visitingHero; + + bool blocksPack(const CPackForServer * pack) const final; +}; + +class MapObjectVisitQuery final : public VisitQuery +{ +public: + bool removeObjectAfterVisit; + + MapObjectVisitQuery(CGameHandler * owner, const CGObjectInstance * Obj, const CGHeroInstance * Hero); + + void onRemoval(PlayerColor color) final; + void onExposure(QueryPtr topQuery) final; +}; + +class TownBuildingVisitQuery final : public VisitQuery +{ + struct BuildingVisit + { + const CGHeroInstance * hero; + BuildingID building; + }; + + const CGTownInstance * visitedTown; + std::vector visitedBuilding; + +public: + TownBuildingVisitQuery(CGameHandler * owner, const CGTownInstance * Obj, std::vector heroes, std::vector buildingToVisit); + + void onAdded(PlayerColor color) final; + void onExposure(QueryPtr topQuery) final; +}; diff --git a/serverapp/EntryPoint.cpp b/serverapp/EntryPoint.cpp index f473ff05d..685e70049 100644 --- a/serverapp/EntryPoint.cpp +++ b/serverapp/EntryPoint.cpp @@ -15,6 +15,7 @@ #include "../lib/logging/CBasicLogConfigurator.h" #include "../lib/VCMIDirs.h" #include "../lib/VCMI_Lib.h" +#include "../lib/CConfigHandler.h" #include @@ -86,12 +87,12 @@ int main(int argc, const char * argv[]) { bool connectToLobby = opts.count("lobby"); bool runByClient = opts.count("runByClient"); - uint16_t port = 3030; + uint16_t port = settings["server"]["localPort"].Integer(); if(opts.count("port")) port = opts["port"].as(); - CVCMIServer server(port, connectToLobby, runByClient); - + CVCMIServer server(port, runByClient); + server.prepare(connectToLobby); server.run(); // CVCMIServer destructor must be called here - before VLC cleanup diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index b0e26de4f..70149a566 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -39,7 +39,7 @@ set(test_SRCS map/CMapFormatTest.cpp map/MapComparer.cpp - netpacks/EntitiesChangedTest.cpp + netpacks/NetPackFixture.cpp spells/AbilityCasterTest.cpp diff --git a/test/Test.cbp b/test/Test.cbp deleted file mode 100644 index 2774c9e55..000000000 --- a/test/Test.cbp +++ /dev/null @@ -1,240 +0,0 @@ - - - - - - diff --git a/test/Test.vcxproj b/test/Test.vcxproj deleted file mode 100644 index 46d8c3ac4..000000000 --- a/test/Test.vcxproj +++ /dev/null @@ -1,252 +0,0 @@ - - - - - Debug - Win32 - - - Debug - x64 - - - RD - Win32 - - - RD - x64 - - - - {BA25F3F0-EB87-4164-AAB9-073C50A3557A} - VCMI_client - 10.0 - - - - Application - Unicode - true - v142 - - - Application - Unicode - true - v140_xp - - - Application - Unicode - v140_xp - - - Application - Unicode - v140_xp - - - - - - - - - - - - - - - - - - - - - - - - - - - <_ProjectFileVersion>10.0.30128.1 - $(VCMI_Out) - $(VCMI_Out) - $(Configuration)\ - $(Configuration)\ - .. - $(VCMI_Out) - $(Configuration)\ - $(Configuration)\ - AllRules.ruleset - AllRules.ruleset - - - - - AllRules.ruleset - AllRules.ruleset - - - - - - - $(SolutionDir)\AI\FuzzyLite\fuzzylite;$(SolutionDir)\test\googletest\googlemock\include;$(SolutionDir)\test\googletest\googletest\include;$(IncludePath) - - - - - - 4251;%(DisableSpecificWarnings) - NoListing - Use - StdInc.h - /MP4 /Zm150 - - - VCMI_lib.lib;%(AdditionalDependencies) - NotSet - false - true - ..\..\libs - - - - - - false - false - 4251;%(DisableSpecificWarnings) - NoListing - Use - StdInc.h - /MP4 /Zm150 - - - VCMI_lib.lib;%(AdditionalDependencies) - LinkVerbose - false - true - - - - - - Use - StdInc.h - - - true - - - VCMI_lib.lib;VCAI.lib;FuzzyLite.lib;gmock.lib;gtest.lib;%(AdditionalDependencies) - NotSet - - - NotSet - ..\..\libs;.. - /LTCG %(AdditionalOptions) - - - - - /MP4 /Zm150 - Use - StdInc.h - - - VCMI_lib.lib;%(AdditionalDependencies) - NotSet - - - NotSet - - - - - {b952ffc5-3039-4de1-9f08-90acda483d8f} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Create - Create - Create - Create - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/test/Test.vcxproj.filters b/test/Test.vcxproj.filters deleted file mode 100644 index 2b357e83a..000000000 --- a/test/Test.vcxproj.filters +++ /dev/null @@ -1,220 +0,0 @@ - - - - - - - - - map - - - map - - - map - - - battle - - - battle - - - battle - - - battle - - - battle - - - battle - - - game - - - spells\effects - - - spells\effects - - - spells\effects - - - spells\effects - - - spells\effects - - - spells\effects - - - spells\effects - - - spells\effects - - - spells\effects - - - spells\effects - - - spells\targetConditions - - - spells\targetConditions - - - spells\targetConditions - - - spells\targetConditions - - - spells\targetConditions - - - spells\targetConditions - - - spells\targetConditions - - - spells\targetConditions - - - spells\targetConditions - - - spells\targetConditions - - - spells\targetConditions - - - spells\targetConditions - - - spells - - - spells - - - vcai - - - vcai - - - vcai - - - mock - - - mock - - - mock - - - mock - - - - - - - - map - - - spells\effects - - - spells\targetConditions - - - vcai - - - vcai - - - vcai - - - vcai - - - mock - - - mock - - - mock - - - mock - - - mock - - - mock - - - mock - - - mock - - - mock - - - mock - - - mock - - - mock - - - - - {418ff473-dca6-4c43-bba7-d575536c5791} - - - {01a5ea57-0094-4f54-94a5-10184cb7518c} - - - {db53f45d-1e4d-4e6b-9bc1-fa0e15f1def2} - - - {9b00f38e-f370-413e-ad10-644a21be00b4} - - - {dc596868-45d4-4ee4-8191-34c2f76b47fc} - - - {ac23eabd-5463-468f-862e-38a0934b866e} - - - {f7e35d1b-7e06-4c22-a3a3-6f7d1357e028} - - - {53399b0b-1a51-43f7-91cc-4fc47dfbad84} - - - \ No newline at end of file diff --git a/test/battle/BattleHexTest.cpp b/test/battle/BattleHexTest.cpp index d2179587d..075952bba 100644 --- a/test/battle/BattleHexTest.cpp +++ b/test/battle/BattleHexTest.cpp @@ -92,16 +92,16 @@ TEST(BattleHexTest, getClosestTile) possibilities.insert(119); possibilities.insert(186); - EXPECT_EQ(mainHex.getClosestTile(0,mainHex,possibilities), 3); + EXPECT_EQ(mainHex.getClosestTile(BattleSide::ATTACKER,mainHex,possibilities), 3); mainHex = 139; - EXPECT_EQ(mainHex.getClosestTile(1,mainHex,possibilities), 119); + EXPECT_EQ(mainHex.getClosestTile(BattleSide::DEFENDER,mainHex,possibilities), 119); mainHex = 16; - EXPECT_EQ(mainHex.getClosestTile(1,mainHex,possibilities), 100); + EXPECT_EQ(mainHex.getClosestTile(BattleSide::DEFENDER,mainHex,possibilities), 100); mainHex = 166; - EXPECT_EQ(mainHex.getClosestTile(0,mainHex,possibilities), 186); + EXPECT_EQ(mainHex.getClosestTile(BattleSide::ATTACKER,mainHex,possibilities), 186); mainHex = 76; - EXPECT_EQ(mainHex.getClosestTile(1,mainHex,possibilities), 3); - EXPECT_EQ(mainHex.getClosestTile(0,mainHex,possibilities), 100); + EXPECT_EQ(mainHex.getClosestTile(BattleSide::DEFENDER,mainHex,possibilities), 3); + EXPECT_EQ(mainHex.getClosestTile(BattleSide::ATTACKER,mainHex,possibilities), 100); } TEST(BattleHexTest, moveEDir) diff --git a/test/battle/CBattleInfoCallbackTest.cpp b/test/battle/CBattleInfoCallbackTest.cpp index d7b3ae386..3c7237d37 100644 --- a/test/battle/CBattleInfoCallbackTest.cpp +++ b/test/battle/CBattleInfoCallbackTest.cpp @@ -40,11 +40,32 @@ public: bonusFake.addNewBonus(b); } + void addCreatureAbility(BonusType bonusType) + { + addNewBonus( + std::make_shared( + BonusDuration::PERMANENT, + bonusType, + BonusSource::CREATURE_ABILITY, + 0, + CreatureID(0))); + } + void makeAlive() { EXPECT_CALL(*this, alive()).WillRepeatedly(Return(true)); } + void setupPoisition(BattleHex pos) + { + EXPECT_CALL(*this, getPosition()).WillRepeatedly(Return(pos)); + } + + void makeDoubleWide() + { + EXPECT_CALL(*this, doubleWide()).WillRepeatedly(Return(true)); + } + void makeWarMachine() { addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::SIEGE_WEAPON, BonusSource::CREATURE_ABILITY, 1, BonusSourceID())); @@ -52,13 +73,13 @@ public: void redirectBonusesToFake() { - ON_CALL(*this, getAllBonuses(_, _, _, _)).WillByDefault(Invoke(&bonusFake, &BonusBearerMock::getAllBonuses)); + ON_CALL(*this, getAllBonuses(_, _, _)).WillByDefault(Invoke(&bonusFake, &BonusBearerMock::getAllBonuses)); ON_CALL(*this, getTreeVersion()).WillByDefault(Invoke(&bonusFake, &BonusBearerMock::getTreeVersion)); } void expectAnyBonusSystemCall() { - EXPECT_CALL(*this, getAllBonuses(_, _, _, _)).Times(AtLeast(0)); + EXPECT_CALL(*this, getAllBonuses(_, _, _)).Times(AtLeast(0)); EXPECT_CALL(*this, getTreeVersion()).Times(AtLeast(0)); } @@ -83,7 +104,7 @@ class UnitsFake public: std::vector> allUnits; - UnitFake & add(ui8 side) + UnitFake & add(BattleSide side) { auto * unit = new UnitFake(); EXPECT_CALL(*unit, unitSide()).WillRepeatedly(Return(side)); @@ -183,6 +204,190 @@ public: } }; +class AttackableHexesTest : public CBattleInfoCallbackTest +{ +public: + UnitFake & addRegularMelee(BattleHex hex, BattleSide side) + { + auto & unit = unitsFake.add(side); + + unit.makeAlive(); + unit.setDefaultState(); + unit.setupPoisition(hex); + unit.redirectBonusesToFake(); + + return unit; + } + + UnitFake & addDragon(BattleHex hex, BattleSide side) + { + auto & unit = addRegularMelee(hex, side); + + unit.addCreatureAbility(BonusType::TWO_HEX_ATTACK_BREATH); + unit.makeDoubleWide(); + + return unit; + } + + Units getAttackedUnits(UnitFake & attacker, UnitFake & defender, BattleHex defenderHex) + { + startBattle(); + redirectUnitsToFake(); + + return subject.getAttackedBattleUnits( + &attacker, &defender, + defenderHex, false, + attacker.getPosition(), + defender.getPosition()); + } +}; + +TEST_F(AttackableHexesTest, DragonRightRegular_RightHorithontalBreath) +{ + // X A D # + UnitFake & attacker = addDragon(35, BattleSide::ATTACKER); + UnitFake & defender = addRegularMelee(36, BattleSide::DEFENDER); + UnitFake & next = addRegularMelee(37, BattleSide::DEFENDER); + + auto attacked = getAttackedUnits(attacker, defender, defender.getPosition()); + + EXPECT_TRUE(vstd::contains(attacked, &next)); +} + +TEST_F(AttackableHexesTest, DragonDragonBottomRightHead_BottomRightBreathFromHead) +{ + // X A + // D X target D + // # + UnitFake & attacker = addDragon(35, BattleSide::ATTACKER); + UnitFake & defender = addDragon(attacker.getPosition().cloneInDirection(BattleHex::BOTTOM_RIGHT), BattleSide::DEFENDER); + UnitFake & next = addRegularMelee(defender.getPosition().cloneInDirection(BattleHex::BOTTOM_RIGHT), BattleSide::DEFENDER); + + auto attacked = getAttackedUnits(attacker, defender, defender.getPosition()); + + EXPECT_TRUE(vstd::contains(attacked, &next)); +} + +TEST_F(AttackableHexesTest, DragonDragonVerticalDownHead_VerticalDownBreathFromHead) +{ + // X A + // D X target D + // # + UnitFake & attacker = addDragon(35, BattleSide::ATTACKER); + UnitFake & defender = addDragon(attacker.getPosition().cloneInDirection(BattleHex::BOTTOM_LEFT), BattleSide::DEFENDER); + UnitFake & next = addRegularMelee(defender.getPosition().cloneInDirection(BattleHex::BOTTOM_RIGHT), BattleSide::DEFENDER); + + auto attacked = getAttackedUnits(attacker, defender, defender.getPosition()); + + EXPECT_TRUE(vstd::contains(attacked, &next)); +} + +TEST_F(AttackableHexesTest, DragonDragonVerticalDownHeadReverse_VerticalDownBreathFromHead) +{ + // A X + // X D target D + // # + UnitFake & attacker = addDragon(36, BattleSide::DEFENDER); + UnitFake & defender = addDragon(attacker.getPosition().cloneInDirection(BattleHex::BOTTOM_RIGHT), BattleSide::ATTACKER); + UnitFake & next = addRegularMelee(defender.getPosition().cloneInDirection(BattleHex::BOTTOM_LEFT), BattleSide::ATTACKER); + + auto attacked = getAttackedUnits(attacker, defender, defender.getPosition()); + + EXPECT_TRUE(vstd::contains(attacked, &next)); +} + +TEST_F(AttackableHexesTest, DragonDragonVerticalDownBack_VerticalDownBreath) +{ + // X A + // D X target X + // # + UnitFake & attacker = addDragon(37, BattleSide::ATTACKER); + UnitFake & defender = addDragon(attacker.occupiedHex().cloneInDirection(BattleHex::BOTTOM_LEFT), BattleSide::DEFENDER); + UnitFake & next = addRegularMelee(defender.getPosition().cloneInDirection(BattleHex::BOTTOM_RIGHT), BattleSide::DEFENDER); + + auto attacked = getAttackedUnits(attacker, defender, defender.occupiedHex()); + + EXPECT_TRUE(vstd::contains(attacked, &next)); +} + +TEST_F(AttackableHexesTest, DragonDragonHeadBottomRight_BottomRightBreathFromHead) +{ + // X A + // D X target D + // # + UnitFake & attacker = addDragon(37, BattleSide::ATTACKER); + UnitFake & defender = addDragon(attacker.occupiedHex().cloneInDirection(BattleHex::BOTTOM_LEFT), BattleSide::DEFENDER); + UnitFake & next = addRegularMelee(defender.getPosition().cloneInDirection(BattleHex::BOTTOM_RIGHT), BattleSide::DEFENDER); + + auto attacked = getAttackedUnits(attacker, defender, defender.getPosition()); + + EXPECT_TRUE(vstd::contains(attacked, &next)); +} + +TEST_F(AttackableHexesTest, DragonVerticalDownDragonBackReverse_VerticalDownBreath) +{ + // A X + // X D target X + // # + UnitFake & attacker = addDragon(36, BattleSide::DEFENDER); + UnitFake & defender = addDragon(attacker.occupiedHex().cloneInDirection(BattleHex::BOTTOM_RIGHT), BattleSide::ATTACKER); + UnitFake & next = addRegularMelee(defender.getPosition().cloneInDirection(BattleHex::BOTTOM_LEFT), BattleSide::ATTACKER); + + auto attacked = getAttackedUnits(attacker, defender, defender.occupiedHex()); + + EXPECT_TRUE(vstd::contains(attacked, &next)); +} + +TEST_F(AttackableHexesTest, DragonRightBottomDragonHeadReverse_RightBottomBreathFromHeadHex) +{ + // A X + // X D target D + UnitFake & attacker = addDragon(36, BattleSide::DEFENDER); + UnitFake & defender = addDragon(attacker.occupiedHex().cloneInDirection(BattleHex::BOTTOM_RIGHT), BattleSide::ATTACKER); + UnitFake & next = addRegularMelee(defender.getPosition().cloneInDirection(BattleHex::BOTTOM_LEFT), BattleSide::ATTACKER); + + auto attacked = getAttackedUnits(attacker, defender, defender.getPosition()); + + EXPECT_TRUE(vstd::contains(attacked, &next)); +} + +TEST_F(AttackableHexesTest, DragonLeftBottomDragonBackToBack_LeftBottomBreathFromBackHex) +{ + // X A + // D X target X + // # + UnitFake & attacker = addDragon(8, BattleSide::ATTACKER); + UnitFake & defender = addDragon(attacker.occupiedHex().cloneInDirection(BattleHex::BOTTOM_LEFT).cloneInDirection(BattleHex::LEFT), BattleSide::DEFENDER); + UnitFake & next = addRegularMelee(defender.getPosition().cloneInDirection(BattleHex::BOTTOM_RIGHT), BattleSide::DEFENDER); + + auto attacked = getAttackedUnits(attacker, defender, defender.occupiedHex()); + + EXPECT_TRUE(vstd::contains(attacked, &next)); +} + +TEST_F(AttackableHexesTest, DefenderPositionOverride_BreathCountsHypoteticDefenderPosition) +{ + // # N + // X D target D + // A X + UnitFake & attacker = addDragon(35, BattleSide::DEFENDER); + UnitFake & defender = addDragon(8, BattleSide::ATTACKER); + UnitFake & next = addDragon(2, BattleSide::ATTACKER); + + startBattle(); + redirectUnitsToFake(); + + auto attacked = subject.getAttackedBattleUnits( + &attacker, + &defender, + 19, + false, + attacker.getPosition(), + 19); + + EXPECT_TRUE(vstd::contains(attacked, &next)); +} + class BattleFinishedTest : public CBattleInfoCallbackTest { public: @@ -197,10 +402,10 @@ public: auto ret = subject.battleIsFinished(); EXPECT_TRUE(ret); - EXPECT_EQ(*ret, 2); + EXPECT_EQ(*ret, BattleSide::NONE); } - void expectBattleWinner(ui8 side) + void expectBattleWinner(BattleSide side) { auto ret = subject.battleIsFinished(); @@ -208,12 +413,12 @@ public: EXPECT_EQ(*ret, side); } - void expectBattleLooser(ui8 side) + void expectBattleLooser(BattleSide side) { auto ret = subject.battleIsFinished(); EXPECT_TRUE(ret); - EXPECT_NE(*ret, (int)side); + EXPECT_NE(*ret, side); } void setDefaultExpectations() @@ -238,21 +443,21 @@ TEST_F(BattleFinishedTest, EmptyBattleIsDraw) TEST_F(BattleFinishedTest, LastAliveUnitWins) { - UnitFake & unit = unitsFake.add(1); + UnitFake & unit = unitsFake.add(BattleSide::DEFENDER); unit.makeAlive(); unit.setDefaultState(); setDefaultExpectations(); startBattle(); - expectBattleWinner(1); + expectBattleWinner(BattleSide::DEFENDER); } TEST_F(BattleFinishedTest, TwoUnitsContinueFight) { - UnitFake & unit1 = unitsFake.add(0); + UnitFake & unit1 = unitsFake.add(BattleSide::ATTACKER); unit1.makeAlive(); - UnitFake & unit2 = unitsFake.add(1); + UnitFake & unit2 = unitsFake.add(BattleSide::DEFENDER); unit2.makeAlive(); setDefaultExpectations(); @@ -263,7 +468,7 @@ TEST_F(BattleFinishedTest, TwoUnitsContinueFight) TEST_F(BattleFinishedTest, LastWarMachineNotWins) { - UnitFake & unit = unitsFake.add(0); + UnitFake & unit = unitsFake.add(BattleSide::ATTACKER); unit.makeAlive(); unit.makeWarMachine(); unit.setDefaultState(); @@ -271,18 +476,18 @@ TEST_F(BattleFinishedTest, LastWarMachineNotWins) setDefaultExpectations(); startBattle(); - expectBattleLooser(0); + expectBattleLooser(BattleSide::ATTACKER); } TEST_F(BattleFinishedTest, LastWarMachineLoose) { try { - UnitFake & unit1 = unitsFake.add(0); + UnitFake & unit1 = unitsFake.add(BattleSide::ATTACKER); unit1.makeAlive(); unit1.setDefaultState(); - UnitFake & unit2 = unitsFake.add(1); + UnitFake & unit2 = unitsFake.add(BattleSide::DEFENDER); unit2.makeAlive(); unit2.makeWarMachine(); unit2.setDefaultState(); @@ -290,7 +495,7 @@ TEST_F(BattleFinishedTest, LastWarMachineLoose) setDefaultExpectations(); startBattle(); - expectBattleWinner(0); + expectBattleWinner(BattleSide::ATTACKER); } catch(const std::exception & e) { diff --git a/test/battle/CHealthTest.cpp b/test/battle/CHealthTest.cpp index ac32cee5e..98aa95bf6 100644 --- a/test/battle/CHealthTest.cpp +++ b/test/battle/CHealthTest.cpp @@ -29,7 +29,7 @@ public: void setDefaultExpectations() { - EXPECT_CALL(mock, getAllBonuses(_, _, _, _)).WillRepeatedly(Invoke(&bonusMock, &BonusBearerMock::getAllBonuses)); + EXPECT_CALL(mock, getAllBonuses(_, _, _)).WillRepeatedly(Invoke(&bonusMock, &BonusBearerMock::getAllBonuses)); EXPECT_CALL(mock, getTreeVersion()).WillRepeatedly(Return(1)); bonusMock.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::STACK_HEALTH, BonusSource::CREATURE_ABILITY, UNIT_HEALTH, BonusSourceID())); @@ -235,7 +235,7 @@ TEST_F(HealthTest, singleUnitStack) //one Titan - EXPECT_CALL(mock, getAllBonuses(_, _, _, _)).WillRepeatedly(Invoke(&bonusMock, &BonusBearerMock::getAllBonuses)); + EXPECT_CALL(mock, getAllBonuses(_, _, _)).WillRepeatedly(Invoke(&bonusMock, &BonusBearerMock::getAllBonuses)); EXPECT_CALL(mock, getTreeVersion()).WillRepeatedly(Return(1)); bonusMock.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::STACK_HEALTH, BonusSource::CREATURE_ABILITY, 300, BonusSourceID())); diff --git a/test/battle/battle_UnitTest.cpp b/test/battle/battle_UnitTest.cpp index 390c35a29..b6ead9ab2 100644 --- a/test/battle/battle_UnitTest.cpp +++ b/test/battle/battle_UnitTest.cpp @@ -15,7 +15,7 @@ TEST(battle_Unit_getSurroundingHexes, oneWide) { BattleHex position(77); - auto actual = battle::Unit::getSurroundingHexes(position, false, 0); + auto actual = battle::Unit::getSurroundingHexes(position, false, BattleSide::ATTACKER); EXPECT_EQ(actual, position.neighbouringTiles()); } @@ -24,7 +24,7 @@ TEST(battle_Unit_getSurroundingHexes, oneWideLeftCorner) { BattleHex position(34); - auto actual = battle::Unit::getSurroundingHexes(position, false, 0); + auto actual = battle::Unit::getSurroundingHexes(position, false, BattleSide::ATTACKER); EXPECT_EQ(actual, position.neighbouringTiles()); } @@ -33,7 +33,7 @@ TEST(battle_Unit_getSurroundingHexes, oneWideRightCorner) { BattleHex position(117); - auto actual = battle::Unit::getSurroundingHexes(position, false, 0); + auto actual = battle::Unit::getSurroundingHexes(position, false, BattleSide::ATTACKER); EXPECT_EQ(actual, position.neighbouringTiles()); } 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/CFactionTest.cpp b/test/entity/CFactionTest.cpp index f7f1e1c60..3e1b9e31f 100644 --- a/test/entity/CFactionTest.cpp +++ b/test/entity/CFactionTest.cpp @@ -9,7 +9,8 @@ */ #include "StdInc.h" -#include "../../lib/CTownHandler.h" +#include "../../lib/entities/faction/CTown.h" +#include "../../lib/entities/faction/CFaction.h" namespace test { 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/erm/ERM_MF.cpp b/test/erm/ERM_MF.cpp index 36613488d..1759d02c4 100644 --- a/test/erm/ERM_MF.cpp +++ b/test/erm/ERM_MF.cpp @@ -53,7 +53,7 @@ TEST_F(ERM_MF, ChangesDamage) SCOPED_TRACE("\n" + subject->code); runClientServer(); - EXPECT_CALL(event, getInitalDamage()).WillOnce(Return(23450)); + EXPECT_CALL(event, getInitialDamage()).WillOnce(Return(23450)); EXPECT_CALL(event, setDamage(Eq(23460))).Times(1); eventBus.executeEvent(event); diff --git a/test/erm/ExamplesTest.cpp b/test/erm/ExamplesTest.cpp index f24e409d0..7639f7d8b 100644 --- a/test/erm/ExamplesTest.cpp +++ b/test/erm/ExamplesTest.cpp @@ -37,7 +37,7 @@ public: { } - void setDefaultExpectaions() + void setDefaultExpectations() { EXPECT_CALL(infoMock, getLocalPlayer()).WillRepeatedly(Return(PlayerColor(3))); EXPECT_CALL(serverMock, apply(Matcher(_))).WillRepeatedly(Invoke(this, &ExamplesTest::onCommit)); @@ -83,7 +83,7 @@ protected: TEST_F(ExamplesTest, ALL) { - setDefaultExpectaions(); + setDefaultExpectations(); auto sources = CResourceHandler::get()->getFilteredFiles([](const ResourceID & ident) { return ident.getType() == EResType::ERM && boost::algorithm::starts_with(ident.getName(), "SCRIPTS/TEST/ERM/"); diff --git a/test/game/CGameStateTest.cpp b/test/game/CGameStateTest.cpp index aa37088e4..9313a0f8b 100644 --- a/test/game/CGameStateTest.cpp +++ b/test/game/CGameStateTest.cpp @@ -24,6 +24,7 @@ #include "../../lib/TerrainHandler.h" #include "../../lib/battle/BattleInfo.h" +#include "../../lib/battle/BattleLayout.h" #include "../../lib/CStack.h" #include "../../lib/filesystem/ResourcePath.h" @@ -62,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); } @@ -121,6 +122,10 @@ public: return gameState.get(); } + void createBoat(const int3 & visitablePosition, BoatId type, PlayerColor initiator) override + { + } + bool moveHero(ObjectInstanceID hid, int3 dst, EMovementMode movementMode) override { return false; @@ -142,9 +147,7 @@ public: StartInfo si; si.mapname = "anything";//does not matter, map service mocked si.difficulty = 0; - si.mapfileChecksum = 0; si.mode = EStartMode::NEW_GAME; - si.seedToBeUsed = 42; std::unique_ptr header = mapService.loadMapHeader(ResourcePath(si.mapname)); @@ -173,8 +176,6 @@ public: pset.heroNameTextId = pinfo.mainCustomHeroNameTextId; pset.heroPortrait = HeroTypeID(pinfo.mainCustomHeroPortrait); } - - pset.handicap = PlayerSettings::NO_HANDICAP; } @@ -188,24 +189,25 @@ public: void startTestBattle(const CGHeroInstance * attacker, const CGHeroInstance * defender) { - const CGHeroInstance * heroes[2] = {attacker, defender}; - const CArmedInstance * armedInstancies[2] = {attacker, defender}; + BattleSideArray heroes = {attacker, defender}; + BattleSideArray armedInstancies = {attacker, defender}; int3 tile(4,4,0); 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); //send info about battles - BattleInfo * battle = BattleInfo::setupBattle(tile, terrain, terType, armedInstancies, heroes, false, nullptr); + BattleInfo * battle = BattleInfo::setupBattle(tile, terrain, terType, armedInstancies, heroes, layout, nullptr); BattleStart bs; bs.info = battle; ASSERT_EQ(gameState->currentBattles.size(), 0); - gameCallback->sendAndApply(&bs); + gameCallback->sendAndApply(bs); ASSERT_EQ(gameState->currentBattles.size(), 1); } @@ -230,17 +232,11 @@ TEST_F(CGameStateTest, DISABLED_issue2765) ASSERT_NE(attacker->tempOwner, defender->tempOwner); { - CArtifactInstance * a = new CArtifactInstance(); - a->artType = const_cast(ArtifactID(ArtifactID::BALLISTA).toArtifact()); - NewArtifact na; - na.art = a; - gameCallback->sendAndApply(&na); - - PutArtifact pack; - pack.al = ArtifactLocation(defender->id, ArtifactPosition::MACH1); - pack.art = a; - gameCallback->sendAndApply(&pack); + na.artHolder = defender->id; + na.artId = ArtifactID::BALLISTA; + na.pos = ArtifactPosition::MACH1; + gameCallback->sendAndApply(na); } startTestBattle(attacker, defender); @@ -251,13 +247,13 @@ TEST_F(CGameStateTest, DISABLED_issue2765) info.count = 1; info.type = CreatureID(69); info.side = BattleSide::ATTACKER; - info.position = gameState->currentBattles.front()->getAvaliableHex(info.type, info.side); + info.position = gameState->currentBattles.front()->getAvailableHex(info.type, info.side); info.summoned = false; 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,17 +320,11 @@ TEST_F(CGameStateTest, DISABLED_battleResurrection) attacker->mana = attacker->manaLimit(); { - CArtifactInstance * a = new CArtifactInstance(); - a->artType = const_cast(ArtifactID(ArtifactID::SPELLBOOK).toArtifact()); - NewArtifact na; - na.art = a; - gameCallback->sendAndApply(&na); - - PutArtifact pack; - pack.al = ArtifactLocation(attacker->id, ArtifactPosition::SPELLBOOK); - pack.art = a; - gameCallback->sendAndApply(&pack); + na.artHolder = attacker->id; + na.artId = ArtifactID::SPELLBOOK; + na.pos = ArtifactPosition::SPELLBOOK; + gameCallback->sendAndApply(na); } startTestBattle(attacker, defender); @@ -347,13 +337,13 @@ TEST_F(CGameStateTest, DISABLED_battleResurrection) info.count = 10; info.type = CreatureID(13); info.side = BattleSide::ATTACKER; - info.position = gameState->currentBattles.front()->getAvaliableHex(info.type, info.side); + info.position = gameState->currentBattles.front()->getAvailableHex(info.type, info.side); info.summoned = false; BattleUnitsChanged pack; pack.changedStacks.emplace_back(info.id, UnitChanges::EOperation::ADD); info.save(pack.changedStacks.back().data); - gameCallback->sendAndApply(&pack); + gameCallback->sendAndApply(pack); } { @@ -362,13 +352,13 @@ TEST_F(CGameStateTest, DISABLED_battleResurrection) info.count = 10; info.type = CreatureID(13); info.side = BattleSide::DEFENDER; - info.position = gameState->currentBattles.front()->getAvaliableHex(info.type, info.side); + info.position = gameState->currentBattles.front()->getAvailableHex(info.type, info.side); info.summoned = false; 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); @@ -411,15 +401,3 @@ TEST_F(CGameStateTest, DISABLED_battleResurrection) EXPECT_EQ(unit->health.getCount(), 10); EXPECT_EQ(unit->health.getResurrected(), 0); } - -TEST_F(CGameStateTest, updateEntity) -{ - using ::testing::SaveArg; - using ::testing::Eq; - using ::testing::_; - - JsonNode actual; - EXPECT_CALL(services, updateEntity(Eq(Metatype::CREATURE), Eq(424242), _)).WillOnce(SaveArg<2>(&actual)); - gameState->updateEntity(Metatype::CREATURE, 424242, JsonNode("TEST")); - EXPECT_EQ(actual.String(), "TEST"); -} diff --git a/test/googletest b/test/googletest index b796f7d44..b514bdc89 160000 --- a/test/googletest +++ b/test/googletest @@ -1 +1 @@ -Subproject commit b796f7d44681514f58a683a3a71ff17c94edb0c1 +Subproject commit b514bdc898e2951020cbdca1304b75f5950d1f59 diff --git a/test/map/CMapEditManagerTest.cpp b/test/map/CMapEditManagerTest.cpp index 0020a6e93..542167082 100644 --- a/test/map/CMapEditManagerTest.cpp +++ b/test/map/CMapEditManagerTest.cpp @@ -25,38 +25,39 @@ TEST(MapManager, DrawTerrain_Type) try { auto map = std::make_unique(nullptr); + CRandomGenerator rand; map->width = 52; map->height = 52; map->initTerrain(); auto editManager = map->getEditManager(); - editManager->clearTerrain(); + editManager->clearTerrain(&rand); // 1x1 Blow up editManager->getTerrainSelection().select(int3(5, 5, 0)); - editManager->drawTerrain(ETerrainId::GRASS, 10); + editManager->drawTerrain(ETerrainId::GRASS, 10, &rand); 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); - EXPECT_EQ(map->getTile(int3(6, 4, 0)).terType->getId(), ETerrainId::GRASS); + editManager->drawTerrain(ETerrainId::GRASS, 10, &rand); + EXPECT_EQ(map->getTile(int3(6, 4, 0)).getTerrainID(), ETerrainId::GRASS); editManager->getTerrainSelection().select(int3(6, 5, 0)); - editManager->drawTerrain(ETerrainId::LAVA, 10); - EXPECT_EQ(map->getTile(int3(4, 4, 0)).terType->getId(), ETerrainId::GRASS); - EXPECT_EQ(map->getTile(int3(7, 4, 0)).terType->getId(), ETerrainId::LAVA); + editManager->drawTerrain(ETerrainId::LAVA, 10, &rand); + 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)); - editManager->drawTerrain(ETerrainId::GRASS, 10); + editManager->drawTerrain(ETerrainId::GRASS, 10, &rand); editManager->getTerrainSelection().selectRange(MapRect(int3(15, 17, 0), 10, 5)); - editManager->drawTerrain(ETerrainId::GRASS, 10); + editManager->drawTerrain(ETerrainId::GRASS, 10, &rand); editManager->getTerrainSelection().select(int3(21, 16, 0)); - editManager->drawTerrain(ETerrainId::GRASS, 10); - EXPECT_EQ(map->getTile(int3(20, 15, 0)).terType->getId(), ETerrainId::GRASS); + editManager->drawTerrain(ETerrainId::GRASS, 10, &rand); + 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), @@ -66,17 +67,17 @@ TEST(MapManager, DrawTerrain_Type) { editManager->getTerrainSelection().select(tile); } - editManager->drawTerrain(ETerrainId::GRASS, 10); - EXPECT_EQ(map->getTile(int3(35, 44, 0)).terType->getId(), ETerrainId::WATER); + editManager->drawTerrain(ETerrainId::GRASS, 10, &rand); + EXPECT_EQ(map->getTile(int3(35, 44, 0)).getTerrainID(), ETerrainId::WATER); // Rock case editManager->getTerrainSelection().selectRange(MapRect(int3(1, 1, 1), 15, 15)); - editManager->drawTerrain(ETerrainId::SUBTERRANEAN, 10); + editManager->drawTerrain(ETerrainId::SUBTERRANEAN, 10, &rand); std::vector vec({ int3(6, 6, 1), int3(7, 6, 1), int3(8, 6, 1), int3(5, 7, 1), int3(6, 7, 1), int3(7, 7, 1), 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); - EXPECT_TRUE(!map->getTile(int3(5, 6, 1)).terType->isPassable() || !map->getTile(int3(7, 8, 1)).terType->isPassable()); + editManager->drawTerrain(ETerrainId::ROCK, 10, &rand); + 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 @@ -143,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/CMapFormatTest.cpp b/test/map/CMapFormatTest.cpp index 176d5d916..21dec61b0 100644 --- a/test/map/CMapFormatTest.cpp +++ b/test/map/CMapFormatTest.cpp @@ -95,7 +95,7 @@ static JsonNode getFromArchive(CZipLoader & archive, const std::string & archive auto data = archive.load(resource)->readAll(); - JsonNode res(reinterpret_cast(data.first.get()), data.second); + JsonNode res(reinterpret_cast(data.first.get()), data.second, resource.getName()); return res; } 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.cpp b/test/mock/BattleFake.cpp index c22e5e3a4..ad66cbaf2 100644 --- a/test/mock/BattleFake.cpp +++ b/test/mock/BattleFake.cpp @@ -34,17 +34,17 @@ void UnitFake::makeDead() void UnitFake::redirectBonusesToFake() { - ON_CALL(*this, getAllBonuses(_, _, _, _)).WillByDefault(Invoke(&bonusFake, &BonusBearerMock::getAllBonuses)); + ON_CALL(*this, getAllBonuses(_, _, _)).WillByDefault(Invoke(&bonusFake, &BonusBearerMock::getAllBonuses)); ON_CALL(*this, getTreeVersion()).WillByDefault(Invoke(&bonusFake, &BonusBearerMock::getTreeVersion)); } void UnitFake::expectAnyBonusSystemCall() { - EXPECT_CALL(*this, getAllBonuses(_, _, _, _)).Times(AtLeast(0)); + EXPECT_CALL(*this, getAllBonuses(_, _, _)).Times(AtLeast(0)); EXPECT_CALL(*this, getTreeVersion()).Times(AtLeast(0)); } -UnitFake & UnitsFake::add(ui8 side) +UnitFake & UnitsFake::add(BattleSide side) { auto * unit = new UnitFake(); ON_CALL(*unit, unitSide()).WillByDefault(Return(side)); diff --git a/test/mock/BattleFake.h b/test/mock/BattleFake.h index 4a8d04b4d..59f49a80d 100644 --- a/test/mock/BattleFake.h +++ b/test/mock/BattleFake.h @@ -52,7 +52,7 @@ class UnitsFake public: std::vector> allUnits; - UnitFake & add(ui8 side); + UnitFake & add(BattleSide side); battle::Units getUnitsIf(battle::UnitFilter predicate) const; @@ -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_BonusBearer.cpp b/test/mock/mock_BonusBearer.cpp index b9c8e730b..72696cbc3 100644 --- a/test/mock/mock_BonusBearer.cpp +++ b/test/mock/mock_BonusBearer.cpp @@ -25,7 +25,7 @@ void BonusBearerMock::addNewBonus(const std::shared_ptr & b) treeVersion++; } -TConstBonusListPtr BonusBearerMock::getAllBonuses(const CSelector & selector, const CSelector & limit, const CBonusSystemNode * root, const std::string & cachingStr) const +TConstBonusListPtr BonusBearerMock::getAllBonuses(const CSelector & selector, const CSelector & limit, const std::string & cachingStr) const { if(cachedLast != treeVersion) { diff --git a/test/mock/mock_BonusBearer.h b/test/mock/mock_BonusBearer.h index d7f314c8d..0808bd4ce 100644 --- a/test/mock/mock_BonusBearer.h +++ b/test/mock/mock_BonusBearer.h @@ -23,7 +23,7 @@ public: void addNewBonus(const std::shared_ptr & b); - TConstBonusListPtr getAllBonuses(const CSelector & selector, const CSelector & limit, const CBonusSystemNode * root = nullptr, const std::string & cachingStr = "") const override; + TConstBonusListPtr getAllBonuses(const CSelector & selector, const CSelector & limit, const std::string & cachingStr = "") const override; int64_t getTreeVersion() const override; private: diff --git a/test/mock/mock_Creature.h b/test/mock/mock_Creature.h index 7df167aec..cb51a595a 100644 --- a/test/mock/mock_Creature.h +++ b/test/mock/mock_Creature.h @@ -27,6 +27,7 @@ public: MOCK_CONST_METHOD0(getIndex, int32_t()); MOCK_CONST_METHOD0(getIconIndex, int32_t()); MOCK_CONST_METHOD0(getJsonKey, std::string ()); + MOCK_CONST_METHOD0(getModScope, std::string ()); MOCK_CONST_METHOD0(getName, const std::string &()); MOCK_CONST_METHOD0(getId, CreatureID()); MOCK_CONST_METHOD0(getBonusBearer, const IBonusBearer *()); @@ -43,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_IBattleInfoCallback.h b/test/mock/mock_IBattleInfoCallback.h index f28dd37f6..f2eb2b35b 100644 --- a/test/mock/mock_IBattleInfoCallback.h +++ b/test/mock/mock_IBattleInfoCallback.h @@ -22,10 +22,10 @@ public: MOCK_CONST_METHOD0(battleTerrainType, TerrainId()); MOCK_CONST_METHOD0(battleGetBattlefieldType, BattleField()); - MOCK_CONST_METHOD0(battleIsFinished, std::optional()); + MOCK_CONST_METHOD0(battleIsFinished, std::optional()); MOCK_CONST_METHOD0(battleTacticDist, si8()); - MOCK_CONST_METHOD0(battleGetTacticsSide, si8()); + MOCK_CONST_METHOD0(battleGetTacticsSide, BattleSide()); MOCK_CONST_METHOD0(battleNextUnitId, uint32_t()); diff --git a/test/mock/mock_IGameCallback.cpp b/test/mock/mock_IGameCallback.cpp index 13a6ea0ff..378d43b95 100644 --- a/test/mock/mock_IGameCallback.cpp +++ b/test/mock/mock_IGameCallback.cpp @@ -27,7 +27,12 @@ void GameCallbackMock::setGameState(CGameState * gameState) gs = gameState; } -void GameCallbackMock::sendAndApply(CPackForClient * pack) +void GameCallbackMock::sendAndApply(CPackForClient & pack) { upperCallback->apply(pack); } + +vstd::RNG & GameCallbackMock::getRandomGenerator() +{ + throw std::runtime_error("Not implemented!"); +} diff --git a/test/mock/mock_IGameCallback.h b/test/mock/mock_IGameCallback.h index 9ab8675f1..3ae574345 100644 --- a/test/mock/mock_IGameCallback.h +++ b/test/mock/mock_IGameCallback.h @@ -37,16 +37,21 @@ public: void setObjPropertyValue(ObjectInstanceID objid, ObjProperty prop, int32_t value = 0) override {} void setObjPropertyID(ObjectInstanceID objid, ObjProperty prop, ObjPropertyID identifier) override {} + void setBankObjectConfiguration(ObjectInstanceID objid, const BankConfig & configuration) override {} + void setRewardableObjectConfiguration(ObjectInstanceID mapObjectID, const Rewardable::Configuration & configuration) override {} + void setRewardableObjectConfiguration(ObjectInstanceID townInstanceID, BuildingID buildingID, const Rewardable::Configuration & configuration) override {} + 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 createObject(const int3 & visitablePosition, const PlayerColor & initiator, MapObjectID type, MapObjectSubID subtype) override {}; + void createBoat(const int3 & visitablePosition, BoatId type, PlayerColor initiator) override {} void setOwner(const CGObjectInstance * objid, PlayerColor owner) override {} void giveExperience(const CGHeroInstance * hero, TExpType val) override {} void changePrimSkill(const CGHeroInstance * hero, PrimarySkill which, si64 val, bool abs=false) override {} void changeSecSkill(const CGHeroInstance * hero, SecondarySkill which, int val, bool abs=false) override {} - void showBlockingDialog(BlockingDialog *iw) override {} + void showBlockingDialog(const IObjectInterface * caller, BlockingDialog *iw) override {} void showGarrisonDialog(ObjectInstanceID upobj, ObjectInstanceID hid, bool removableUnits) override {} //cb will be called when player closes garrison window void showTeleportDialog(TeleportDialog *iw) override {} void showObjectWindow(const CGObjectInstance * object, EOpenWindowMode window, const CGHeroInstance * visitor, bool addQuery) override {}; @@ -66,17 +71,17 @@ public: void removeAfterVisit(const CGObjectInstance *object) override {} //object will be destroyed when interaction is over. Do not call when interaction is not ongoing! - bool giveHeroNewArtifact(const CGHeroInstance * h, const CArtifact * artType, ArtifactPosition pos) override {return false;} - bool putArtifact(const ArtifactLocation & al, const CArtifactInstance * art, std::optional askAssemble) 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 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;} void heroVisitCastle(const CGTownInstance * obj, const CGHeroInstance * hero) override {} void stopHeroVisitCastle(const CGTownInstance * obj, const CGHeroInstance * hero) override {} void visitCastleObjects(const CGTownInstance * obj, const CGHeroInstance * hero) override {} - void startBattlePrimary(const CArmedInstance *army1, const CArmedInstance *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool creatureBank = false, const CGTownInstance *town = nullptr) override {} //use hero=nullptr for no hero - void startBattleI(const CArmedInstance *army1, const CArmedInstance *army2, int3 tile, bool creatureBank = false) override {} //if any of armies is hero, hero will be used - void startBattleI(const CArmedInstance *army1, const CArmedInstance *army2, bool creatureBank = false) override {} //if any of armies is hero, hero will be used, visitable tile of second obj is place of battle + void startBattle(const CArmedInstance *army1, const CArmedInstance *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, const BattleLayout & layout, const CGTownInstance *town) override {} //use hero=nullptr for no hero + void startBattle(const CArmedInstance *army1, const CArmedInstance *army2) override {} bool moveHero(ObjectInstanceID hid, int3 dst, EMovementMode movementMode, bool transit = false, PlayerColor asker = PlayerColor::NEUTRAL) override {return false;} bool swapGarrisonOnSiege(ObjectInstanceID tid) override {return false;} void giveHeroBonus(GiveBonus * bonus) override {} @@ -87,11 +92,13 @@ public: void changeObjPos(ObjectInstanceID objid, int3 newPos, const PlayerColor & initiator) override {} void heroExchange(ObjectInstanceID hero1, ObjectInstanceID hero2) override {} //when two heroes meet on adventure map void changeFogOfWar(int3 center, ui32 radius, PlayerColor player, ETileVisibility mode) override {} - void changeFogOfWar(std::unordered_set &tiles, PlayerColor player, ETileVisibility mode) override {} + void changeFogOfWar(const std::unordered_set &tiles, PlayerColor player, ETileVisibility mode) override {} 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; #if SCRIPTING_ENABLED MOCK_CONST_METHOD0(getGlobalContextPool, scripting::Pool *()); diff --git a/test/mock/mock_IGameInfoCallback.h b/test/mock/mock_IGameInfoCallback.h index 94939fc24..479f8656a 100644 --- a/test/mock/mock_IGameInfoCallback.h +++ b/test/mock/mock_IGameInfoCallback.h @@ -17,6 +17,7 @@ class IGameInfoCallbackMock : public IGameInfoCallback public: //various MOCK_CONST_METHOD1(getDate, int(Date)); + MOCK_CONST_METHOD1(getStartInfo, const StartInfo *(bool)); MOCK_CONST_METHOD1(isAllowed, bool(SpellID)); MOCK_CONST_METHOD1(isAllowed, bool(ArtifactID)); 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_Services.h b/test/mock/mock_Services.h index 00330beb8..cd6159141 100644 --- a/test/mock/mock_Services.h +++ b/test/mock/mock_Services.h @@ -28,7 +28,7 @@ public: MOCK_CONST_METHOD0(skills, const SkillService * ()); MOCK_CONST_METHOD0(battlefields, const BattleFieldService *()); MOCK_CONST_METHOD0(obstacles, const ObstacleService *()); - MOCK_CONST_METHOD0(settings, const IGameSettings *()); + MOCK_CONST_METHOD0(engineSettings, const IGameSettings *()); MOCK_METHOD3(updateEntity, void(Metatype, int32_t, const JsonNode &)); diff --git a/test/mock/mock_UnitInfo.h b/test/mock/mock_UnitInfo.h index fda7aa8e1..a336fbbd5 100644 --- a/test/mock/mock_UnitInfo.h +++ b/test/mock/mock_UnitInfo.h @@ -18,7 +18,7 @@ public: MOCK_CONST_METHOD0(unitBaseAmount, int32_t()); MOCK_CONST_METHOD0(unitId, uint32_t()); - MOCK_CONST_METHOD0(unitSide, ui8()); + MOCK_CONST_METHOD0(unitSide, BattleSide()); MOCK_CONST_METHOD0(unitOwner, PlayerColor()); MOCK_CONST_METHOD0(unitSlot, SlotID()); diff --git a/test/mock/mock_battle_IBattleState.h b/test/mock/mock_battle_IBattleState.h index 6ef0998d8..02389dc42 100644 --- a/test/mock/mock_battle_IBattleState.h +++ b/test/mock/mock_battle_IBattleState.h @@ -11,6 +11,7 @@ #pragma once #include "../../lib/battle/IBattleState.h" +#include "../../lib/battle/BattleLayout.h" #include "../../lib/int3.h" class BattleStateMock : public IBattleState @@ -25,20 +26,20 @@ public: MOCK_CONST_METHOD0(getDefendedTown, const CGTownInstance *()); MOCK_CONST_METHOD1(getWallState, EWallState(EWallPart)); MOCK_CONST_METHOD0(getGateState, EGateState()); - MOCK_CONST_METHOD1(getSidePlayer, PlayerColor(ui8)); - MOCK_CONST_METHOD1(getSideArmy, const CArmedInstance *(ui8)); - MOCK_CONST_METHOD1(getSideHero, const CGHeroInstance *(ui8)); - MOCK_CONST_METHOD1(getCastSpells, uint32_t(ui8)); - MOCK_CONST_METHOD1(getEnchanterCounter, int32_t(ui8)); + MOCK_CONST_METHOD1(getSidePlayer, PlayerColor(BattleSide)); + MOCK_CONST_METHOD1(getSideArmy, const CArmedInstance *(BattleSide)); + MOCK_CONST_METHOD1(getSideHero, const CGHeroInstance *(BattleSide)); + MOCK_CONST_METHOD1(getCastSpells, uint32_t(BattleSide)); + MOCK_CONST_METHOD1(getEnchanterCounter, int32_t(BattleSide)); MOCK_CONST_METHOD0(getTacticDist, ui8()); - MOCK_CONST_METHOD0(getTacticsSide, ui8()); + MOCK_CONST_METHOD0(getTacticsSide, BattleSide()); MOCK_CONST_METHOD0(getBonusBearer, const IBonusBearer *()); MOCK_CONST_METHOD0(nextUnitId, uint32_t()); MOCK_CONST_METHOD3(getActualDamage, int64_t(const DamageRange &, int32_t, vstd::RNG &)); MOCK_CONST_METHOD0(getBattleID, BattleID()); MOCK_CONST_METHOD0(getLocation, int3()); - MOCK_CONST_METHOD0(isCreatureBank, bool()); - MOCK_CONST_METHOD1(getUsedSpells, std::vector(ui8)); + MOCK_CONST_METHOD0(getLayout, BattleLayout()); + MOCK_CONST_METHOD1(getUsedSpells, std::vector(BattleSide)); MOCK_METHOD0(nextRound, void()); MOCK_METHOD1(nextTurn, void(uint32_t)); diff --git a/test/mock/mock_battle_Unit.h b/test/mock/mock_battle_Unit.h index c9ea0625a..7560bfdc8 100644 --- a/test/mock/mock_battle_Unit.h +++ b/test/mock/mock_battle_Unit.h @@ -15,7 +15,7 @@ class UnitMock : public battle::Unit { public: - MOCK_CONST_METHOD4(getAllBonuses, TConstBonusListPtr(const CSelector &, const CSelector &, const CBonusSystemNode *, const std::string &)); + MOCK_CONST_METHOD3(getAllBonuses, TConstBonusListPtr(const CSelector &, const CSelector &, const std::string &)); MOCK_CONST_METHOD0(getTreeVersion, int64_t()); MOCK_CONST_METHOD0(getCasterUnitId, int32_t()); @@ -38,7 +38,7 @@ public: MOCK_CONST_METHOD0(unitBaseAmount, int32_t()); MOCK_CONST_METHOD0(unitId, uint32_t()); - MOCK_CONST_METHOD0(unitSide, ui8()); + MOCK_CONST_METHOD0(unitSide, BattleSide()); MOCK_CONST_METHOD0(unitOwner, PlayerColor()); MOCK_CONST_METHOD0(unitSlot, SlotID()); MOCK_CONST_METHOD0(unitType, const CCreature * ()); @@ -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)); @@ -93,7 +93,7 @@ public: MOCK_METHOD1(load, void(const JsonNode &)); MOCK_METHOD1(damage, void(int64_t &)); - MOCK_METHOD3(heal, void(int64_t &, EHealLevel, EHealPower)); + MOCK_METHOD3(heal, battle::HealInfo(int64_t &, EHealLevel, EHealPower)); }; diff --git a/test/mock/mock_events_ApplyDamage.h b/test/mock/mock_events_ApplyDamage.h index 492fa5da3..4d39b4a33 100644 --- a/test/mock/mock_events_ApplyDamage.h +++ b/test/mock/mock_events_ApplyDamage.h @@ -20,7 +20,7 @@ class ApplyDamageMock : public ApplyDamage { public: MOCK_CONST_METHOD0(isEnabled, bool()); - MOCK_CONST_METHOD0(getInitalDamage, int64_t()); + MOCK_CONST_METHOD0(getInitialDamage, int64_t()); MOCK_CONST_METHOD0(getDamage, int64_t()); MOCK_METHOD1(setDamage, void(int64_t)); MOCK_CONST_METHOD0(getTarget, const battle::Unit *()); diff --git a/test/mock/mock_spells_Problem.h b/test/mock/mock_spells_Problem.h index 82c8728f8..41de00cb4 100644 --- a/test/mock/mock_spells_Problem.h +++ b/test/mock/mock_spells_Problem.h @@ -12,7 +12,7 @@ #include -#include "../../lib/MetaString.h" +#include "../../lib/texts/MetaString.h" namespace spells { diff --git a/test/mock/mock_spells_Spell.h b/test/mock/mock_spells_Spell.h index ccd33d129..512649dae 100644 --- a/test/mock/mock_spells_Spell.h +++ b/test/mock/mock_spells_Spell.h @@ -27,6 +27,7 @@ public: MOCK_CONST_METHOD0(getIndex, int32_t()); MOCK_CONST_METHOD0(getIconIndex, int32_t()); MOCK_CONST_METHOD0(getJsonKey, std::string ()); + MOCK_CONST_METHOD0(getModScope, std::string ()); MOCK_CONST_METHOD0(getName, const std::string &()); MOCK_CONST_METHOD0(getId, SpellID()); MOCK_CONST_METHOD0(getLevel, int32_t()); @@ -46,6 +47,8 @@ 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 &)); MOCK_CONST_METHOD0(getCastSound, const std::string &()); diff --git a/test/mock/mock_vstd_RNG.h b/test/mock/mock_vstd_RNG.h index 2fe2b04e1..f3723eb19 100644 --- a/test/mock/mock_vstd_RNG.h +++ b/test/mock/mock_vstd_RNG.h @@ -18,8 +18,16 @@ namespace vstd class RNGMock : public RNG { public: - MOCK_METHOD2(getInt64Range, TRandI64(int64_t, int64_t)); - MOCK_METHOD2(getDoubleRange, TRand(double, double)); + MOCK_METHOD2(nextInt, int(int lower, int upper)); + MOCK_METHOD2(nextInt64, int64_t(int64_t lower, int64_t upper)); + MOCK_METHOD2(nextDouble, double(double lower, double upper)); + + MOCK_METHOD1(nextInt, int(int upper)); + MOCK_METHOD1(nextInt64, int64_t(int64_t upper)); + MOCK_METHOD1(nextDouble, double(double upper)); + + MOCK_METHOD0(nextInt, int()); + MOCK_METHOD2(nextBinomialInt, int(int coinsCount, double coinChance)); }; } diff --git a/test/netpacks/EntitiesChangedTest.cpp b/test/netpacks/EntitiesChangedTest.cpp deleted file mode 100644 index e4423176a..000000000 --- a/test/netpacks/EntitiesChangedTest.cpp +++ /dev/null @@ -1,47 +0,0 @@ -/* - * EntitiesChangedTest.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 "NetPackFixture.h" -#include "../../lib/networkPacks/PacksForClient.h" - -namespace test -{ -using namespace ::testing; - -class EntitiesChanged : public Test, public NetPackFixture -{ -public: - std::shared_ptr<::EntitiesChanged> subject; -protected: - void SetUp() override - { - subject = std::make_shared<::EntitiesChanged>(); - NetPackFixture::setUp(); - } - -}; - -TEST_F(EntitiesChanged, Apply) -{ - subject->changes.emplace_back(); - - EntityChanges & changes = subject->changes.back(); - changes.metatype = Metatype::CREATURE; - changes.entityIndex = 424242; - changes.data.String() = "TEST"; - - EXPECT_CALL(*gameState, updateEntity(Eq(Metatype::CREATURE), Eq(424242), _)); - - gameState->apply(subject.get()); -} - -} 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/scripting/ScriptFixture.cpp b/test/scripting/ScriptFixture.cpp index 0ff6e3b2b..27fb3ae75 100644 --- a/test/scripting/ScriptFixture.cpp +++ b/test/scripting/ScriptFixture.cpp @@ -9,6 +9,7 @@ */ #include "StdInc.h" #include "lib/modding/ModScope.h" +#include "lib/VCMI_Lib.h" #include "ScriptFixture.h" diff --git a/test/spells/AbilityCasterTest.cpp b/test/spells/AbilityCasterTest.cpp index 02c3a9e44..922142d72 100644 --- a/test/spells/AbilityCasterTest.cpp +++ b/test/spells/AbilityCasterTest.cpp @@ -33,7 +33,7 @@ public: protected: void SetUp() override { - ON_CALL(actualCaster, getAllBonuses(_, _, _, _)).WillByDefault(Invoke(&casterBonuses, &BonusBearerMock::getAllBonuses)); + ON_CALL(actualCaster, getAllBonuses(_, _, _)).WillByDefault(Invoke(&casterBonuses, &BonusBearerMock::getAllBonuses)); ON_CALL(actualCaster, getTreeVersion()).WillByDefault(Invoke(&casterBonuses, &BonusBearerMock::getTreeVersion)); } @@ -57,7 +57,7 @@ TEST_F(AbilityCasterTest, MagicAbilityAffectedByGenericBonus) casterBonuses.addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::MAGIC_SCHOOL_SKILL, BonusSource::OTHER, 2, BonusSourceID(), BonusSubtypeID(SpellSchool::ANY))); - EXPECT_CALL(actualCaster, getAllBonuses(_, _, _, _)).Times(AtLeast(1)); + EXPECT_CALL(actualCaster, getAllBonuses(_, _, _)).Times(AtLeast(1)); EXPECT_CALL(actualCaster, getTreeVersion()).Times(AtLeast(0)); setupSubject(1); @@ -65,13 +65,13 @@ TEST_F(AbilityCasterTest, MagicAbilityAffectedByGenericBonus) EXPECT_EQ(subject->getSpellSchoolLevel(&spellMock), 2); } -TEST_F(AbilityCasterTest, MagicAbilityIngoresSchoolBonus) +TEST_F(AbilityCasterTest, MagicAbilityIgnoresSchoolBonus) { EXPECT_CALL(spellMock, getLevel()).WillRepeatedly(Return(1)); casterBonuses.addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::MAGIC_SCHOOL_SKILL, BonusSource::OTHER, 2, BonusSourceID(), BonusSubtypeID(SpellSchool::AIR))); - EXPECT_CALL(actualCaster, getAllBonuses(_, _, _, _)).Times(AtLeast(1)); + EXPECT_CALL(actualCaster, getAllBonuses(_, _, _)).Times(AtLeast(1)); EXPECT_CALL(actualCaster, getTreeVersion()).Times(AtLeast(0)); setupSubject(1); diff --git a/test/spells/effects/CatapultTest.cpp b/test/spells/effects/CatapultTest.cpp index 1cb55c5f2..e6e2d2f18 100644 --- a/test/spells/effects/CatapultTest.cpp +++ b/test/spells/effects/CatapultTest.cpp @@ -14,6 +14,7 @@ #include #include "../../../lib/mapObjects/CGTownInstance.h" +#include "../../../lib/json/JsonNode.h" namespace test @@ -63,7 +64,7 @@ TEST_F(CatapultTest, NotApplicableInVillage) TEST_F(CatapultTest, NotApplicableForDefenderIfSmart) { auto fakeTown = std::make_shared(nullptr); - fakeTown->builtBuildings.insert(BuildingID::FORT); + fakeTown->addBuilding(BuildingID::FORT); mechanicsMock.casterSide = BattleSide::DEFENDER; EXPECT_CALL(*battleFake, getDefendedTown()).WillRepeatedly(Return(fakeTown.get())); @@ -77,7 +78,7 @@ TEST_F(CatapultTest, NotApplicableForDefenderIfSmart) TEST_F(CatapultTest, DISABLED_ApplicableInTown) { auto fakeTown = std::make_shared(nullptr); - fakeTown->builtBuildings.insert(BuildingID::FORT); + fakeTown->addBuilding(BuildingID::FORT); EXPECT_CALL(*battleFake, getDefendedTown()).WillRepeatedly(Return(fakeTown.get())); EXPECT_CALL(mechanicsMock, adaptProblem(_, _)).Times(0); @@ -107,7 +108,7 @@ protected: { EffectFixture::setUp(); fakeTown = std::make_shared(nullptr); - fakeTown->builtBuildings.insert(BuildingID::FORT); + fakeTown->addBuilding(BuildingID::FORT); } private: std::shared_ptr fakeTown; @@ -133,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 fb53586b3..4087498cc 100644 --- a/test/spells/effects/DispelTest.cpp +++ b/test/spells/effects/DispelTest.cpp @@ -34,7 +34,7 @@ public: { } - void setDefaultExpectaions() + void setDefaultExpectations() { EXPECT_CALL(mechanicsMock, spells()).Times(AnyNumber()); @@ -82,7 +82,7 @@ TEST_F(DispelTest, DISABLED_ApplicableToAliveUnitWithTimedEffect) EXPECT_CALL(mechanicsMock, isSmart()).WillOnce(Return(false)); EXPECT_CALL(mechanicsMock, getSpellIndex()).Times(AtLeast(1)).WillRepeatedly(Return(neutralID.toEnum())); - setDefaultExpectaions(); + setDefaultExpectations(); unitsFake.setDefaultBonusExpectations(); EffectTarget target; @@ -110,7 +110,7 @@ TEST_F(DispelTest, DISABLED_IgnoresOwnEffects) EXPECT_CALL(mechanicsMock, ownerMatches(Eq(&unit))).Times(AtMost(1)).WillRepeatedly(Return(true)); EXPECT_CALL(mechanicsMock, getSpellIndex()).Times(AtLeast(1)).WillRepeatedly(Return(neutralID.toEnum())); - setDefaultExpectaions(); + setDefaultExpectations(); unitsFake.setDefaultBonusExpectations(); EffectTarget target; @@ -209,10 +209,10 @@ 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)); - setDefaultExpectaions(); + setDefaultExpectations(); unitsFake.setDefaultBonusExpectations(); setupDefaultRNG(); diff --git a/test/spells/effects/EffectFixture.cpp b/test/spells/effects/EffectFixture.cpp index 5cf558a44..0f31118c7 100644 --- a/test/spells/effects/EffectFixture.cpp +++ b/test/spells/effects/EffectFixture.cpp @@ -92,36 +92,30 @@ 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 vstd::TRandI64 getInt64RangeDef(int64_t lower, int64_t upper) +static int64_t getInt64Range(int64_t lower, int64_t upper) { - return [=]()->int64_t - { - return (lower + upper)/2; - }; + return (lower + upper)/2; } -static vstd::TRand getDoubleRangeDef(double lower, double upper) +static double getDoubleRange(double lower, double upper) { - return [=]()->double - { - return (lower + upper)/2; - }; + return (lower + upper)/2; } void EffectFixture::setupDefaultRNG() { EXPECT_CALL(serverMock, getRNG()).Times(AtLeast(0)); - EXPECT_CALL(rngMock, getInt64Range(_,_)).WillRepeatedly(Invoke(&getInt64RangeDef)); - EXPECT_CALL(rngMock, getDoubleRange(_,_)).WillRepeatedly(Invoke(&getDoubleRangeDef)); + EXPECT_CALL(rngMock, nextInt64(_,_)).WillRepeatedly(Invoke(&getInt64Range)); + EXPECT_CALL(rngMock, nextDouble(_,_)).WillRepeatedly(Invoke(&getDoubleRange)); } } 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 fbc322340..00815b3cd 100644 --- a/test/spells/effects/SummonTest.cpp +++ b/test/spells/effects/SummonTest.cpp @@ -164,7 +164,7 @@ public: { } - void setDefaultExpectaions() + void setDefaultExpectations() { EXPECT_CALL(mechanicsMock, creatures()).Times(AnyNumber()); EXPECT_CALL(creatureServiceMock, getById(Eq(toSummon))).WillRepeatedly(Return(&toSummonType)); @@ -221,11 +221,11 @@ protected: TEST_P(SummonApplyTest, DISABLED_SpawnsNewUnit) { - setDefaultExpectaions(); + setDefaultExpectations(); 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); @@ -242,7 +242,7 @@ TEST_P(SummonApplyTest, DISABLED_SpawnsNewUnit) TEST_P(SummonApplyTest, DISABLED_UpdatesOldUnit) { - setDefaultExpectaions(); + setDefaultExpectations(); acquired = std::make_shared(); acquired->addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::STACK_HEALTH, BonusSource::CREATURE_ABILITY, unitHealth, BonusSourceID())); @@ -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 541a35ecf..3f3f56ff1 100644 --- a/test/spells/effects/TimedTest.cpp +++ b/test/spells/effects/TimedTest.cpp @@ -51,7 +51,7 @@ public: { } - void setDefaultExpectaions() + void setDefaultExpectations() { unitsFake.setDefaultBonusExpectations(); EXPECT_CALL(mechanicsMock, getSpellIndex()).WillRepeatedly(Return(spellIndex)); @@ -116,9 +116,9 @@ TEST_P(TimedApplyTest, DISABLED_ChangesBonuses) EXPECT_CALL(*battleFake, updateUnitBonus(Eq(unitId),_)).WillOnce(SaveArg<1>(&actualBonus)); } - setDefaultExpectaions(); + setDefaultExpectations(); - EXPECT_CALL(serverMock, apply(Matcher(_))).Times(1); + EXPECT_CALL(serverMock, apply(Matcher(_))).Times(1); subject->apply(&serverMock, &mechanicsMock, target); diff --git a/test/spells/targetConditions/AbsoluteLevelConditionTest.cpp b/test/spells/targetConditions/AbsoluteLevelConditionTest.cpp index ae2641c6e..358561c49 100644 --- a/test/spells/targetConditions/AbsoluteLevelConditionTest.cpp +++ b/test/spells/targetConditions/AbsoluteLevelConditionTest.cpp @@ -24,7 +24,7 @@ public: void setDefaultExpectations() { EXPECT_CALL(mechanicsMock, isMagicalEffect()).WillRepeatedly(Return(true)); - EXPECT_CALL(unitMock, getAllBonuses(_, _, _, _)).Times(AtLeast(1)); + EXPECT_CALL(unitMock, getAllBonuses(_, _, _)).Times(AtLeast(1)); EXPECT_CALL(unitMock, getTreeVersion()).Times(AtLeast(0)); } diff --git a/test/spells/targetConditions/AbsoluteSpellConditionTest.cpp b/test/spells/targetConditions/AbsoluteSpellConditionTest.cpp index a042b51c1..fa77c64dd 100644 --- a/test/spells/targetConditions/AbsoluteSpellConditionTest.cpp +++ b/test/spells/targetConditions/AbsoluteSpellConditionTest.cpp @@ -24,7 +24,7 @@ public: void setDefaultExpectations() { - EXPECT_CALL(unitMock, getAllBonuses(_, _, _, _)).Times(AtLeast(1)); + EXPECT_CALL(unitMock, getAllBonuses(_, _, _)).Times(AtLeast(1)); EXPECT_CALL(unitMock, getTreeVersion()).Times(AtLeast(0)); EXPECT_CALL(mechanicsMock, getSpellIndex()).WillRepeatedly(Return(castSpell)); } diff --git a/test/spells/targetConditions/BonusConditionTest.cpp b/test/spells/targetConditions/BonusConditionTest.cpp index bd0e7d62c..388db8389 100644 --- a/test/spells/targetConditions/BonusConditionTest.cpp +++ b/test/spells/targetConditions/BonusConditionTest.cpp @@ -21,7 +21,7 @@ class BonusConditionTest : public TargetConditionItemTest public: void setDefaultExpectations() { - EXPECT_CALL(unitMock, getAllBonuses(_, _, _, _)).Times(AtLeast(1)); + EXPECT_CALL(unitMock, getAllBonuses(_, _, _)).Times(AtLeast(1)); EXPECT_CALL(unitMock, getTreeVersion()).Times(AtLeast(0)); } diff --git a/test/spells/targetConditions/CreatureConditionTest.cpp b/test/spells/targetConditions/CreatureConditionTest.cpp index 155d9e68f..83e5eed3f 100644 --- a/test/spells/targetConditions/CreatureConditionTest.cpp +++ b/test/spells/targetConditions/CreatureConditionTest.cpp @@ -21,7 +21,7 @@ class CreatureConditionTest : public TargetConditionItemTest public: void setDefaultExpectations() { - EXPECT_CALL(unitMock, getAllBonuses(_, _, _, _)).Times(0); + EXPECT_CALL(unitMock, getAllBonuses(_, _, _)).Times(0); EXPECT_CALL(unitMock, getTreeVersion()).Times(0); } diff --git a/test/spells/targetConditions/ElementalConditionTest.cpp b/test/spells/targetConditions/ElementalConditionTest.cpp index b0085dee7..aab797aca 100644 --- a/test/spells/targetConditions/ElementalConditionTest.cpp +++ b/test/spells/targetConditions/ElementalConditionTest.cpp @@ -23,7 +23,7 @@ public: void setDefaultExpectations() { - EXPECT_CALL(unitMock, getAllBonuses(_, _, _, _)).Times(AtLeast(1)); + EXPECT_CALL(unitMock, getAllBonuses(_, _, _)).Times(AtLeast(1)); EXPECT_CALL(unitMock, getTreeVersion()).Times(AtLeast(0)); EXPECT_CALL(mechanicsMock, getSpell()).Times(AtLeast(1)).WillRepeatedly(Return(&spellMock)); diff --git a/test/spells/targetConditions/HealthValueConditionTest.cpp b/test/spells/targetConditions/HealthValueConditionTest.cpp index e142c58e4..0754fe4f2 100644 --- a/test/spells/targetConditions/HealthValueConditionTest.cpp +++ b/test/spells/targetConditions/HealthValueConditionTest.cpp @@ -23,7 +23,7 @@ public: const int64_t EFFECT_VALUE = 101; void setDefaultExpectations() { - EXPECT_CALL(unitMock, getAllBonuses(_, _, _, _)).Times(0); + EXPECT_CALL(unitMock, getAllBonuses(_, _, _)).Times(0); EXPECT_CALL(unitMock, getTreeVersion()).Times(0); EXPECT_CALL(unitMock, getAvailableHealth()).WillOnce(Return(UNIT_HP)); EXPECT_CALL(mechanicsMock, getEffectValue()).WillOnce(Return(EFFECT_VALUE)); diff --git a/test/spells/targetConditions/ImmunityNegationConditionTest.cpp b/test/spells/targetConditions/ImmunityNegationConditionTest.cpp index d6aa51b98..748b5a6de 100644 --- a/test/spells/targetConditions/ImmunityNegationConditionTest.cpp +++ b/test/spells/targetConditions/ImmunityNegationConditionTest.cpp @@ -30,7 +30,7 @@ public: { ownerMatches = ::testing::get<0>(GetParam()); isMagicalEffect = ::testing::get<1>(GetParam()); - EXPECT_CALL(unitMock, getAllBonuses(_, _, _, _)).Times(AtLeast(0)); + EXPECT_CALL(unitMock, getAllBonuses(_, _, _)).Times(AtLeast(0)); EXPECT_CALL(unitMock, getTreeVersion()).Times(AtLeast(0)); EXPECT_CALL(mechanicsMock, isMagicalEffect()).Times(AtLeast(0)).WillRepeatedly(Return(isMagicalEffect)); EXPECT_CALL(mechanicsMock, ownerMatches(Eq(&unitMock), Field(&boost::logic::tribool::value, boost::logic::tribool::false_value))).WillRepeatedly(Return(ownerMatches)); diff --git a/test/spells/targetConditions/NormalLevelConditionTest.cpp b/test/spells/targetConditions/NormalLevelConditionTest.cpp index e6895e232..5abe9f595 100644 --- a/test/spells/targetConditions/NormalLevelConditionTest.cpp +++ b/test/spells/targetConditions/NormalLevelConditionTest.cpp @@ -27,7 +27,7 @@ public: isMagicalEffect = GetParam(); EXPECT_CALL(mechanicsMock, isMagicalEffect()).WillRepeatedly(Return(isMagicalEffect)); if(isMagicalEffect) - EXPECT_CALL(unitMock, getAllBonuses(_, _, _, _)).Times(AtLeast(1)); + EXPECT_CALL(unitMock, getAllBonuses(_, _, _)).Times(AtLeast(1)); EXPECT_CALL(unitMock, getTreeVersion()).Times(AtLeast(0)); } diff --git a/test/spells/targetConditions/NormalSpellConditionTest.cpp b/test/spells/targetConditions/NormalSpellConditionTest.cpp index 22ae5bd47..7add7cf08 100644 --- a/test/spells/targetConditions/NormalSpellConditionTest.cpp +++ b/test/spells/targetConditions/NormalSpellConditionTest.cpp @@ -24,7 +24,7 @@ public: void setDefaultExpectations() { - EXPECT_CALL(unitMock, getAllBonuses(_, _, _, _)).Times(AtLeast(1)); + EXPECT_CALL(unitMock, getAllBonuses(_, _, _)).Times(AtLeast(1)); EXPECT_CALL(unitMock, getTreeVersion()).Times(AtLeast(0)); EXPECT_CALL(mechanicsMock, getSpellIndex()).WillRepeatedly(Return(castSpell)); } diff --git a/test/spells/targetConditions/ReceptiveFeatureConditionTest.cpp b/test/spells/targetConditions/ReceptiveFeatureConditionTest.cpp index 5aef4f202..8a6e6bb17 100644 --- a/test/spells/targetConditions/ReceptiveFeatureConditionTest.cpp +++ b/test/spells/targetConditions/ReceptiveFeatureConditionTest.cpp @@ -27,7 +27,7 @@ public: isPositive = ::testing::get<0>(GetParam()); hasBonus = ::testing::get<1>(GetParam()); - EXPECT_CALL(unitMock, getAllBonuses(_, _, _, _)).Times(AtLeast(0)); + EXPECT_CALL(unitMock, getAllBonuses(_, _, _)).Times(AtLeast(0)); EXPECT_CALL(unitMock, getTreeVersion()).Times(AtLeast(0)); EXPECT_CALL(mechanicsMock, isPositiveSpell()).WillRepeatedly(Return(isPositive)); if(hasBonus) diff --git a/test/spells/targetConditions/SpellEffectConditionTest.cpp b/test/spells/targetConditions/SpellEffectConditionTest.cpp index 99c5ef554..466f6ef87 100644 --- a/test/spells/targetConditions/SpellEffectConditionTest.cpp +++ b/test/spells/targetConditions/SpellEffectConditionTest.cpp @@ -21,7 +21,7 @@ class SpellEffectConditionTest : public TargetConditionItemTest public: void setDefaultExpectations() { - EXPECT_CALL(unitMock, getAllBonuses(_, _, _, _)).Times(AtLeast(1)); + EXPECT_CALL(unitMock, getAllBonuses(_, _, _)).Times(AtLeast(1)); EXPECT_CALL(unitMock, getTreeVersion()).Times(AtLeast(0)); } diff --git a/test/spells/targetConditions/TargetConditionItemFixture.h b/test/spells/targetConditions/TargetConditionItemFixture.h index 67df578a4..0099b1b93 100644 --- a/test/spells/targetConditions/TargetConditionItemFixture.h +++ b/test/spells/targetConditions/TargetConditionItemFixture.h @@ -37,7 +37,7 @@ protected: void SetUp() override { using namespace ::testing; - ON_CALL(unitMock, getAllBonuses(_, _, _, _)).WillByDefault(Invoke(&unitBonuses, &BonusBearerMock::getAllBonuses)); + ON_CALL(unitMock, getAllBonuses(_, _, _)).WillByDefault(Invoke(&unitBonuses, &BonusBearerMock::getAllBonuses)); ON_CALL(unitMock, getTreeVersion()).WillByDefault(Invoke(&unitBonuses, &BonusBearerMock::getTreeVersion)); } }; diff --git a/test/testdata/ObjectPropertyTest/objects.ex.json b/test/testdata/ObjectPropertyTest/objects.ex.json index daffa0120..ac2db97af 100644 --- a/test/testdata/ObjectPropertyTest/objects.ex.json +++ b/test/testdata/ObjectPropertyTest/objects.ex.json @@ -257,7 +257,7 @@ "x" : 6, "y" : 1, "options" : { - "text" : "Previus one have random sign" + "text" : "Previous one have random sign" } }, "oceanBottle_11" : { @@ -7376,7 +7376,7 @@ "x" : 31, "y" : 22, "options" : { - "text" : "Arts: have spellbook, head: Admirals hat, shoulders: angel wings, right hand: armagedons blade; left hand: shield of damned; torso: armor of wonder" + "text" : "Arts: have spellbook, head: Admirals hat, shoulders: angel wings, right hand: armageddons blade; left hand: shield of damned; torso: armor of wonder" } }, "sign_272" : { diff --git a/test/testdata/ObjectPropertyTest/objects.json b/test/testdata/ObjectPropertyTest/objects.json index 0baed931b..58ef8e38f 100644 --- a/test/testdata/ObjectPropertyTest/objects.json +++ b/test/testdata/ObjectPropertyTest/objects.json @@ -257,7 +257,7 @@ "x" : 6, "y" : 1, "options" : { - "text" : "Previus one have random sign" + "text" : "Previous one have random sign" } }, "oceanBottle_11" : { @@ -7376,7 +7376,7 @@ "x" : 31, "y" : 22, "options" : { - "text" : "Arts: have spellbook, head: Admirals hat, shoulders: angel wings, right hand: armagedons blade; left hand: shield of damned; torso: armor of wonder" + "text" : "Arts: have spellbook, head: Admirals hat, shoulders: angel wings, right hand: armageddons blade; left hand: shield of damned; torso: armor of wonder" } }, "sign_272" : { diff --git a/test/vcai/ResurceManagerTest.cpp b/test/vcai/ResourceManagerTest.cpp similarity index 90% rename from test/vcai/ResurceManagerTest.cpp rename to test/vcai/ResourceManagerTest.cpp index 7a4e4920e..abeba4748 100644 --- a/test/vcai/ResurceManagerTest.cpp +++ b/test/vcai/ResourceManagerTest.cpp @@ -34,7 +34,7 @@ struct ResourceManagerTest : public Test//, public IResourceManager { rm = make_unique>(&gcm, &aim); - //note: construct new goal for modfications + //note: construct new goal for modifications invalidGoal = sptr(StrictMock()); gatherArmy = sptr(StrictMock()); buildThis = sptr(StrictMock()); @@ -76,8 +76,8 @@ TEST_F(ResourceManagerTest, canAffordMaths) TResources armyCost(0, 0, 0, 0, 0, 0, 54321); EXPECT_FALSE(rm->canAfford(armyCost)); - rm->reserveResoures(armyCost, gatherArmy); - EXPECT_FALSE(rm->canAfford(buildingCost)) << "Reserved value should be substracted from free resources"; + rm->reserveResources(armyCost, gatherArmy); + EXPECT_FALSE(rm->canAfford(buildingCost)) << "Reserved value should be subtracted from free resources"; } TEST_F(ResourceManagerTest, notifyGoalImplemented) @@ -88,12 +88,12 @@ TEST_F(ResourceManagerTest, notifyGoalImplemented) EXPECT_FALSE(rm->hasTasksLeft()); TResources res(0,0,0,0,0,0,12345); - rm->reserveResoures(res, invalidGoal); + rm->reserveResources(res, invalidGoal); ASSERT_FALSE(rm->hasTasksLeft()) << "Can't push Invalid goal"; EXPECT_FALSE(rm->notifyGoalCompleted(invalidGoal)); EXPECT_FALSE(rm->notifyGoalCompleted(gatherArmy)) << "Queue should be empty"; - rm->reserveResoures(res, gatherArmy); + rm->reserveResources(res, gatherArmy); EXPECT_TRUE(rm->notifyGoalCompleted(gatherArmy)) << "Not implemented"; //TODO: try it with not a copy EXPECT_FALSE(rm->notifyGoalCompleted(gatherArmy)); //already completed } @@ -102,9 +102,9 @@ TEST_F(ResourceManagerTest, notifyFulfillsAll) { TResources res; ASSERT_TRUE(buildAny->fulfillsMe(buildThis)) << "Goal dependency implemented incorrectly"; //TODO: goal mock? - rm->reserveResoures(res, buildAny); - rm->reserveResoures(res, buildAny); - rm->reserveResoures(res, buildAny); + rm->reserveResources(res, buildAny); + rm->reserveResources(res, buildAny); + rm->reserveResources(res, buildAny); ASSERT_TRUE(rm->hasTasksLeft()); //regardless if duplicates are allowed or not rm->notifyGoalCompleted(buildThis); ASSERT_FALSE(rm->hasTasksLeft()) << "BuildThis didn't remove Build Any!"; @@ -129,9 +129,9 @@ TEST_F(ResourceManagerTest, queueOrder) buildVeryHigh->setpriority(10).setbid(5); TResources price(0, 0, 0, 0, 0, 0, 1000); - rm->reserveResoures(price, buildLow); - rm->reserveResoures(price, buildHigh); - rm->reserveResoures(price, buildMed); + rm->reserveResources(price, buildLow); + rm->reserveResources(price, buildHigh); + rm->reserveResources(price, buildMed); ON_CALL(gcm, getResourceAmount()) .WillByDefault(Return(TResources(0,0,0,0,0,0,4000,0))); //we can afford 4 top goals @@ -139,8 +139,8 @@ TEST_F(ResourceManagerTest, queueOrder) auto goal = rm->whatToDo(); EXPECT_EQ(goal->goalType, Goals::BUILD_STRUCTURE); ASSERT_EQ(rm->whatToDo()->bid, 4); - rm->reserveResoures(price, buildBit); - rm->reserveResoures(price, buildVeryHigh); + rm->reserveResources(price, buildBit); + rm->reserveResources(price, buildVeryHigh); goal = rm->whatToDo(); EXPECT_EQ(goal->goalType, Goals::BUILD_STRUCTURE); ASSERT_EQ(goal->bid, 5); @@ -171,7 +171,7 @@ TEST_F(ResourceManagerTest, updateGoalImplemented) EXPECT_FALSE(rm->updateGoal(buildThis)); //try update with no objectives -> fail - rm->reserveResoures(res, buildThis); + rm->reserveResources(res, buildThis); ASSERT_TRUE(rm->hasTasksLeft()); buildThis->setpriority(4.444f); @@ -224,11 +224,11 @@ TEST_F(ResourceManagerTest, freeResourcesWithManyGoals) ASSERT_EQ(rm->freeResources(), TResources(20, 10, 20, 10, 10, 10, 20000, 0)); - rm->reserveResoures(TResources(0, 4, 0, 0, 0, 0, 13000), gatherArmy); + rm->reserveResources(TResources(0, 4, 0, 0, 0, 0, 13000), gatherArmy); ASSERT_EQ(rm->freeResources(), TResources(20, 6, 20, 10, 10, 10, 7000, 0)); - rm->reserveResoures(TResources(5, 4, 5, 4, 4, 4, 5000), buildThis); + rm->reserveResources(TResources(5, 4, 5, 4, 4, 4, 5000), buildThis); ASSERT_EQ(rm->freeResources(), TResources(15, 2, 15, 6, 6, 6, 2000, 0)); - rm->reserveResoures(TResources(0, 0, 0, 0, 0, 0, 2500), recruitHero); + rm->reserveResources(TResources(0, 0, 0, 0, 0, 0, 2500), recruitHero); auto res = rm->freeResources(); EXPECT_EQ(res[EGameResID::GOLD], 0) << "We should have 0 gold left"; diff --git a/test/vcai/mock_ResourceManager.cpp b/test/vcai/mock_ResourceManager.cpp index 577a6efe4..b3c701457 100644 --- a/test/vcai/mock_ResourceManager.cpp +++ b/test/vcai/mock_ResourceManager.cpp @@ -11,9 +11,9 @@ #include "mock_ResourceManager.h" -void ResourceManagerMock::reserveResoures(const TResources &res, Goals::TSubgoal goal) +void ResourceManagerMock::reserveResources(const TResources &res, Goals::TSubgoal goal) { - ResourceManager::reserveResoures(res, goal); + ResourceManager::reserveResources(res, goal); } bool ResourceManagerMock::updateGoal(Goals::TSubgoal goal) diff --git a/test/vcai/mock_ResourceManager.h b/test/vcai/mock_ResourceManager.h index 3d5a317c8..e308e0185 100644 --- a/test/vcai/mock_ResourceManager.h +++ b/test/vcai/mock_ResourceManager.h @@ -22,7 +22,7 @@ class ResourceManagerMock : public ResourceManager public: using ResourceManager::ResourceManager; //access protected members, TODO: consider other architecture? - void reserveResoures(const TResources &res, Goals::TSubgoal goal = Goals::TSubgoal()) override; + void reserveResources(const TResources &res, Goals::TSubgoal goal = Goals::TSubgoal()) override; bool updateGoal(Goals::TSubgoal goal) override; bool notifyGoalCompleted(Goals::TSubgoal goal) override; }; \ No newline at end of file diff --git a/vcmi.workspace b/vcmi.workspace deleted file mode 100644 index 134d8126e..000000000 --- a/vcmi.workspace +++ /dev/null @@ -1,42 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 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 93% rename from launcher/jsonutils.cpp rename to vcmiqt/jsonutils.cpp index 4635e9f50..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)) @@ -89,8 +89,8 @@ QVariant JsonFromFile(QString filename) } const auto data = file.readAll(); - JsonNode node(reinterpret_cast(data.data()), data.size()); - return toVariant(node); + JsonNode node(reinterpret_cast(data.data()), data.size(), filename.toStdString()); + 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/mapeditor/jsonutils.h b/vcmiqt/jsonutils.h similarity index 57% rename from mapeditor/jsonutils.h rename to vcmiqt/jsonutils.h index 791711eb0..f65d666b4 100644 --- a/mapeditor/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